cli: add option to include bundled packages in repo build + parse options

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-02-05 18:47:21 +01:00
parent d59b90852a
commit 5b54608615
7 changed files with 138 additions and 28 deletions
+7 -4
View File
@@ -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,
})),
});
@@ -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,
@@ -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<void> {
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),
});
}
+5 -1
View File
@@ -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)));
}
+101 -6
View File
@@ -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<void> {
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<string, any> = {};
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<void> {
const packages = await PackageGraph.listTargetPackages();
const bundledPackages = new Array<ExtendedPackage>();
const options = packages.flatMap(pkg => {
const role = pkg.packageJson.backstage?.role;
@@ -32,7 +84,13 @@ export async function command(): Promise<void> {
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<void> {
);
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<void> {
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,
});
}),
);
}
}
+16 -13
View File
@@ -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,
};
+2
View File
@@ -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;