cli: add experimental type builds

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-01-14 19:39:06 +01:00
parent 71b09c83be
commit 093e5e06e4
9 changed files with 195 additions and 10 deletions
+8
View File
@@ -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"
},
@@ -21,5 +21,6 @@ export default async (cmd: Command) => {
await buildPackage({
outputs: new Set([Output.cjs, Output.types]),
minify: cmd.minify,
useApiExtractor: cmd.experimentalTypeBuild,
});
};
+5 -1
View File
@@ -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,
});
};
+3
View File
@@ -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 <formats>', '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
@@ -21,5 +21,6 @@ export default async (cmd: Command) => {
await buildPackage({
outputs: new Set([Output.esm, Output.types]),
minify: cmd.minify,
useApiExtractor: cmd.experimentalTypeBuild,
});
};
@@ -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`,
);
}
}
+4 -4
View File
@@ -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<RollupOptions[]> => {
): Promise<RollupOptions[]> {
const configs = new Array<RollupOptions>();
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;
};
}
+14 -5
View File
@@ -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);
};
+1
View File
@@ -23,4 +23,5 @@ export enum Output {
export type BuildOptions = {
outputs: Set<Output>;
minify?: boolean;
useApiExtractor?: boolean;
};