cli: refactor package detection to support inc/ecl + entry point

Co-authored-by: Philipp Hugenroth <philipph@spotify.com>
Co-authored-by: Johan Haals <johan.haals@gmail.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-09-05 15:34:58 +02:00
parent 1e2cf52e54
commit d2fb430784
5 changed files with 107 additions and 70 deletions
+7 -6
View File
@@ -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 });
+1 -1
View File
@@ -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'],
+88 -54
View File
@@ -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<string[]> {
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];
}
+10 -9
View File
@@ -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);
+1
View File
@@ -26,6 +26,7 @@ export type BundlingOptions = {
frontendAppConfigs: AppConfig[];
baseUrl: URL;
parallelism?: number;
additionalEntryPoints?: string[];
};
export type ServeOptions = BundlingPathsOptions & {