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 { NotFoundError } from './errors.js' import zlib from 'node:zlib' import { promisify } from 'node:util' 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)), 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 }) } 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) { try { return fetchFileWithHeadersFromBaseUrl(path, module.meta.baseUrl, version) } catch (err) { logger.debug(`[Files] File ${path} had a baseUrl but could not be found on that server: ${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 }) }) }