Skip to content
Merged
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
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
10 changes: 8 additions & 2 deletions src/generators/jsx-ast/utils/buildContent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SKIP, visit } from 'unist-util-visit';
import { createJSXElement } from './ast.mjs';
import { extractHeadings, extractTextContent } from './buildBarProps.mjs';
import { enforceArray } from '../../../utils/array.mjs';
import { extractPrimitives } from '../../../utils/misc.mjs';
import { omitKeys } from '../../../utils/misc.mjs';
import { JSX_IMPORTS } from '../../web/constants.mjs';
import {
STABILITY_LEVELS,
Expand Down Expand Up @@ -296,7 +296,13 @@ export const createDocumentLayout = (entries, metadata) =>
* @returns {Promise<JSXContent>}
*/
const buildContent = async (metadataEntries, head) => {
const metadata = extractPrimitives(head);
// The metadata is the heading without the node children
const metadata = omitKeys(head, [
'content',
'heading',
'stability',
'changes',
]);

// Create root document AST with all layout components and processed content
const root = createDocumentLayout(metadataEntries, metadata);
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}`) |
| `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',
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>
13 changes: 8 additions & 5 deletions src/generators/web/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { JSXContent } from '../jsx-ast/utils/buildContent.mjs';

export type Configuration = {
templatePath: string;
title: string;
imports: Record<string, string>;
virtualImports: Record<string, string>;
};

export type Generator = GeneratorMetadata<
{
templatePath: string;
title: string;
imports: Record<string, string>;
},
Configuration,
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
20 changes: 17 additions & 3 deletions src/generators/web/ui/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import { GlobalConfiguration } from '../../../utils/configuration/types';
import { MetadataEntry } from '../../metadata/types';
import { Configuration } from '../types';

declare global {
const SERVER: boolean;
const CLIENT: boolean;
}

declare module '#theme/config' {
export const title: string;
export const repository: string;
// From global configuration
export const repository: GlobalConfiguration['repository'];
export const input: GlobalConfiguration['input'];
export const ignore: GlobalConfiguration['ignore'];
export const output: GlobalConfiguration['output'];
export const minify: GlobalConfiguration['minify'];
export const baseURL: GlobalConfiguration['baseURL'];
export const ref: GlobalConfiguration['ref'];

// From web configuration
export const templatePath: Configuration['templatePath'];
export const title: Configuration['title'];

// From config generation
export const version: string;
export const versions: Array<{
url: string;
Expand All @@ -16,7 +30,7 @@ declare module '#theme/config' {
}>;
export const editURL: string;
export const pages: Array<[string, string]>;
export const languageDisplayNameMap: Map<string, string>;
export const languageDisplayNameMap: Map<string[], string>;
}

// Omit Primitives from Metadata
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', () => {
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
52 changes: 26 additions & 26 deletions src/generators/web/utils/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ import { LANGS } from '@node-core/rehype-shiki';
import getConfig from '../../../utils/configuration/index.mjs';
import { populate } from '../../../utils/configuration/templates.mjs';
import { getVersionFromSemVer } from '../../../utils/generators.mjs';
import { omitKeys } from '../../../utils/misc.mjs';
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
* @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 +73,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);

// Partially populate URL templates: resolve config-level placeholders,
// leave {path} for per-page resolution by components
const editURL = populate(config.editURL, templateVars);
const exports = {
...omitKeys(
config,
// These are keys that are large, and not needed by components, so we ignore them
['changelog', 'index', 'imports', 'virtualImports']
),
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(
`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');
}
Loading
Loading