From de0eb04000911e807a8a58372dea8264c131a1d2 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Thu, 10 Oct 2024 19:00:44 +0200 Subject: [PATCH] feat(workspace-plugin): add `generateApi` functionality/flag to build executor (#32976) --- tools/workspace-plugin/STYLE-GUIDE.md | 18 +++++-- .../libs/proj/config/api-extractor.json | 37 ++++++++++++++ .../executor/libs/proj/etc/api.md | 12 +++++ .../executor/libs/proj/package.json | 3 ++ .../executor/libs/proj/tsconfig.json | 15 ++++++ .../executor/libs/proj/tsconfig.lib.json | 8 ++++ .../src/executors/build/executor.spec.ts | 48 ++++++++++++++++++- .../src/executors/build/executor.ts | 27 ++++++----- .../src/executors/build/lib/babel.ts | 6 +-- .../src/executors/build/lib/clean.ts | 2 +- .../src/executors/build/lib/shared.ts | 30 +++++++++++- .../src/executors/build/schema.d.ts | 7 ++- .../src/executors/build/schema.json | 7 ++- 13 files changed, 193 insertions(+), 27 deletions(-) create mode 100644 tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/config/api-extractor.json create mode 100644 tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/etc/api.md create mode 100644 tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/package.json create mode 100644 tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.json create mode 100644 tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.lib.json diff --git a/tools/workspace-plugin/STYLE-GUIDE.md b/tools/workspace-plugin/STYLE-GUIDE.md index 8598c72ee3bf9..d4217d75790e5 100644 --- a/tools/workspace-plugin/STYLE-GUIDE.md +++ b/tools/workspace-plugin/STYLE-GUIDE.md @@ -2,7 +2,7 @@ ## Generators -Generators should live in the `tools/generators` folder. [Learn more about Nx generators](https://nx.dev/generators/workspace-generators). +Generators live in the `tools/workspace-plugin/src/generators` folder. [Learn more about Nx generators](https://nx.dev/generators/workspace-generators). ### Scaffolding @@ -52,7 +52,7 @@ Integration tests for the generator as a whole. TypeScript interface that matches `schema.json`. You can generate this from the json file by running: -- `npx json-schema-to-typescript -i tools/generators//schema.json -o tools/generators//schema.ts --additionalProperties false` +- `npx json-schema-to-typescript@latest -i tools/workspace-plugin/src/generators//schema.json -o tools/workspace-plugin/src/generators//schema.d.ts --additionalProperties false` **`schema.json`** @@ -95,12 +95,20 @@ Migrations follow same rules as [Generators](#Generators) as they behave the sam ## Executors -Executors should live in the `tools/executors` folder. [Learn more about Nx executors](https://nx.dev/executors/using-builders). +Executors live in the `tools/workspace-plugin/src/executors` folder. [Learn more about Nx executors](https://nx.dev/executors/using-builders). ### Scaffolding -TBA +**`schema.d.ts`** + +TypeScript interface that matches `schema.json`. You can generate this from the json file by running: + +- `npx json-schema-to-typescript@latest -i tools/workspace-plugin/src/executors//schema.json -o tools/workspace-plugin/src/executors//schema.d.ts --additionalProperties false` + +**`schema.json`** + +Provides a description of the generator, available options, validation information, and default values. This is processed by nx cli when invoking generator to provide argument validations/processing/prompts. ### Bootstrap new executor -TBA +`yarn nx g @nx/plugin:executor --directory tools/workspace-plugin/src/executors/` diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/config/api-extractor.json b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/config/api-extractor.json new file mode 100644 index 0000000000000..e5e4ce7ca1c1a --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/config/api-extractor.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "/dist/out-tsc/index.d.ts", + "docModel": { + "enabled": false + }, + "apiReport": { + "enabled": true, + "reportFileName": "api.md" + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.ts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "ae-forgotten-export": { + "logLevel": "none", + "addToApiReportFile": false + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-unresolved-link": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none", + "addToApiReportFile": false + } + } + }, + "newlineKind": "os" +} diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/etc/api.md b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/etc/api.md new file mode 100644 index 0000000000000..590206ebeb54e --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/etc/api.md @@ -0,0 +1,12 @@ +## API Report File for "proj" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export function greeter(greeting: string, user: User): string; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/package.json b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/package.json new file mode 100644 index 0000000000000..77820f1e6c312 --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/package.json @@ -0,0 +1,3 @@ +{ + "name": "proj" +} diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.json b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.json new file mode 100644 index 0000000000000..25052e8384499 --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "moduleResolution": "Node", + "target": "ES2019", + "skipLibCheck": true, + "pretty": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.lib.json b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.lib.json new file mode 100644 index 0000000000000..0dfe54218565f --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.lib.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist/out-tsc", + "declaration": true + }, + "include": ["src/*.ts"] +} diff --git a/tools/workspace-plugin/src/executors/build/executor.spec.ts b/tools/workspace-plugin/src/executors/build/executor.spec.ts index 9d7bec167a544..3ec0d4e028d01 100644 --- a/tools/workspace-plugin/src/executors/build/executor.spec.ts +++ b/tools/workspace-plugin/src/executors/build/executor.spec.ts @@ -30,6 +30,7 @@ const options: BuildExecutorSchema = { glob: '*.md', }, ], + generateApi: true, clean: true, }; @@ -73,6 +74,15 @@ const measureEndMock = measureEnd as jest.Mock; describe('Build Executor', () => { it('can run', async () => { + // mute api extractor - START + jest.spyOn(console, 'warn').mockImplementation(() => { + return; + }); + jest.spyOn(console, 'log').mockImplementation(() => { + return; + }); + // mute api extractor - END + const loggerLogSpy = jest.spyOn(logger, 'log').mockImplementation(() => { return; }); @@ -122,12 +132,14 @@ describe('Build Executor', () => { }, ]); - expect(measureStartMock).toHaveBeenCalledTimes(1); - expect(measureEndMock).toHaveBeenCalledTimes(1); + expect(measureStartMock).toHaveBeenCalledTimes(2); + expect(measureEndMock).toHaveBeenCalledTimes(2); // ===================== // assert build Assets // ===================== + expect(existsSync(join(workspaceRoot, 'libs/proj/etc', 'api.md'))).toBe(true); + expect(existsSync(join(workspaceRoot, 'libs/proj/dist', 'index.d.ts'))).toBe(true); expect(existsSync(join(workspaceRoot, 'libs/proj/dist/assets', 'spec.md'))).toBe(true); expect(readdirSync(join(workspaceRoot, 'libs/proj/lib'))).toEqual([ 'greeter.js', @@ -146,6 +158,38 @@ describe('Build Executor', () => { 'index.js.map', ]); + // ==================================== + // assert generateAPI output based on settings + // ==================================== + expect(readFileSync(join(workspaceRoot, 'libs/proj/dist/index.d.ts'), 'utf-8')).toMatchInlineSnapshot(` + "export declare function greeter(greeting: string, user: User): string; + + declare type User = { + name: string; + hometown?: { + name: string; + }; + }; + + export { } + " + `); + expect(readFileSync(join(workspaceRoot, 'libs/proj/etc/api.md'), 'utf-8')).toMatchInlineSnapshot(` + "## API Report File for \\"proj\\" + + > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + + \`\`\`ts + + // @public (undocumented) + export function greeter(greeting: string, user: User): string; + + // (No @packageDocumentation comment for this package) + + \`\`\` + " + `); + // ==================================== // assert swc output based on settings // ==================================== diff --git a/tools/workspace-plugin/src/executors/build/executor.ts b/tools/workspace-plugin/src/executors/build/executor.ts index 3d67df9bba485..668a93c3300dc 100644 --- a/tools/workspace-plugin/src/executors/build/executor.ts +++ b/tools/workspace-plugin/src/executors/build/executor.ts @@ -4,9 +4,10 @@ import { compileSwc } from './lib/swc'; import { compileWithGriffelStylesAOT, hasStylesFilesToProcess } from './lib/babel'; import { assetGlobsToFiles, copyAssets } from './lib/assets'; import { cleanOutput } from './lib/clean'; -import { NormalizedOptions, normalizeOptions, processAsyncQueue } from './lib/shared'; +import { NormalizedOptions, normalizeOptions, processAsyncQueue, runInParallel, runSerially } from './lib/shared'; import { measureEnd, measureStart } from '../../utils'; +import generateApiExecutor from '../generate-api/executor'; import { type BuildExecutorSchema } from './schema'; @@ -14,8 +15,17 @@ const runExecutor: PromiseExecutor = async (schema, context measureStart('BuildExecutor'); const options = normalizeOptions(schema, context); + const assetFiles = assetGlobsToFiles(options.assets ?? [], context.root, options.outputPathRoot); - const success = await runBuild(options, context); + const success = await runSerially( + () => cleanOutput(options, assetFiles), + () => + runInParallel( + () => runBuild(options, context), + () => (options.generateApi ? generateApiExecutor({}, context).then(res => res.success) : Promise.resolve(true)), + ), + () => copyAssets(assetFiles), + ); measureEnd('BuildExecutor'); @@ -26,21 +36,14 @@ export default runExecutor; // =========== -async function runBuild(options: NormalizedOptions, context: ExecutorContext): Promise { - const assetFiles = assetGlobsToFiles(options.assets ?? [], context.root, options.outputPathRoot); - - const cleanResult = await cleanOutput(options, assetFiles); - if (!cleanResult) { - return false; - } - +async function runBuild(options: NormalizedOptions, _context: ExecutorContext): Promise { if (hasStylesFilesToProcess(options)) { - return compileWithGriffelStylesAOT(options, () => copyAssets(assetFiles)); + return compileWithGriffelStylesAOT(options); } const compilationQueue = options.moduleOutput.map(outputConfig => { return compileSwc(outputConfig, options); }); - return processAsyncQueue(compilationQueue, () => copyAssets(assetFiles)); + return processAsyncQueue(compilationQueue); } diff --git a/tools/workspace-plugin/src/executors/build/lib/babel.ts b/tools/workspace-plugin/src/executors/build/lib/babel.ts index 314a87ea01a51..04576101348ab 100644 --- a/tools/workspace-plugin/src/executors/build/lib/babel.ts +++ b/tools/workspace-plugin/src/executors/build/lib/babel.ts @@ -25,7 +25,7 @@ export function hasStylesFilesToProcess(normalizedOptions: NormalizedOptions) { return files.length > 0; } -export async function compileWithGriffelStylesAOT(options: NormalizedOptions, successCallback: () => Promise) { +export async function compileWithGriffelStylesAOT(options: NormalizedOptions) { const { esmConfig, restOfConfigs } = options.moduleOutput.reduce<{ esmConfig: NormalizedOptions['moduleOutput'][number] | null; restOfConfigs: NormalizedOptions['moduleOutput']; @@ -47,7 +47,7 @@ export async function compileWithGriffelStylesAOT(options: NormalizedOptions, su const compilationQueue = restOfConfigs.map(outputConfig => { return compileSwc(outputConfig, options); }); - return processAsyncQueue(compilationQueue, successCallback); + return processAsyncQueue(compilationQueue); } await compileSwc(esmConfig, options); @@ -65,7 +65,7 @@ export async function compileWithGriffelStylesAOT(options: NormalizedOptions, su }); }); - return processAsyncQueue(compilationQueue, successCallback); + return processAsyncQueue(compilationQueue); } async function babel(esmModuleOutput: NormalizedOptions['moduleOutput'][number], normalizedOptions: NormalizedOptions) { diff --git a/tools/workspace-plugin/src/executors/build/lib/clean.ts b/tools/workspace-plugin/src/executors/build/lib/clean.ts index ada3d8d001f6d..281ed3387743e 100644 --- a/tools/workspace-plugin/src/executors/build/lib/clean.ts +++ b/tools/workspace-plugin/src/executors/build/lib/clean.ts @@ -6,7 +6,7 @@ import { type NormalizedOptions, processAsyncQueue } from './shared'; export async function cleanOutput(options: NormalizedOptions, assetFiles: Array<{ input: string; output: string }>) { if (!options.clean) { - return; + return true; } const swcOutputPaths = options.moduleOutput.map(outputConfig => { diff --git a/tools/workspace-plugin/src/executors/build/lib/shared.ts b/tools/workspace-plugin/src/executors/build/lib/shared.ts index 1b329fbc18517..8d6af2b6f2724 100644 --- a/tools/workspace-plugin/src/executors/build/lib/shared.ts +++ b/tools/workspace-plugin/src/executors/build/lib/shared.ts @@ -3,10 +3,35 @@ import { join } from 'node:path'; import { type BuildExecutorSchema } from '../schema'; -export async function processAsyncQueue(value: Promise[], successCallback?: () => Promise) { +type Tasks = () => Promise; + +export async function runInParallel(...tasks: Tasks[]): Promise { + const processes = tasks.map(task => task()); + + return Promise.all(processes) + .then(() => { + return true; + }) + .catch(err => { + logger.error(err); + return false; + }); +} + +export async function runSerially(...tasks: Tasks[]): Promise { + for (const task of tasks) { + const result = await task(); + if (!result) { + return false; + } + } + return true; +} + +export async function processAsyncQueue(value: Promise[]): Promise { return Promise.all(value) .then(() => { - return successCallback ? successCallback() : true; + return true; }) .catch(err => { logger.error(err); @@ -17,6 +42,7 @@ export async function processAsyncQueue(value: Promise[], successCallba export interface NormalizedOptions extends ReturnType {} export function normalizeOptions(schema: BuildExecutorSchema, context: ExecutorContext) { const defaults = { + generateApi: true, clean: true, }; const project = context.projectsConfigurations!.projects[context.projectName!]; diff --git a/tools/workspace-plugin/src/executors/build/schema.d.ts b/tools/workspace-plugin/src/executors/build/schema.d.ts index dcfcdd834be62..1c099ab6cef01 100644 --- a/tools/workspace-plugin/src/executors/build/schema.d.ts +++ b/tools/workspace-plugin/src/executors/build/schema.d.ts @@ -1,3 +1,4 @@ +/* eslint-disable */ /** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, @@ -5,7 +6,7 @@ */ /** - * Builds TS files with SWC to specified JS module type, transforms *.styles.* files via Babel(Griffel) and copies provided Assets. + * Transpiles TS files with SWC to specified JS module type, Generates rolluped .d.ts + api.md, transforms *.styles.* files via Babel(Griffel) and copies provided Assets. */ export interface BuildExecutorSchema { /** @@ -29,6 +30,10 @@ export interface BuildExecutorSchema { */ outputPath: string; }[]; + /** + * Generate rolluped 'd.ts' bundle including 'api.md' that provides project public API + */ + generateApi?: boolean; /** * List of static assets. */ diff --git a/tools/workspace-plugin/src/executors/build/schema.json b/tools/workspace-plugin/src/executors/build/schema.json index 71b736d27b252..bf4373b83626b 100644 --- a/tools/workspace-plugin/src/executors/build/schema.json +++ b/tools/workspace-plugin/src/executors/build/schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/schema", "version": 2, "title": "Build executor", - "description": "Builds TS files with SWC to specified JS module type, transforms *.styles.* files via Babel(Griffel) and copies provided Assets.", + "description": "Transpiles TS files with SWC to specified JS module type, Generates rolluped .d.ts + api.md, transforms *.styles.* files via Babel(Griffel) and copies provided Assets.", "type": "object", "properties": { "sourceRoot": { @@ -28,6 +28,11 @@ "required": ["module", "outputPath"] } }, + "generateApi": { + "type": "boolean", + "description": "Generate rolluped 'd.ts' bundle including 'api.md' that provides project public API", + "default": true + }, "assets": { "type": "array", "description": "List of static assets.",