Skip to content

Commit eaecb85

Browse files
committed
refactor: make all.html a composed page of the Node.js preset
all.html, every module page's content on one page, is Node.js's, but html built it in, behind `generateAllPage`, and the streaming work spread its bookkeeping through generate.mjs. html now takes `composedPages`: modules default-exporting a page's metadata and which pages are its parts. It assembles each from its parts' compiled content in sidebar order, compiles it once they all are, and renders it first, all in utils/composed.mjs. all.html moves to @node-core/doc-kit as one of them, which the Node.js preset lists, and `generateAllPage` goes away. Built with the preset, the Node.js API docs come out byte-identical, all.html included. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ad5bde5 commit eaecb85

20 files changed

Lines changed: 310 additions & 154 deletions

File tree

‎.changeset/composed-pages.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@doc-kit/generator-react': minor
3+
'@node-core/doc-kit': minor
4+
---
5+
6+
The `html` generator's `generateAllPage` option is replaced by `composedPages`: modules default-exporting a page made of other pages' content, `{ head, isPart }`, which `html` assembles from those pages' compiled content, in sidebar order, once they all are. `all.html` is now one of them, added by the Node.js preset (`@node-core/doc-kit/config`), so sites built without the preset no longer get one.

‎.changeset/hold-less-in-memory.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
'@doc-kit/generator-react': patch
55
---
66

7-
Builds hold less in memory. The nodes of `metadata`'s entries no longer keep their `position` in the source, so neither does the output of `json-simple`, now about a third of its size. Each chunk of a streamed output is let go once every generator reading it has read that chunk, rather than once they have all read all of it, and `runGenerators(configuration, { results: false })` keeps none of the requested generators' output, which the CLI now uses. `all.html` renders one module at a time, rather than building every module's elements first.
7+
Builds hold less in memory. The nodes of `metadata`'s entries no longer keep their `position` in the source, so neither does the output of `json-simple`, now about a third of its size. Each chunk of a streamed output is let go once every generator reading it has read that chunk, rather than once they have all read all of it, and `runGenerators(configuration, { results: false })` keeps none of the requested generators' output, which the CLI now uses. A composed page, such as the Node.js preset's `all.html`, renders one part at a time, rather than building every part's elements first.

‎e2e/doc-kit.config.mjs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,10 @@ export default {
33
// The announcement banner under test is opt-in: point it at the URL the
44
// spec intercepts (no real request is made).
55
remoteConfigUrl: 'https://nodejs.org/site.json',
6+
7+
// The navigation under test goes to the Node.js preset's `all.html`
8+
composedPages: [
9+
new URL('../packages/node/src/config/all-page.mjs', import.meta.url).href,
10+
],
611
},
712
};

‎packages/node/src/config/__tests__/index.test.mjs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import assert from 'node:assert/strict';
22
import { existsSync } from 'node:fs';
3+
import { join } from 'node:path';
34
import { describe, it } from 'node:test';
45

6+
import allPage from '../all-page.mjs';
57
import preset from '../index.mjs';
68

