Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 2 additions & 36 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/generators/jsx-ast/generate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export async function processChunk(slicedInput, itemIndices) {

const content = await buildContent(entries, head);

// Preserve the raw section entries so downstream generators (e.g. `web`)
// can build synthetic pages (all.html, index.html) without recomputing
// metadata.
content.sectionEntries = entries;

results.push(content);
}

Expand Down
25 changes: 14 additions & 11 deletions src/generators/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ The `web` generator transforms JSX AST entries into complete web bundles, produc

The `web` generator accepts the following configuration options:

| Name | Type | Default | Description |
| ----------------- | --------- | --------------------------------------------- | --------------------------------------------------------------------- |
| `output` | `string` | - | The directory where HTML, JavaScript, and CSS files will be written |
| `templatePath` | `string` | `'template.html'` | Path to the HTML template file |
| `project` | `string` | `'Node.js'` | Project name used in page titles and the version selector |
| `title` | `string` | `'{project} v{version} Documentation'` | Title template for HTML pages (supports `{project}`, `{version}`) |
| `useAbsoluteURLs` | `boolean` | `false` | When `true`, all internal links use absolute URLs based on `baseURL` |
| `editURL` | `string` | `'${GITHUB_EDIT_URL}/doc/api{path}.md'` | URL template for "edit this page" links |
| `pageURL` | `string` | `'{baseURL}/latest-{version}/api{path}.html'` | URL template for documentation page links |
| `imports` | `object` | See below | Object mapping `#theme/` aliases to component paths for customization |
| `virtualImports` | `object` | `{}` | Additional virtual module mappings merged into the build |
| Name | Type | Default | Description |
| ---------------------- | --------- | --------------------------------------------- | --------------------------------------------------------------------- |
| `output` | `string` | - | The directory where HTML, JavaScript, and CSS files will be written |
| `templatePath` | `string` | `'template.html'` | Path to the HTML template file |
| `project` | `string` | `'Node.js'` | Project name used in page titles and the version selector |
| `title` | `string` | `'{project} v{version} Documentation'` | Title template for HTML pages (supports `{project}`, `{version}`) |
| `useAbsoluteURLs` | `boolean` | `false` | When `true`, all internal links use absolute URLs based on `baseURL` |
| `editURL` | `string` | `'${GITHUB_EDIT_URL}/doc/api{path}.md'` | URL template for "edit this page" links |
| `pageURL` | `string` | `'{baseURL}/latest-{version}/api{path}.html'` | URL template for documentation page links |
| `generateAllPage` | `boolean` | `true` | When `true`, emits `all.html` containing every module's content |
| `generateIndexPage` | `boolean` | `true` | When `true`, emits `index.html` with a stability overview |
| `generateNotFoundPage` | `boolean` | `true` | When `true`, emits `404.html` |
| `imports` | `object` | See below | Object mapping `#theme/` aliases to component paths for customization |
| `virtualImports` | `object` | `{}` | Additional virtual module mappings merged into the build |

#### Default `imports`

