From c27a0e6391b5bb9a78924695a0e938c4e6c0b385 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 16 Nov 2024 21:29:08 -0700 Subject: [PATCH 1/3] cli: refactor dev server reload and fix for rspack Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/bundler/server.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 44d6d54379..f8d259f467 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -59,6 +59,26 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be let latestFrontendAppConfigs: AppConfig[] = []; + /** Triggers a full reload of all clients */ + const triggerReload = () => { + if (viteServer) { + viteServer.restart(); + } + + if (webpackServer) { + webpackServer.invalidate(); + + // For the Rspack server it's not enough to invalidate, we also need to + // tell the browser to reload, which we do with a 'static-changed' message + if (process.env.EXPERIMENTAL_RSPACK) { + webpackServer.sendMessage( + webpackServer.webSocketServer?.clients ?? [], + 'static-changed', + ); + } + } + }; + const cliConfig = await loadCliConfig({ args: options.configPaths, fromPackage: name, @@ -66,8 +86,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be watch(appConfigs) { latestFrontendAppConfigs = appConfigs; - webpackServer?.invalidate(); - viteServer?.restart(); + triggerReload(); }, }); latestFrontendAppConfigs = cliConfig.frontendAppConfigs; @@ -102,8 +121,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be config: fullConfig, targetPath: paths.targetPath, watch() { - webpackServer?.invalidate(); - viteServer?.restart(); + triggerReload(); }, }); From 44b82da79a9fb8e9cca423c448e4110fe9b2c340 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 16 Nov 2024 21:39:39 -0700 Subject: [PATCH 2/3] core-app-api: defaultConfigLoader no longer requires APP_CONFIG to be set Signed-off-by: Patrik Oldsberg --- .changeset/sweet-pigs-pump.md | 6 ++++++ .../src/app/defaultConfigLoader.test.tsx | 10 ++++------ .../core-app-api/src/app/defaultConfigLoader.ts | 15 ++++++++------- 3 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 .changeset/sweet-pigs-pump.md diff --git a/.changeset/sweet-pigs-pump.md b/.changeset/sweet-pigs-pump.md new file mode 100644 index 0000000000..9e88e65dbc --- /dev/null +++ b/.changeset/sweet-pigs-pump.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-app-api': patch +'@backstage/frontend-defaults': patch +--- + +The default config loader no longer requires `process.env.APP_CONFIG` to be set, allowing config to be read from other sources instead. diff --git a/packages/core-app-api/src/app/defaultConfigLoader.test.tsx b/packages/core-app-api/src/app/defaultConfigLoader.test.tsx index 44d93aa625..556e4c928e 100644 --- a/packages/core-app-api/src/app/defaultConfigLoader.test.tsx +++ b/packages/core-app-api/src/app/defaultConfigLoader.test.tsx @@ -29,6 +29,10 @@ describe('defaultConfigLoaderSync', () => { delete anyWindow.__APP_CONFIG__; }); + it('loads nothing is config is missing', () => { + expect(defaultConfigLoaderSync()).toEqual([]); + }); + it('loads static config', () => { anyEnv.APP_CONFIG = [ { data: { my: 'config' }, context: 'a' }, @@ -95,12 +99,6 @@ describe('defaultConfigLoaderSync', () => { expect(ConfigReader.fromConfigs(configs).get('my')).toBe('override-config'); }); - it('fails to load invalid missing config', () => { - expect(() => defaultConfigLoaderSync()).toThrow( - 'No static configuration provided', - ); - }); - it('fails to load invalid static config', () => { anyEnv.APP_CONFIG = { my: 'invalid-config' }; expect(() => defaultConfigLoaderSync()).toThrow( diff --git a/packages/core-app-api/src/app/defaultConfigLoader.ts b/packages/core-app-api/src/app/defaultConfigLoader.ts index bfcef390ec..0a65112041 100644 --- a/packages/core-app-api/src/app/defaultConfigLoader.ts +++ b/packages/core-app-api/src/app/defaultConfigLoader.ts @@ -40,14 +40,15 @@ export function defaultConfigLoaderSync( // It's a param so we can test it, but at runtime this will always fall back to default. runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', ) { - const appConfig = process.env.APP_CONFIG; - if (!appConfig) { - throw new Error('No static configuration provided'); + const configs = new Array(); + + const staticConfig = process.env.APP_CONFIG; + if (staticConfig) { + if (!Array.isArray(staticConfig)) { + throw new Error('Static configuration has invalid format'); + } + configs.push(...staticConfig); } - if (!Array.isArray(appConfig)) { - throw new Error('Static configuration has invalid format'); - } - const configs = appConfig.slice() as unknown as AppConfig[]; // Check if we have any config script tags, otherwise fall back to injected config const configScripts = document.querySelectorAll( From a2f055902410c6d57a36c0cbab6116f68f83ba63 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 16 Nov 2024 21:58:18 -0700 Subject: [PATCH 3/3] cli: switch config injection to index.html Signed-off-by: Patrik Oldsberg --- .changeset/metal-carrots-brush.md | 9 +++ .../ConfigInjectingHtmlWebpackPlugin.ts | 55 +++++++++++++++++++ packages/cli/src/lib/bundler/config.ts | 46 +++++++++++----- 3 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 .changeset/metal-carrots-brush.md create mode 100644 packages/cli/src/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts diff --git a/.changeset/metal-carrots-brush.md b/.changeset/metal-carrots-brush.md new file mode 100644 index 0000000000..776e1d3e56 --- /dev/null +++ b/.changeset/metal-carrots-brush.md @@ -0,0 +1,9 @@ +--- +'@backstage/cli': patch +--- + +When using the experimental Rspack flag the app build and dev server now injects configuration via a `` tag in `index.html` rather than the `process.env.APP_CONFIG` definition, which will now be defined as an empty array instead. + +This requires the app to be using the config loader from the 1.31 release of Backstage. Make sure your app is using at least that version if you are upgrading to this version of the CLI. + +If you have copied the implementation of the `defaultConfigLoader`, make sure to update it to the new implementation. In particular the config loader needs to be able to read configuration from `script` tags with the type `backstage.io/config`. diff --git a/packages/cli/src/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts b/packages/cli/src/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts new file mode 100644 index 0000000000..5aa8b75098 --- /dev/null +++ b/packages/cli/src/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppConfig } from '@backstage/config'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; + +export class ConfigInjectingHtmlWebpackPlugin extends HtmlWebpackPlugin { + readonly name = 'ConfigInjectingHtmlWebpackPlugin'; + readonly #getFrontendAppConfigs: () => AppConfig[]; + + constructor( + options: HtmlWebpackPlugin.Options, + getFrontendAppConfigs: () => AppConfig[], + ) { + super(options); + this.#getFrontendAppConfigs = getFrontendAppConfigs; + } + + apply: HtmlWebpackPlugin['apply'] = compiler => { + super.apply(compiler); + + compiler.hooks.compilation.tap(this.name, compilation => { + const hooks = HtmlWebpackPlugin.getCompilationHooks(compilation); + hooks.alterAssetTagGroups.tap(this.name, ctx => { + if (ctx.plugin !== this) { + return ctx; + } + return { + ...ctx, + headTags: [ + ...ctx.headTags, + HtmlWebpackPlugin.createHtmlTagObject( + 'script', + { type: 'backstage.io/config' }, + `\n${JSON.stringify(this.#getFrontendAppConfigs(), null, 2)}\n`, + ), + ], + }; + }); + }); + }; +} diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 4e9f67c4fa..5e379783ba 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -37,6 +37,7 @@ import { version } from '../../lib/version'; import yn from 'yn'; import { hasReactDomClient } from './hasReactDomClient'; import { createWorkspaceLinkingPlugins } from './linkWorkspaces'; +import { ConfigInjectingHtmlWebpackPlugin } from './ConfigInjectingHtmlWebpackPlugin'; const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE'; @@ -182,19 +183,35 @@ export async function createConfig( ); if (options.moduleFederation?.mode !== 'remote') { - plugins.push( - // `rspack.HtmlRspackPlugin` does not support object type `templateParameters` value, `frontendConfig` in this case - new HtmlWebpackPlugin({ - meta: { - 'backstage-app-mode': options?.appMode ?? 'public', - }, - template: paths.targetHtml, - templateParameters: { - publicPath, - config: frontendConfig, - }, - }), - ); + const templateOptions = { + meta: { + 'backstage-app-mode': options?.appMode ?? 'public', + }, + template: paths.targetHtml, + templateParameters: { + publicPath, + config: frontendConfig, + }, + }; + if (rspack) { + // With Rspack we inject config via index.html, this is both because we + // can't use APP_CONFIG due to the lack of support for runtime values, but + // also because we are able to do it and it lines up better with what the + // app-backend is doing. + // + // We still use the html plugin from WebPack, since the Rspack one won't + // let us inject complex objects like the config. + plugins.push( + new ConfigInjectingHtmlWebpackPlugin( + templateOptions, + options.getFrontendAppConfigs, + ), + ); + } else { + // Config injection via index.html doesn't work across reloads with + // WebPack, so we rely on the APP_CONFIG injection instead + plugins.push(new HtmlWebpackPlugin(templateOptions)); + } plugins.push( new HtmlWebpackPlugin({ meta: { @@ -284,8 +301,7 @@ export async function createConfig( new bundler.DefinePlugin({ 'process.env.BUILD_INFO': JSON.stringify(buildInfo), 'process.env.APP_CONFIG': rspack - ? // FIXME: see also https://github.com/web-infra-dev/rspack/issues/5606 - JSON.stringify(options.getFrontendAppConfigs()) + ? JSON.stringify([]) // Inject via index.html instead : bundler.DefinePlugin.runtimeValue( () => JSON.stringify(options.getFrontendAppConfigs()), true,