79
describe('Node.js preset', () => {
@@ -22,4 +24,19 @@ describe('Node.js preset', () => {
2224
it('should ship the Node.js llms.txt template', () => {
2325
assert.ok(existsSync(preset['llms-txt'].templatePath));
2426
});
27+
28+
it('should compose all.html of the module pages', () => {
29+
assert.deepEqual(preset.html.composedPages, [
30+
join(import.meta.dirname, '..', 'all-page.mjs'),
31+
]);
32+
assert.equal(allPage.head.api, 'all');
33+
34+
assert.equal(allPage.isPart({ api: 'fs' }), true);
35+
assert.equal(allPage.isPart({ api: 'index' }), false);
36+
assert.equal(allPage.isPart({ api: '404', synthetic: true }), false);
37+
assert.equal(
38+
allPage.isPart({ api: 'fs', chunk: { name: 'fs.promises' } }),
39+
false
40+
);
41+
});
2542
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict';
2+
3+
/**
4+
* `all.html`: every module page's content on one page, in the order the
5+
* sidebar lists them, made of the module pages rather than built again (see
6+
* the `html` generator's `composedPages`).
7+
*/
8+
export default {
9+
head: {
10+
api: 'all',
11+
path: '/all',
12+
basename: 'all',
13+
synthetic: true,
14+
heading: {
15+
type: 'heading',
16+
depth: 1,
17+
children: [{ type: 'text', value: 'All' }],
18+
data: { name: 'All', text: 'All', slug: 'all' },
19+
},
20+
},
21+
22+
/**
23+
* Whether a page is part of `all.html`: the module pages, minus the index.
24+
* Chunk pages and the other synthetic pages are left out, as the module
25+
* pages carry their content.
26+
*
27+
* @param {{ api: string, synthetic?: boolean, chunk?: object }} data
28+
* @returns {boolean}
29+
*/
30+
isPart: data => !data.synthetic && !data.chunk && data.api !== 'index',
31+
};

‎packages/node/src/config/index.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export default {
7070

7171
stylesheets: [join(import.meta.dirname, 'theme.css')],
7272

73+
composedPages: [join(import.meta.dirname, 'all-page.mjs')],
74+
7375
imports: {
7476
'#theme/Logo': '@node-core/ui-components/Common/NodejsLogo',
7577
},

‎packages/react/src/html/README.md‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ library once and the client assets once, as soon as every page's metadata is
1313
known, and the worker pool imports, renders, templates, minifies and writes
1414
the pages one at a time as they are compiled, so memory scales with the largest
1515
page rather than with the site. With one thread, each step waits for the one
16-
before it to finish. `all.html` is assembled from the module pages' compiled
17-
content rather than built again from scratch.
16+
before it to finish. A composed page, such as the Node.js preset's
17+
`all.html`, is assembled from other pages' compiled content rather than built
18+
again from scratch.
1819

1920
## Configuring
2021

@@ -50,10 +51,13 @@ content rather than built again from scratch.
5051
JSX-in-MDX. See [`components`](#components). **Default:** `{}`.
5152
- `navigation` {Object} Sidebar groups and navigation bar items. See
5253
[`navigation`](#navigation). **Default:** `{}`.
53-
- `generateAllPage` {boolean} When `true`, writes `all.html`: every module
54-
page's content on one page, in sidebar order, assembled from the module pages
55-
rather than built again. Chunk pages and the index are left out.
56-
**Default:** `true`.
54+
- `composedPages` {Array} Pages made of other pages' content, one after the
55+
other in sidebar order, with their tables of contents: modules (paths relative
56+
to the working directory, or URLs) default-exporting `{ head, isPart }`, the
57+
page's metadata and whether a page's metadata is one of its parts. Such a
58+
page is compiled once all its parts are, and none is built again. The
59+
Node.js preset adds `all.html`, every module page minus the index.
60+
**Default:** `[]`.
5761
- `bundler` {WebBundler} Adapter that bundles the component library and the
5862
client assets, and compiles page programs. See
5963
[Bundler adapters](#bundler-adapters). **Default:** `createViteBundler()`.
@@ -482,7 +486,7 @@ export default ({ metadata }) => (
482486
- `headings` {Array} Pre-computed table of contents heading entries.
483487
- `readingTime` {string|undefined} Estimated reading time (e.g. `'5 min read'`).
484488
Only present when the `jsx-ast` generator's `showReadingTime` option is
485-
enabled. `all.html` has none.
489+
enabled. Composed pages have none.
486490
- `children` {ComponentChildren} Processed page content.
487491
488492
The `Layout` component receives the props above. Custom Layout components can use
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// A composed page of every module page, as the Node.js preset's `all.html`
2+
export default {
3+
head: {
4+
api: 'all',
5+
path: '/all',
6+
basename: 'all',
7+
synthetic: true,
8+
heading: {
9+
type: 'heading',
10+
depth: 1,
11+
children: [{ type: 'text', value: 'All' }],
12+
data: { name: 'All', text: 'All', slug: 'all' },
13+
},
14+
},
15+
isPart: data => !data.synthetic && !data.chunk && data.api !== 'index',
16+
};

‎packages/react/src/html/__tests__/generate.test.mjs‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ async function* streamInOneBatch(items) {
2929
yield items;
3030
}
3131

32+
// The Node.js preset's `all.html`, as a composed page
33+
const ALL_PAGE = import.meta.resolve('./fixtures/all-page.mjs');
34+
3235
/**
3336
* Runs the generator over the given pages, streamed in as one batch, as
3437
* `jsx-ast` yields them.
@@ -105,9 +108,6 @@ const createTestConfiguration = async (context, target = ['html']) => {
105108
},
106109
});
107110

108-
// The pages under test are the module pages themselves
109-
config.html.generateAllPage = false;
110-
111111
return { config, output };
112112
};
113113

@@ -253,7 +253,7 @@ describe('web generate', () => {
253253

254254
it('assembles all.html from the module pages, in sidebar order', async context => {
255255
const { config, output } = await createTestConfiguration(context);
256-
config.html.generateAllPage = true;
256+
config.html.composedPages = [ALL_PAGE];
257257

258258
const entries = [
259259
createEntry('zlib', 'Zlib'),
@@ -280,7 +280,7 @@ describe('web generate', () => {
280280

281281
it('numbers the ids of all.html across its parts', async context => {
282282
const { config, output } = await createTestConfiguration(context);
283-
config.html.generateAllPage = true;
283+
config.html.composedPages = [ALL_PAGE];
284284

285285
// Each module has code tabs, whose elements take ids
286286
const entries = ['zlib', 'fs'].map(api => {
@@ -556,7 +556,7 @@ describe('web generate', () => {
556556
const bundled = Promise.withResolvers();
557557

558558
config.threads = 2;
559-
config.html.generateAllPage = true;
559+
config.html.composedPages = [ALL_PAGE];
560560
config.html.bundler = createStandInBundler(calls, { bundled });
561561

562562
async function* input() {
@@ -601,7 +601,7 @@ describe('web generate', () => {
601601
const { config, output } = await createTestConfiguration(context);
602602

603603
config.threads = threads;
604-
config.html.generateAllPage = true;
604+
config.html.composedPages = [ALL_PAGE];
605605
config.html.bundler = createStandInBundler([]);
606606

607607
await generatePages(input);

‎packages/react/src/html/generate.mjs‎

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import logger from '@doc-kit/core/logger/index.mjs';
1010
import { createChannel } from '@doc-kit/core/utils/channel.mjs';
1111
import getConfig from '@doc-kit/core/utils/configuration/index.mjs';
1212
import { toBatches } from '@doc-kit/core/utils/generators.mjs';
13+
import { importFromURL } from '@doc-kit/core/utils/loaders.mjs';
1314

1415
import { headingSortFn } from '../jsx-ast/utils/getSortedHeadNodes.mjs';
1516
import { resolveBundler } from './bundlers/index.mjs';
16-
import { buildAllPage, createAllPageHead, isModulePage } from './utils/all.mjs';
17+
import { createComposedPages } from './utils/composed.mjs';
1718
import { copyStaticAssets } from './utils/copying.mjs';
1819
import createProgramBuilder, { moduleFileName } from './utils/generate.mjs';
1920
import { createPageWriter } from './utils/render.mjs';
@@ -28,8 +29,8 @@ const htmlLogger = logger.child('html');
2829
* page's program is compiled to a plain module as it arrives, the component
2930
* library and the client assets are bundled once every page's metadata is
3031
* known, and the worker pool renders the pages as they are compiled, one in
31-
* memory per worker. `all.html` imports the module pages' content, so it is
32-
* compiled once they all are. With one thread, the input arrives whole.
32+
* memory per worker. A composed page (see `composedPages`) imports the content
33+
* of the pages it is made of, so it is compiled once they all are. With one thread, the input arrives whole.
3334
*
3435
* @type {import('./types').Generator['generate']}
3536
*/
@@ -138,7 +139,7 @@ const buildSite = async (input, options) => {
138139
const datas = [
139140
...heads.filter(data => !data.synthetic).toSorted(headingSortFn),
140141
...heads.filter(data => data.synthetic),
141-
...(config.generateAllPage ? [createAllPageHead()] : []),
142+
...composed.heads,
142143
];
143144

144145
// An empty directory of its own: the compiled pages are already in `outDir`
@@ -191,22 +192,11 @@ const buildSite = async (input, options) => {
191192

192193
const compiled = [];
193194

194-
let allWritten = !config.generateAllPage;
195+
const definitions = await Promise.all(
196+
config.composedPages.map(module => importFromURL(module))
197+
);
195198

196-
// The module pages `all.html` is made of that aren't compiled yet
197-
let missingParts;
198-
199-
/**
200-
* Compiles `all.html`, which imports the module pages' compiled programs,
201-
* and has the worker pool render it first, as it takes by far the longest.
202-
*/
203-
const writeAllPage = async () => {
204-
const allTask = await compile(buildAllPage(compiled));
205-
206-
rendering.prepend([allTask]);
207-
208-
allWritten = true;
209-
};
199+
const composed = createComposedPages(definitions, { compile, rendering });
210200

211201
// With one thread, pages are rendered on this thread once all are compiled,
212202
// so the manifest is kept until then
@@ -232,13 +222,7 @@ const buildSite = async (input, options) => {
232222
manifest = item.manifest;
233223
}
234224

235-
missingParts = new Set(
236-
item.manifest.filter(isModulePage).map(({ api }) => api)
237-
);
238-
239-
for (const { data } of compiled) {
240-
missingParts.delete(data.api);
241-
}
225+
composed.expect(item.manifest);
242226

243227
continue;
244228
}
@@ -247,17 +231,14 @@ const buildSite = async (input, options) => {
247231

248232
compiled.push(task);
249233
tasks.push(task);
250-
251-
missingParts?.delete(task.data.api);
234+
composed.add(task);
252235
}
253236

254237
if (tasks.length > 0) {
255238
rendering.push(tasks);
256239
}
257240

258-
if (!allWritten && missingParts?.size === 0) {
259-
await writeAllPage();
260-
}
241+
await composed.write();
261242
}
262243
};
263244

@@ -275,9 +256,7 @@ const buildSite = async (input, options) => {
275256
manifest = undefined;
276257
}
277258

278-
if (!allWritten) {
279-
await writeAllPage();
280-
}
259+
await composed.write(true);
281260

282261
htmlLogger.debug(`Compiled ${compiled.length} page programs`);
283262
} catch (error) {

0 commit comments

Comments
 (0)