Skip to content

Commit 5decb29

Browse files
committed
fixup!
1 parent 7d9abc6 commit 5decb29

15 files changed

Lines changed: 386 additions & 38 deletions

File tree

.github/workflows/generate.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ jobs:
106106
input: './node/doc/api/*.md'
107107
compare: file-size
108108

109+
- target: web-all
110+
input: './node/doc/api/*.md'
111+
109112
- target: llms-txt
110113
input: './node/doc/api/*.md'
111114
compare: file-size

scripts/vercel-build.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ node bin/cli.mjs generate \
33
-t legacy-json \
44
-t llms-txt \
55
-t web \
6+
-t web-all \
67
-i "./node/doc/api/*.md" \
78
-o "./out" \
89
-c "./node/CHANGELOG.md" \

src/generators/web-all/README.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
## `web-all` Generator
22

3-
The `web-all` generator creates a single `all.html` file containing all API documentation modules rendered through the `web` generator pipeline. This is the modern equivalent of `legacy-html-all` for the new web tooling.
3+
The `web-all` generator complements `web` by producing the pages that the
4+
per-module pipeline does not generate on its own:
45

5-
It is intended for offline browsing and for environments where JavaScript is unavailable: the in-page searchbar shipped with `web` requires JS, so a single all-in-one page makes browser-native text search (`Ctrl`+`F`) usable across the full documentation set.
6+
- `all.html`: every API module concatenated into a single page. Useful for
7+
offline browsing and for environments where JavaScript is unavailable, since
8+
the in-page searchbar shipped with `web` requires JS, so a single all-in-one
9+
page makes browser-native text search (`Ctrl`+`F`) usable across the full
10+
documentation set.
11+
- `index.html`: a synthetic landing page listing every module alongside a
12+
Stability Overview table. The `web` generator skips its `index` entry so
13+
this file is the canonical index.
14+
- `404.html`: a static not-found page wired into the same Layout, so hosts
15+
serving the docs can use it as a fallback.
616

717
### Configuring
818

919
The `web-all` generator accepts the following configuration options:
1020

11-
| Name | Type | Default | Description |
12-
| -------------- | -------- | -------------------- | ---------------------------------------------- |
13-
| `output` | `string` | - | The directory where `all.html` will be written |
14-
| `templatePath` | `string` | Inherited from `web` | Path to the HTML template file |
21+
| Name | Type | Default | Description |
22+
| -------------- | -------- | -------------------- | ------------------------------ |
23+
| `templatePath` | `string` | Inherited from `web` | Path to the HTML template file |

src/generators/web-all/generate.mjs

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,43 @@
33
import { readFile } from 'node:fs/promises';
44
import { join } from 'node:path';
55

6+
import { buildNotFoundPage } from './utils/404.mjs';
7+
import { buildAllPage } from './utils/all.mjs';
8+
import { buildIndexPage } from './utils/index.mjs';
69
import getConfig from '../../utils/configuration/index.mjs';
710
import { writeFile } from '../../utils/file.mjs';
811
import buildContent from '../jsx-ast/utils/buildContent.mjs';
912
import { processJSXEntries } from '../web/utils/processing.mjs';
1013

