From b2db40b700a6da3ca812cb6789b72f8b86f48c50 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 29 Jan 2022 01:23:44 +0100 Subject: [PATCH 01/11] cli: add support for specifying target dir in build options Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/builder/config.ts | 18 +++++++++++------- packages/cli/src/lib/builder/types.ts | 1 + 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 617e354864..65c4d6c740 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -16,7 +16,7 @@ import chalk from 'chalk'; import fs from 'fs-extra'; -import { relative as relativePath } from 'path'; +import { relative as relativePath, resolve as resolvePath } from 'path'; import peerDepsExternal from 'rollup-plugin-peer-deps-external'; import commonjs from '@rollup/plugin-commonjs'; import resolve from '@rollup/plugin-node-resolve'; @@ -37,6 +37,9 @@ export async function makeRollupConfigs( options: BuildOptions, ): Promise { const configs = new Array(); + const targetDir = options.targetDir ?? paths.targetDir; + + const distDir = resolvePath(targetDir, 'dist'); if (options.outputs.has(Output.cjs) || options.outputs.has(Output.esm)) { const output = new Array(); @@ -44,7 +47,7 @@ export async function makeRollupConfigs( if (options.outputs.has(Output.cjs)) { output.push({ - dir: 'dist', + dir: distDir, entryFileNames: 'index.cjs.js', chunkFileNames: 'cjs/[name]-[hash].cjs.js', format: 'commonjs', @@ -53,7 +56,7 @@ export async function makeRollupConfigs( } if (options.outputs.has(Output.esm)) { output.push({ - dir: 'dist', + dir: distDir, entryFileNames: 'index.esm.js', chunkFileNames: 'esm/[name]-[hash].esm.js', format: 'module', @@ -64,12 +67,13 @@ export async function makeRollupConfigs( } configs.push({ - input: 'src/index.ts', + input: resolvePath(targetDir, 'src/index.ts'), output, preserveEntrySignatures: 'strict', external: require('module').builtinModules, plugins: [ peerDepsExternal({ + packageJsonPath: resolvePath(targetDir, 'package.json'), includeDependencies: true, }), resolve({ mainFields }), @@ -109,13 +113,13 @@ export async function makeRollupConfigs( if (options.outputs.has(Output.types) && !options.useApiExtractor) { const typesInput = paths.resolveTargetRoot( 'dist-types', - relativePath(paths.targetRoot, paths.targetDir), + relativePath(paths.targetRoot, targetDir), 'src/index.d.ts', ); const declarationsExist = await fs.pathExists(typesInput); if (!declarationsExist) { - const path = relativePath(paths.targetDir, typesInput); + const path = relativePath(targetDir, typesInput); throw new Error( `No declaration files found at ${path}, be sure to run ${chalk.bgRed.white( 'yarn tsc', @@ -126,7 +130,7 @@ export async function makeRollupConfigs( configs.push({ input: typesInput, output: { - file: 'dist/index.d.ts', + file: resolvePath(distDir, 'index.d.ts'), format: 'es', }, plugins: [dts()], diff --git a/packages/cli/src/lib/builder/types.ts b/packages/cli/src/lib/builder/types.ts index a02afef01a..c9233c024e 100644 --- a/packages/cli/src/lib/builder/types.ts +++ b/packages/cli/src/lib/builder/types.ts @@ -21,6 +21,7 @@ export enum Output { } export type BuildOptions = { + targetDir?: string; outputs: Set; minify?: boolean; useApiExtractor?: boolean; From ee4931047470df16e9fa407d48c864caaf729fac Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 29 Jan 2022 01:55:02 +0100 Subject: [PATCH 02/11] cli: custom warning handler for rollup + support for log prefix Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/builder/config.ts | 14 +++++++++++++- packages/cli/src/lib/builder/types.ts | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 65c4d6c740..0a214f1a7a 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -26,7 +26,7 @@ import svgr from '@svgr/rollup'; import dts from 'rollup-plugin-dts'; import json from '@rollup/plugin-json'; import yaml from '@rollup/plugin-yaml'; -import { RollupOptions, OutputOptions } from 'rollup'; +import { RollupOptions, OutputOptions, RollupWarning } from 'rollup'; import { forwardFileImports } from './plugins'; import { BuildOptions, Output } from './types'; @@ -38,6 +38,16 @@ export async function makeRollupConfigs( ): Promise { const configs = new Array(); const targetDir = options.targetDir ?? paths.targetDir; + const onwarn = ({ code, message }: RollupWarning) => { + if (code === 'EMPTY_BUNDLE') { + return; // We don't care about this one + } + if (options.logPrefix) { + console.log(options.logPrefix + message); + } else { + console.log(message); + } + }; const distDir = resolvePath(targetDir, 'dist'); @@ -69,6 +79,7 @@ export async function makeRollupConfigs( configs.push({ input: resolvePath(targetDir, 'src/index.ts'), output, + onwarn, preserveEntrySignatures: 'strict', external: require('module').builtinModules, plugins: [ @@ -133,6 +144,7 @@ export async function makeRollupConfigs( file: resolvePath(distDir, 'index.d.ts'), format: 'es', }, + onwarn, plugins: [dts()], }); } diff --git a/packages/cli/src/lib/builder/types.ts b/packages/cli/src/lib/builder/types.ts index c9233c024e..f43d84cd92 100644 --- a/packages/cli/src/lib/builder/types.ts +++ b/packages/cli/src/lib/builder/types.ts @@ -21,6 +21,7 @@ export enum Output { } export type BuildOptions = { + logPrefix?: string; targetDir?: string; outputs: Set; minify?: boolean; From 4917c71d00e0049dd8ed75cb676cec1ee6eefd18 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 29 Jan 2022 11:10:33 +0100 Subject: [PATCH 03/11] cli: allow optimized type definition builds across packages Signed-off-by: Patrik Oldsberg --- .../src/lib/builder/buildTypeDefinitions.ts | 165 ++++++++++-------- 1 file changed, 91 insertions(+), 74 deletions(-) diff --git a/packages/cli/src/lib/builder/buildTypeDefinitions.ts b/packages/cli/src/lib/builder/buildTypeDefinitions.ts index ba67532bdb..6822e22ccc 100644 --- a/packages/cli/src/lib/builder/buildTypeDefinitions.ts +++ b/packages/cli/src/lib/builder/buildTypeDefinitions.ts @@ -71,91 +71,108 @@ function prepareApiExtractor() { return apiExtractor!; } -export async function buildTypeDefinitions() { - const { Extractor, ExtractorConfig } = prepareApiExtractor(); +export async function buildTypeDefinitions( + targetDirs: string[] = [paths.targetDir], +) { + const { Extractor, ExtractorConfig, CompilerState } = prepareApiExtractor(); - const distTypesPackageDir = paths.resolveTargetRoot( - 'dist-types', - relativePath(paths.targetRoot, paths.targetDir), + const packageDirs = targetDirs.map(dir => + relativePath(paths.targetRoot, dir), + ); + const entryPoints = packageDirs.map(dir => + paths.resolveTargetRoot('dist-types', dir, 'src/index.d.ts'), ); - const entryPoint = resolvePath(distTypesPackageDir, 'src/index.d.ts'); - const declarationsExist = await fs.pathExists(entryPoint); - if (!declarationsExist) { - const path = relativePath(paths.targetDir, entryPoint); - throw new Error( - `No declaration files found at ${path}, be sure to run ${chalk.bgRed.white( - 'yarn tsc', - )} to generate .d.ts files before packaging`, - ); - } + let compilerState; - const extractorConfig = ExtractorConfig.prepare({ - configObject: { - mainEntryPointFilePath: entryPoint, - bundledPackages: [], + for (const packageDir of packageDirs) { + const targetDir = paths.resolveTargetRoot(packageDir); + const targetTypesDir = paths.resolveTargetRoot('dist-types', packageDir); + const entryPoint = resolvePath(targetTypesDir, 'src/index.d.ts'); - compiler: { - skipLibCheck: true, - tsconfigFilePath: paths.resolveTargetRoot('tsconfig.json'), + const declarationsExist = await fs.pathExists(entryPoint); + if (!declarationsExist) { + throw new Error( + `No declaration files found at ${entryPoint}, be sure to run ${chalk.bgRed.white( + 'yarn tsc', + )} to generate .d.ts files before packaging`, + ); + } + + const extractorConfig = ExtractorConfig.prepare({ + configObject: { + mainEntryPointFilePath: entryPoint, + bundledPackages: [], + + compiler: { + skipLibCheck: true, + tsconfigFilePath: paths.resolveTargetRoot('tsconfig.json'), + }, + + dtsRollup: { + enabled: true, + untrimmedFilePath: resolvePath(targetDir, 'dist/index.alpha.d.ts'), + betaTrimmedFilePath: resolvePath(targetDir, 'dist/index.beta.d.ts'), + publicTrimmedFilePath: resolvePath(targetDir, 'dist/index.d.ts'), + }, + + newlineKind: 'lf', + + projectFolder: targetDir, }, + configObjectFullPath: targetDir, + packageJsonFullPath: resolvePath(targetDir, 'package.json'), + }); - dtsRollup: { - enabled: true, - untrimmedFilePath: paths.resolveTarget('dist/index.alpha.d.ts'), - betaTrimmedFilePath: paths.resolveTarget('dist/index.beta.d.ts'), - publicTrimmedFilePath: paths.resolveTarget('dist/index.d.ts'), - }, + if (!compilerState) { + compilerState = CompilerState.create(extractorConfig, { + additionalEntryPoints: entryPoints, + }); + } - newlineKind: 'lf', + const typescriptDir = paths.resolveTargetRoot('node_modules/typescript'); + const hasTypescript = await fs.pathExists(typescriptDir); + const extractorResult = Extractor.invoke(extractorConfig, { + typescriptCompilerFolder: hasTypescript ? typescriptDir : undefined, + compilerState, + localBuild: false, + showVerboseMessages: false, + showDiagnostics: false, + messageCallback(message) { + message.handled = true; + if (ignoredMessages.has(message.messageId)) { + return; + } - projectFolder: paths.targetDir, - }, - configObjectFullPath: paths.targetDir, - packageJsonFullPath: paths.resolveTarget('package.json'), - }); - - const typescriptDir = paths.resolveTargetRoot('node_modules/typescript'); - const hasTypescript = await fs.pathExists(typescriptDir); - const extractorResult = Extractor.invoke(extractorConfig, { - typescriptCompilerFolder: hasTypescript ? typescriptDir : undefined, - localBuild: false, - showVerboseMessages: false, - showDiagnostics: false, - messageCallback(message) { - message.handled = true; - if (ignoredMessages.has(message.messageId)) { - return; - } - - let text = `${message.text} (${message.messageId})`; - if (message.sourceFilePath) { - text += ' at '; - text += relativePath(distTypesPackageDir, message.sourceFilePath); - if (message.sourceFileLine) { - text += `:${message.sourceFileLine}`; - if (message.sourceFileColumn) { - text += `:${message.sourceFileColumn}`; + let text = `${message.text} (${message.messageId})`; + if (message.sourceFilePath) { + text += ' at '; + text += relativePath(targetTypesDir, message.sourceFilePath); + if (message.sourceFileLine) { + text += `:${message.sourceFileLine}`; + if (message.sourceFileColumn) { + text += `:${message.sourceFileColumn}`; + } } } - } - if (message.logLevel === 'error') { - console.error(chalk.red(`Error: ${text}`)); - } else if ( - message.logLevel === 'warning' || - message.category === 'Extractor' - ) { - console.warn(`Warning: ${text}`); - } else { - console.log(text); - } - }, - }); + if (message.logLevel === 'error') { + console.error(chalk.red(`Error: ${text}`)); + } else if ( + message.logLevel === 'warning' || + message.category === 'Extractor' + ) { + console.warn(`Warning: ${text}`); + } else { + console.log(text); + } + }, + }); - if (!extractorResult.succeeded) { - throw new Error( - `Type definition build completed with ${extractorResult.errorCount} errors` + - ` and ${extractorResult.warningCount} warnings`, - ); + if (!extractorResult.succeeded) { + throw new Error( + `Type definition build completed with ${extractorResult.errorCount} errors` + + ` and ${extractorResult.warningCount} warnings`, + ); + } } } From 03b39bbcc3c467bd28ee39ea6b787dca5d66fde0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 29 Jan 2022 12:20:27 +0100 Subject: [PATCH 04/11] cli: run API Extractor type builds in worker thread Signed-off-by: Patrik Oldsberg --- .../src/lib/builder/buildTypeDefinitions.ts | 152 +++++++----------- .../lib/builder/buildTypeDefinitionsWorker.ts | 105 ++++++++++++ 2 files changed, 163 insertions(+), 94 deletions(-) create mode 100644 packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts diff --git a/packages/cli/src/lib/builder/buildTypeDefinitions.ts b/packages/cli/src/lib/builder/buildTypeDefinitions.ts index 6822e22ccc..85e54850e4 100644 --- a/packages/cli/src/lib/builder/buildTypeDefinitions.ts +++ b/packages/cli/src/lib/builder/buildTypeDefinitions.ts @@ -16,92 +16,47 @@ import fs from 'fs-extra'; import chalk from 'chalk'; -import { - relative as relativePath, - resolve as resolvePath, - dirname, -} from 'path'; +import { Worker } from 'worker_threads'; +import { relative as relativePath, resolve as resolvePath } from 'path'; import { paths } from '../paths'; +import { buildTypeDefinitionsWorker } from './buildTypeDefinitionsWorker'; // These message types are ignored since we want to avoid duplicating the logic of // handling them correctly, and we already have the API Reports warning about them. const ignoredMessages = new Set(['tsdoc-undefined-tag', 'ae-forgotten-export']); -let apiExtractor: undefined | typeof import('@microsoft/api-extractor'); -function prepareApiExtractor() { - if (apiExtractor) { - return apiExtractor; - } - - try { - apiExtractor = require('@microsoft/api-extractor'); - } catch (error) { - throw new Error( - 'Failed to resolve @microsoft/api-extractor, it must best installed ' + - 'as a dependency of your project in order to use experimental type builds', - ); - } - - /** - * All of this monkey patching below is because MUI has these bare package.json file as a method - * for making TypeScript accept imports like `@material-ui/core/Button`, and improve tree-shaking - * by declaring them side effect free. - * - * The package.json lookup logic in api-extractor really doesn't like that though, as it enforces - * that the 'name' field exists in all package.json files that it discovers. This below is just - * making sure that we ignore those file package.json files instead of crashing. - */ - const { - PackageJsonLookup, - // eslint-disable-next-line import/no-extraneous-dependencies - } = require('@rushstack/node-core-library/lib/PackageJsonLookup'); - - const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor; - PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor = - function tryGetPackageJsonFilePathForPatch(path: string) { - if ( - path.includes('@material-ui') && - !dirname(path).endsWith('@material-ui') - ) { - return undefined; - } - return old.call(this, path); - }; - - return apiExtractor!; -} - export async function buildTypeDefinitions( targetDirs: string[] = [paths.targetDir], ) { - const { Extractor, ExtractorConfig, CompilerState } = prepareApiExtractor(); - const packageDirs = targetDirs.map(dir => relativePath(paths.targetRoot, dir), ); - const entryPoints = packageDirs.map(dir => - paths.resolveTargetRoot('dist-types', dir, 'src/index.d.ts'), + const entryPoints = await Promise.all( + packageDirs.map(async dir => { + const entryPoint = paths.resolveTargetRoot( + 'dist-types', + dir, + 'src/index.d.ts', + ); + + const declarationsExist = await fs.pathExists(entryPoint); + if (!declarationsExist) { + throw new Error( + `No declaration files found at ${entryPoint}, be sure to run ${chalk.bgRed.white( + 'yarn tsc', + )} to generate .d.ts files before packaging`, + ); + } + return entryPoint; + }), ); - let compilerState; - - for (const packageDir of packageDirs) { + const workerConfigs = packageDirs.map(packageDir => { const targetDir = paths.resolveTargetRoot(packageDir); const targetTypesDir = paths.resolveTargetRoot('dist-types', packageDir); - const entryPoint = resolvePath(targetTypesDir, 'src/index.d.ts'); - - const declarationsExist = await fs.pathExists(entryPoint); - if (!declarationsExist) { - throw new Error( - `No declaration files found at ${entryPoint}, be sure to run ${chalk.bgRed.white( - 'yarn tsc', - )} to generate .d.ts files before packaging`, - ); - } - - const extractorConfig = ExtractorConfig.prepare({ + const extractorOptions = { configObject: { - mainEntryPointFilePath: entryPoint, + mainEntryPointFilePath: resolvePath(targetTypesDir, 'src/index.d.ts'), bundledPackages: [], compiler: { @@ -122,24 +77,40 @@ export async function buildTypeDefinitions( }, configObjectFullPath: targetDir, packageJsonFullPath: resolvePath(targetDir, 'package.json'), + }; + return { extractorOptions, targetTypesDir }; + }); + + const typescriptDir = paths.resolveTargetRoot('node_modules/typescript'); + const hasTypescript = await fs.pathExists(typescriptDir); + const typescriptCompilerFolder = hasTypescript ? typescriptDir : undefined; + + const worker = new Worker(`(${buildTypeDefinitionsWorker})()`, { + eval: true, + workerData: { + entryPoints, + workerConfigs, + typescriptCompilerFolder, + }, + }); + + await new Promise((resolve, reject) => { + worker.once('error', reject); + worker.once('exit', code => { + if (code) { + reject(new Error(`Worker exited with code ${code}`)); + } }); + worker.on('message', data => { + if (data.type === 'done') { + if (data.error) { + reject(data.error); + } else { + resolve(); + } + } else if (data.type === 'message') { + const { message, targetTypesDir } = data; - if (!compilerState) { - compilerState = CompilerState.create(extractorConfig, { - additionalEntryPoints: entryPoints, - }); - } - - const typescriptDir = paths.resolveTargetRoot('node_modules/typescript'); - const hasTypescript = await fs.pathExists(typescriptDir); - const extractorResult = Extractor.invoke(extractorConfig, { - typescriptCompilerFolder: hasTypescript ? typescriptDir : undefined, - compilerState, - localBuild: false, - showVerboseMessages: false, - showDiagnostics: false, - messageCallback(message) { - message.handled = true; if (ignoredMessages.has(message.messageId)) { return; } @@ -165,14 +136,7 @@ export async function buildTypeDefinitions( } else { console.log(text); } - }, + } }); - - if (!extractorResult.succeeded) { - throw new Error( - `Type definition build completed with ${extractorResult.errorCount} errors` + - ` and ${extractorResult.warningCount} warnings`, - ); - } - } + }); } diff --git a/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts new file mode 100644 index 0000000000..8cb843f58e --- /dev/null +++ b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts @@ -0,0 +1,105 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * NOTE: This is a worker thread function that is stringified and executed + * withing a `worker_threads.Worker`. Everything in this function must + * be self-contained. + * Using TypeScript is fine as it is transpiled before being stringified. + */ +export function buildTypeDefinitionsWorker() { + try { + require('@microsoft/api-extractor'); + } catch (error) { + throw new Error( + 'Failed to resolve @microsoft/api-extractor, it must best installed ' + + 'as a dependency of your project in order to use experimental type builds', + ); + } + + const { dirname } = require('path'); + const { workerData, parentPort } = require('worker_threads'); + const { entryPoints, workerConfigs, typescriptCompilerFolder } = workerData; + + const apiExtractor = require('@microsoft/api-extractor'); + const { Extractor, ExtractorConfig, CompilerState } = apiExtractor; + + /** + * All of this monkey patching below is because MUI has these bare package.json file as a method + * for making TypeScript accept imports like `@material-ui/core/Button`, and improve tree-shaking + * by declaring them side effect free. + * + * The package.json lookup logic in api-extractor really doesn't like that though, as it enforces + * that the 'name' field exists in all package.json files that it discovers. This below is just + * making sure that we ignore those file package.json files instead of crashing. + */ + const { + PackageJsonLookup, + // eslint-disable-next-line import/no-extraneous-dependencies + } = require('@rushstack/node-core-library/lib/PackageJsonLookup'); + + const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor; + PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor = + function tryGetPackageJsonFilePathForPatch(path: string) { + if ( + path.includes('@material-ui') && + !dirname(path).endsWith('@material-ui') + ) { + return undefined; + } + return old.call(this, path); + }; + + let success = true; + let compilerState; + for (const { extractorOptions, targetTypesDir } of workerConfigs) { + const extractorConfig = ExtractorConfig.prepare(extractorOptions); + + if (!compilerState) { + compilerState = CompilerState.create(extractorConfig, { + additionalEntryPoints: entryPoints, + }); + } + + const extractorResult = Extractor.invoke(extractorConfig, { + compilerState, + localBuild: false, + typescriptCompilerFolder, + showVerboseMessages: false, + showDiagnostics: false, + messageCallback: (message: any) => { + message.handled = true; + parentPort.postMessage({ type: 'message', message, targetTypesDir }); + }, + }); + + if (!extractorResult.succeeded) { + parentPort.postMessage({ + type: 'done', + error: new Error( + `Type definition build completed with ${extractorResult.errorCount} errors` + + ` and ${extractorResult.warningCount} warnings`, + ), + }); + success = false; + break; + } + } + + if (success) { + parentPort.postMessage({ type: 'done' }); + } +} From fe571d2b0652e541819d3ff6c9eef84c088fb234 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 29 Jan 2022 12:31:16 +0100 Subject: [PATCH 05/11] cli: add buildPackages for building multiple packages at once Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/builder/index.ts | 2 +- packages/cli/src/lib/builder/packager.ts | 27 +++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/builder/index.ts b/packages/cli/src/lib/builder/index.ts index c39d964ade..de6ccac0ae 100644 --- a/packages/cli/src/lib/builder/index.ts +++ b/packages/cli/src/lib/builder/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export { buildPackage } from './packager'; +export { buildPackage, buildPackages } from './packager'; export { Output } from './types'; export type { BuildOptions } from './types'; diff --git a/packages/cli/src/lib/builder/packager.ts b/packages/cli/src/lib/builder/packager.ts index 96c488fce2..9ef1954677 100644 --- a/packages/cli/src/lib/builder/packager.ts +++ b/packages/cli/src/lib/builder/packager.ts @@ -17,7 +17,7 @@ import fs from 'fs-extra'; import { rollup, RollupOptions } from 'rollup'; import chalk from 'chalk'; -import { relative as relativePath } from 'path'; +import { relative as relativePath, resolve as resolvePath } from 'path'; import { paths } from '../paths'; import { makeRollupConfigs } from './config'; import { BuildOptions, Output } from './types'; @@ -116,3 +116,28 @@ export const buildPackage = async (options: BuildOptions) => { await Promise.all(buildTasks); }; + +export const buildPackages = async ( + options: (BuildOptions & { targetDir: string })[], +) => { + const rollupConfigs = await Promise.all(options.map(makeRollupConfigs)); + + await Promise.all( + options.map(({ targetDir }) => fs.remove(resolvePath(targetDir, 'dist'))), + ); + + const buildTasks = rollupConfigs.flat().map(rollupBuild); + + const typeDefinitionTargetDirs = options + .filter( + ({ outputs, useApiExtractor }) => + outputs.has(Output.types) && useApiExtractor, + ) + .map(_ => _.targetDir); + + if (typeDefinitionTargetDirs.length > 0) { + buildTasks.push(buildTypeDefinitions(typeDefinitionTargetDirs)); + } + + await Promise.all(buildTasks); +}; From 336b3101519c32428e73599ec4f41f91fe8ef31a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 29 Jan 2022 12:43:51 +0100 Subject: [PATCH 06/11] cli: add initial experimental repo sub-command with build Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 16 ++++++ packages/cli/src/commands/repo/build.ts | 75 +++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 packages/cli/src/commands/repo/build.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index a33e7d4afe..b7de9ced14 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -25,6 +25,21 @@ const configOption = [ Array(), ] as const; +export function registerRepoCommand(program: CommanderStatic) { + const command = program + .command('repo [command]', { hidden: true }) + .description( + 'Command that run across an entire Backstage project [EXPERIMENTAL]', + ); + + command + .command('build') + .description( + 'Build all packages in the project that use the standard backstage build script', + ) + .action(lazy(() => import('./repo/build').then(m => m.command))); +} + export function registerScriptCommand(program: CommanderStatic) { const command = program .command('script [command]', { hidden: true }) @@ -312,6 +327,7 @@ export function registerCommands(program: CommanderStatic) { .description('Print configuration schema') .action(lazy(() => import('./config/schema').then(m => m.default))); + registerRepoCommand(program); registerScriptCommand(program); registerMigrateCommand(program); diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts new file mode 100644 index 0000000000..50198772d7 --- /dev/null +++ b/packages/cli/src/commands/repo/build.ts @@ -0,0 +1,75 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import chalk from 'chalk'; +import { relative as relativePath } from 'path'; +import { buildPackages, Output } from '../../lib/builder'; +import { PackageGraph } from '../../lib/monorepo'; +import { paths } from '../../lib/paths'; +import { getRoleInfo } from '../../lib/role'; + +const outputMap = { + esm: Output.esm, + cjs: Output.cjs, + types: Output.types, + bundle: undefined, +}; + +export async function command(): Promise { + const packages = await PackageGraph.listTargetPackages(); + + const options = packages.flatMap(pkg => { + const role = pkg.packageJson.backstage?.role; + if (!role) { + console.warn(`Ignored ${pkg.packageJson.name} because it has no role`); + return []; + } + + const roleInfo = getRoleInfo(role); + const outputs = roleInfo.output + .map(output => outputMap[output]) + .filter((x): x is Output => Boolean(x)); + if (outputs.length === 0) { + console.warn(`Ignored ${pkg.packageJson.name} because it has no output`); + return []; + } + + const buildScript = pkg.packageJson.scripts?.build; + if (!buildScript) { + console.warn( + `Ignored ${pkg.packageJson.name} because it has no build script`, + ); + return []; + } + if (!buildScript.startsWith('backstage-cli script build')) { + console.warn( + `Ignored ${pkg.packageJson.name} because it has a custom build script, '${buildScript}'`, + ); + return []; + } + + return { + targetDir: pkg.dir, + outputs: new Set(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'), + }; + }); + + await buildPackages(options); +} From f8e529030eeb788ff814b0fe21635e30de932dbf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 29 Jan 2022 18:55:31 +0100 Subject: [PATCH 07/11] cli: added getOutputsForRole utility Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/repo/build.ts | 19 ++++--------------- packages/cli/src/lib/builder/index.ts | 2 +- packages/cli/src/lib/builder/packager.ts | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index 50198772d7..7b1e4771ec 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -16,17 +16,9 @@ import chalk from 'chalk'; import { relative as relativePath } from 'path'; -import { buildPackages, Output } from '../../lib/builder'; +import { buildPackages, getOutputsForRole } from '../../lib/builder'; import { PackageGraph } from '../../lib/monorepo'; import { paths } from '../../lib/paths'; -import { getRoleInfo } from '../../lib/role'; - -const outputMap = { - esm: Output.esm, - cjs: Output.cjs, - types: Output.types, - bundle: undefined, -}; export async function command(): Promise { const packages = await PackageGraph.listTargetPackages(); @@ -38,11 +30,8 @@ export async function command(): Promise { return []; } - const roleInfo = getRoleInfo(role); - const outputs = roleInfo.output - .map(output => outputMap[output]) - .filter((x): x is Output => Boolean(x)); - if (outputs.length === 0) { + const outputs = getOutputsForRole(role); + if (outputs.size === 0) { console.warn(`Ignored ${pkg.packageJson.name} because it has no output`); return []; } @@ -63,7 +52,7 @@ export async function command(): Promise { return { targetDir: pkg.dir, - outputs: new Set(outputs), + 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'), diff --git a/packages/cli/src/lib/builder/index.ts b/packages/cli/src/lib/builder/index.ts index de6ccac0ae..00cc463cfb 100644 --- a/packages/cli/src/lib/builder/index.ts +++ b/packages/cli/src/lib/builder/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export { buildPackage, buildPackages } from './packager'; +export { buildPackage, buildPackages, getOutputsForRole } from './packager'; export { Output } from './types'; export type { BuildOptions } from './types'; diff --git a/packages/cli/src/lib/builder/packager.ts b/packages/cli/src/lib/builder/packager.ts index 9ef1954677..c4963373fa 100644 --- a/packages/cli/src/lib/builder/packager.ts +++ b/packages/cli/src/lib/builder/packager.ts @@ -22,6 +22,7 @@ import { paths } from '../paths'; import { makeRollupConfigs } from './config'; import { BuildOptions, Output } from './types'; import { buildTypeDefinitions } from './buildTypeDefinitions'; +import { getRoleInfo } from '../role'; export function formatErrorMessage(error: any) { let msg = ''; @@ -141,3 +142,21 @@ export const buildPackages = async ( await Promise.all(buildTasks); }; + +export function getOutputsForRole(role: string): Set { + const outputs = new Set(); + + for (const output of getRoleInfo(role).output) { + if (output === 'cjs') { + outputs.add(Output.cjs); + } + if (output === 'esm') { + outputs.add(Output.esm); + } + if (output === 'types') { + outputs.add(Output.types); + } + } + + return outputs; +} From d20f260e6dcb2eba8b3c23f63b16665d97995193 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 29 Jan 2022 18:57:16 +0100 Subject: [PATCH 08/11] cli: tweak buildPackages to use standard BuildOptions Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/builder/packager.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/lib/builder/packager.ts b/packages/cli/src/lib/builder/packager.ts index c4963373fa..2413662a17 100644 --- a/packages/cli/src/lib/builder/packager.ts +++ b/packages/cli/src/lib/builder/packager.ts @@ -118,13 +118,14 @@ export const buildPackage = async (options: BuildOptions) => { await Promise.all(buildTasks); }; -export const buildPackages = async ( - options: (BuildOptions & { targetDir: string })[], -) => { +export const buildPackages = async (options: BuildOptions[]) => { + if (options.some(opt => !opt.targetDir)) { + throw new Error('targetDir must be set for all build options'); + } const rollupConfigs = await Promise.all(options.map(makeRollupConfigs)); await Promise.all( - options.map(({ targetDir }) => fs.remove(resolvePath(targetDir, 'dist'))), + options.map(({ targetDir }) => fs.remove(resolvePath(targetDir!, 'dist'))), ); const buildTasks = rollupConfigs.flat().map(rollupBuild); @@ -134,7 +135,7 @@ export const buildPackages = async ( ({ outputs, useApiExtractor }) => outputs.has(Output.types) && useApiExtractor, ) - .map(_ => _.targetDir); + .map(_ => _.targetDir!); if (typeDefinitionTargetDirs.length > 0) { buildTasks.push(buildTypeDefinitions(typeDefinitionTargetDirs)); From d59b90852a6d5893bd8bf4f00046329e2a2e9d93 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 30 Jan 2022 19:24:10 +0100 Subject: [PATCH 09/11] changesets: added changesets for CLI repo command and type worker thread Signed-off-by: Patrik Oldsberg --- .changeset/many-terms-type.md | 5 +++++ .changeset/twenty-colts-applaud.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/many-terms-type.md create mode 100644 .changeset/twenty-colts-applaud.md diff --git a/.changeset/many-terms-type.md b/.changeset/many-terms-type.md new file mode 100644 index 0000000000..fd11d14106 --- /dev/null +++ b/.changeset/many-terms-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The experimental types build enabled by `--experimental-type-build` now runs in a separate worker thread. diff --git a/.changeset/twenty-colts-applaud.md b/.changeset/twenty-colts-applaud.md new file mode 100644 index 0000000000..2390e21a6e --- /dev/null +++ b/.changeset/twenty-colts-applaud.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Introduced an experimental and hidden `repo` sub-command, that contains commands that operate on an entire monorepo rather than individual packages. From 5b54608615a2ba012fccd5110506b84eba30a358 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Feb 2022 18:47:21 +0100 Subject: [PATCH 10/11] cli: add option to include bundled packages in repo build + parse options Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/build/buildApp.ts | 11 +- .../cli/src/commands/build/buildBackend.ts | 8 +- packages/cli/src/commands/build/command.ts | 3 + packages/cli/src/commands/index.ts | 6 +- packages/cli/src/commands/repo/build.ts | 107 +++++++++++++++++- packages/cli/src/lib/bundler/paths.ts | 29 ++--- packages/cli/src/lib/bundler/types.ts | 2 + 7 files changed, 138 insertions(+), 28 deletions(-) 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; From 9b0f6458b84d8d61cc5cd4a0a58cdc2f47f8268a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Feb 2022 23:51:31 +0100 Subject: [PATCH 11/11] cli: refactored build script parser Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/repo/build.ts | 120 +++++++++++++----------- 1 file changed, 63 insertions(+), 57 deletions(-) diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index 84dbe10fa3..e8058887c5 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -25,56 +25,61 @@ import { getRoleInfo } from '../../lib/role'; import { buildApp } from '../build/buildApp'; import { buildBackend } from '../build/buildBackend'; -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}'`); +function createScriptOptionsParser(anyCmd: Command, commandPath: string[]) { + // Regardless of what command instance is passed in we want to find + // the root command and resolve the path from there + let rootCmd = anyCmd; + while (rootCmd.parent) { + rootCmd = rootCmd.parent; } - 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; + // Now find the command that was requested + let targetCmd = rootCmd as Command | undefined; + for (const name of commandPath) { + targetCmd = targetCmd?.commands.find(c => c.name() === name) as + | Command + | undefined; } - const argsStr = scriptStr.slice(expectedPrefix.length).trim(); - return parseScriptOptions(cmd, expectedScript, argsStr.split(' ')); + if (!targetCmd) { + throw new Error(`Could not find script command '${commandPath.join(' ')}'`); + } + const cmd = targetCmd; + + const expectedScript = `backstage-cli ${commandPath.join(' ')}`; + + return (scriptStr?: string) => { + if (!scriptStr || !scriptStr.startsWith(expectedScript)) { + return undefined; + } + + const argsStr = scriptStr.slice(expectedScript.length).trim(); + + // Can't clone or copy or even use commands as prototype, so we mutate + // the necessary members instead, and then reset them once we're done + const currentOpts = cmd._optionValues; + const currentStore = cmd._storeOptionsAsProperties; + + const result: Record = {}; + cmd._storeOptionsAsProperties = false; + cmd._optionValues = result; + + // Triggers the writing of options to the result object + cmd.parseOptions(argsStr.split(' ')); + + cmd._storeOptionsAsProperties = currentOpts; + cmd._optionValues = currentStore; + + return result; + }; } export async function command(cmd: Command): Promise { const packages = await PackageGraph.listTargetPackages(); const bundledPackages = new Array(); + const parseBuildScript = createScriptOptionsParser(cmd, ['script', 'build']); + const options = packages.flatMap(pkg => { const role = pkg.packageJson.backstage?.role; if (!role) { @@ -94,18 +99,10 @@ export async function command(cmd: Command): Promise { return []; } - const buildScript = pkg.packageJson.scripts?.build; - if (!buildScript) { - console.warn( - `Ignored ${pkg.packageJson.name} because it has no build script`, - ); - return []; - } - - const buildOptions = parseBackstageScript(cmd, 'build', buildScript); + const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build); if (!buildOptions) { console.warn( - `Ignored ${pkg.packageJson.name} because it has a custom build script, '${buildScript}'`, + `Ignored ${pkg.packageJson.name} because it does not have a matching build script`, ); return []; } @@ -130,15 +127,17 @@ export async function command(cmd: Command): Promise { console.log('Building apps'); await Promise.all( apps.map(async pkg => { - const buildOptions = parseBackstageScript( - cmd, - 'build', - pkg.packageJson.scripts?.build, - ); + const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build); + if (!buildOptions) { + console.warn( + `Ignored ${pkg.packageJson.name} because it does not have a matching build script`, + ); + return; + } await buildApp({ targetDir: pkg.dir, - configPaths: (buildOptions?.config as string[]) ?? [], - writeStats: Boolean(buildOptions?.stats), + configPaths: (buildOptions.config as string[]) ?? [], + writeStats: Boolean(buildOptions.stats), }); }), ); @@ -149,6 +148,13 @@ export async function command(cmd: Command): Promise { ); await Promise.all( backends.map(async pkg => { + const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build); + if (!buildOptions) { + console.warn( + `Ignored ${pkg.packageJson.name} because it does not have a matching build script`, + ); + return; + } await buildBackend({ targetDir: pkg.dir, skipBuildDependencies: true,