diff --git a/packages/cli/src/commands/build/buildApp.ts b/packages/cli/src/commands/build/buildApp.ts index 3d86d796b7..62d51a0810 100644 --- a/packages/cli/src/commands/build/buildApp.ts +++ b/packages/cli/src/commands/build/buildApp.ts @@ -15,24 +15,27 @@ */ import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; import { buildBundle } from '../../lib/bundler'; import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; import { loadCliConfig } from '../../lib/config'; -import { paths } from '../../lib/paths'; interface BuildAppOptions { + targetDir: string; writeStats: boolean; configPaths: string[]; } export async function buildApp(options: BuildAppOptions) { - const { name } = await fs.readJson(paths.resolveTarget('package.json')); + const { targetDir, writeStats, configPaths } = options; + const { name } = await fs.readJson(resolvePath(targetDir, 'package.json')); await buildBundle({ + targetDir, entry: 'src/index', parallel: parseParallel(process.env[PARALLEL_ENV_VAR]), - statsJsonEnabled: options.writeStats, + statsJsonEnabled: writeStats, ...(await loadCliConfig({ - args: options.configPaths, + args: configPaths, fromPackage: name, })), }); diff --git a/packages/cli/src/commands/build/buildBackend.ts b/packages/cli/src/commands/build/buildBackend.ts index a4d8858cf8..490cc413a4 100644 --- a/packages/cli/src/commands/build/buildBackend.ts +++ b/packages/cli/src/commands/build/buildBackend.ts @@ -19,7 +19,6 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import tar, { CreateOptions } from 'tar'; import { createDistWorkspace } from '../../lib/packager'; -import { paths } from '../../lib/paths'; import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; import { buildPackage, Output } from '../../lib/builder'; @@ -27,12 +26,13 @@ const BUNDLE_FILE = 'bundle.tar.gz'; const SKELETON_FILE = 'skeleton.tar.gz'; interface BuildBackendOptions { + targetDir: string; skipBuildDependencies: boolean; } export async function buildBackend(options: BuildBackendOptions) { - const targetDir = paths.resolveTarget('dist'); - const pkg = await fs.readJson(paths.resolveTarget('package.json')); + const { targetDir, skipBuildDependencies } = options; + const pkg = await fs.readJson(resolvePath(targetDir, 'package.json')); // We build the target package without generating type declarations. await buildPackage({ outputs: new Set([Output.cjs]) }); @@ -41,7 +41,7 @@ export async function buildBackend(options: BuildBackendOptions) { try { await createDistWorkspace([pkg.name], { targetDir: tmpDir, - buildDependencies: !options.skipBuildDependencies, + buildDependencies: !skipBuildDependencies, buildExcludes: [pkg.name], parallel: parseParallel(process.env[PARALLEL_ENV_VAR]), skeleton: SKELETON_FILE, diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index 8c13b515d7..1189883b12 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -17,6 +17,7 @@ import { Command } from 'commander'; import { buildPackage, Output } from '../../lib/builder'; import { findRoleFromCommand, getRoleInfo } from '../../lib/role'; +import { paths } from '../../lib/paths'; import { buildApp } from './buildApp'; import { buildBackend } from './buildBackend'; @@ -25,12 +26,14 @@ export async function command(cmd: Command): Promise { if (role === 'app') { return buildApp({ + targetDir: paths.resolveTarget('dist'), configPaths: cmd.config as string[], writeStats: Boolean(cmd.stats), }); } if (role === 'backend') { return buildBackend({ + targetDir: paths.resolveTarget('dist'), skipBuildDependencies: Boolean(cmd.skipBuildDependencies), }); } diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index b7de9ced14..2211af1dda 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -35,7 +35,11 @@ export function registerRepoCommand(program: CommanderStatic) { command .command('build') .description( - 'Build all packages in the project that use the standard backstage build script', + 'Build packages in the project, excluding bundled app and backend packages.', + ) + .option( + '--all', + 'Build all packages, including bundled app and backend packages.', ) .action(lazy(() => import('./repo/build').then(m => m.command))); } diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index 7b1e4771ec..84dbe10fa3 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -15,13 +15,65 @@ */ import chalk from 'chalk'; +import { Command } from 'commander'; import { relative as relativePath } from 'path'; import { buildPackages, getOutputsForRole } from '../../lib/builder'; import { PackageGraph } from '../../lib/monorepo'; +import { ExtendedPackage } from '../../lib/monorepo/PackageGraph'; import { paths } from '../../lib/paths'; +import { getRoleInfo } from '../../lib/role'; +import { buildApp } from '../build/buildApp'; +import { buildBackend } from '../build/buildBackend'; -export async function command(): Promise { +function parseScriptOptions( + cmd: Command, + scriptCommandName: string, + args: string[], +) { + let rootCommand = cmd; + while (rootCommand.parent) { + rootCommand = rootCommand.parent; + } + const scriptCommand = rootCommand.commands.find(c => c.name() === 'script')!; + const targetCommand = scriptCommand.commands.find( + c => c.name() === scriptCommandName, + ); + if (!targetCommand) { + throw new Error(`Could not find script command '${scriptCommandName}'`); + } + + const currentOpts = targetCommand._optionValues; + const currentStore = targetCommand._storeOptionsAsProperties; + + const result: Record = {}; + targetCommand._storeOptionsAsProperties = false; + targetCommand._optionValues = result; + + targetCommand.parseOptions(args); + + targetCommand._storeOptionsAsProperties = currentOpts; + targetCommand._optionValues = currentStore; + + return result; +} + +function parseBackstageScript( + cmd: Command, + expectedScript: string, + scriptStr?: string, +) { + const expectedPrefix = `backstage-cli script ${expectedScript}`; + if (!scriptStr || !scriptStr.startsWith(expectedPrefix)) { + return undefined; + } + + const argsStr = scriptStr.slice(expectedPrefix.length).trim(); + return parseScriptOptions(cmd, expectedScript, argsStr.split(' ')); +} + +export async function command(cmd: Command): Promise { const packages = await PackageGraph.listTargetPackages(); + const bundledPackages = new Array(); const options = packages.flatMap(pkg => { const role = pkg.packageJson.backstage?.role; @@ -32,7 +84,13 @@ export async function command(): Promise { const outputs = getOutputsForRole(role); if (outputs.size === 0) { - console.warn(`Ignored ${pkg.packageJson.name} because it has no output`); + if (getRoleInfo(role).output.includes('bundle')) { + bundledPackages.push(pkg); + } else { + console.warn( + `Ignored ${pkg.packageJson.name} because it has no output`, + ); + } return []; } @@ -43,7 +101,9 @@ export async function command(): Promise { ); return []; } - if (!buildScript.startsWith('backstage-cli script build')) { + + const buildOptions = parseBackstageScript(cmd, 'build', buildScript); + if (!buildOptions) { console.warn( `Ignored ${pkg.packageJson.name} because it has a custom build script, '${buildScript}'`, ); @@ -54,11 +114,46 @@ export async function command(): Promise { targetDir: pkg.dir, outputs, logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `, - // TODO(Rugvip): Use commander to parse the script and grab these instead - minify: buildScript.includes('--minify'), - useApiExtractor: buildScript.includes('--experimental-type-build'), + minify: buildOptions.minify, + useApiExtractor: buildOptions.experimentalTypeBuild, }; }); + console.log('Building packages'); await buildPackages(options); + + if (cmd.all) { + const apps = bundledPackages.filter( + pkg => pkg.packageJson.backstage?.role === 'app', + ); + + console.log('Building apps'); + await Promise.all( + apps.map(async pkg => { + const buildOptions = parseBackstageScript( + cmd, + 'build', + pkg.packageJson.scripts?.build, + ); + await buildApp({ + targetDir: pkg.dir, + configPaths: (buildOptions?.config as string[]) ?? [], + writeStats: Boolean(buildOptions?.stats), + }); + }), + ); + + console.log('Building backends'); + const backends = bundledPackages.filter( + pkg => pkg.packageJson.backstage?.role === 'backend', + ); + await Promise.all( + backends.map(async pkg => { + await buildBackend({ + targetDir: pkg.dir, + skipBuildDependencies: true, + }); + }), + ); + } } diff --git a/packages/cli/src/lib/bundler/paths.ts b/packages/cli/src/lib/bundler/paths.ts index d465b3010f..3d4925c66b 100644 --- a/packages/cli/src/lib/bundler/paths.ts +++ b/packages/cli/src/lib/bundler/paths.ts @@ -15,55 +15,58 @@ */ import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; import { paths } from '../paths'; export type BundlingPathsOptions = { // bundle entrypoint, e.g. 'src/index' entry: string; + // Target directory, defaulting to paths.targetDir + targetDir?: string; }; export function resolveBundlingPaths(options: BundlingPathsOptions) { - const { entry } = options; + const { entry, targetDir = paths.targetDir } = options; const resolveTargetModule = (pathString: string) => { for (const ext of ['mjs', 'js', 'ts', 'tsx', 'jsx']) { - const filePath = paths.resolveTarget(`${pathString}.${ext}`); + const filePath = resolvePath(targetDir, `${pathString}.${ext}`); if (fs.pathExistsSync(filePath)) { return filePath; } } - return paths.resolveTarget(`${pathString}.js`); + return resolvePath(targetDir, `${pathString}.js`); }; let targetPublic = undefined; - let targetHtml = paths.resolveTarget('public/index.html'); + let targetHtml = resolvePath(targetDir, 'public/index.html'); // Prefer public folder if (fs.pathExistsSync(targetHtml)) { - targetPublic = paths.resolveTarget('public'); + targetPublic = resolvePath(targetDir, 'public'); } else { - targetHtml = paths.resolveTarget(`${entry}.html`); + targetHtml = resolvePath(targetDir, `${entry}.html`); if (!fs.pathExistsSync(targetHtml)) { targetHtml = paths.resolveOwn('templates/serve_index.html'); } } // Backend plugin dev run file - const targetRunFile = paths.resolveTarget('src/run.ts'); + const targetRunFile = resolvePath(targetDir, 'src/run.ts'); const runFileExists = fs.pathExistsSync(targetRunFile); return { targetHtml, targetPublic, - targetPath: paths.resolveTarget('.'), + targetPath: resolvePath(targetDir, '.'), targetRunFile: runFileExists ? targetRunFile : undefined, - targetDist: paths.resolveTarget('dist'), - targetAssets: paths.resolveTarget('assets'), - targetSrc: paths.resolveTarget('src'), - targetDev: paths.resolveTarget('dev'), + targetDist: resolvePath(targetDir, 'dist'), + targetAssets: resolvePath(targetDir, 'assets'), + targetSrc: resolvePath(targetDir, 'src'), + targetDev: resolvePath(targetDir, 'dev'), targetEntry: resolveTargetModule(entry), targetTsConfig: paths.resolveTargetRoot('tsconfig.json'), - targetPackageJson: paths.resolveTarget('package.json'), + targetPackageJson: resolvePath(targetDir, 'package.json'), rootNodeModules: paths.resolveTargetRoot('node_modules'), root: paths.targetRoot, }; diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index eae0452d8d..14e60c8892 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -35,6 +35,8 @@ export type ServeOptions = BundlingPathsOptions & { }; export type BuildOptions = BundlingPathsOptions & { + // Target directory, defaulting to paths.targetDir + targetDir?: string; statsJsonEnabled: boolean; parallel?: ParallelOption; schema?: ConfigSchema;