diff --git a/package-lock.json b/package-lock.json index 377d7d1f..abc8a038 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@node-core/doc-kit", - "version": "1.2.1", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@node-core/doc-kit", - "version": "1.2.1", + "version": "1.3.0", "dependencies": { "@actions/core": "^3.0.0", "@heroicons/react": "^2.2.0", diff --git a/package.json b/package.json index 839292ba..0ae02bc4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@node-core/doc-kit", "type": "module", - "version": "1.2.1", + "version": "1.3.0", "repository": { "type": "git", "url": "git+https://github.com/nodejs/doc-kit.git" diff --git a/scripts/update-type-map.mjs b/scripts/update-type-map.mjs index 914948e3..288279ad 100644 --- a/scripts/update-type-map.mjs +++ b/scripts/update-type-map.mjs @@ -1,7 +1,7 @@ import { writeFile } from 'node:fs/promises'; import { MDN_COMPAT_URL, MDN_TYPE_MAP } from './constants.mjs'; -import { loadFromURL } from '../src/utils/url.mjs'; +import { loadFromURL } from '../src/utils/loaders.mjs'; const compat = JSON.parse(await loadFromURL(MDN_COMPAT_URL)); diff --git a/src/generators/jsx-ast/README.md b/src/generators/jsx-ast/README.md index d7fc71e9..a0c0492a 100644 --- a/src/generators/jsx-ast/README.md +++ b/src/generators/jsx-ast/README.md @@ -6,9 +6,7 @@ The `jsx-ast` generator converts MDAST (Markdown Abstract Syntax Tree) to JSX AS The `jsx-ast` generator accepts the following configuration options: -| Name | Type | Default | Description | -| --------- | -------- | --------------------------------------------- | ------------------------------------------------------------------------ | -| `ref` | `string` | `'main'` | Git reference/branch for linking to source files | -| `pageURL` | `string` | `'{baseURL}/latest-{version}/api{path}.html'` | URL template for documentation page links | -| `editURL` | `string` | `'${GITHUB_EDIT_URL}/doc/api{path}.md'` | URL template for "edit this page" links | -| `index` | `array` | - | Array of `{ section, api }` objects defining the documentation structure | +| Name | Type | Default | Description | +| ------- | -------- | -------- | ------------------------------------------------------------------------ | +| `ref` | `string` | `'main'` | Git reference/branch for linking to source files | +| `index` | `array` | - | Array of `{ section, api }` objects defining the documentation structure | diff --git a/src/generators/jsx-ast/generate.mjs b/src/generators/jsx-ast/generate.mjs index 160971fd..614340ed 100644 --- a/src/generators/jsx-ast/generate.mjs +++ b/src/generators/jsx-ast/generate.mjs @@ -1,11 +1,6 @@ -import { buildSideBarProps } from './utils/buildBarProps.mjs'; import buildContent from './utils/buildContent.mjs'; import { getSortedHeadNodes } from './utils/getSortedHeadNodes.mjs'; import { groupNodesByModule } from '../../utils/generators.mjs'; -import { getRemarkRecma } from '../../utils/remark.mjs'; -import { relative } from '../../utils/url.mjs'; - -const remarkRecma = getRemarkRecma(); /** * Process a chunk of items in a worker thread. @@ -16,28 +11,13 @@ const remarkRecma = getRemarkRecma(); * * @type {import('./types').Generator['processChunk']} */ -export async function processChunk(slicedInput, itemIndices, docPages) { +export async function processChunk(slicedInput, itemIndices) { const results = []; for (const idx of itemIndices) { const { head, entries } = slicedInput[idx]; - const sideBarProps = buildSideBarProps( - head, - docPages.map(([heading, path]) => [ - heading, - head.path === path - ? `${head.basename}.html` - : `${relative(path, head.path)}.html`, - ]) - ); - - const content = await buildContent( - entries, - head, - sideBarProps, - remarkRecma - ); + const content = await buildContent(entries, head); results.push(content); } @@ -55,8 +35,6 @@ export async function* generate(input, worker) { const headNodes = getSortedHeadNodes(input); - const docPages = headNodes.map(node => [node.heading.data.name, node.path]); - // Create sliced input: each item contains head + its module's entries // This avoids sending all 4700+ entries to every worker const entries = headNodes.map(head => ({ @@ -64,7 +42,7 @@ export async function* generate(input, worker) { entries: groupedModules.get(head.api), })); - for await (const chunkResult of worker.stream(entries, docPages)) { + for await (const chunkResult of worker.stream(entries)) { yield chunkResult; } } diff --git a/src/generators/jsx-ast/index.mjs b/src/generators/jsx-ast/index.mjs index a83839f4..da61cb56 100644 --- a/src/generators/jsx-ast/index.mjs +++ b/src/generators/jsx-ast/index.mjs @@ -1,6 +1,5 @@ 'use strict'; -import { GITHUB_EDIT_URL } from '../../utils/configuration/templates.mjs'; import { createLazyGenerator } from '../../utils/generators.mjs'; /** @@ -19,8 +18,6 @@ export default createLazyGenerator({ defaultConfiguration: { ref: 'main', - pageURL: '{baseURL}/latest-{version}/api{path}.html', - editURL: `${GITHUB_EDIT_URL}/doc/api{path}.md`, }, hasParallelProcessor: true, diff --git a/src/generators/jsx-ast/types.d.ts b/src/generators/jsx-ast/types.d.ts index 6fcc2961..c6576d57 100644 --- a/src/generators/jsx-ast/types.d.ts +++ b/src/generators/jsx-ast/types.d.ts @@ -9,7 +9,6 @@ export type Generator = GeneratorMetadata< Generate, AsyncGenerator>, ProcessChunk< { head: MetadataEntry; entries: Array }, - JSXContent, - Array<[string, string]> + JSXContent > >; diff --git a/src/generators/jsx-ast/utils/__tests__/buildBarProps.test.mjs b/src/generators/jsx-ast/utils/__tests__/buildBarProps.test.mjs index e91ba049..82484d18 100644 --- a/src/generators/jsx-ast/utils/__tests__/buildBarProps.test.mjs +++ b/src/generators/jsx-ast/utils/__tests__/buildBarProps.test.mjs @@ -1,42 +1,8 @@ import assert from 'node:assert/strict'; -import { describe, it, mock } from 'node:test'; +import { describe, it } from 'node:test'; -import { SemVer } from 'semver'; - -import { setConfig } from '../../../../utils/configuration/index.mjs'; -import * as generatorsExports from '../../../../utils/generators.mjs'; - -mock.module('reading-time', { - defaultExport: () => ({ text: '5 min read' }), -}); - -mock.module('../../../../utils/generators.mjs', { - namedExports: { - ...generatorsExports, - getCompatibleVersions: () => [ - { version: '18.0.0', isLts: true, isCurrent: false }, - { version: '19.0.0', isLts: false, isCurrent: true }, - ], - leftHandAssign: Object.assign, - getVersionFromSemVer: version => `${version.major}.x`, - getVersionURL: (version, api) => `/api/${version}/${api}`, - }, -}); - -const { - extractTextContent, - buildMetaBarProps, - formatVersionOptions, - buildSideBarProps, -} = await import('../buildBarProps.mjs'); - -await setConfig({ - version: 'v17.0.0', - changelog: [ - { version: new SemVer('16.0.0'), isLts: true, isCurrent: false }, - { version: new SemVer('17.0.0'), isLts: false, isCurrent: true }, - ], -}); +const { extractTextContent, extractHeadings } = + await import('../buildBarProps.mjs'); describe('extractTextContent', () => { it('combines text and code node values from entries', () => { @@ -66,104 +32,55 @@ describe('extractTextContent', () => { }); }); -describe('buildMetaBarProps', () => { - it('creates meta bar properties from entries', () => { - const head = { - basename: 'fs', - path: '/fs', - added: 'v1.0.0', - }; - +describe('extractHeadings', () => { + it('extracts headings from entries that qualify for ToC', () => { const entries = [ { - content: { - type: 'root', - children: [{ type: 'text', value: 'Content' }], + heading: { + depth: 2, + data: { + text: 'fs.readFile(path)', + name: 'readFile', + slug: 'fs-readfile', + type: 'method', + }, }, + stability: { data: { index: '2' } }, + }, + { heading: { depth: 2, data: { - text: 'Heading', - name: 'Heading', - slug: 'heading', - depth: 2, + text: 'fs.writeFile(path)', + name: 'writeFile', + slug: 'fs-writefile', + type: 'method', }, }, + stability: null, }, ]; - const result = buildMetaBarProps(head, entries); - - assert.equal(result.addedIn, 'v1.0.0'); - assert.equal(result.readingTime, '5 min read'); - assert.deepEqual(result.viewAs, [ - ['JSON', 'fs.json'], - ['MD', 'fs.md'], - ]); - assert.equal( - result.editThisPage, - 'https://github.com/nodejs/node/edit/main/doc/api/fs.md' - ); - assert.ok(Array.isArray(result.headings)); - }); - - it('falls back to introduced_in if added is missing', () => { - const head = { - api: 'fs', - introduced_in: 'v2.0.0', - }; - - const entries = []; + const result = extractHeadings(entries); - const result = buildMetaBarProps(head, entries); - assert.equal(result.addedIn, 'v2.0.0'); + assert.equal(result.length, 2); + assert.equal(result[0].slug, 'fs-readfile'); + assert.equal(result[0].depth, 2); + assert.equal(result[0].stability, 2); + assert.equal(result[1].stability, 2); }); -}); - -describe('formatVersionOptions', () => { - it('formats version options with proper labels', () => { - const versions = [ - { version: new SemVer('16.0.0'), isLts: true, isCurrent: false }, - { version: new SemVer('17.0.0'), isLts: false, isCurrent: true }, - { version: new SemVer('18.0.0'), isLts: false, isCurrent: false }, - ]; - const result = formatVersionOptions(versions, '/http'); - - assert.deepStrictEqual(result, [ - { - value: 'https://nodejs.org/docs/latest-v16.x/api/http.html', - label: 'v16.x (LTS)', - }, - { - value: 'https://nodejs.org/docs/latest-v17.x/api/http.html', - label: 'v17.x (Current)', - }, + it('filters out entries with empty heading text', () => { + const entries = [ { - value: 'https://nodejs.org/docs/latest-v18.x/api/http.html', - label: 'v18.x', + heading: { + depth: 2, + data: { text: '', name: '', slug: '', type: 'method' }, + }, }, - ]); - }); -}); - -describe('buildSideBarProps', () => { - it('creates sidebar properties with versions and navigation', () => { - const entry = { - path: 'http', - basename: 'http', - introduced_in: 'v0.10.0', - }; - - const docPages = [ - ['HTTP', 'http.html'], - ['HTTPS', 'https.html'], ]; - const result = buildSideBarProps(entry, docPages); - - assert.equal(result.currentVersion, 'v17.0.0'); - assert.equal(result.pathname, 'http.html'); - assert.deepEqual(result.docPages, docPages); + const result = extractHeadings(entries); + assert.equal(result.length, 0); }); }); diff --git a/src/generators/jsx-ast/utils/buildBarProps.mjs b/src/generators/jsx-ast/utils/buildBarProps.mjs index 447f685d..6f21b183 100644 --- a/src/generators/jsx-ast/utils/buildBarProps.mjs +++ b/src/generators/jsx-ast/utils/buildBarProps.mjs @@ -1,14 +1,7 @@ 'use strict'; -import readingTime from 'reading-time'; import { visit } from 'unist-util-visit'; -import getConfig from '../../../utils/configuration/index.mjs'; -import { populate } from '../../../utils/configuration/templates.mjs'; -import { - getCompatibleVersions, - getVersionFromSemVer, -} from '../../../utils/generators.mjs'; import { TOC_MAX_HEADING_DEPTH } from '../constants.mjs'; /** @@ -71,80 +64,3 @@ const extractHeading = entry => { */ export const extractHeadings = entries => entries.filter(shouldIncludeEntryInToC).map(extractHeading); - -/** - * Builds metadata for the meta bar (right panel). - * - * @param {import('../../metadata/types').MetadataEntry} head - Main API metadata entry (used as reference point) - * @param {Array} entries - All documentation entries for a given API item - */ -export const buildMetaBarProps = (head, entries) => { - const config = getConfig('jsx-ast'); - - return { - headings: extractHeadings(entries), - addedIn: head.added || head.introduced_in || '', - readingTime: readingTime(extractTextContent(entries)).text, - viewAs: [ - ['JSON', `${head.basename}.json`], - ['MD', `${head.basename}.md`], - ], - editThisPage: populate(config.editURL, { ...config, path: head.path }), - }; -}; - -/** - * Converts a compatible version entry into a version label and link. - * - * @param {Array} compatibleVersions - Compatible versions - * @param {string} path - path for the version URL - */ -export const formatVersionOptions = (compatibleVersions, path) => { - const config = getConfig('jsx-ast'); - - return compatibleVersions.map(({ version, isLts, isCurrent }) => { - let label = `v${getVersionFromSemVer(version)}`; - - const value = populate(config.pageURL, { - ...config, - path, - version: label, - }); - - if (isLts) { - label += ' (LTS)'; - } - - if (isCurrent) { - label += ' (Current)'; - } - - return { - value, - label, - }; - }); -}; - -/** - * Builds metadata for the sidebar (left panel). - * - * @param {import('../../metadata/types').MetadataEntry} entry - Current documentation entry - * @param {Array<[string, string]>} docPages - Available doc pages for sidebar navigation - */ -export const buildSideBarProps = (entry, docPages) => { - const config = getConfig('jsx-ast'); - - const compatibleVersions = getCompatibleVersions( - entry.introduced_in, - config.changelog, - true - ); - - return { - versions: formatVersionOptions(compatibleVersions, entry.path), - currentVersion: `v${config.version.version}`, - pathname: `${entry.basename}.html`, - docPages, - }; -}; diff --git a/src/generators/jsx-ast/utils/buildContent.mjs b/src/generators/jsx-ast/utils/buildContent.mjs index c3dddbe8..911771bb 100644 --- a/src/generators/jsx-ast/utils/buildContent.mjs +++ b/src/generators/jsx-ast/utils/buildContent.mjs @@ -2,12 +2,14 @@ import { h as createElement } from 'hastscript'; import { slice } from 'mdast-util-slice-markdown'; +import readingTime from 'reading-time'; import { u as createTree } from 'unist-builder'; import { SKIP, visit } from 'unist-util-visit'; import { createJSXElement } from './ast.mjs'; -import { buildMetaBarProps } from './buildBarProps.mjs'; +import { extractHeadings, extractTextContent } from './buildBarProps.mjs'; import { enforceArray } from '../../../utils/array.mjs'; +import { extractPrimitives } from '../../../utils/misc.mjs'; import { JSX_IMPORTS } from '../../web/constants.mjs'; import { STABILITY_LEVELS, @@ -273,14 +275,14 @@ export const processEntry = entry => { /** * Builds the overall document layout tree * @param {Array} entries - API documentation metadata entries - * @param {ReturnType} sideBarProps - Props for the sidebar component - * @param {ReturnType} metaBarProps - Props for the meta bar component + * @param {Object} metadata - Raw page metadata from the head entry */ -export const createDocumentLayout = (entries, sideBarProps, metaBarProps) => +export const createDocumentLayout = (entries, metadata) => createTree('root', [ createJSXElement(JSX_IMPORTS.Layout.name, { - sideBarProps, - metaBarProps, + metadata, + headings: extractHeadings(entries), + readingTime: readingTime(extractTextContent(entries)).text, children: entries.map(processEntry), }), ]); @@ -291,19 +293,13 @@ export const createDocumentLayout = (entries, sideBarProps, metaBarProps) => * Transforms API metadata entries into processed MDX content * @param {Array} metadataEntries - API documentation metadata entries * @param {import('../../metadata/types').MetadataEntry} head - Main API metadata entry with version information - * @param {Object} sideBarProps - Props for the sidebar component * @returns {Promise} */ -const buildContent = async (metadataEntries, head, sideBarProps) => { - // Build props for the MetaBar from head and entries - const metaBarProps = buildMetaBarProps(head, metadataEntries); +const buildContent = async (metadataEntries, head) => { + const metadata = extractPrimitives(head); // Create root document AST with all layout components and processed content - const root = createDocumentLayout( - metadataEntries, - sideBarProps, - metaBarProps - ); + const root = createDocumentLayout(metadataEntries, metadata); // Run remark processor to transform AST (parse markdown, plugins, etc.) const ast = await remark().run(root); diff --git a/src/generators/metadata/generate.mjs b/src/generators/metadata/generate.mjs index ecb830bb..b5691258 100644 --- a/src/generators/metadata/generate.mjs +++ b/src/generators/metadata/generate.mjs @@ -1,8 +1,8 @@ 'use strict'; import { parseApiDoc } from './utils/parse.mjs'; -import { parseTypeMap } from '../../parsers/json.mjs'; import getConfig from '../../utils/configuration/index.mjs'; +import { loadFromURL } from '../../utils/loaders.mjs'; /** * Process a chunk of API doc files in a worker thread. @@ -28,7 +28,9 @@ export async function processChunk(fullInput, itemIndices, typeMap) { export async function* generate(inputs, worker) { const { metadata: config } = getConfig(); - const typeMap = await parseTypeMap(config.typeMap); + const typeMap = config.typeMap + ? JSON.parse(await loadFromURL(config.typeMap)) + : {}; // Stream chunks as they complete - allows dependent generators // to start collecting/preparing while we're still processing diff --git a/src/generators/web/README.md b/src/generators/web/README.md index 7a9361ec..0941b8a7 100644 --- a/src/generators/web/README.md +++ b/src/generators/web/README.md @@ -6,11 +6,13 @@ 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 | -| `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 | +| `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 | #### Default `imports` @@ -34,3 +36,58 @@ export default { }, }; ``` + +### `#theme/config` virtual module + +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'; +``` + +#### Available exports + +| Export | Type | Description | +| ------------------------ | ------------------------------ | --------------------------------------------------------------------------------------------------- | +| `title` | `string` | Site title (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) | +| `editURL` | `string` | Partially populated "edit this page" URL template (only `{path}` remains) | +| `pages` | `Array<[string, string]>` | Sorted `[name, path]` tuples for sidebar navigation | +| `languageDisplayNameMap` | `Map` | Shiki language alias → display name map for code blocks | + +#### Usage in custom components + +When overriding a `#theme/*` component, import only the config values you need: + +```jsx +// my-custom-sidebar.jsx +import { pages, versions, version } from '#theme/config'; + +export default ({ metadata }) => ( + +); +``` + +### Layout props + +The `Layout` component receives the following props: + +| Prop | Type | Description | +| ------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `metadata` | `object` | Serialized page metadata — all YAML frontmatter properties plus `addedIn`, `basename`, `path`, and any custom user-defined fields | +| `headings` | `Array` | Pre-computed table of contents heading entries | +| `readingTime` | `string` | Estimated reading time (e.g. `'5 min read'`) | +| `children` | `ComponentChildren` | Processed page content | + +Custom Layout components can use any combination of these props alongside `#theme/config` imports. diff --git a/src/generators/web/generate.mjs b/src/generators/web/generate.mjs index 57ca2a75..fab595e0 100644 --- a/src/generators/web/generate.mjs +++ b/src/generators/web/generate.mjs @@ -1,10 +1,8 @@ 'use strict'; import { readFile } from 'node:fs/promises'; -import { createRequire } from 'node:module'; import { join } from 'node:path'; -import createASTBuilder from './utils/generate.mjs'; import { processJSXEntries } from './utils/processing.mjs'; import getConfig from '../../utils/configuration/index.mjs'; import { writeFile } from '../../utils/file.mjs'; @@ -19,20 +17,8 @@ export async function generate(input) { const template = await readFile(config.templatePath, 'utf-8'); - // Create AST builders for server and client programs - const astBuilders = createASTBuilder(); - - // Create require function for resolving external packages in server code - const requireFn = createRequire(import.meta.url); - // Process all entries: convert JSX to HTML/CSS/JS - const { results, css, chunks } = await processJSXEntries( - input, - template, - astBuilders, - requireFn, - config - ); + const { results, css, chunks } = await processJSXEntries(input, template); // Process all entries together (required for code-split bundles) if (config.output) { diff --git a/src/generators/web/index.mjs b/src/generators/web/index.mjs index f881bebf..6d783fbd 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -2,6 +2,7 @@ import { join } from 'node:path'; +import { GITHUB_EDIT_URL } from '../../utils/configuration/templates.mjs'; import { createLazyGenerator } from '../../utils/generators.mjs'; /** @@ -29,6 +30,8 @@ export default createLazyGenerator({ defaultConfiguration: { templatePath: join(import.meta.dirname, 'template.html'), title: 'Node.js', + editURL: `${GITHUB_EDIT_URL}/doc/api{path}.md`, + pageURL: '{baseURL}/latest-{version}/api{path}.html', imports: { '#theme/Logo': '@node-core/ui-components/Common/NodejsLogo', '#theme/Navigation': join(import.meta.dirname, './ui/components/NavBar'), diff --git a/src/generators/web/ui/components/CodeBox.jsx b/src/generators/web/ui/components/CodeBox.jsx index 8a32b189..15f46ffb 100644 --- a/src/generators/web/ui/components/CodeBox.jsx +++ b/src/generators/web/ui/components/CodeBox.jsx @@ -1,8 +1,6 @@ import BaseCodeBox from '@node-core/ui-components/Common/BaseCodeBox'; -import { STATIC_DATA } from '../constants.mjs'; - -const languageDisplayNameMap = new Map(STATIC_DATA.shikiDisplayNameMap); +import { languageDisplayNameMap } from '#theme/config'; /** * Get the display name of a language diff --git a/src/generators/web/ui/components/Layout/index.jsx b/src/generators/web/ui/components/Layout/index.jsx index 508f4db9..855bbc33 100644 --- a/src/generators/web/ui/components/Layout/index.jsx +++ b/src/generators/web/ui/components/Layout/index.jsx @@ -13,23 +13,24 @@ import SideBar from '#theme/Sidebar'; * main content, meta bar, and footer. Override via `#theme/Layout` in your * configuration's `imports` to customize the entire page structure. * - * @param {{ sideBarProps: object, metaBarProps: object, children: import('preact').ComponentChildren }} props + * @param {{ metadata: import('../../types').SerializedMetadata, headings: Array, readingTime: string, children: import('preact').ComponentChildren }} props */ -export default ({ sideBarProps, metaBarProps, children }) => ( +export default ({ metadata, headings, readingTime, children }) => ( <>
- +
- +
{children}
- +