From d2fb430784c6c815e27c9bce5ecbf1eff8e272ae Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 5 Sep 2023 15:34:58 +0200 Subject: [PATCH] cli: refactor package detection to support inc/ecl + entry point Co-authored-by: Philipp Hugenroth Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/bundler/bundle.ts | 13 ++- packages/cli/src/lib/bundler/config.ts | 2 +- packages/cli/src/lib/bundler/discover.ts | 142 ++++++++++++++--------- packages/cli/src/lib/bundler/server.ts | 19 +-- packages/cli/src/lib/bundler/types.ts | 1 + 5 files changed, 107 insertions(+), 70 deletions(-) diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index 9c67eb9bc3..6d5b2ff799 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -27,7 +27,7 @@ import { createConfig, resolveBaseUrl } from './config'; import { BuildOptions } from './types'; import { resolveBundlingPaths } from './paths'; import chalk from 'chalk'; -import { buildDetectedPlugins } from './discover'; +import { createDetectedModulesEntrypoint } from './discover'; // TODO(Rugvip): Limits from CRA, we might want to tweak these though. const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; @@ -42,17 +42,18 @@ export async function buildBundle(options: BuildOptions) { const paths = resolveBundlingPaths(options); + const detectedModulesEntryPoint = await createDetectedModulesEntrypoint({ + config: options.fullConfig, + targetPath: paths.targetPath, + }); + const config = await createConfig(paths, { ...options, checksEnabled: false, isDev: false, baseUrl: resolveBaseUrl(options.frontendConfig), getFrontendAppConfigs: () => options.frontendAppConfigs, - }); - - await buildDetectedPlugins({ - config: options.fullConfig, - targetPath: paths.targetPath, + additionalEntryPoints: detectedModulesEntryPoint, }); const isCi = yn(process.env.CI, { default: false }); diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index b9ef77d528..c9238e7e48 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -161,7 +161,7 @@ export async function createConfig( }, devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map', context: paths.targetPath, - entry: paths.targetEntry, + entry: [...(options.additionalEntryPoints ?? []), paths.targetEntry], resolve: { extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx', '.json', '.wasm'], mainFields: ['browser', 'module', 'main'], diff --git a/packages/cli/src/lib/bundler/discover.ts b/packages/cli/src/lib/bundler/discover.ts index 9325c9bcd8..c30c1131d4 100644 --- a/packages/cli/src/lib/bundler/discover.ts +++ b/packages/cli/src/lib/bundler/discover.ts @@ -21,29 +21,72 @@ import fs from 'fs-extra'; import { join as joinPath, resolve as resolvePath } from 'path'; import { paths as cliPaths } from '../../lib/paths'; -export async function buildDetectedPlugins(options: { - config: Config; - targetPath: string; - watch?: () => void; -}) { - const { watch, targetPath } = options; - if (watch) { - const watcher = chokidar.watch(resolvePath(targetPath, 'package.json')); +const DETECTED_MODULES_MODULE_NAME = '__backstage-autodetected-plugins__'; - watcher.on('change', async () => { - await writeDetectedPluginsModule(options); - watch(); - }); - } - - await writeDetectedPluginsModule(options); +interface PackageDetectionConfig { + include?: string[]; + exclude?: string[]; } -async function writeDetectedPluginsModule(options: { - config: Config; - targetPath: string; -}) { - const requirePackageScript = (await detectPlugins(options)) +function readPackageDetectionConfig( + config: Config, +): PackageDetectionConfig | undefined { + const packages = config.getOptional('app.experimental.packages'); + if (!packages) { + return undefined; + } + + if (typeof packages === 'string') { + if (packages !== 'all') { + throw new Error( + `Invalid app.experimental.packages mode, got '${packages}', expected 'all'`, + ); + } + return {}; + } + + return { + include: config.getOptionalStringArray('app.experimental.packages.include'), + exclude: config.getOptionalStringArray('app.experimental.packages.exclude'), + }; +} + +async function detectPackages( + targetPath: string, + { include, exclude }: PackageDetectionConfig, +) { + const pkg: BackstagePackageJson = await fs.readJson( + resolvePath(targetPath, 'package.json'), + ); + + return Object.keys(pkg.dependencies ?? {}) + .flatMap(depName => { + const depPackageJson: BackstagePackageJson = require(require.resolve( + `${depName}/package.json`, + { paths: [targetPath] }, + )); + if ( + ['frontend-plugin', 'frontend-plugin-module'].includes( + depPackageJson.backstage?.role ?? '', + ) + ) { + return [depName]; + } + return []; + }) + .filter(name => { + if (exclude?.includes(name)) { + return false; + } + if (include && !include.includes(name)) { + return false; + } + return true; + }); +} + +async function writeDetectedPackagesModule(packageNames: string[]) { + const requirePackageScript = packageNames ?.map(pkg => `{name: '${pkg}', module: require('${pkg}')}`) .join(','); @@ -51,47 +94,38 @@ async function writeDetectedPluginsModule(options: { joinPath( cliPaths.targetRoot, 'node_modules', - '__backstage-autodetected-plugins__.js', + `${DETECTED_MODULES_MODULE_NAME}.js`, ), `export const modules = [${requirePackageScript}];`, ); } -async function detectPlugins({ - config, - targetPath, -}: { +export async function createDetectedModulesEntrypoint(options: { config: Config; targetPath: string; -}) { - const pkg: BackstagePackageJson = await fs.readJson( - resolvePath(targetPath, 'package.json'), + watch?: () => void; +}): Promise { + const { config, watch, targetPath } = options; + + const detectionConfig = readPackageDetectionConfig(config); + if (!detectionConfig) { + return []; + } + + if (watch) { + const watcher = chokidar.watch(resolvePath(targetPath, 'package.json')); + + watcher.on('change', async () => { + await writeDetectedPackagesModule( + await detectPackages(targetPath, detectionConfig), + ); + watch(); + }); + } + + await writeDetectedPackagesModule( + await detectPackages(targetPath, detectionConfig), ); - // TODO: proper - // Assumption for config string based on https://github.com/backstage/backstage/issues/18372 ^ - const packageDetectionMode = - config.getOptional('app.experimental.packages') || 'all'; - - const allowedPackages = - packageDetectionMode === 'all' - ? Object.keys(pkg.dependencies ?? {}) - : config.getStringArray('app.experimental.packages'); - - return allowedPackages - .map(depName => { - const depPackageJson: BackstagePackageJson = require(require.resolve( - `${depName}/package.json`, - { paths: [targetPath] }, - )); - if ( - ['frontend-plugin', 'frontend-plugin-module'].includes( - depPackageJson.backstage?.role || '', - ) - ) { - return depName; - } - return undefined; - }) - .filter((d): d is string => !!d); + return [DETECTED_MODULES_MODULE_NAME]; } diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 43a87db684..70cf1645ba 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -31,7 +31,7 @@ import { paths as libPaths } from '../../lib/paths'; import { loadCliConfig } from '../config'; import { Lockfile } from '../versioning'; import { createConfig, resolveBaseUrl } from './config'; -import { buildDetectedPlugins } from './discover'; +import { createDetectedModulesEntrypoint as createDetectedModulesEntryPoint } from './discover'; import { resolveBundlingPaths } from './paths'; import { ServeOptions } from './types'; @@ -119,6 +119,14 @@ export async function serveBundle(options: ServeOptions) { Number(url.port) || (url.protocol === 'https:' ? 443 : 80); + const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({ + config: fullConfig, + targetPath: paths.targetPath, + watch() { + server?.invalidate(); + }, + }); + const config = await createConfig(paths, { ...options, checksEnabled: options.checksEnabled, @@ -129,14 +137,7 @@ export async function serveBundle(options: ServeOptions) { getFrontendAppConfigs: () => { return latestFrontendAppConfigs; }, - }); - - await buildDetectedPlugins({ - config: fullConfig, - targetPath: paths.targetPath, - watch() { - server?.invalidate(); - }, + additionalEntryPoints: detectedModulesEntryPoint, }); const compiler = webpack(config); diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 43b2328239..67cdd80531 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -26,6 +26,7 @@ export type BundlingOptions = { frontendAppConfigs: AppConfig[]; baseUrl: URL; parallelism?: number; + additionalEntryPoints?: string[]; }; export type ServeOptions = BundlingPathsOptions & {