Skip to content

Commit b5a647b

Browse files
committed
feat: properly implement streaming, worker chunking and proper worker thread scheduler
1 parent f7dd452 commit b5a647b

File tree

29 files changed

+1519
-633
lines changed

29 files changed

+1519
-633
lines changed

bin/cli.mjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,28 @@ import { Command, Option } from 'commander';
77
import commands from './commands/index.mjs';
88
import interactive from './commands/interactive.mjs';
99
import { errorWrap } from './utils.mjs';
10+
import logger from '../src/logger/index.mjs';
1011

1112
const program = new Command()
1213
.name('@nodejs/doc-kit')
1314
.description('CLI tool to generate the Node.js API documentation');
1415

16+
// Add global log level option
17+
program.addOption(
18+
new Option('--log-level <level>', 'Log level')
19+
.choices(['debug', 'info', 'warn', 'error', 'fatal'])
20+
.default('info')
21+
);
22+
23+
// Set log level before any command runs
24+
program.hook('preAction', thisCommand => {
25+
const logLevel = thisCommand.opts().logLevel;
26+
27+
if (logLevel) {
28+
logger.setLogLevel(logLevel);
29+
}
30+
});
31+
1532
// Registering commands
1633
commands.forEach(({ name, description, options, action }) => {
1734
const cmd = program.command(name).description(description);

bin/commands/generate.mjs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import { resolve } from 'node:path';
33

44
import { coerce } from 'semver';
55

6-
import {
7-
DOC_NODE_CHANGELOG_URL,
8-
DOC_NODE_VERSION,
9-
} from '../../src/constants.mjs';
6+
import { NODE_CHANGELOG_URL, NODE_VERSION } from '../../src/constants.mjs';
107
import { publicGenerators } from '../../src/generators/index.mjs';
118
import createGenerator from '../../src/generators.mjs';
129
import { parseChangelog, parseIndex } from '../../src/parsers/markdown.mjs';
@@ -18,7 +15,10 @@ const availableGenerators = Object.keys(publicGenerators);
1815

1916
// Half of available logical CPUs guarantees in general all physical CPUs are being used
2017
// which in most scenarios is the best way to maximize performance
21-
const optimalThreads = Math.floor(cpus().length / 2) + 1;
18+
// When spawning more than a said number of threads, the overhead of context switching
19+
// and CPU contention starts to degrade performance rather than improve it.
20+
// Therefore, we set the optimal threads to half the number of CPU cores, with a minimum of 6.
21+
const optimalThreads = Math.min(Math.floor(cpus().length / 2), 6);
2222

2323
/**
2424
* @typedef {Object} Options
@@ -88,7 +88,7 @@ export default {
8888
prompt: {
8989
type: 'text',
9090
message: 'Enter Node.js version',
91-
initialValue: DOC_NODE_VERSION,
91+
initialValue: NODE_VERSION,
9292
},
9393
},
9494
changelog: {
@@ -97,7 +97,7 @@ export default {
9797
prompt: {
9898
type: 'text',
9999
message: 'Enter changelog URL',
100-
initialValue: DOC_NODE_CHANGELOG_URL,
100+
initialValue: NODE_CHANGELOG_URL,
101101
},
102102
},
103103
gitRef: {

src/__tests__/generators.test.mjs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { ok, strictEqual } from 'node:assert';
2+
import { describe, it } from 'node:test';
3+
4+
import createGenerator from '../generators.mjs';
5+
import { isAsyncGenerator } from '../streaming.mjs';
6+
7+
describe('createGenerator', () => {
8+
// Simple mock input for testing
9+
const mockInput = [
10+
{
11+
file: { stem: 'test', basename: 'test.md' },
12+
tree: { type: 'root', children: [] },
13+
},
14+
];
15+
16+
// Mock options with minimal required fields
17+
const mockOptions = {
18+
input: '/tmp/test',
19+
output: '/tmp/output',
20+
generators: ['metadata'],
21+
version: { major: 22, minor: 0, patch: 0 },
22+
releases: [],
23+
index: [],
24+
gitRef: 'https://github.com/nodejs/node/tree/HEAD',
25+
threads: 1,
26+
chunkSize: 20,
27+
typeMap: {},
28+
};
29+
30+
it('should create a generator orchestrator with runGenerators method', () => {
31+
const { runGenerators } = createGenerator(mockInput);
32+
33+
ok(runGenerators);
34+
strictEqual(typeof runGenerators, 'function');
35+
});
36+
37+
it('should return the ast input directly when generators list is empty', async () => {
38+
const { runGenerators } = createGenerator(mockInput);
39+
40+
const result = await runGenerators({
41+
...mockOptions,
42+
generators: ['ast'],
43+
});
44+
45+
// The 'ast' key should resolve to the original input
46+
ok(result);
47+
});
48+
49+
it('should run metadata generator', async () => {
50+
const { runGenerators } = createGenerator(mockInput);
51+
52+
const result = await runGenerators({
53+
...mockOptions,
54+
generators: ['metadata'],
55+
});
56+
57+
// metadata returns an async generator
58+
ok(isAsyncGenerator(result));
59+
});
60+
61+
it('should handle generator with dependency', async () => {
62+
const { runGenerators } = createGenerator(mockInput);
63+
64+
// legacy-html depends on metadata
65+
const result = await runGenerators({
66+
...mockOptions,
67+
generators: ['legacy-html'],
68+
});
69+
70+
// Should complete without error
71+
ok(result !== undefined);
72+
});
73+
74+
it('should skip already scheduled generators', async () => {
75+
const { runGenerators } = createGenerator(mockInput);
76+
77+
// Running with ['metadata', 'metadata'] should skip the second
78+
const result = await runGenerators({
79+
...mockOptions,
80+
generators: ['metadata', 'metadata'],
81+
});
82+
83+
ok(isAsyncGenerator(result));
84+
});
85+
86+
it('should handle multiple generators in sequence', async () => {
87+
const { runGenerators } = createGenerator(mockInput);
88+
89+
// Run metadata twice - the system should skip the already scheduled one
90+
// Avoid json-simple since it writes to disk
91+
const result = await runGenerators({
92+
...mockOptions,
93+
generators: ['metadata'],
94+
});
95+
96+
// Result should be from the last generator
97+
ok(result !== undefined);
98+
});
99+
100+
it('should collect async generator results for dependents', async () => {
101+
const { runGenerators } = createGenerator(mockInput);
102+
103+
// legacy-json depends on metadata (async generator)
104+
const result = await runGenerators({
105+
...mockOptions,
106+
generators: ['legacy-json'],
107+
});
108+
109+
ok(result !== undefined);
110+
});
111+
112+
it('should use multiple threads when specified', async () => {
113+
const { runGenerators } = createGenerator(mockInput);
114+
115+
const result = await runGenerators({
116+
...mockOptions,
117+
threads: 4,
118+
generators: ['metadata'],
119+
});
120+
121+
ok(isAsyncGenerator(result));
122+
});
123+
124+
it('should pass options to generators', async () => {
125+
const { runGenerators } = createGenerator(mockInput);
126+
127+
const customTypeMap = { TestType: 'https://example.com/TestType' };
128+
129+
const result = await runGenerators({
130+
...mockOptions,
131+
typeMap: customTypeMap,
132+
generators: ['metadata'],
133+
});
134+
135+
ok(isAsyncGenerator(result));
136+
});
137+
});

0 commit comments

Comments
 (0)