From 815b7021488dcc8e69aca4cd838323f61750717a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 15 Nov 2024 18:10:54 -0700 Subject: [PATCH] app-backend: serve templated index.html from memory Signed-off-by: Patrik Oldsberg --- .changeset/five-tables-remain.md | 5 +++ .changeset/itchy-masks-work.md | 5 +++ .../src/lib/config/injectConfig.ts | 15 ++++--- .../lib/config/injectConfigIntoHtml.test.ts | 22 ++++------ .../src/lib/config/injectConfigIntoHtml.ts | 12 ++---- .../app-backend/src/service/appPlugin.test.ts | 41 +++++++++++++++++-- plugins/app-backend/src/service/router.ts | 24 +++++++---- 7 files changed, 81 insertions(+), 43 deletions(-) create mode 100644 .changeset/five-tables-remain.md create mode 100644 .changeset/itchy-masks-work.md diff --git a/.changeset/five-tables-remain.md b/.changeset/five-tables-remain.md new file mode 100644 index 0000000000..dc7411c5b2 --- /dev/null +++ b/.changeset/five-tables-remain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-backend': patch +--- + +The `index.html` templating is now done and served from memory rather than written to the filesystem. This means that you can now use config injection with a read-only filesystem, and you no longer need to use the `app.disableConfigInjection` flag. diff --git a/.changeset/itchy-masks-work.md b/.changeset/itchy-masks-work.md new file mode 100644 index 0000000000..f8fc792fab --- /dev/null +++ b/.changeset/itchy-masks-work.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-backend': minor +--- + +Configuration is no longer injected into static assets if a `index.html.tmpl` file is present. diff --git a/plugins/app-backend/src/lib/config/injectConfig.ts b/plugins/app-backend/src/lib/config/injectConfig.ts index 9d63d219f4..fddd4ed1b5 100644 --- a/plugins/app-backend/src/lib/config/injectConfig.ts +++ b/plugins/app-backend/src/lib/config/injectConfig.ts @@ -24,13 +24,12 @@ import { InjectOptions } from './types'; */ export async function injectConfig( options: InjectOptions, -): Promise { - // In order to minimize the potential impact when rolling out the new config - // injection, we use both methods for a few releases. This allows the frontend - // app to be behind the backend by a version or two, but temporarily increases - // config injection overhead. - // TODO(Rugvip): After the 1.32 release we can stop calling the static injection if the HTML one is successful - await injectConfigIntoHtml(options); +): Promise<{ injectedPath?: string | undefined; indexHtmlContent?: Buffer }> { + const indexHtmlContent = await injectConfigIntoHtml(options); + if (indexHtmlContent) { + return { indexHtmlContent }; + } - return injectConfigIntoStatic(options); + const injectedPath = await injectConfigIntoStatic(options); + return { injectedPath }; } diff --git a/plugins/app-backend/src/lib/config/injectConfigIntoHtml.test.ts b/plugins/app-backend/src/lib/config/injectConfigIntoHtml.test.ts index 56c916fbb9..c28f5b33a4 100644 --- a/plugins/app-backend/src/lib/config/injectConfigIntoHtml.test.ts +++ b/plugins/app-backend/src/lib/config/injectConfigIntoHtml.test.ts @@ -38,25 +38,22 @@ describe('injectConfigIntoHtml', () => { mockDir.setContent({ 'index.html.tmpl': "<%= config.getNumber('x') %>", }); - await injectConfigIntoHtml({ + const result = await injectConfigIntoHtml({ ...baseOptions, appConfigs: [{ context: 'mock', data: { x: 1 } }], }); - expect(mockDir.content()).toMatchObject({ - 'index.html': '1', - }); + expect(result?.toString('utf8')).toBe('1'); }); it('should inject config', async () => { mockDir.setContent({ 'index.html.tmpl': '', }); - await injectConfigIntoHtml({ + const result = await injectConfigIntoHtml({ ...baseOptions, appConfigs: [{ context: 'mock', data: { x: 1 } }], }); - expect(mockDir.content()).toMatchObject({ - 'index.html': ` + expect(result?.toString('utf8')).toBe(` -`, - }); +`); }); it('should trim script tag endings from injected config', async () => { mockDir.setContent({ 'index.html.tmpl': '', }); - await injectConfigIntoHtml({ + const result = await injectConfigIntoHtml({ ...baseOptions, appConfigs: [ { @@ -84,8 +80,7 @@ describe('injectConfigIntoHtml', () => { }, ], }); - expect(mockDir.content()).toMatchObject({ - 'index.html': ` + expect(result?.toString('utf8')).toBe(` -`, - }); +`); }); }); diff --git a/plugins/app-backend/src/lib/config/injectConfigIntoHtml.ts b/plugins/app-backend/src/lib/config/injectConfigIntoHtml.ts index c0c1ca200a..dbd03e6ab5 100644 --- a/plugins/app-backend/src/lib/config/injectConfigIntoHtml.ts +++ b/plugins/app-backend/src/lib/config/injectConfigIntoHtml.ts @@ -25,13 +25,13 @@ const HTML_TEMPLATE_NAME = 'index.html.tmpl'; /** @internal */ export async function injectConfigIntoHtml( options: InjectOptions, -): Promise { +): Promise { const { rootDir, appConfigs } = options; const templatePath = resolvePath(rootDir, HTML_TEMPLATE_NAME); if (!(await fs.exists(templatePath))) { - return false; + return undefined; } const templateContent = await fs.readFile( @@ -67,13 +67,7 @@ ${JSON.stringify(appConfigs, null, 2) `, ); - await fs.writeFile( - resolvePath(rootDir, 'index.html'), - indexHtmlContentWithConfig, - 'utf8', - ); - - return true; + return Buffer.from(indexHtmlContentWithConfig, 'utf8'); } export function resolvePublicPath(config: Config) { diff --git a/plugins/app-backend/src/service/appPlugin.test.ts b/plugins/app-backend/src/service/appPlugin.test.ts index 1fc4d27cdc..901b97dd63 100644 --- a/plugins/app-backend/src/service/appPlugin.test.ts +++ b/plugins/app-backend/src/service/appPlugin.test.ts @@ -34,7 +34,11 @@ overridePackagePathResolution({ createRootLogger(); describe('appPlugin', () => { - beforeEach(() => { + afterEach(() => { + mockDir.clear(); + }); + + it('boots', async () => { mockDir.setContent({ 'package.json': '{}', dist: { @@ -42,9 +46,7 @@ describe('appPlugin', () => { 'index.html': 'winning', }, }); - }); - it('boots', async () => { const { server } = await startTestBackend({ features: [ appPlugin, @@ -67,4 +69,37 @@ describe('appPlugin', () => { fetch(`http://localhost:${server.port()}`).then(res => res.text()), ).resolves.toBe('winning'); }); + + it('injects config into index.html', async () => { + mockDir.setContent({ + 'package.json': '{}', + dist: { + static: {}, + 'index.html.tmpl': '', + }, + }); + + const { server } = await startTestBackend({ + features: [ + appPlugin, + mockServices.rootConfig.factory({ + data: { + app: { + disableStaticFallbackCache: true, + }, + }, + }), + ], + }); + + const htmlContent = await fetch( + `http://localhost:${server.port()}/api/app/some/html5/route`, + ).then(res => res.text()); + + expect(htmlContent).toBe(` + +`); + }); }); diff --git a/plugins/app-backend/src/service/router.ts b/plugins/app-backend/src/service/router.ts index a49a2918c7..2c579d44e7 100644 --- a/plugins/app-backend/src/service/router.ts +++ b/plugins/app-backend/src/service/router.ts @@ -267,7 +267,7 @@ async function createEntryPointRouter({ }) { const staticDir = resolvePath(rootDir, 'static'); - const injectedConfigPath = + const injectResult = appConfigs && (await injectConfig({ appConfigs, logger, rootDir, staticDir })); @@ -278,7 +278,7 @@ async function createEntryPointRouter({ staticRouter.use( express.static(staticDir, { setHeaders: (res, path) => { - if (path === injectedConfigPath) { + if (injectResult?.injectedPath === path) { res.setHeader('Cache-Control', CACHE_CONTROL_REVALIDATE_CACHE); } else { res.setHeader('Cache-Control', CACHE_CONTROL_MAX_CACHE); @@ -317,13 +317,19 @@ async function createEntryPointRouter({ ); router.get('/*', (_req, res) => { - res.sendFile(resolvePath(rootDir, 'index.html'), { - headers: { - // The Cache-Control header instructs the browser to not cache the index.html since it might - // link to static assets from recently deployed versions. - 'cache-control': CACHE_CONTROL_NO_CACHE, - }, - }); + if (injectResult?.indexHtmlContent) { + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.setHeader('Cache-Control', CACHE_CONTROL_NO_CACHE); + res.send(injectResult.indexHtmlContent); + } else { + res.sendFile(resolvePath(rootDir, 'index.html'), { + headers: { + // The Cache-Control header instructs the browser to not cache the index.html since it might + // link to static assets from recently deployed versions. + 'cache-control': CACHE_CONTROL_NO_CACHE, + }, + }); + } }); return router;