Merge pull request #18660 from backstage/mob/watch-config-changes

Watching app config changes via frontend dev server
This commit is contained in:
Ben Lambert
2023-07-20 13:31:30 +02:00
committed by GitHub
7 changed files with 150 additions and 112 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Reload the frontend when app config changes
@@ -14,15 +14,7 @@
* 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';
import { paths } from '../../lib/paths';
interface StartAppOptions {
verifyVersions?: boolean;
@@ -32,94 +24,12 @@ 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 { name } = await fs.readJson(paths.resolveTarget('package.json'));
const config = await loadCliConfig({
args: options.configPaths,
fromPackage: name,
withFilteredKeys: true,
});
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,
...config,
configPaths: options.configPaths,
verifyVersions: options.verifyVersions,
});
await waitForExit();
+1
View File
@@ -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 });
+4 -6
View File
@@ -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,
),
}),
);
+120 -9
View File
@@ -18,17 +18,100 @@ 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';
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) {
const url = resolveBaseUrl(options.frontendConfig);
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;
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);
@@ -36,14 +119,18 @@ export async function serveBundle(options: ServeOptions) {
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;
},
});
const compiler = webpack(config);
const server = new WebpackDevServer(
server = new WebpackDevServer(
{
hot: !process.env.CI,
devMiddleware: {
@@ -67,8 +154,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,
@@ -84,7 +171,7 @@ export async function serveBundle(options: ServeOptions) {
);
await new Promise<void>((resolve, reject) => {
server.startCallback((err?: Error) => {
server?.startCallback((err?: Error) => {
if (err) {
reject(err);
return;
@@ -98,7 +185,7 @@ export async function serveBundle(options: ServeOptions) {
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();
});
@@ -110,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 */
}
}
+3 -4
View File
@@ -22,16 +22,15 @@ export type BundlingOptions = {
checksEnabled: boolean;
isDev: boolean;
frontendConfig: Config;
frontendAppConfigs: AppConfig[];
getFrontendAppConfigs(): AppConfig[];
baseUrl: URL;
parallelism?: number;
};
export type ServeOptions = BundlingPathsOptions & {
checksEnabled: boolean;
frontendConfig: Config;
frontendAppConfigs: AppConfig[];
fullConfig: Config;
configPaths: string[];
verifyVersions?: boolean;
};
export type BuildOptions = BundlingPathsOptions & {
+15 -1
View File
@@ -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