diff --git a/spec/file_caching_test.js b/spec/file_caching_test.js index c545637e4046f8c714c28c310018e41ed51abf94..cf11011fdd17445a7abb548243479cd1f8d6ddca 100644 --- a/spec/file_caching_test.js +++ b/spec/file_caching_test.js @@ -41,7 +41,8 @@ describe('File caching service', () => { '/test.txt': (req, res) => res.setHeader('content-type', 'text/plain').status(200).send('this is test'), '/index.html.js': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('this is index.html.js'), '/index.html': (req, res) => res.setHeader('content-type', 'text/html').status(200).send('<html><head></head><body>it\'s me</body></html>'), - '/main.css': (req, res) => res.setHeader('content-type', 'text/css').status(200).send('.foo { color: #000; }') + '/main.css': (req, res) => res.setHeader('content-type', 'text/css').status(200).send('.foo { color: #000; }'), + '/favicon.ico': 'not really a favicon, though' }) await request(app).get('/ready') @@ -77,4 +78,15 @@ describe('File caching service', () => { expect(response.headers['content-type']).toBe('text/html; charset=utf-8') expect(response.text).toBe('<html><head></head><body>it\'s me</body></html>') }) + + it('directly fetches files not referenced in manifest.json files from the upstream servers', async () => { + const response = await request(app).get('/favicon.ico') + expect(response.statusCode).toBe(200) + expect(response.body).toBe('not really a favicon, though') + }) + + it('returns 404 if file can not be resolved', async () => { + const response = await request(app).get('/unknown-file.txt') + expect(response.statusCode).toBe(404) + }) }) diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000000000000000000000000000000000000..2747ac60a64b32f682f56bcb859c02cd8f0ef1d7 --- /dev/null +++ b/src/config.js @@ -0,0 +1,17 @@ +import fs from 'fs/promises' +import yaml from 'js-yaml' + +class Config { + async load () { + const urlsSource = await fs.readFile('./config/manifests/urls.yaml', 'utf8') + this._urls = yaml.load(urlsSource).manifests + } + + get urls () { + return this._urls || [] + } +} + +export const config = new Config() + +export default config diff --git a/src/createApp.js b/src/createApp.js index f68d9b312fa3a1b2595b156bbf6d8c0819d84b0b..ef282800548e0ee0097bee2f40251e511a2d5aaf 100644 --- a/src/createApp.js +++ b/src/createApp.js @@ -90,6 +90,14 @@ export function createApp () { next() }) + app.use(async (req, res, next) => { + try { + const { 'content-type': contentType, content } = await fileCache.fetchAndStore(req.path) + if (content) return res.setHeader('content-type', contentType).status(200).send(content) + } catch (e) {} + next() + }) + app.use(function (err, req, res, next) { logger.error(err) res.status(500).end() diff --git a/src/files.js b/src/files.js index b059dc3c83f0dfbb9a739012d8f4e11aaea608e6..6bc4d9ca1fc9b132ad87cf145796254ab62a557e 100644 --- a/src/files.js +++ b/src/files.js @@ -1,6 +1,18 @@ import fetch from 'node-fetch' import crypto from 'crypto' +import { config } from './config.js' +async function fetchData (path, baseUrl) { + const response = await fetch(new URL(path, baseUrl)) + if (!response.ok) return null + const content = await response.buffer() + const sha256Sum = crypto.createHash('sha256').update(content).digest('base64') + return [path, { + 'content-type': response.headers.get('content-type'), + sha256Sum, + content + }] +} class FileCache { constructor () { this._cache = {} @@ -23,15 +35,7 @@ class FileCache { console.error('could not find manifest for', file) return null } - const response = await fetch(new URL(file, manifest.meta.baseUrl)) - if (!response.ok) return null - const content = await response.buffer() - const sha256Sum = crypto.createHash('sha256').update(content).digest('base64') - return [file, { - 'content-type': response.headers.get('content-type'), - sha256Sum, - content - }] + return fetchData(file, manifest.meta.baseUrl) } catch (e) { console.error(e) } }))).filter(data => Array.isArray(data) && data.length === 2)) } @@ -40,6 +44,13 @@ class FileCache { this._cache = cache } + async fetchAndStore (path) { + if (config.urls.length === 0) await config.load() + const [key, value] = await Promise.race(config.urls.map(baseUrl => fetchData(path, baseUrl))) + this._cache[key] = value + return value + } + get (path) { return this?._cache[path.slice(1)] || {} } diff --git a/src/manifests.js b/src/manifests.js index 44b925028d725c5be9acf5f6c250d8c197983f16..a043e2786f67ce4a784793c892433f574d0a6546 100644 --- a/src/manifests.js +++ b/src/manifests.js @@ -1,9 +1,8 @@ -import fs from 'fs/promises' -import yaml from 'js-yaml' import fetch from 'node-fetch' import path from 'path' import { URL } from 'url' import { fileCache } from './files.js' +import { config } from './config.js' export const loadViteManifests = (() => { let cache @@ -13,12 +12,10 @@ export const loadViteManifests = (() => { const CACHE_TTL = parseInt(process.env.CACHE_TTL) if (!cache || useCache === false || +new Date() > lastCacheTime + CACHE_TTL) { - const urlsSource = await fs.readFile('./config/manifests/urls.yaml', 'utf8') - const urls = yaml.load(urlsSource).manifests - + await config.load() // vite manifests contains a set of objects with the vite-manifests // from the corresponding registered services - const viteManifests = await Promise.all(urls.map(async url => { + const viteManifests = await Promise.all(config.urls.map(async url => { const { origin } = new URL(url) // fetch the manifests const result = await fetch(url)