From 093e5e06e43bd15cd12b57e7ce8134b56bcd8e7c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 14 Jan 2022 19:39:06 +0100 Subject: [PATCH] cli: add experimental type builds Signed-off-by: Patrik Oldsberg --- packages/cli/package.json | 8 + packages/cli/src/commands/backend/build.ts | 1 + packages/cli/src/commands/build.ts | 6 +- packages/cli/src/commands/index.ts | 3 + packages/cli/src/commands/plugin/build.ts | 1 + .../src/lib/builder/buildTypeDefinitions.ts | 158 ++++++++++++++++++ packages/cli/src/lib/builder/config.ts | 8 +- packages/cli/src/lib/builder/packager.ts | 19 ++- packages/cli/src/lib/builder/types.ts | 1 + 9 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 packages/cli/src/lib/builder/buildTypeDefinitions.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index edb2cfb3ce..be7a170daf 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -144,6 +144,14 @@ "nodemon": "^2.0.2", "ts-node": "^10.0.0" }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.19.2" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + } + }, "resolutions": { "@types/webpack-dev-server/@types/webpack": "^5.28.0" }, diff --git a/packages/cli/src/commands/backend/build.ts b/packages/cli/src/commands/backend/build.ts index 8c25f56746..5d4e17e392 100644 --- a/packages/cli/src/commands/backend/build.ts +++ b/packages/cli/src/commands/backend/build.ts @@ -21,5 +21,6 @@ export default async (cmd: Command) => { await buildPackage({ outputs: new Set([Output.cjs, Output.types]), minify: cmd.minify, + useApiExtractor: cmd.experimentalTypeBuild, }); }; diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index d312811396..86ab5c4300 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -33,5 +33,9 @@ export default async (cmd: Command) => { outputs = new Set([Output.types, Output.esm, Output.cjs]); } - await buildPackage({ outputs, minify: cmd.minify }); + await buildPackage({ + outputs, + minify: cmd.minify, + useApiExtractor: cmd.experimentalTypeBuild, + }); }; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index dfe427b27b..12fce66bd4 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -48,6 +48,7 @@ export function registerCommands(program: CommanderStatic) { .command('backend:build') .description('Build a backend plugin') .option('--minify', 'Minify the generated code') + .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./backend/build').then(m => m.default))); program @@ -118,6 +119,7 @@ export function registerCommands(program: CommanderStatic) { .command('plugin:build') .description('Build a plugin') .option('--minify', 'Minify the generated code') + .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./plugin/build').then(m => m.default))); program @@ -139,6 +141,7 @@ export function registerCommands(program: CommanderStatic) { .description('Build a package for publishing') .option('--outputs ', 'List of formats to output [types,cjs,esm]') .option('--minify', 'Minify the generated code') + .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./build').then(m => m.default))); program diff --git a/packages/cli/src/commands/plugin/build.ts b/packages/cli/src/commands/plugin/build.ts index 9c5e173989..56f99f59b1 100644 --- a/packages/cli/src/commands/plugin/build.ts +++ b/packages/cli/src/commands/plugin/build.ts @@ -21,5 +21,6 @@ export default async (cmd: Command) => { await buildPackage({ outputs: new Set([Output.esm, Output.types]), minify: cmd.minify, + useApiExtractor: cmd.experimentalTypeBuild, }); }; diff --git a/packages/cli/src/lib/builder/buildTypeDefinitions.ts b/packages/cli/src/lib/builder/buildTypeDefinitions.ts new file mode 100644 index 0000000000..b4ca87b825 --- /dev/null +++ b/packages/cli/src/lib/builder/buildTypeDefinitions.ts @@ -0,0 +1,158 @@ +/* + * 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 fs from 'fs-extra'; +import chalk from 'chalk'; +import { + relative as relativePath, + resolve as resolvePath, + dirname, +} from 'path'; +import { paths } from '../paths'; + +// 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), + ); + 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`, + ); + } + + const extractorConfig = ExtractorConfig.prepare({ + configObject: { + mainEntryPointFilePath: entryPoint, + bundledPackages: [], + + compiler: { + tsconfigFilePath: paths.resolveTargetRoot('tsconfig.json'), + }, + + dtsRollup: { + enabled: true, + untrimmedFilePath: paths.resolveTarget('dist/index.d.ts'), + }, + + newlineKind: 'lf', + + 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}`; + } + } + } + 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`, + ); + } +} diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index ba60e8fea9..617e354864 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -33,9 +33,9 @@ import { BuildOptions, Output } from './types'; import { paths } from '../paths'; import { svgrTemplate } from '../svgrTemplate'; -export const makeConfigs = async ( +export async function makeRollupConfigs( options: BuildOptions, -): Promise => { +): Promise { const configs = new Array(); if (options.outputs.has(Output.cjs) || options.outputs.has(Output.esm)) { @@ -106,7 +106,7 @@ export const makeConfigs = async ( }); } - if (options.outputs.has(Output.types)) { + if (options.outputs.has(Output.types) && !options.useApiExtractor) { const typesInput = paths.resolveTargetRoot( 'dist-types', relativePath(paths.targetRoot, paths.targetDir), @@ -134,4 +134,4 @@ export const makeConfigs = async ( } return configs; -}; +} diff --git a/packages/cli/src/lib/builder/packager.ts b/packages/cli/src/lib/builder/packager.ts index 3e0f1b9a46..96c488fce2 100644 --- a/packages/cli/src/lib/builder/packager.ts +++ b/packages/cli/src/lib/builder/packager.ts @@ -19,8 +19,9 @@ import { rollup, RollupOptions } from 'rollup'; import chalk from 'chalk'; import { relative as relativePath } from 'path'; import { paths } from '../paths'; -import { makeConfigs } from './config'; -import { BuildOptions } from './types'; +import { makeRollupConfigs } from './config'; +import { BuildOptions, Output } from './types'; +import { buildTypeDefinitions } from './buildTypeDefinitions'; export function formatErrorMessage(error: any) { let msg = ''; @@ -71,7 +72,7 @@ export function formatErrorMessage(error: any) { return msg; } -async function build(config: RollupOptions) { +async function rollupBuild(config: RollupOptions) { try { const bundle = await rollup(config); if (config.output) { @@ -103,7 +104,15 @@ export const buildPackage = async (options: BuildOptions) => { /* Errors ignored, this is just a warning */ } - const configs = await makeConfigs(options); + const rollupConfigs = await makeRollupConfigs(options); + await fs.remove(paths.resolveTarget('dist')); - await Promise.all(configs.map(build)); + + const buildTasks = rollupConfigs.map(rollupBuild); + + if (options.outputs.has(Output.types) && options.useApiExtractor) { + buildTasks.push(buildTypeDefinitions()); + } + + await Promise.all(buildTasks); }; diff --git a/packages/cli/src/lib/builder/types.ts b/packages/cli/src/lib/builder/types.ts index e88c388013..a02afef01a 100644 --- a/packages/cli/src/lib/builder/types.ts +++ b/packages/cli/src/lib/builder/types.ts @@ -23,4 +23,5 @@ export enum Output { export type BuildOptions = { outputs: Set; minify?: boolean; + useApiExtractor?: boolean; };