import { configMap } from './configMap.js' import { isJSFile } from './util.js' import { getCSSDependenciesFor, getViteManifests } from './manifests.js' import * as cache from './cache.js' import { logger } from './logger.js' import { isVersionMismatchError, NotFoundError, VersionMismatchError } from './errors.js' import zlib from 'node:zlib' import { promisify } from 'node:util' import { getVersionInfo, updateVersionProcessor } from './version.js' const gzip = promisify(zlib.gzip) const brotliCompress = promisify(zlib.brotliCompress) const compressFileSize = Number(process.env.COMPRESS_FILE_SIZE) const compressionMimeTypes = (process.env.COMPRESS_FILE_TYPES || '').replace(/([.+*?^$()[\]{}|])/g, '\\$1').split(' ') const compressionWhitelistRegex = new RegExp(`^(${compressionMimeTypes.join('|')})($|;)`, 'i') export function createWritable (body) { if (typeof body !== 'string' && !(body instanceof Buffer)) return JSON.stringify(body) return body } async function createFileBuffer (response, dependencies) { const cssString = dependencies && dependencies.map(file => `"${file}"`).join(',') const appendix = cssString && `\n/*injected by ui-middleware*/document.dispatchEvent(new CustomEvent("load-css",{detail:{css:[${cssString}]}}))` const resBuffer = await response.arrayBuffer() const appendixLength = appendix?.length || 0 const buffer = Buffer.alloc(resBuffer.byteLength + appendixLength) buffer.fill(Buffer.from(resBuffer), 0, resBuffer.byteLength) if (appendix) buffer.write(appendix, resBuffer.byteLength) return buffer } export async function fetchFileWithHeadersFromBaseUrl ({ path, baseUrl, version }) { const [response, dependencies] = await Promise.all([ fetch(new URL(path, baseUrl), { cache: 'no-store' }), isJSFile(path) && getCSSDependenciesFor({ file: path.substr(1), version }) ]) if (!response.ok) { if (response.status === 404) logger.trace(`[Files] "${path}" could not be found on "${baseUrl}". Responded with: ${response.status}`) else logger.error(`[Files] Unexpected result from file retrieval "${path}" on "${baseUrl}", responded with: ${response.status}`) throw new NotFoundError(`Error fetching file: ${path}`, { status: response.status }) } if (response.headers.get('version')) { const requestedVersion = (await getVersionInfo()).details[baseUrl] const receivedVersion = response.headers.get('version') if (requestedVersion !== receivedVersion) { throw new VersionMismatchError(`${path} does not contain the right version. Needs ${requestedVersion} but received ${receivedVersion}`) } } const result = { body: await createFileBuffer(response, dependencies), headers: { 'content-type': response.headers.get('content-type'), dependencies } } if (result.body.length > compressFileSize && compressionWhitelistRegex.test(result.headers['content-type'])) { if (path === '/index.html') { result.body = await gzip(result.body) result.headers['content-encoding'] = 'gzip' } else { result.body = await brotliCompress(result.body) result.headers['content-encoding'] = 'br' } } return result } export async function fetchFileWithHeaders ({ path, version }) { const viteManifests = await getViteManifests({ version }) const module = viteManifests[path.substr(1)] if (module?.meta?.baseUrl) { const baseUrl = module?.meta?.baseUrl try { return fetchFileWithHeadersFromBaseUrl({ path, baseUrl, version }) } catch (err) { logger.debug(`[Files] File ${path} had a baseUrl but could not be found on that server: ${err}`) if (err instanceof VersionMismatchError) throw err } } return Promise.any(configMap.urls.map(baseUrl => fetchFileWithHeadersFromBaseUrl({ path, baseUrl, version }))) } export function getFile ({ version, path }) { return cache.getFile({ name: path, version }, ({ name: path, version }) => { return fetchFileWithHeaders({ version, path }).catch((err) => { if (!isVersionMismatchError(err)) throw err logger.warn(`[Files] The file ${path} has been delivered with the wrong version from the UI container.`) updateVersionProcessor({ immediate: true }) throw err }) }) } export async function warmCache ({ version }) { const start = +new Date() logger.info('[File] start warming up the cache') const viteManifests = await getViteManifests({ version }) for (const key of Object.keys(viteManifests)) { const path = `/${viteManifests[key].file}` if (!path) continue try { await getFile({ version, path }) } catch (err) { if (isVersionMismatchError(err)) { logger.info(`[File] Cache warming has been canceled because of a version mismatch at "${path}". Canceled after ${Math.floor((+new Date() - start) / 1000)}s`) return } logger.info(`[File] could not prefetch file ${path}`) } } logger.info(`[File] finished warming up the cache in ${Math.floor((+new Date() - start) / 1000)}s`) }