Merge pull request #27695 from backstage/rugvip/dev-inject
cli: inject config via index.html for rspack
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
When using the experimental Rspack flag the app build and dev server now injects configuration via a `<script type="backstage.io/config">...</script>` 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`.
|
||||
@@ -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.
|
||||
@@ -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`,
|
||||
),
|
||||
],
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<AppConfig>();
|
||||
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user