1114
/**
12-
* Generates a single `all.html` file containing every API documentation
13-
* module rendered through the `web` generator pipeline.
15+
* Generates the `web-all` output: `all.html`, `index.html`, and `404.html`.
1416
*
1517
* @type {import('./types').Generator['generate']}
1618
*/
1719
export async function generate(input) {
1820
const config = getConfig('web-all');
19-
2021
const template = await readFile(config.templatePath, 'utf-8');
2122

22-
// Match `legacy-html-all`: skip the synthetic `index` entries.
23+
// Drop the synthetic `index` entry — we re-generate `index.html` ourselves.
2324
const entries = input.filter(entry => entry.api !== 'index');
2425

25-
// Build a single combined JSXContent that wraps every entry's content in
26-
// one Layout component, mirroring how `jsx-ast` produces per-module pages.
27-
const combined = await buildContent(entries, {
28-
api: 'all',
29-
path: '/all',
30-
basename: 'all.html',
31-
heading: {
32-
type: 'heading',
33-
depth: 1,
34-
children: [{ type: 'text', value: 'All' }],
35-
data: { name: 'All', text: 'All', slug: 'all' },
36-
},
37-
});
38-
39-
// Pass the original metadata entries as the sidebar source so `all.html`
40-
// exposes the same navigation as the per-module pages produced by `web`.
26+
const pages = [
27+
buildAllPage(entries),
28+
buildIndexPage(entries),
29+
buildNotFoundPage(),
30+
];
31+
32+
const jsxContents = await Promise.all(
33+
pages.map(({ head, entries: pageEntries }) =>
34+
buildContent(pageEntries, head)
35+
)
36+
);
37+
38+
// Sidebar still needs to link to every module page produced by `web`.
4139
const sidebarEntries = entries.map(entry => ({ data: entry }));
4240

4341
const { results, css, chunks } = await processJSXEntries(
44-
[combined],
42+
jsxContents,
4543
template,
4644
sidebarEntries
4745
);
@@ -58,5 +56,8 @@ export async function generate(input) {
5856
await writeFile(join(config.output, 'styles.css'), css, 'utf-8');
5957
}
6058

61-
return { html: results[0].html.toString(), css };
59+
return {
60+
html: results.map(({ html }) => html.toString()),
61+
css,
62+
};
6263
}

src/generators/web-all/index.mjs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@ import { createLazyGenerator } from '../../utils/generators.mjs';
44
import web from '../web/index.mjs';
55

