Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@node-core/doc-kit",
"type": "module",
"version": "1.3.1",
"version": "1.3.2",
"repository": {
"type": "git",
"url": "git+https://github.com/nodejs/doc-kit.git"
Expand Down
23 changes: 14 additions & 9 deletions src/generators/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ 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 |
| `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 |
| 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}`) |
Comment thread
avivkeller marked this conversation as resolved.
| `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 |

#### Default `imports`

Expand Down Expand Up @@ -42,14 +45,16 @@ export default {
The `web` generator provides a `#theme/config` virtual module that exposes pre-computed configuration as named exports. Any component (including custom overrides) can import the values it needs, and tree-shaking removes the rest.

```js
import { title, repository, editURL } from '#theme/config';
import { project, repository, editURL } from '#theme/config';
```

#### Available exports

All scalar (non-object) configuration values are automatically exported. The defaults include:

| Export | Type | Description |
| ------------------------ | ------------------------------ | --------------------------------------------------------------------------------------------------- |
| `title` | `string` | Site title (e.g. `'Node.js'`) |
| `project` | `string` | Project name (e.g. `'Node.js'`) |
| `repository` | `string` | GitHub repository in `owner/repo` format |
| `version` | `string` | Current version label (e.g. `'v22.x'`) |
| `versions` | `Array<{ url, label, major }>` | Pre-computed version entries with labels and URL templates (only `{path}` remains for per-page use) |
Expand Down
4 changes: 3 additions & 1 deletion src/generators/web/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export default createLazyGenerator({

defaultConfiguration: {
templatePath: join(import.meta.dirname, 'template.html'),
title: 'Node.js',
project: 'Node.js',
title: '{project} v{version} Documentation',
Comment thread
avivkeller marked this conversation as resolved.
editURL: `${GITHUB_EDIT_URL}/doc/api{path}.md`,
pageURL: '{baseURL}/latest-{version}/api{path}.html',
imports: {
Expand All @@ -40,5 +41,6 @@ export default createLazyGenerator({
'#theme/Footer': join(import.meta.dirname, './ui/components/NoOp'),
'#theme/Layout': join(import.meta.dirname, './ui/components/Layout'),
},
virtualImports: {},
},
});
14 changes: 7 additions & 7 deletions src/generators/web/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://nodejs.org/static/images/favicons/favicon.png"/>
<title>{{title}}</title>
<title>{title}</title>
<meta name="description" content="Node.js® is a free, open-source, cross-platform JavaScript runtime environment that lets developers create servers, web apps, command line tools and scripts.">
<link rel="stylesheet" href="{{root}}styles.css" />
<meta property="og:title" content="{{ogTitle}}">
<link rel="stylesheet" href="{root}styles.css" />
<meta property="og:title" content="{title}">
<meta property="og:description" content="Node.js® is a free, open-source, cross-platform JavaScript runtime environment that lets developers create servers, web apps, command line tools and scripts.">
<meta property="og:image" content="https://nodejs.org/en/next-data/og/announcement/Node.js%20%E2%80%94%20Run%20JavaScript%20Everywhere" />
<meta property="og:type" content="website">
Expand All @@ -20,12 +20,12 @@

<!-- Apply theme before paint to avoid Flash of Unstyled Content -->
<script>document.documentElement.setAttribute("data-theme", document.documentElement.style.colorScheme = localStorage.getItem("theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"));</script>
<script type="importmap">{{importMap}}</script>
<script type="speculationrules">{{speculationRules}}</script>
<script type="importmap">{importMap}</script>
<script type="speculationrules">{speculationRules}</script>
</head>

<body>
<div id="root">{{dehydrated}}</div>
<script type="module" src="{{root}}{{entrypoint}}"></script>
<div id="root">{dehydrated}</div>
<script type="module" src="{root}{entrypoint}"></script>
</body>
</html>
1 change: 1 addition & 0 deletions src/generators/web/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type Generator = GeneratorMetadata<
templatePath: string;
title: string;
imports: Record<string, string>;
virtualImports: Record<string, string>;
},
Generate<Array<JSXContent>, AsyncGenerator<{ html: string; css: string }>>
>;
4 changes: 2 additions & 2 deletions src/generators/web/ui/components/NavBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import GitHubIcon from '@node-core/ui-components/Icons/Social/GitHub';
import SearchBox from './SearchBox';
import { useTheme } from '../hooks/useTheme.mjs';

import { title, repository } from '#theme/config';
import { repository } from '#theme/config';
import Logo from '#theme/Logo';

/**
Expand All @@ -28,7 +28,7 @@ export default ({ metadata }) => {
/>
<a
href={`https://github.com/${repository}`}
aria-label={`${title} GitHub`}
aria-label={`View ${repository} on GitHub`}
className={styles.ghIconWrapper}
>
<GitHubIcon />
Expand Down
10 changes: 9 additions & 1 deletion src/generators/web/ui/components/SearchBox/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import SearchResults from '@node-core/ui-components/Common/Search/Results';
import SearchHit from '@node-core/ui-components/Common/Search/Results/Hit';

import styles from './index.module.css';
import { relative } from '../../../../../utils/url.mjs';
import useOrama from '../../hooks/useOrama.mjs';

const SearchBox = ({ pathname }) => {
Expand All @@ -18,7 +19,14 @@ const SearchBox = ({ pathname }) => {
<div className={styles.searchResultsContainer}>
<SearchResults
noResultsTitle="No results found for"
onHit={hit => <SearchHit document={hit.document} />}
onHit={hit => (
<SearchHit
document={{
...hit.document,
href: relative(hit.document.href, pathname),
}}
/>
)}
/>
</div>

Expand Down
4 changes: 2 additions & 2 deletions src/generators/web/ui/components/SideBar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import SideBar from '@node-core/ui-components/Containers/Sidebar';
import styles from './index.module.css';
import { relative } from '../../../../../utils/url.mjs';

import { title, version, versions, pages } from '#theme/config';
import { project, version, versions, pages } from '#theme/config';

/**
* Extracts the major version number from a version string.
Expand Down Expand Up @@ -54,7 +54,7 @@ export default ({ metadata }) => {
>
<div>
<Select
label={`${title} version`}
label={`${project} version`}
values={compatibleVersions}
inline={true}
className={styles.select}
Expand Down
30 changes: 10 additions & 20 deletions src/generators/web/utils/__tests__/config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,11 @@ const makeEntry = (api, name, path) => ({

describe('buildVersionEntries', () => {
it('creates version entries with labels and URL templates', () => {
const config = {
changelog: [
const result = buildVersionEntries(
[
{ version: new SemVer('20.0.0'), isLts: true, isCurrent: false },
{ version: new SemVer('22.0.0'), isLts: false, isCurrent: true },
],
};

const result = buildVersionEntries(
config,
'https://nodejs.org/docs/latest-{version}/api{path}.html'
);

Expand All @@ -75,25 +71,19 @@ describe('buildVersionEntries', () => {
});

it('does not append a label suffix for versions that are neither LTS nor Current', () => {
const config = {
changelog: [
{ version: new SemVer('18.0.0'), isLts: false, isCurrent: false },
],
};

const result = buildVersionEntries(config, '{version}');
const result = buildVersionEntries(
[{ version: new SemVer('18.0.0'), isLts: false, isCurrent: false }],
'{version}'
);

assert.equal(result[0].label, 'v18.x');
});

it('formats minor versions when minor is non-zero', () => {
Comment thread
avivkeller marked this conversation as resolved.
const config = {
changelog: [
{ version: new SemVer('21.7.0'), isLts: false, isCurrent: false },
],
};

const result = buildVersionEntries(config, '{version}');
const result = buildVersionEntries(
[{ version: new SemVer('21.7.0'), isLts: false, isCurrent: false }],
'{version}'
);

assert.equal(result[0].label, 'v21.7.x');
assert.equal(result[0].major, 21);
Expand Down
51 changes: 25 additions & 26 deletions src/generators/web/utils/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import { getSortedHeadNodes } from '../../jsx-ast/utils/getSortedHeadNodes.mjs';
* Pre-compute version entries with labels and URL templates.
* Each entry's `url` still contains `{path}` for per-page resolution.
*
* @param {object} config
* @param {object} changelog
Comment thread
avivkeller marked this conversation as resolved.
* @param {string} pageURLBase
* @returns {Array<{url: string, label: string, major: number}>}
*/
export function buildVersionEntries(config, pageURLBase) {
return config.changelog.map(({ version, isLts, isCurrent }) => {
export function buildVersionEntries(changelog, pageURLBase) {
return changelog.map(({ version, isLts, isCurrent }) => {
let label = `v${getVersionFromSemVer(version)}`;
const url = pageURLBase.replace('{version}', label);
if (isLts) {
Expand Down Expand Up @@ -72,32 +72,31 @@ export function buildLanguageDisplayNameMap() {
* @returns {string} JavaScript source code string with named exports
*/
export default function createConfigSource(input) {
const config = getConfig('web');
const { version: configVersion, ...config } = getConfig('web');

const currentVersion = `v${config.version.version}`;
const templateVars = { ...config, version: currentVersion };
const version = `v${configVersion.version}`;
const editURL = populate(config.editURL, { ...config, version });
const pageURL = populate(config.pageURL, config);
Comment thread
cursor[bot] marked this conversation as resolved.

// Partially populate URL templates: resolve config-level placeholders,
// leave {path} for per-page resolution by components
const editURL = populate(config.editURL, templateVars);
const exports = {
...Object.fromEntries(
Object.entries(config).filter(
([, v]) => v === null || typeof v !== 'object'
)
),
Comment thread
avivkeller marked this conversation as resolved.
version,
versions: buildVersionEntries(config.changelog, pageURL),
editURL,
pages: buildPageList(input),
};

// Resolve the pageURL template once with config-level values, leaving
// {version} and {path} as the only remaining placeholders
// eslint-disable-next-line no-unused-vars
const { version, ...configWithoutVersion } = config;
const pageURLBase = populate(config.pageURL, configWithoutVersion);
const lines = Object.entries(exports).map(
([k, v]) => `export const ${k} = ${JSON.stringify(v)};`
);

const versions = buildVersionEntries(config, pageURLBase);
const pages = buildPageList(input);
const shikiDisplayNameMap = buildLanguageDisplayNameMap();
lines.push(
Comment thread
avivkeller marked this conversation as resolved.
`export const languageDisplayNameMap = new Map(${JSON.stringify(buildLanguageDisplayNameMap())});`
);

return [
`export const title = ${JSON.stringify(config.title)};`,
`export const repository = ${JSON.stringify(config.repository)};`,
`export const version = ${JSON.stringify(currentVersion)};`,
`export const versions = ${JSON.stringify(versions)};`,
`export const editURL = ${JSON.stringify(editURL)};`,
`export const pages = ${JSON.stringify(pages)};`,
`export const languageDisplayNameMap = new Map(${JSON.stringify(shikiDisplayNameMap)});`,
].join('\n');
return lines.join('\n');
}
30 changes: 16 additions & 14 deletions src/generators/web/utils/processing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createChunkedRequire } from './chunks.mjs';
import createConfigSource from './config.mjs';
import createASTBuilder from './generate.mjs';
import getConfig from '../../../utils/configuration/index.mjs';
import { populate } from '../../../utils/configuration/templates.mjs';
import { minifyHTML } from '../../../utils/html-minifier.mjs';
import { relative } from '../../../utils/url.mjs';
import { SPECULATION_RULES } from '../constants.mjs';
Expand Down Expand Up @@ -79,11 +80,12 @@ async function executeServerCode(serverCodeMap, requireFn, virtualImports) {
* @param {string} template - The HTML template string for the output pages.
*/
export async function processJSXEntries(entries, template) {
const { version } = getConfig('web');
const config = getConfig('web');
const astBuilders = createASTBuilder();
const requireFn = createRequire(import.meta.url);
const virtualImports = {
'#theme/config': createConfigSource(entries),
...config.virtualImports,
Comment thread
avivkeller marked this conversation as resolved.
};
// Step 1: Convert JSX AST to JavaScript
const { serverCodeMap, clientCodeMap } = convertJSXToCode(
Expand All @@ -98,26 +100,26 @@ export async function processJSXEntries(entries, template) {
bundleCode(clientCodeMap, virtualImports),
]);

const titleSuffix = `Node.js v${version.version} Documentation`;
const titleSuffix = populate(config.title, {
...config,
version: config.version.version,
});

// Step 3: Render final HTML pages
const results = await Promise.all(
entries.map(async ({ data: { api, path, heading } }) => {
const title = `${heading.data.name} | ${titleSuffix}`;
const root = `${relative('/', path)}/`;

// Replace template placeholders with actual content
const renderedHtml = template
.replace('{{title}}', title)
.replace('{{dehydrated}}', serverBundle.pages.get(`${api}.js`) ?? '')
.replace(
'{{importMap}}',
clientBundle.importMap?.replaceAll('/', root) ?? ''
)
.replace('{{entrypoint}}', `${api}.js?${randomUUID()}`)
.replace('{{speculationRules}}', SPECULATION_RULES)
.replace('{{ogTitle}}', title)
.replaceAll('{{root}}', root);
const renderedHtml = populate(template, {
title: `${heading.data.name} | ${titleSuffix}`,
dehydrated: serverBundle.pages.get(`${api}.js`) ?? '',
importMap: clientBundle.importMap?.replaceAll('/', root) ?? '',
entrypoint: `${api}.js?${randomUUID()}`,
Comment thread
avivkeller marked this conversation as resolved.
speculationRules: SPECULATION_RULES,
root,
path,
});

return { html: await minifyHTML(renderedHtml), path };
})
Expand Down
Loading