Expand Down
4 changes: 4 additions & 0 deletions src/generators/web/constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const JSX_IMPORTS = {
name: 'AlertBox',
source: '@node-core/ui-components/Common/AlertBox',
},
Badge: {
name: 'Badge',
source: '@node-core/ui-components/Common/Badge',
},
Blockquote: {
name: 'Blockquote',
source: '@node-core/ui-components/Common/Blockquote',
Expand Down
55 changes: 49 additions & 6 deletions src/generators/web/generate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,78 @@ import { readFile } from 'node:fs/promises';
import { join } from 'node:path';

import { processJSXEntries } from './utils/processing.mjs';
import { buildNotFoundPage } from './utils/synthetic/404.mjs';
import { buildAllPage } from './utils/synthetic/all.mjs';
import { buildIndexPage } from './utils/synthetic/index.mjs';
import getConfig from '../../utils/configuration/index.mjs';
import { writeFile } from '../../utils/file.mjs';
import buildContent from '../jsx-ast/utils/buildContent.mjs';

/**
* Main generation function that processes JSX AST entries into web bundles.
*
* Generates the regular per-module pages plus, when enabled by configuration,
* the synthetic `all`, `index`, and `404` pages. Everything is bundled in a
* single pass so shared component chunks and CSS are produced once.
*
* @type {import('./types').Generator['generate']}
*/
export async function generate(input) {
const config = getConfig('web');

const template = await readFile(config.templatePath, 'utf-8');

// Process all entries: convert JSX to HTML/CSS/JS
const { results, css, chunks } = await processJSXEntries(input, template);
// The synthetic `index` entry from Core is replaced by our own
// `index.html` (with stability overview) when `generateIndexPage` is on.
//
// TODO(@avivkeller): Once this lands in core, remove the `index.html`
// page from Core, then remove this check.
const moduleEntries = input.filter(entry => entry.data.api !== 'index');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Index entry unconditionally removed regardless of config

Medium Severity

The filter input.filter(entry => entry.data.api !== 'index') unconditionally removes Core's index entry from the pipeline. However, the synthetic replacement index page is only generated when config.generateIndexPage is true. If a user sets generateIndexPage: false, the Core index entry is still removed but no replacement is created, resulting in no index.html in the output at all. The comment even states this replacement happens "when generateIndexPage is on", but the filtering isn't gated by that condition.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 468b6ce. Configure here.


// Reconstruct the flat metadata list from the per-module section entries
// attached by `jsx-ast`. Used to build the synthetic `all` and `index`
// pages without a separate `metadata` dependency.
const synthDescriptors = [];

if (config.generateAllPage || config.generateIndexPage) {
const metadata = moduleEntries.flatMap(entry => entry.sectionEntries);

if (config.generateAllPage) {
synthDescriptors.push(buildAllPage(metadata));
}
if (config.generateIndexPage) {
synthDescriptors.push(buildIndexPage(metadata));
}
}

if (config.generateNotFoundPage) {
synthDescriptors.push(buildNotFoundPage());
}

const syntheticEntries = await Promise.all(
synthDescriptors.map(({ head, entries }) => buildContent(entries, head))
);

// Sidebar lists only the real module pages.
const sidebarEntries = moduleEntries.map(entry => ({ data: entry.data }));

const allEntries = [...moduleEntries, ...syntheticEntries];

const { results, css, chunks } = await processJSXEntries(
allEntries,
template,
sidebarEntries
);

// Process all entries together (required for code-split bundles)
if (config.output) {
// Write HTML files
for (const { html, path } of results) {
await writeFile(join(config.output, `${path}.html`), html, 'utf-8');
}

// Write code-split JavaScript chunks
for (const chunk of chunks) {
await writeFile(join(config.output, chunk.fileName), chunk.code, 'utf-8');
}

// Write CSS bundle
await writeFile(join(config.output, 'styles.css'), css, 'utf-8');
}

Expand Down
3 changes: 3 additions & 0 deletions src/generators/web/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export default createLazyGenerator({
editURL: `${GITHUB_EDIT_URL}/doc/api{path}.md`,
pageURL: '{baseURL}/latest-{version}/api{path}.html',
remoteConfigUrl: 'https://nodejs.org/site.json',
generateAllPage: true,
generateIndexPage: true,
generateNotFoundPage: true,
imports: {
'#theme/Logo': '@node-core/ui-components/Common/NodejsLogo',
'#theme/Navigation': join(import.meta.dirname, './ui/components/NavBar'),
Expand Down
5 changes: 4 additions & 1 deletion src/generators/web/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ export type Configuration = {
templatePath: string;
title: string;
useAbsoluteURLs: boolean;
generateAllPage: boolean;
generateIndexPage: boolean;
generateNotFoundPage: boolean;
imports: Record<string, string>;
virtualImports: Record<string, string>;
};

export type Generator = GeneratorMetadata<
Configuration,
Generate<Array<JSXContent>, AsyncGenerator<{ html: string; css: string }>>
Generate<Array<JSXContent>, Promise<Array<{ html: string; css: string }>>>
>;
9 changes: 7 additions & 2 deletions src/generators/web/utils/processing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,18 @@ async function executeServerCode(serverCodeMap, requireFn, virtualImports) {
*
* @param {Array<import('../../jsx-ast/utils/buildContent.mjs').JSXContent>} entries - The JSX AST entries to process.
* @param {string} template - The HTML template string for the output pages.
* @param {Array<{ data: import('../../metadata/types').MetadataEntry }>} [sidebarEntries] - Entries used to build the sidebar page list. Defaults to `entries`. Pass the full set when rendering a subset (e.g. the `all` page) so the sidebar still links to every module.
*/
export async function processJSXEntries(entries, template) {
export async function processJSXEntries(
entries,
template,
sidebarEntries = entries
) {
const config = getConfig('web');
const astBuilders = createASTBuilder();
const requireFn = createRequire(import.meta.url);
const virtualImports = {
'#theme/config': createConfigSource(entries),
'#theme/config': createConfigSource(sidebarEntries),
...config.virtualImports,
};
// Step 1: Convert JSX AST to JavaScript
Expand Down
37 changes: 37 additions & 0 deletions src/generators/web/utils/synthetic/404.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

import { createSyntheticHead, wrapAsEntry } from './synthetic.mjs';

/**
* Builds the page descriptor for `404.html`
*/
export const buildNotFoundPage = () => {
const head = createSyntheticHead('404', 'Page Not Found');

return {
head,
entries: [
wrapAsEntry(head, [
Comment thread
avivkeller marked this conversation as resolved.
{
type: 'paragraph',
children: [
{
type: 'text',
value:
'The page you requested could not be found. Use the navigation to find the documentation you are looking for, or return to the ',
},
{
type: 'link',
url: 'index.html',
children: [{ type: 'text', value: 'API index' }],
},
{
type: 'text',
value: '.',
},
],
},
]),
],
};
};
Loading
Loading