Skip to content

Commit 470ac2c

Browse files
ovflowdavivkeller
andauthored
feat: introduce lazy generators (#609)
* feat: introduce lazy generators * chore: code review Co-authored-by: Aviv Keller <me@aviv.sh> * fix: tests * chore: use lazy --------- Co-authored-by: Aviv Keller <me@aviv.sh>
1 parent 8ea0559 commit 470ac2c

File tree

13 files changed

+106
-89
lines changed

13 files changed

+106
-89
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "@nodejs/doc-kit",
3+
"type": "module",
34
"repository": {
45
"type": "git",
56
"url": "git+https://github.com/nodejs/api-docs-tooling.git"

src/generators.mjs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,17 @@ const createGenerator = () => {
4848
* @param {string} generatorName - Generator to schedule
4949
* @param {import('./utils/configuration/types').Configuration} configuration - Runtime options
5050
*/
51-
const scheduleGenerator = (generatorName, configuration) => {
51+
const scheduleGenerator = async (generatorName, configuration) => {
5252
if (generatorName in cachedGenerators) {
5353
return;
5454
}
5555

56-
const { dependsOn, generate, processChunk } = allGenerators[generatorName];
56+
const { dependsOn, generate, processChunk } =
57+
await allGenerators[generatorName]();
5758

5859
// Schedule dependency first
5960
if (dependsOn && !(dependsOn in cachedGenerators)) {
60-
scheduleGenerator(dependsOn, configuration);
61+
await scheduleGenerator(dependsOn, configuration);
6162
}
6263

6364
generatorsLogger.debug(`Scheduling "${generatorName}"`, {
@@ -74,9 +75,9 @@ const createGenerator = () => {
7475
// Create parallel worker for streaming generators
7576
const worker = processChunk
7677
? createParallelWorker(generatorName, pool, configuration)
77-
: null;
78+
: Promise.resolve(null);
7879

79-
const result = await generate(dependencyInput, worker);
80+
const result = await generate(dependencyInput, await worker);
8081

8182
// For streaming generators, "Completed" is logged when collection finishes
8283
// (in streamingCache.getOrCollect), not here when the generator returns
@@ -107,7 +108,7 @@ const createGenerator = () => {
107108

108109
// Schedule all generators
109110
for (const name of generators) {
110-
scheduleGenerator(name, configuration);
111+
await scheduleGenerator(name, configuration);
111112
}
112113

113114
// Start all collections in parallel (don't await sequentially)

src/generators/__tests__/index.test.mjs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@ import semver from 'semver';
66
import { allGenerators } from '../index.mjs';
77

88
const validDependencies = Object.keys(allGenerators);
9-
const generatorEntries = Object.entries(allGenerators);
9+
10+
const allGeneratorsReaolved = await Promise.all(
11+
Object.entries(allGenerators).map(async ([key, loader]) => [
12+
key,
13+
await loader(),
14+
])
15+
);
1016

1117
describe('All Generators', () => {
12-
it('should have keys matching their name property', () => {
13-
generatorEntries.forEach(([key, generator]) => {
18+
it('should have keys matching their name property', async () => {
19+
allGeneratorsReaolved.forEach(([key, generator]) => {
1420
assert.equal(
1521
key,
1622
generator.name,
@@ -19,8 +25,8 @@ describe('All Generators', () => {
1925
});
2026
});
2127

22-
it('should have valid semver versions', () => {
23-
generatorEntries.forEach(([key, generator]) => {
28+
it('should have valid semver versions', async () => {
29+
allGeneratorsReaolved.forEach(([key, generator]) => {
2430
const isValid = semver.valid(generator.version);
2531
assert.ok(
2632
isValid,
@@ -29,8 +35,8 @@ describe('All Generators', () => {
2935
});
3036
});
3137

32-
it('should have valid dependsOn references', () => {
33-
generatorEntries.forEach(([key, generator]) => {
38+
it('should have valid dependsOn references', async () => {
39+
allGeneratorsReaolved.forEach(([key, generator]) => {
3440
if (generator.dependsOn) {
3541
assert.ok(
3642
validDependencies.includes(generator.dependsOn),
@@ -40,10 +46,11 @@ describe('All Generators', () => {
4046
});
4147
});
4248

43-
it('should have ast generator as a top-level generator with no dependencies', () => {
44-
assert.ok(allGenerators.ast, 'ast generator should exist');
49+
it('should have ast generator as a top-level generator with no dependencies', async () => {
50+
const ast = await allGenerators.ast();
51+
assert.ok(ast, 'ast generator should exist');
4552
assert.equal(
46-
allGenerators.ast.dependsOn,
53+
ast.dependsOn,
4754
undefined,
4855
'ast generator should have no dependencies'
4956
);

src/generators/api-links/__tests__/fixtures.test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe('api links', () => {
3535
join(relativePath, 'fixtures', sourceFile).replaceAll(sep, '/'),
3636
];
3737

38-
const worker = createParallelWorker('ast-js', pool, config);
38+
const worker = await createParallelWorker('ast-js', pool, config);
3939

4040
// Collect results from the async generator
4141
const astJsResults = [];

src/generators/index.mjs

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,38 @@
11
'use strict';
22

3-
import addonVerify from './addon-verify/index.mjs';
4-
import apiLinks from './api-links/index.mjs';
5-
import ast from './ast/index.mjs';
6-
import astJs from './ast-js/index.mjs';
7-
import jsonSimple from './json-simple/index.mjs';
8-
import jsxAst from './jsx-ast/index.mjs';
9-
import legacyHtml from './legacy-html/index.mjs';
10-
import legacyHtmlAll from './legacy-html-all/index.mjs';
11-
import legacyJson from './legacy-json/index.mjs';
12-
import legacyJsonAll from './legacy-json-all/index.mjs';
13-
import llmsTxt from './llms-txt/index.mjs';
14-
import manPage from './man-page/index.mjs';
15-
import metadata from './metadata/index.mjs';
16-
import oramaDb from './orama-db/index.mjs';
17-
import sitemap from './sitemap/index.mjs';
18-
import web from './web/index.mjs';
3+
import { lazy } from '../utils/misc.mjs';
4+
5+
/**
6+
* Wraps a dynamic import into a lazy loader that resolves to the default export.
7+
*
8+
* @template T
9+
* @param {() => Promise<{default: T}>} loader
10+
* @returns {() => Promise<T>}
11+
*/
12+
const lazyDefault = loader => lazy(() => loader().then(m => m.default));
1913

2014
export const publicGenerators = {
21-
'json-simple': jsonSimple,
22-
'legacy-html': legacyHtml,
23-
'legacy-html-all': legacyHtmlAll,
24-
'man-page': manPage,
25-
'legacy-json': legacyJson,
26-
'legacy-json-all': legacyJsonAll,
27-
'addon-verify': addonVerify,
28-
'api-links': apiLinks,
29-
'orama-db': oramaDb,
30-
'llms-txt': llmsTxt,
31-
sitemap,
32-
web,
15+
'json-simple': lazyDefault(() => import('./json-simple/index.mjs')),
16+
'legacy-html': lazyDefault(() => import('./legacy-html/index.mjs')),
17+
'legacy-html-all': lazyDefault(() => import('./legacy-html-all/index.mjs')),
18+
'man-page': lazyDefault(() => import('./man-page/index.mjs')),
19+
'legacy-json': lazyDefault(() => import('./legacy-json/index.mjs')),
20+
'legacy-json-all': lazyDefault(() => import('./legacy-json-all/index.mjs')),
21+
'addon-verify': lazyDefault(() => import('./addon-verify/index.mjs')),
22+
'api-links': lazyDefault(() => import('./api-links/index.mjs')),
23+
'orama-db': lazyDefault(() => import('./orama-db/index.mjs')),
24+
'llms-txt': lazyDefault(() => import('./llms-txt/index.mjs')),
25+
sitemap: lazyDefault(() => import('./sitemap/index.mjs')),
26+
web: lazyDefault(() => import('./web/index.mjs')),
3327
};
3428

3529
// These ones are special since they don't produce standard output,
3630
// and hence, we don't expose them to the CLI.
3731
const internalGenerators = {
38-
ast,
39-
metadata,
40-
'jsx-ast': jsxAst,
41-
'ast-js': astJs,
32+
ast: lazyDefault(() => import('./ast/index.mjs')),
33+
metadata: lazyDefault(() => import('./metadata/index.mjs')),
34+
'jsx-ast': lazyDefault(() => import('./jsx-ast/index.mjs')),
35+
'ast-js': lazyDefault(() => import('./ast-js/index.mjs')),
4236
};
4337

4438
export const allGenerators = {

src/generators/jsx-ast/utils/__tests__/buildBarProps.test.mjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ mock.module('../../../../utils/generators.mjs', {
1818
{ version: '19.0.0', isLts: false, isCurrent: true },
1919
],
2020
leftHandAssign: Object.assign,
21-
getVersionFromSemVer: version => version.split('.')[0],
21+
getVersionFromSemVer: version => `${version.major}.x`,
2222
getVersionURL: (version, api) => `/api/${version}/${api}`,
2323
},
2424
});
@@ -94,7 +94,7 @@ describe('buildMetaBarProps', () => {
9494
const result = buildMetaBarProps(head, entries);
9595

9696
assert.equal(result.addedIn, 'v1.0.0');
97-
assert.equal(result.readingTime, '1 min read');
97+
assert.equal(result.readingTime, '5 min read');
9898
assert.deepEqual(result.viewAs, [
9999
['JSON', 'fs.json'],
100100
['MD', 'fs.md'],
@@ -134,15 +134,15 @@ describe('formatVersionOptions', () => {
134134
assert.deepStrictEqual(result, [
135135
{
136136
label: 'v16.x (LTS)',
137-
value: 'https://nodejs.org/docs/latest-v16.x/api/http.html',
137+
value: '/api/16.x/http',
138138
},
139139
{
140140
label: 'v17.x (Current)',
141-
value: 'https://nodejs.org/docs/latest-v17.x/api/http.html',
141+
value: '/api/17.x/http',
142142
},
143143
{
144144
label: 'v18.x',
145-
value: 'https://nodejs.org/docs/latest-v18.x/api/http.html',
145+
value: '/api/18.x/http',
146146
},
147147
]);
148148
});

src/generators/types.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import type { publicGenerators, allGenerators } from './index.mjs';
22

33
declare global {
4+
/**
5+
* A lazy generator loader that returns a promise resolving to the generator metadata.
6+
*/
7+
export type LazyGenerator<T = GeneratorMetadata<any, any, any>> =
8+
() => Promise<T>;
9+
410
// Public generators exposed to the CLI
511
export type AvailableGenerators = typeof publicGenerators;
612

713
// All generators including internal ones (metadata, jsx-ast, ast-js)
814
export type AllGenerators = typeof allGenerators;
915

16+
// The resolved type of a loaded generator
17+
export type ResolvedGenerator<K extends keyof AllGenerators> = Awaited<
18+
ReturnType<AllGenerators[K]>
19+
>;
20+
1021
/**
1122
* ParallelWorker interface for distributing work across Node.js worker threads.
1223
* Streams results as chunks complete, enabling pipeline parallelism.

src/threading/__tests__/parallel.test.mjs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ async function collectChunks(generator) {
4141
describe('createParallelWorker', () => {
4242
it('should create a ParallelWorker with stream method', async () => {
4343
const pool = createWorkerPool(2);
44-
const worker = createParallelWorker('metadata', pool, { threads: 2 });
44+
const worker = await createParallelWorker('metadata', pool, { threads: 2 });
4545

4646
ok(worker);
4747
strictEqual(typeof worker.stream, 'function');
@@ -51,7 +51,7 @@ describe('createParallelWorker', () => {
5151

5252
it('should handle empty items array', async () => {
5353
const pool = createWorkerPool(2);
54-
const worker = createParallelWorker('ast-js', pool, {
54+
const worker = await createParallelWorker('ast-js', pool, {
5555
threads: 2,
5656
chunkSize: 10,
5757
});
@@ -65,7 +65,7 @@ describe('createParallelWorker', () => {
6565

6666
it('should distribute items to multiple worker threads', async () => {
6767
const pool = createWorkerPool(4);
68-
const worker = createParallelWorker('metadata', pool, {
68+
const worker = await createParallelWorker('metadata', pool, {
6969
threads: 4,
7070
chunkSize: 1,
7171
});
@@ -104,7 +104,7 @@ describe('createParallelWorker', () => {
104104

105105
it('should yield results as chunks complete', async () => {
106106
const pool = createWorkerPool(2);
107-
const worker = createParallelWorker('metadata', pool, {
107+
const worker = await createParallelWorker('metadata', pool, {
108108
threads: 2,
109109
chunkSize: 1,
110110
});
@@ -131,7 +131,7 @@ describe('createParallelWorker', () => {
131131

132132
it('should work with single thread and items', async () => {
133133
const pool = createWorkerPool(2);
134-
const worker = createParallelWorker('metadata', pool, {
134+
const worker = await createParallelWorker('metadata', pool, {
135135
threads: 2,
136136
chunkSize: 5,
137137
});
@@ -155,7 +155,7 @@ describe('createParallelWorker', () => {
155155

156156
it('should use sliceInput for metadata generator', async () => {
157157
const pool = createWorkerPool(2);
158-
const worker = createParallelWorker('metadata', pool, {
158+
const worker = await createParallelWorker('metadata', pool, {
159159
threads: 2,
160160
chunkSize: 1,
161161
});

src/threading/chunk-worker.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default async ({
1717
}) => {
1818
await setConfig(configuration);
1919

20-
const generator = allGenerators[generatorName];
20+
const generator = await allGenerators[generatorName]();
2121

2222
return generator.processChunk(input, itemIndices, extra);
2323
};

src/threading/parallel.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,14 @@ const createTask = (
6363
* @param {import('../utils/configuration/types').Configuration} configuration - Generator options
6464
* @returns {ParallelWorker}
6565
*/
66-
export default function createParallelWorker(
66+
export default async function createParallelWorker(
6767
generatorName,
6868
pool,
6969
configuration
7070
) {
7171
const { threads, chunkSize } = configuration;
7272

73-
const generator = allGenerators[generatorName];
73+
const generator = await allGenerators[generatorName]();
7474

7575
return {
7676
/**

0 commit comments

Comments
 (0)