Merge pull request #9258 from backstage/rugvip/role-build

cli: add experimental repo-wide build based on package roles
This commit is contained in:
Patrik Oldsberg
2022-02-07 17:08:53 +01:00
committed by GitHub
15 changed files with 509 additions and 154 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
The experimental types build enabled by `--experimental-type-build` now runs in a separate worker thread.
+5
View File
@@ -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.
+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),
});
}
+20
View File
@@ -25,6 +25,25 @@ const configOption = [
Array<string>(),
] 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 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)));
}
export function registerScriptCommand(program: CommanderStatic) {
const command = program
.command('script [command]', { hidden: true })
@@ -312,6 +331,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);
+165
View File
@@ -0,0 +1,165 @@
/*
* 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 { 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';
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;
}
// 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;
}
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<string, any> = {};
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<void> {
const packages = await PackageGraph.listTargetPackages();
const bundledPackages = new Array<ExtendedPackage>();
const parseBuildScript = createScriptOptionsParser(cmd, ['script', 'build']);
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 outputs = getOutputsForRole(role);
if (outputs.size === 0) {
if (getRoleInfo(role).output.includes('bundle')) {
bundledPackages.push(pkg);
} else {
console.warn(
`Ignored ${pkg.packageJson.name} because it has no output`,
);
}
return [];
}
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 [];
}
return {
targetDir: pkg.dir,
outputs,
logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `,
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 = 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),
});
}),
);
console.log('Building backends');
const backends = bundledPackages.filter(
pkg => pkg.packageJson.backstage?.role === 'backend',
);
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,
});
}),
);
}
}
@@ -16,146 +16,127 @@
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() {
const { Extractor, ExtractorConfig } = prepareApiExtractor();
const distTypesPackageDir = paths.resolveTargetRoot(
'dist-types',
relativePath(paths.targetRoot, paths.targetDir),
export async function buildTypeDefinitions(
targetDirs: string[] = [paths.targetDir],
) {
const packageDirs = targetDirs.map(dir =>
relativePath(paths.targetRoot, dir),
);
const entryPoint = resolvePath(distTypesPackageDir, '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) {
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`,
);
}
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;
}),
);
const extractorConfig = ExtractorConfig.prepare({
configObject: {
mainEntryPointFilePath: entryPoint,
bundledPackages: [],
const workerConfigs = packageDirs.map(packageDir => {
const targetDir = paths.resolveTargetRoot(packageDir);
const targetTypesDir = paths.resolveTargetRoot('dist-types', packageDir);
const extractorOptions = {
configObject: {
mainEntryPointFilePath: resolvePath(targetTypesDir, 'src/index.d.ts'),
bundledPackages: [],
compiler: {
skipLibCheck: true,
tsconfigFilePath: paths.resolveTargetRoot('tsconfig.json'),
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,
},
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'),
},
newlineKind: 'lf',
projectFolder: paths.targetDir,
},
configObjectFullPath: paths.targetDir,
packageJsonFullPath: paths.resolveTarget('package.json'),
configObjectFullPath: targetDir,
packageJsonFullPath: resolvePath(targetDir, 'package.json'),
};
return { extractorOptions, targetTypesDir };
});
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;
}
const typescriptCompilerFolder = hasTypescript ? typescriptDir : undefined;
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}`;
}
}
}
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);
}
const worker = new Worker(`(${buildTypeDefinitionsWorker})()`, {
eval: true,
workerData: {
entryPoints,
workerConfigs,
typescriptCompilerFolder,
},
});
if (!extractorResult.succeeded) {
throw new Error(
`Type definition build completed with ${extractorResult.errorCount} errors` +
` and ${extractorResult.warningCount} warnings`,
);
}
await new Promise<void>((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 (ignoredMessages.has(message.messageId)) {
return;
}
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);
}
}
});
});
}
@@ -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' });
}
}
+24 -8
View File
@@ -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';
@@ -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';
@@ -37,6 +37,19 @@ export async function makeRollupConfigs(
options: BuildOptions,
): Promise<RollupOptions[]> {
const configs = new Array<RollupOptions>();
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');
if (options.outputs.has(Output.cjs) || options.outputs.has(Output.esm)) {
const output = new Array<OutputOptions>();
@@ -44,7 +57,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 +66,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 +77,14 @@ export async function makeRollupConfigs(
}
configs.push({
input: 'src/index.ts',
input: resolvePath(targetDir, 'src/index.ts'),
output,
onwarn,
preserveEntrySignatures: 'strict',
external: require('module').builtinModules,
plugins: [
peerDepsExternal({
packageJsonPath: resolvePath(targetDir, 'package.json'),
includeDependencies: true,
}),
resolve({ mainFields }),
@@ -109,13 +124,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,9 +141,10 @@ export async function makeRollupConfigs(
configs.push({
input: typesInput,
output: {
file: 'dist/index.d.ts',
file: resolvePath(distDir, 'index.d.ts'),
format: 'es',
},
onwarn,
plugins: [dts()],
});
}
+1 -1
View File
@@ -14,6 +14,6 @@
* limitations under the License.
*/
export { buildPackage } from './packager';
export { buildPackage, buildPackages, getOutputsForRole } from './packager';
export { Output } from './types';
export type { BuildOptions } from './types';
+46 -1
View File
@@ -17,11 +17,12 @@
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';
import { buildTypeDefinitions } from './buildTypeDefinitions';
import { getRoleInfo } from '../role';
export function formatErrorMessage(error: any) {
let msg = '';
@@ -116,3 +117,47 @@ export const buildPackage = async (options: BuildOptions) => {
await Promise.all(buildTasks);
};
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'))),
);
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);
};
export function getOutputsForRole(role: string): Set<Output> {
const outputs = new Set<Output>();
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;
}
+2
View File
@@ -21,6 +21,8 @@ export enum Output {
}
export type BuildOptions = {
logPrefix?: string;
targetDir?: string;
outputs: Set<Output>;
minify?: boolean;
useApiExtractor?: boolean;
+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;