66
/**
7-
* Web "all" generator - produces a single `all.html` page that contains every
8-
* API documentation module rendered into one combined web bundle.
9-
*
10-
* Mirrors the role of `legacy-html-all` for the new `web` generator. Useful
11-
* for offline browsing and for reading the full documentation in environments
12-
* where JavaScript is unavailable, since the `web` searchbar requires JS.
137
*
148
* @type {import('./types').Generator}
159
*/
@@ -18,8 +12,7 @@ export default createLazyGenerator({
1812

1913
version: '1.0.0',
2014

21-
description:
22-
'Generates the `all.html` file from the `web` generator, which includes all the modules in one single file',
15+
description: 'Generates the additional files from the `web` generator',
2316

2417
dependsOn: 'metadata',
2518

src/generators/web-all/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ export type Configuration = {
66

77
export type Generator = GeneratorMetadata<
88
Configuration,
9-
Generate<Array<MetadataEntry>, Promise<{ html: string; css: string }>>
9+
Generate<Array<MetadataEntry>, Promise<{ html: Array<string>; css: string }>>
1010
>;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
import { createSyntheticHead, wrapAsEntry } from './synthetic.mjs';
4+
5+
/**
6+
* Builds the page descriptor for `404.html`
7+
*/
8+
export const buildNotFoundPage = () => {
9+
const head = createSyntheticHead('404', 'Page Not Found');
10+
11+
return {
12+
head,
13+
entries: [
14+
wrapAsEntry(head, [
15+
{
16+
type: 'paragraph',
17+
children: [
18+
{
19+
type: 'text',
20+
value:
21+
'The page you requested could not be found. Use the navigation to find the documentation you are looking for.',
22+
},
23+
],
24+
},
25+
]),
26+
],
27+
};
28+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
4+
import { buildNotFoundPage } from '../404.mjs';
5+
6+
describe('buildNotFoundPage', () => {
7+
it('uses a `404` head with a "Page Not Found" heading', () => {
8+
const { head } = buildNotFoundPage();
9+
10+
assert.equal(head.api, '404');
11+
assert.equal(head.path, '/404');
12+
assert.equal(head.basename, '404');
13+
assert.equal(head.heading.data.name, 'Page Not Found');
14+
});
15+
16+
it('produces a single synthetic entry with a not-found paragraph', () => {
17+
const { entries } = buildNotFoundPage();
18+
19+
assert.equal(entries.length, 1);
20+
21+
const paragraph = entries[0].content.children.find(
22+
child => child.type === 'paragraph'
23+
);
24+
25+
assert.ok(paragraph, 'expected a paragraph node in the content tree');
26+
assert.match(paragraph.children[0].value, /could not be found/);
27+
});
28+
29+
it('places the head heading at the start of the content tree', () => {
30+
const { head, entries } = buildNotFoundPage();
31+
32+
assert.equal(entries[0].content.children[0], head.heading);
33+
});
34+
35+
it('returns the same shape on every call', () => {
36+
const a = buildNotFoundPage();
37+
const b = buildNotFoundPage();
38+
39+
assert.deepEqual(a.head, b.head);
40+
assert.equal(a.entries.length, b.entries.length);
41+
});
42+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
4+
import { buildAllPage } from '../all.mjs';
5+
6+
describe('buildAllPage', () => {
7+
it('returns a synthetic `all` head with an "All" heading', () => {
8+
const { head } = buildAllPage([]);
9+
10+
assert.equal(head.api, 'all');
11+
assert.equal(head.path, '/all');
12+
assert.equal(head.basename, 'all');
13+
assert.equal(head.heading.data.name, 'All');
14+
});
15+
16+
it('forwards the input entries as the page entries', () => {
17+
const a = { api: 'fs', heading: { depth: 1, data: {} } };
18+
const b = { api: 'http', heading: { depth: 1, data: {} } };
19+
20+
const { entries } = buildAllPage([a, b]);
21+
22+
assert.deepEqual(entries, [a, b]);
23+
});
24+
25+
it('does not mutate the input array', () => {
26+
const input = [{ api: 'fs' }];
27+
28+
buildAllPage(input);
29+
30+
assert.equal(input.length, 1);
31+
});
32+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
4+
import { buildStabilityOverview } from '../index.mjs';
5+
6+
const fakeHead = (api, name, stabilityIndex, depth = 1) => ({
7+
api,
8+
heading: { depth, data: { name, text: name, slug: api } },
9+
stability:
10+
stabilityIndex == null
11+
? null
12+
: {
13+
data: {
14+
index: String(stabilityIndex),
15+
description: `${name} stable. Long-form description.`,
16+
},
17+
},
18+
});
19+
20+
const findChild = (node, tagName) =>
21+
node.children.find(child => child.tagName === tagName);
22+
23+
describe('buildStabilityOverview', () => {
24+
it('renders a header row and one body row per entry', () => {
25+
const table = buildStabilityOverview([
26+
fakeHead('fs', 'fs', 2),
27+
fakeHead('crypto', 'crypto', 1),
28+
]);
29+
30+
assert.equal(table.tagName, 'table');
31+
const headerRow = findChild(findChild(table, 'thead'), 'tr');
32+
assert.deepEqual(
33+
headerRow.children.map(c => c.children[0].value),
34+
['API', 'Stability']
35+
);
36+
37+
assert.equal(findChild(table, 'tbody').children.length, 2);
38+
});
39+
40+
it('formats the stability cell as `(index) <first sentence>`', () => {
41+
const table = buildStabilityOverview([fakeHead('fs', 'fs', 1)]);
42+
43+
const row = findChild(table, 'tbody').children[0];
44+
const stabilityCell = row.children[1];
45+
46+
assert.equal(stabilityCell.children[0].value, '(1) fs stable');
47+
});
48+
49+
it('builds a relative link to the module HTML page', () => {
50+
const table = buildStabilityOverview([fakeHead('fs', 'fs', 2)]);
51+
52+
const row = findChild(table, 'tbody').children[0];
53+
const link = row.children[0].children[0];
54+
55+
assert.equal(link.tagName, 'a');
56+
assert.equal(link.properties.href, 'fs.html');
57+
assert.equal(link.children[0].value, 'fs');
58+
});
59+
60+
it('renders an empty body when no entries are passed', () => {
61+
const table = buildStabilityOverview([]);
62+
63+
assert.equal(findChild(table, 'tbody').children.length, 0);
64+
});
65+
});

0 commit comments

Comments
 (0)