From 2a56c3d0753ce47272bb3f0d44fe300b7d038771 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Wed, 12 Jul 2023 14:57:54 +0200 Subject: [PATCH 1/7] Watching config change; update over MessageChannel Co-authored-by: Jack Palmer Co-authored-by: Patrik Oldsberg Co-authored-by: Vincenzo Scamporlino Signed-off-by: Philipp Hugenroth --- packages/cli/src/commands/start/startFrontend.ts | 6 ++++++ packages/cli/src/lib/bundler/config.ts | 10 ++++------ packages/cli/src/lib/bundler/server.ts | 13 +++++++++++++ packages/cli/src/lib/bundler/types.ts | 3 ++- packages/cli/src/lib/config.ts | 16 +++++++++++++++- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index 00f7818b41..536b313dfe 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -91,11 +91,16 @@ export async function startFrontend(options: StartAppOptions) { checkReactVersion(); + const configChannel = new MessageChannel(); + const { name } = await fs.readJson(paths.resolveTarget('package.json')); const config = await loadCliConfig({ args: options.configPaths, fromPackage: name, withFilteredKeys: true, + watch(appConfigs) { + configChannel.port1.postMessage(appConfigs); + }, }); const appBaseUrl = config.frontendConfig.getString('app.baseUrl'); @@ -119,6 +124,7 @@ export async function startFrontend(options: StartAppOptions) { const waitForExit = await serveBundle({ entry: options.entry, checksEnabled: options.checksEnabled, + configChannel, ...config, }); diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 94b2a1c561..2f09a66e18 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -117,12 +117,6 @@ export async function createConfig( }), ); - plugins.push( - new webpack.EnvironmentPlugin({ - APP_CONFIG: options.frontendAppConfigs, - }), - ); - plugins.push( new HtmlWebpackPlugin({ template: paths.targetHtml, @@ -137,6 +131,10 @@ export async function createConfig( plugins.push( new webpack.DefinePlugin({ 'process.env.BUILD_INFO': JSON.stringify(buildInfo), + 'process.env.APP_CONFIG': webpack.DefinePlugin.runtimeValue( + () => JSON.stringify(options.getFrontendAppConfigs()), + true, + ), }), ); diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index cef55d3265..bef1a6fd7a 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -21,6 +21,7 @@ import openBrowser from 'react-dev-utils/openBrowser'; import { createConfig, resolveBaseUrl } from './config'; import { ServeOptions } from './types'; import { resolveBundlingPaths } from './paths'; +import { AppConfig } from '@backstage/config'; export async function serveBundle(options: ServeOptions) { const url = resolveBaseUrl(options.frontendConfig); @@ -32,6 +33,8 @@ export async function serveBundle(options: ServeOptions) { Number(url.port) || (url.protocol === 'https:' ? 443 : 80); + let latestFrontendAppConfigs = options.frontendAppConfigs; + const paths = resolveBundlingPaths(options); const pkgPath = paths.targetPackageJson; const pkg = await fs.readJson(pkgPath); @@ -39,6 +42,9 @@ export async function serveBundle(options: ServeOptions) { ...options, isDev: true, baseUrl: url, + getFrontendAppConfigs: () => { + return latestFrontendAppConfigs; + }, }); const compiler = webpack(config); @@ -95,6 +101,13 @@ export async function serveBundle(options: ServeOptions) { }); }); + options.configChannel.port2.onmessage = ({ + data, + }: MessageEvent) => { + latestFrontendAppConfigs = data; + server.invalidate(); + }; + const waitForExit = async () => { for (const signal of ['SIGINT', 'SIGTERM'] as const) { process.on(signal, () => { diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 64916e349c..90fff4f3c1 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -22,7 +22,7 @@ export type BundlingOptions = { checksEnabled: boolean; isDev: boolean; frontendConfig: Config; - frontendAppConfigs: AppConfig[]; + getFrontendAppConfigs(): AppConfig[]; baseUrl: URL; parallelism?: number; }; @@ -32,6 +32,7 @@ export type ServeOptions = BundlingPathsOptions & { frontendConfig: Config; frontendAppConfigs: AppConfig[]; fullConfig: Config; + configChannel: MessageChannel; }; export type BuildOptions = BundlingPathsOptions & { diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index aaf3fcfcb9..b0efa8163a 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -19,7 +19,7 @@ import { loadConfig, loadConfigSchema, } from '@backstage/config-loader'; -import { ConfigReader } from '@backstage/config'; +import { AppConfig, ConfigReader } from '@backstage/config'; import { paths } from './paths'; import { isValidUrl } from './urls'; import { getPackages } from '@manypkg/get-packages'; @@ -33,6 +33,7 @@ type Options = { withDeprecatedKeys?: boolean; fullVisibility?: boolean; strict?: boolean; + watch?: (newFrontendAppConfigs: AppConfig[]) => void; }; export async function loadCliConfig(options: Options) { @@ -80,6 +81,19 @@ export async function loadCliConfig(options: Options) { : undefined, configRoot: paths.targetRoot, configTargets: configTargets, + watch: options.watch && { + onChange(newAppConfigs) { + const newFrontendAppConfigs = schema.process(newAppConfigs, { + visibility: options.fullVisibility + ? ['frontend', 'backend', 'secret'] + : ['frontend'], + withFilteredKeys: options.withFilteredKeys, + withDeprecatedKeys: options.withDeprecatedKeys, + ignoreSchemaErrors: !options.strict, + }); + options.watch!(newFrontendAppConfigs); + }, + }, }); // printing to stderr to not clobber stdout in case the cli command From c3039c18061d9893cdd7fc0b15e06fb5cb1da1ae Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 13 Jul 2023 05:43:53 +0200 Subject: [PATCH 2/7] cli: move config to buildBundle Signed-off-by: Vincenzo Scamporlino --- .../cli/src/commands/start/startFrontend.ts | 35 +--------- packages/cli/src/lib/bundler/bundle.ts | 1 + packages/cli/src/lib/bundler/server.ts | 66 ++++++++++++++----- packages/cli/src/lib/bundler/types.ts | 5 +- 4 files changed, 51 insertions(+), 56 deletions(-) diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index 536b313dfe..2cc951f591 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -14,11 +14,9 @@ * limitations under the License. */ -import fs from 'fs-extra'; import chalk from 'chalk'; import uniq from 'lodash/uniq'; import { serveBundle } from '../../lib/bundler'; -import { loadCliConfig } from '../../lib/config'; import { PackageGraph } from '@backstage/cli-node'; import { Lockfile } from '../../lib/versioning'; import { forbiddenDuplicatesFilter, includedFilter } from '../versions/lint'; @@ -91,41 +89,10 @@ export async function startFrontend(options: StartAppOptions) { checkReactVersion(); - const configChannel = new MessageChannel(); - - const { name } = await fs.readJson(paths.resolveTarget('package.json')); - const config = await loadCliConfig({ - args: options.configPaths, - fromPackage: name, - withFilteredKeys: true, - watch(appConfigs) { - configChannel.port1.postMessage(appConfigs); - }, - }); - - const appBaseUrl = config.frontendConfig.getString('app.baseUrl'); - const backendBaseUrl = config.frontendConfig.getString('backend.baseUrl'); - if (appBaseUrl === backendBaseUrl) { - console.log( - chalk.yellow( - `⚠️ Conflict between app baseUrl and backend baseUrl: - - app.baseUrl: ${appBaseUrl} - backend.baseUrl: ${backendBaseUrl} - - Must have unique hostname and/or ports. - - This can be resolved by changing app.baseUrl and backend.baseUrl to point to their respective local development ports. -`, - ), - ); - } - const waitForExit = await serveBundle({ entry: options.entry, checksEnabled: options.checksEnabled, - configChannel, - ...config, + configPaths: options.configPaths, }); await waitForExit(); diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index b26c2f9f0d..7c45528c5c 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -45,6 +45,7 @@ export async function buildBundle(options: BuildOptions) { checksEnabled: false, isDev: false, baseUrl: resolveBaseUrl(options.frontendConfig), + getFrontendAppConfigs: () => options.frontendAppConfigs, }); const isCi = yn(process.env.CI, { default: false }); diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index bef1a6fd7a..2ac717243f 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -21,27 +21,64 @@ import openBrowser from 'react-dev-utils/openBrowser'; import { createConfig, resolveBaseUrl } from './config'; import { ServeOptions } from './types'; import { resolveBundlingPaths } from './paths'; +import { paths as libPaths } from '../../lib/paths'; +import { loadCliConfig } from '../config'; +import chalk from 'chalk'; import { AppConfig } from '@backstage/config'; export async function serveBundle(options: ServeOptions) { - const url = resolveBaseUrl(options.frontendConfig); + const { name } = await fs.readJson(libPaths.resolveTarget('package.json')); + + let server: WebpackDevServer | undefined = undefined; + let latestFrontendAppConfigs: AppConfig[] = []; + + const cliConfig = await loadCliConfig({ + args: options.configPaths, + fromPackage: name, + withFilteredKeys: true, + watch(appConfigs) { + latestFrontendAppConfigs = appConfigs; + server?.invalidate(); + }, + }); + latestFrontendAppConfigs = cliConfig.frontendAppConfigs; + + const appBaseUrl = cliConfig.frontendConfig.getString('app.baseUrl'); + const backendBaseUrl = cliConfig.frontendConfig.getString('backend.baseUrl'); + if (appBaseUrl === backendBaseUrl) { + console.log( + chalk.yellow( + `⚠️ Conflict between app baseUrl and backend baseUrl: + + app.baseUrl: ${appBaseUrl} + backend.baseUrl: ${backendBaseUrl} + + Must have unique hostname and/or ports. + + This can be resolved by changing app.baseUrl and backend.baseUrl to point to their respective local development ports. +`, + ), + ); + } + + const { frontendConfig, fullConfig } = cliConfig; + const url = resolveBaseUrl(frontendConfig); const host = - options.frontendConfig.getOptionalString('app.listen.host') || url.hostname; + frontendConfig.getOptionalString('app.listen.host') || url.hostname; const port = - options.frontendConfig.getOptionalNumber('app.listen.port') || + frontendConfig.getOptionalNumber('app.listen.port') || Number(url.port) || (url.protocol === 'https:' ? 443 : 80); - let latestFrontendAppConfigs = options.frontendAppConfigs; - const paths = resolveBundlingPaths(options); const pkgPath = paths.targetPackageJson; const pkg = await fs.readJson(pkgPath); const config = await createConfig(paths, { - ...options, + checksEnabled: options.checksEnabled, isDev: true, baseUrl: url, + frontendConfig, getFrontendAppConfigs: () => { return latestFrontendAppConfigs; }, @@ -49,7 +86,7 @@ export async function serveBundle(options: ServeOptions) { const compiler = webpack(config); - const server = new WebpackDevServer( + server = new WebpackDevServer( { hot: !process.env.CI, devMiddleware: { @@ -73,8 +110,8 @@ export async function serveBundle(options: ServeOptions) { https: url.protocol === 'https:' ? { - cert: options.fullConfig.getString('app.https.certificate.cert'), - key: options.fullConfig.getString('app.https.certificate.key'), + cert: fullConfig.getString('app.https.certificate.cert'), + key: fullConfig.getString('app.https.certificate.key'), } : false, host, @@ -90,7 +127,7 @@ export async function serveBundle(options: ServeOptions) { ); await new Promise((resolve, reject) => { - server.startCallback((err?: Error) => { + server?.startCallback((err?: Error) => { if (err) { reject(err); return; @@ -101,17 +138,10 @@ export async function serveBundle(options: ServeOptions) { }); }); - options.configChannel.port2.onmessage = ({ - data, - }: MessageEvent) => { - latestFrontendAppConfigs = data; - server.invalidate(); - }; - const waitForExit = async () => { for (const signal of ['SIGINT', 'SIGTERM'] as const) { process.on(signal, () => { - server.close(); + server?.close(); // exit instead of resolve. The process is shutting down and resolving a promise here logs an error process.exit(); }); diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 90fff4f3c1..f96e4255f1 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -29,10 +29,7 @@ export type BundlingOptions = { export type ServeOptions = BundlingPathsOptions & { checksEnabled: boolean; - frontendConfig: Config; - frontendAppConfigs: AppConfig[]; - fullConfig: Config; - configChannel: MessageChannel; + configPaths: string[]; }; export type BuildOptions = BundlingPathsOptions & { From 37491d9c531c8ebe014180e0a4addaaf21f11191 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 13 Jul 2023 13:50:26 +0200 Subject: [PATCH 3/7] cli: move validation Co-Authored-by: Philipp Hugenroth Signed-off-by: Vincenzo Scamporlino --- .../cli/src/commands/start/startFrontend.ts | 64 ----------------- packages/cli/src/lib/bundler/server.ts | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+), 64 deletions(-) diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index 2cc951f591..d7bdc3c384 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -14,13 +14,7 @@ * limitations under the License. */ -import chalk from 'chalk'; -import uniq from 'lodash/uniq'; import { serveBundle } from '../../lib/bundler'; -import { PackageGraph } from '@backstage/cli-node'; -import { Lockfile } from '../../lib/versioning'; -import { forbiddenDuplicatesFilter, includedFilter } from '../versions/lint'; -import { paths } from '../../lib/paths'; interface StartAppOptions { verifyVersions?: boolean; @@ -30,65 +24,7 @@ interface StartAppOptions { configPaths: string[]; } -function checkReactVersion() { - try { - // Make sure we're looking at the root of the target repo - const reactPkgPath = require.resolve('react/package.json', { - paths: [paths.targetRoot], - }); - const reactPkg = require(reactPkgPath); - if (reactPkg.version.startsWith('16.')) { - console.log( - chalk.yellow( - ` -⚠️ ⚠️ -⚠️ You are using React version 16, which is deprecated for use in Backstage. ⚠️ -⚠️ Please upgrade to React 17 by updating your packages/app dependencies. ⚠️ -⚠️ ⚠️ -`, - ), - ); - } - } catch { - /* ignored */ - } -} - export async function startFrontend(options: StartAppOptions) { - if (options.verifyVersions) { - const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); - const result = lockfile.analyze({ - filter: includedFilter, - localPackages: PackageGraph.fromPackages( - await PackageGraph.listTargetPackages(), - ), - }); - const problemPackages = [...result.newVersions, ...result.newRanges] - .map(({ name }) => name) - .filter(forbiddenDuplicatesFilter); - - if (problemPackages.length > 1) { - console.log( - chalk.yellow( - `⚠️ Some of the following packages may be outdated or have duplicate installations: - - ${uniq(problemPackages).join(', ')} - `, - ), - ); - console.log( - chalk.yellow( - `⚠️ This can be resolved using the following command: - - yarn backstage-cli versions:check --fix - `, - ), - ); - } - } - - checkReactVersion(); - const waitForExit = await serveBundle({ entry: options.entry, checksEnabled: options.checksEnabled, diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 2ac717243f..74c6b2a53c 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -18,6 +18,8 @@ import fs from 'fs-extra'; import webpack from 'webpack'; import WebpackDevServer from 'webpack-dev-server'; import openBrowser from 'react-dev-utils/openBrowser'; +import uniq from 'lodash/uniq'; + import { createConfig, resolveBaseUrl } from './config'; import { ServeOptions } from './types'; import { resolveBundlingPaths } from './paths'; @@ -25,8 +27,50 @@ import { paths as libPaths } from '../../lib/paths'; import { loadCliConfig } from '../config'; import chalk from 'chalk'; import { AppConfig } from '@backstage/config'; +import { PackageGraph } from '@backstage/cli-node'; +import { Lockfile } from '../versioning'; +import { + forbiddenDuplicatesFilter, + includedFilter, +} from '../../commands/versions/lint'; export async function serveBundle(options: ServeOptions) { + if (options.verifyVersions) { + const lockfile = await Lockfile.load( + libPaths.resolveTargetRoot('yarn.lock'), + ); + const result = lockfile.analyze({ + filter: includedFilter, + localPackages: PackageGraph.fromPackages( + await PackageGraph.listTargetPackages(), + ), + }); + const problemPackages = [...result.newVersions, ...result.newRanges] + .map(({ name }) => name) + .filter(forbiddenDuplicatesFilter); + + if (problemPackages.length > 1) { + console.log( + chalk.yellow( + `⚠️ Some of the following packages may be outdated or have duplicate installations: + + ${uniq(problemPackages).join(', ')} + `, + ), + ); + console.log( + chalk.yellow( + `⚠️ This can be resolved using the following command: + + yarn backstage-cli versions:check --fix + `, + ), + ); + } + } + + checkReactVersion(); + const { name } = await fs.readJson(libPaths.resolveTarget('package.json')); let server: WebpackDevServer | undefined = undefined; @@ -153,3 +197,27 @@ export async function serveBundle(options: ServeOptions) { return waitForExit; } + +function checkReactVersion() { + try { + // Make sure we're looking at the root of the target repo + const reactPkgPath = require.resolve('react/package.json', { + paths: [libPaths.targetRoot], + }); + const reactPkg = require(reactPkgPath); + if (reactPkg.version.startsWith('16.')) { + console.log( + chalk.yellow( + ` +⚠️ ⚠️ +⚠️ You are using React version 16, which is deprecated for use in Backstage. ⚠️ +⚠️ Please upgrade to React 17 by updating your packages/app dependencies. ⚠️ +⚠️ ⚠️ +`, + ), + ); + } + } catch { + /* ignored */ + } +} From 3f67cefb4780253bfa89e917089499101dba015f Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 13 Jul 2023 13:54:39 +0200 Subject: [PATCH 4/7] cli: add changeset Co-Authored-by: Philipp Hugenroth Signed-off-by: Vincenzo Scamporlino --- .changeset/loud-garlics-press.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/loud-garlics-press.md diff --git a/.changeset/loud-garlics-press.md b/.changeset/loud-garlics-press.md new file mode 100644 index 0000000000..28aee20619 --- /dev/null +++ b/.changeset/loud-garlics-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Reload the frontend when app config changes From 77312229e82fa045693b7874c9c842495baef098 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 13 Jul 2023 13:58:15 +0200 Subject: [PATCH 5/7] cli: add missing dependency Co-Authored-by: Philipp Hugenroth Signed-off-by: Vincenzo Scamporlino --- packages/cli/src/commands/start/startFrontend.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index d7bdc3c384..a809473788 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -29,6 +29,7 @@ export async function startFrontend(options: StartAppOptions) { entry: options.entry, checksEnabled: options.checksEnabled, configPaths: options.configPaths, + verifyVersions: options.verifyVersions, }); await waitForExit(); From cdf82c27102b5ff77374e864e9f5fe587443dd7d Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 13 Jul 2023 14:30:52 +0200 Subject: [PATCH 6/7] cli: safely invoke watch Signed-off-by: Vincenzo Scamporlino --- packages/cli/src/lib/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index b0efa8163a..0c657b640e 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -91,7 +91,7 @@ export async function loadCliConfig(options: Options) { withDeprecatedKeys: options.withDeprecatedKeys, ignoreSchemaErrors: !options.strict, }); - options.watch!(newFrontendAppConfigs); + options.watch?.(newFrontendAppConfigs); }, }, }); From 8fd3ee4ac1a8ae329f968186d428eb69016da7e9 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Tue, 18 Jul 2023 15:15:57 +0200 Subject: [PATCH 7/7] Fix tsc Signed-off-by: Philipp Hugenroth --- packages/cli/src/lib/bundler/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index f96e4255f1..69e75b113d 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -30,6 +30,7 @@ export type BundlingOptions = { export type ServeOptions = BundlingPathsOptions & { checksEnabled: boolean; configPaths: string[]; + verifyVersions?: boolean; }; export type BuildOptions = BundlingPathsOptions & {