Skip to content

Commit de0eb04

Browse files
authored
feat(workspace-plugin): add generateApi functionality/flag to build executor (#32976)
1 parent 95ec5c9 commit de0eb04

File tree

13 files changed

+193
-27
lines changed

13 files changed

+193
-27
lines changed

tools/workspace-plugin/STYLE-GUIDE.md

+13-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Generators
44

5-
Generators should live in the `tools/generators` folder. [Learn more about Nx generators](https://nx.dev/generators/workspace-generators).
5+
Generators live in the `tools/workspace-plugin/src/generators` folder. [Learn more about Nx generators](https://nx.dev/generators/workspace-generators).
66

77
### Scaffolding
88

@@ -52,7 +52,7 @@ Integration tests for the generator as a whole.
5252

5353
TypeScript interface that matches `schema.json`. You can generate this from the json file by running:
5454

55-
- `npx json-schema-to-typescript -i tools/generators/<generator-name>/schema.json -o tools/generators/<generator-name>/schema.ts --additionalProperties false`
55+
- `npx json-schema-to-typescript@latest -i tools/workspace-plugin/src/generators/<name>/schema.json -o tools/workspace-plugin/src/generators/<name>/schema.d.ts --additionalProperties false`
5656

5757
**`schema.json`**
5858

@@ -95,12 +95,20 @@ Migrations follow same rules as [Generators](#Generators) as they behave the sam
9595

9696
## Executors
9797

98-
Executors should live in the `tools/executors` folder. [Learn more about Nx executors](https://nx.dev/executors/using-builders).
98+
Executors live in the `tools/workspace-plugin/src/executors` folder. [Learn more about Nx executors](https://nx.dev/executors/using-builders).
9999

100100
### Scaffolding
101101

102-
TBA
102+
**`schema.d.ts`**
103+
104+
TypeScript interface that matches `schema.json`. You can generate this from the json file by running:
105+
106+
- `npx json-schema-to-typescript@latest -i tools/workspace-plugin/src/executors/<name>/schema.json -o tools/workspace-plugin/src/executors/<name>/schema.d.ts --additionalProperties false`
107+
108+
**`schema.json`**
109+
110+
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.
103111

104112
### Bootstrap new executor
105113

106-
TBA
114+
`yarn nx g @nx/plugin:executor --directory tools/workspace-plugin/src/executors/<name-of-executor>`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3+
"mainEntryPointFilePath": "<projectFolder>/dist/out-tsc/index.d.ts",
4+
"docModel": {
5+
"enabled": false
6+
},
7+
"apiReport": {
8+
"enabled": true,
9+
"reportFileName": "api.md"
10+
},
11+
"dtsRollup": {
12+
"enabled": true,
13+
"untrimmedFilePath": "<projectFolder>/dist/index.d.ts"
14+
},
15+
"tsdocMetadata": {
16+
"enabled": false
17+
},
18+
"messages": {
19+
"extractorMessageReporting": {
20+
"ae-forgotten-export": {
21+
"logLevel": "none",
22+
"addToApiReportFile": false
23+
},
24+
"ae-missing-release-tag": {
25+
"logLevel": "none"
26+
},
27+
"ae-unresolved-link": {
28+
"logLevel": "none"
29+
},
30+
"ae-internal-missing-underscore": {
31+
"logLevel": "none",
32+
"addToApiReportFile": false
33+
}
34+
}
35+
},
36+
"newlineKind": "os"
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## API Report File for "proj"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
// @public (undocumented)
8+
export function greeter(greeting: string, user: User): string;
9+
10+
// (No @packageDocumentation comment for this package)
11+
12+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "proj"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"moduleResolution": "Node",
4+
"target": "ES2019",
5+
"skipLibCheck": true,
6+
"pretty": true
7+
},
8+
"include": [],
9+
"files": [],
10+
"references": [
11+
{
12+
"path": "./tsconfig.lib.json"
13+
}
14+
]
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "./dist/out-tsc",
5+
"declaration": true
6+
},
7+
"include": ["src/*.ts"]
8+
}

tools/workspace-plugin/src/executors/build/executor.spec.ts

+46-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const options: BuildExecutorSchema = {
3030
glob: '*.md',
3131
},
3232
],
33+
generateApi: true,
3334
clean: true,
3435
};
3536

@@ -73,6 +74,15 @@ const measureEndMock = measureEnd as jest.Mock;
7374

7475
describe('Build Executor', () => {
7576
it('can run', async () => {
77+
// mute api extractor - START
78+
jest.spyOn(console, 'warn').mockImplementation(() => {
79+
return;
80+
});
81+
jest.spyOn(console, 'log').mockImplementation(() => {
82+
return;
83+
});
84+
// mute api extractor - END
85+
7686
const loggerLogSpy = jest.spyOn(logger, 'log').mockImplementation(() => {
7787
return;
7888
});
@@ -122,12 +132,14 @@ describe('Build Executor', () => {
122132
},
123133
]);
124134

125-
expect(measureStartMock).toHaveBeenCalledTimes(1);
126-
expect(measureEndMock).toHaveBeenCalledTimes(1);
135+
expect(measureStartMock).toHaveBeenCalledTimes(2);
136+
expect(measureEndMock).toHaveBeenCalledTimes(2);
127137

128138
// =====================
129139
// assert build Assets
130140
// =====================
141+
expect(existsSync(join(workspaceRoot, 'libs/proj/etc', 'api.md'))).toBe(true);
142+
expect(existsSync(join(workspaceRoot, 'libs/proj/dist', 'index.d.ts'))).toBe(true);
131143
expect(existsSync(join(workspaceRoot, 'libs/proj/dist/assets', 'spec.md'))).toBe(true);
132144
expect(readdirSync(join(workspaceRoot, 'libs/proj/lib'))).toEqual([
133145
'greeter.js',
@@ -146,6 +158,38 @@ describe('Build Executor', () => {
146158
'index.js.map',
147159
]);
148160

161+
// ====================================
162+
// assert generateAPI output based on settings
163+
// ====================================
164+
expect(readFileSync(join(workspaceRoot, 'libs/proj/dist/index.d.ts'), 'utf-8')).toMatchInlineSnapshot(`
165+
"export declare function greeter(greeting: string, user: User): string;
166+
167+
declare type User = {
168+
name: string;
169+
hometown?: {
170+
name: string;
171+
};
172+
};
173+
174+
export { }
175+
"
176+
`);
177+
expect(readFileSync(join(workspaceRoot, 'libs/proj/etc/api.md'), 'utf-8')).toMatchInlineSnapshot(`
178+
"## API Report File for \\"proj\\"
179+
180+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
181+
182+
\`\`\`ts
183+
184+
// @public (undocumented)
185+
export function greeter(greeting: string, user: User): string;
186+
187+
// (No @packageDocumentation comment for this package)
188+
189+
\`\`\`
190+
"
191+
`);
192+
149193
// ====================================
150194
// assert swc output based on settings
151195
// ====================================

tools/workspace-plugin/src/executors/build/executor.ts

+15-12
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,28 @@ import { compileSwc } from './lib/swc';
44
import { compileWithGriffelStylesAOT, hasStylesFilesToProcess } from './lib/babel';
55
import { assetGlobsToFiles, copyAssets } from './lib/assets';
66
import { cleanOutput } from './lib/clean';
7-
import { NormalizedOptions, normalizeOptions, processAsyncQueue } from './lib/shared';
7+
import { NormalizedOptions, normalizeOptions, processAsyncQueue, runInParallel, runSerially } from './lib/shared';
88

99
import { measureEnd, measureStart } from '../../utils';
10+
import generateApiExecutor from '../generate-api/executor';
1011

1112
import { type BuildExecutorSchema } from './schema';
1213

1314
const runExecutor: PromiseExecutor<BuildExecutorSchema> = async (schema, context) => {
1415
measureStart('BuildExecutor');
1516

1617
const options = normalizeOptions(schema, context);
18+
const assetFiles = assetGlobsToFiles(options.assets ?? [], context.root, options.outputPathRoot);
1719

18-
const success = await runBuild(options, context);
20+
const success = await runSerially(
21+
() => cleanOutput(options, assetFiles),
22+
() =>
23+
runInParallel(
24+
() => runBuild(options, context),
25+
() => (options.generateApi ? generateApiExecutor({}, context).then(res => res.success) : Promise.resolve(true)),
26+
),
27+
() => copyAssets(assetFiles),
28+
);
1929

2030
measureEnd('BuildExecutor');
2131

@@ -26,21 +36,14 @@ export default runExecutor;
2636

2737
// ===========
2838

29-
async function runBuild(options: NormalizedOptions, context: ExecutorContext): Promise<boolean> {
30-
const assetFiles = assetGlobsToFiles(options.assets ?? [], context.root, options.outputPathRoot);
31-
32-
const cleanResult = await cleanOutput(options, assetFiles);
33-
if (!cleanResult) {
34-
return false;
35-
}
36-
39+
async function runBuild(options: NormalizedOptions, _context: ExecutorContext): Promise<boolean> {
3740
if (hasStylesFilesToProcess(options)) {
38-
return compileWithGriffelStylesAOT(options, () => copyAssets(assetFiles));
41+
return compileWithGriffelStylesAOT(options);
3942
}
4043

4144
const compilationQueue = options.moduleOutput.map(outputConfig => {
4245
return compileSwc(outputConfig, options);
4346
});
4447

45-
return processAsyncQueue(compilationQueue, () => copyAssets(assetFiles));
48+
return processAsyncQueue(compilationQueue);
4649
}

tools/workspace-plugin/src/executors/build/lib/babel.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function hasStylesFilesToProcess(normalizedOptions: NormalizedOptions) {
2525
return files.length > 0;
2626
}
2727

28-
export async function compileWithGriffelStylesAOT(options: NormalizedOptions, successCallback: () => Promise<boolean>) {
28+
export async function compileWithGriffelStylesAOT(options: NormalizedOptions) {
2929
const { esmConfig, restOfConfigs } = options.moduleOutput.reduce<{
3030
esmConfig: NormalizedOptions['moduleOutput'][number] | null;
3131
restOfConfigs: NormalizedOptions['moduleOutput'];
@@ -47,7 +47,7 @@ export async function compileWithGriffelStylesAOT(options: NormalizedOptions, su
4747
const compilationQueue = restOfConfigs.map(outputConfig => {
4848
return compileSwc(outputConfig, options);
4949
});
50-
return processAsyncQueue(compilationQueue, successCallback);
50+
return processAsyncQueue(compilationQueue);
5151
}
5252

5353
await compileSwc(esmConfig, options);
@@ -65,7 +65,7 @@ export async function compileWithGriffelStylesAOT(options: NormalizedOptions, su
6565
});
6666
});
6767

68-
return processAsyncQueue(compilationQueue, successCallback);
68+
return processAsyncQueue(compilationQueue);
6969
}
7070

7171
async function babel(esmModuleOutput: NormalizedOptions['moduleOutput'][number], normalizedOptions: NormalizedOptions) {

tools/workspace-plugin/src/executors/build/lib/clean.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { type NormalizedOptions, processAsyncQueue } from './shared';
66

77
export async function cleanOutput(options: NormalizedOptions, assetFiles: Array<{ input: string; output: string }>) {
88
if (!options.clean) {
9-
return;
9+
return true;
1010
}
1111

1212
const swcOutputPaths = options.moduleOutput.map(outputConfig => {

tools/workspace-plugin/src/executors/build/lib/shared.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,35 @@ import { join } from 'node:path';
33

44
import { type BuildExecutorSchema } from '../schema';
55

6-
export async function processAsyncQueue(value: Promise<unknown>[], successCallback?: () => Promise<boolean>) {
6+
type Tasks = () => Promise<boolean>;
7+
8+
export async function runInParallel(...tasks: Tasks[]): Promise<boolean> {
9+
const processes = tasks.map(task => task());
10+
11+
return Promise.all(processes)
12+
.then(() => {
13+
return true;
14+
})
15+
.catch(err => {
16+
logger.error(err);
17+
return false;
18+
});
19+
}
20+
21+
export async function runSerially(...tasks: Tasks[]): Promise<boolean> {
22+
for (const task of tasks) {
23+
const result = await task();
24+
if (!result) {
25+
return false;
26+
}
27+
}
28+
return true;
29+
}
30+
31+
export async function processAsyncQueue(value: Promise<unknown>[]): Promise<boolean> {
732
return Promise.all(value)
833
.then(() => {
9-
return successCallback ? successCallback() : true;
34+
return true;
1035
})
1136
.catch(err => {
1237
logger.error(err);
@@ -17,6 +42,7 @@ export async function processAsyncQueue(value: Promise<unknown>[], successCallba
1742
export interface NormalizedOptions extends ReturnType<typeof normalizeOptions> {}
1843
export function normalizeOptions(schema: BuildExecutorSchema, context: ExecutorContext) {
1944
const defaults = {
45+
generateApi: true,
2046
clean: true,
2147
};
2248
const project = context.projectsConfigurations!.projects[context.projectName!];

tools/workspace-plugin/src/executors/build/schema.d.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
/* eslint-disable */
12
/**
23
* This file was automatically generated by json-schema-to-typescript.
34
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
45
* and run json-schema-to-typescript to regenerate this file.
56
*/
67

78
/**
8-
* Builds TS files with SWC to specified JS module type, transforms *.styles.* files via Babel(Griffel) and copies provided Assets.
9+
* 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.
910
*/
1011
export interface BuildExecutorSchema {
1112
/**
@@ -29,6 +30,10 @@ export interface BuildExecutorSchema {
2930
*/
3031
outputPath: string;
3132
}[];
33+
/**
34+
* Generate rolluped 'd.ts' bundle including 'api.md' that provides project public API
35+
*/
36+
generateApi?: boolean;
3237
/**
3338
* List of static assets.
3439
*/

tools/workspace-plugin/src/executors/build/schema.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://json-schema.org/schema",
33
"version": 2,
44
"title": "Build executor",
5-
"description": "Builds TS files with SWC to specified JS module type, transforms *.styles.* files via Babel(Griffel) and copies provided Assets.",
5+
"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.",
66
"type": "object",
77
"properties": {
88
"sourceRoot": {
@@ -28,6 +28,11 @@
2828
"required": ["module", "outputPath"]
2929
}
3030
},
31+
"generateApi": {
32+
"type": "boolean",
33+
"description": "Generate rolluped 'd.ts' bundle including 'api.md' that provides project public API",
34+
"default": true
35+
},
3136
"assets": {
3237
"type": "array",
3338
"description": "List of static assets.",

0 commit comments

Comments
 (0)