Merge pull request #9258 from backstage/rugvip/role-build
cli: add experimental repo-wide build based on package roles
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
The experimental types build enabled by `--experimental-type-build` now runs in a separate worker thread.
|
||||
@@ -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.
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
@@ -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()],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ export enum Output {
|
||||
}
|
||||
|
||||
export type BuildOptions = {
|
||||
logPrefix?: string;
|
||||
targetDir?: string;
|
||||
outputs: Set<Output>;
|
||||
minify?: boolean;
|
||||
useApiExtractor?: boolean;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user