app-backend: serve templated index.html from memory
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -24,13 +24,12 @@ import { InjectOptions } from './types';
|
||||
*/
|
||||
export async function injectConfig(
|
||||
options: InjectOptions,
|
||||
): Promise<string | undefined> {
|
||||
// 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 };
|
||||
}
|
||||
|
||||
@@ -38,25 +38,22 @@ describe('injectConfigIntoHtml', () => {
|
||||
mockDir.setContent({
|
||||
'index.html.tmpl': "<html><%= config.getNumber('x') %></html>",
|
||||
});
|
||||
await injectConfigIntoHtml({
|
||||
const result = await injectConfigIntoHtml({
|
||||
...baseOptions,
|
||||
appConfigs: [{ context: 'mock', data: { x: 1 } }],
|
||||
});
|
||||
expect(mockDir.content()).toMatchObject({
|
||||
'index.html': '<html>1</html>',
|
||||
});
|
||||
expect(result?.toString('utf8')).toBe('<html>1</html>');
|
||||
});
|
||||
|
||||
it('should inject config', async () => {
|
||||
mockDir.setContent({
|
||||
'index.html.tmpl': '<html><head></head></html>',
|
||||
});
|
||||
await injectConfigIntoHtml({
|
||||
const result = await injectConfigIntoHtml({
|
||||
...baseOptions,
|
||||
appConfigs: [{ context: 'mock', data: { x: 1 } }],
|
||||
});
|
||||
expect(mockDir.content()).toMatchObject({
|
||||
'index.html': `<html><head>
|
||||
expect(result?.toString('utf8')).toBe(`<html><head>
|
||||
<script type="backstage.io/config">
|
||||
[
|
||||
{
|
||||
@@ -67,15 +64,14 @@ describe('injectConfigIntoHtml', () => {
|
||||
}
|
||||
]
|
||||
</script>
|
||||
</head></html>`,
|
||||
});
|
||||
</head></html>`);
|
||||
});
|
||||
|
||||
it('should trim script tag endings from injected config', async () => {
|
||||
mockDir.setContent({
|
||||
'index.html.tmpl': '<html><head></head></html>',
|
||||
});
|
||||
await injectConfigIntoHtml({
|
||||
const result = await injectConfigIntoHtml({
|
||||
...baseOptions,
|
||||
appConfigs: [
|
||||
{
|
||||
@@ -84,8 +80,7 @@ describe('injectConfigIntoHtml', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockDir.content()).toMatchObject({
|
||||
'index.html': `<html><head>
|
||||
expect(result?.toString('utf8')).toBe(`<html><head>
|
||||
<script type="backstage.io/config">
|
||||
[
|
||||
{
|
||||
@@ -96,7 +91,6 @@ describe('injectConfigIntoHtml', () => {
|
||||
}
|
||||
]
|
||||
</script>
|
||||
</head></html>`,
|
||||
});
|
||||
</head></html>`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,13 +25,13 @@ const HTML_TEMPLATE_NAME = 'index.html.tmpl';
|
||||
/** @internal */
|
||||
export async function injectConfigIntoHtml(
|
||||
options: InjectOptions,
|
||||
): Promise<boolean> {
|
||||
): Promise<Buffer | undefined> {
|
||||
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)
|
||||
</head>`,
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
resolvePath(rootDir, 'index.html'),
|
||||
indexHtmlContentWithConfig,
|
||||
'utf8',
|
||||
);
|
||||
|
||||
return true;
|
||||
return Buffer.from(indexHtmlContentWithConfig, 'utf8');
|
||||
}
|
||||
|
||||
export function resolvePublicPath(config: Config) {
|
||||
|
||||
@@ -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': '<html><head></head></html>',
|
||||
},
|
||||
});
|
||||
|
||||
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(`<html><head>
|
||||
<script type="backstage.io/config">
|
||||
[]
|
||||
</script>
|
||||
</head></html>`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user