Merge pull request #18660 from backstage/mob/watch-config-changes
Watching app config changes via frontend dev server
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user