Skip to content

Commit 251f2cc

Browse files
committed
feat: introduce lazy generators
1 parent a8aacc8 commit 251f2cc

File tree

12 files changed

+111
-89
lines changed

12 files changed

+111
-89
lines changed

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: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,23 @@ 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+
/**
11+
* Resolves all lazy generator loaders into their actual metadata.
12+
* @returns {Promise<[string, import('../types').GeneratorMetadata][]>}
13+
*/
14+
const resolveAllGenerators = async () =>
15+
Promise.all(
16+
Object.entries(allGenerators).map(async ([key, loader]) => [
17+
key,
18+
await loader(),
19+
])
20+
);
1021

1122
describe('All Generators', () => {
12-
it('should have keys matching their name property', () => {
13-
generatorEntries.forEach(([key, generator]) => {
23+
it('should have keys matching their name property', async () => {
24+
const entries = await resolveAllGenerators();
25+
entries.forEach(([key, generator]) => {
1426
assert.equal(
1527
key,
1628
generator.name,
@@ -19,8 +31,9 @@ describe('All Generators', () => {
1931
});
2032
});
2133

22-
it('should have valid semver versions', () => {
23-
generatorEntries.forEach(([key, generator]) => {
34+
it('should have valid semver versions', async () => {
35+
const entries = await resolveAllGenerators();
36+
entries.forEach(([key, generator]) => {
2437
const isValid = semver.valid(generator.version);
2538
assert.ok(
2639
isValid,
@@ -29,8 +42,9 @@ describe('All Generators', () => {
2942
});
3043
});
3144

32-
it('should have valid dependsOn references', () => {
33-
generatorEntries.forEach(([key, generator]) => {
45+
it('should have valid dependsOn references', async () => {
46+
const entries = await resolveAllGenerators();
47+
entries.forEach(([key, generator]) => {
3448
if (generator.dependsOn) {
3549
assert.ok(
3650
validDependencies.includes(generator.dependsOn),
@@ -40,10 +54,11 @@ describe('All Generators', () => {
4054
});
4155
});
4256

43-
it('should have ast generator as a top-level generator with no dependencies', () => {
44-
assert.ok(allGenerators.ast, 'ast generator should exist');
57+
it('should have ast generator as a top-level generator with no dependencies', async () => {
58+
const ast = await allGenerators.ast();
59+
assert.ok(ast, 'ast generator should exist');
4560
assert.equal(
46-
allGenerators.ast.dependsOn,
61+
ast.dependsOn,
4762
undefined,
4863
'ast generator should have no dependencies'
4964
);

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: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,36 @@
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+
/**
4+
* Wraps a dynamic import into a lazy loader that resolves to the default export.
5+
*
6+
* @template T
7+
* @param {() => Promise<{default: T}>} loader
8+
* @returns {() => Promise<T>}
9+
*/
10+
const lazyDefault = loader => () => loader().then(m => m.default);
1911

2012
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,
13+
'json-simple': lazyDefault(() => import('./json-simple/index.mjs')),
14+
'legacy-html': lazyDefault(() => import('./legacy-html/index.mjs')),
15+
'legacy-html-all': lazyDefault(() => import('./legacy-html-all/index.mjs')),
16+
'man-page': lazyDefault(() => import('./man-page/index.mjs')),
17+
'legacy-json': lazyDefault(() => import('./legacy-json/index.mjs')),
18+
'legacy-json-all': lazyDefault(() => import('./legacy-json-all/index.mjs')),
19+
'addon-verify': lazyDefault(() => import('./addon-verify/index.mjs')),
20+
'api-links': lazyDefault(() => import('./api-links/index.mjs')),
21+
'orama-db': lazyDefault(() => import('./orama-db/index.mjs')),
22+
'llms-txt': lazyDefault(() => import('./llms-txt/index.mjs')),
23+
sitemap: lazyDefault(() => import('./sitemap/index.mjs')),
24+
web: lazyDefault(() => import('./web/index.mjs')),
3325
};
3426

3527
// These ones are special since they don't produce standard output,
3628
// and hence, we don't expose them to the CLI.
3729
const internalGenerators = {
38-
ast,
39-
metadata,
40-
'jsx-ast': jsxAst,
41-
'ast-js': astJs,
30+
ast: lazyDefault(() => import('./ast/index.mjs')),
31+
metadata: lazyDefault(() => import('./metadata/index.mjs')),
32+
'jsx-ast': lazyDefault(() => import('./jsx-ast/index.mjs')),
33+
'ast-js': lazyDefault(() => import('./ast-js/index.mjs')),
4234
};
4335

4436
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
/**

src/utils/configuration/__tests__/index.test.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ const createMockConfig = (overrides = {}) => ({
1515
mock.module('../../../generators/index.mjs', {
1616
namedExports: {
1717
allGenerators: {
18-
json: { defaultConfiguration: { format: 'json' } },
19-
html: { defaultConfiguration: { format: 'html' } },
20-
markdown: {},
18+
json: async () => ({ defaultConfiguration: { format: 'json' } }),
19+
html: async () => ({ defaultConfiguration: { format: 'html' } }),
20+
markdown: async () => ({}),
2121
},
2222
},
2323
});

0 commit comments

Comments
 (0)