From 1969ade55a10f172f6f169764cc9872c613362bf Mon Sep 17 00:00:00 2001 From: avivkeller Date: Mon, 17 Aug 2026 10:39:35 -0700 Subject: [PATCH 1/5] chore: improved index page --- .changeset/documentation-index-component.md | 5 + packages/react/src/html/constants.mjs | 4 + .../components/DocumentationIndex/index.jsx | 56 +++++++ .../DocumentationIndex/index.module.css | 72 +++++++++ .../src/html/ui/components/MetaBar/index.jsx | 15 +- .../src/html/ui/components/constants.mjs | 12 ++ .../src/jsx-ast/__tests__/generate.test.mjs | 46 ------ packages/react/src/jsx-ast/constants.mjs | 3 + packages/react/src/jsx-ast/generate.mjs | 35 ++++- .../__tests__/documentationIndex.test.mjs | 144 ------------------ .../src/jsx-ast/utils/documentationIndex.mjs | 86 ----------- 11 files changed, 186 insertions(+), 292 deletions(-) create mode 100644 .changeset/documentation-index-component.md create mode 100644 packages/react/src/html/ui/components/DocumentationIndex/index.jsx create mode 100644 packages/react/src/html/ui/components/DocumentationIndex/index.module.css create mode 100644 packages/react/src/html/ui/components/constants.mjs delete mode 100644 packages/react/src/jsx-ast/utils/__tests__/documentationIndex.test.mjs delete mode 100644 packages/react/src/jsx-ast/utils/documentationIndex.mjs diff --git a/.changeset/documentation-index-component.md b/.changeset/documentation-index-component.md new file mode 100644 index 000000000..ddfdf3ef7 --- /dev/null +++ b/.changeset/documentation-index-component.md @@ -0,0 +1,5 @@ +--- +'@doc-kit/generator-react': patch +--- + +Render `` with a new `DocumentationIndex` UI component diff --git a/packages/react/src/html/constants.mjs b/packages/react/src/html/constants.mjs index 4ca3e783a..3850a2a38 100644 --- a/packages/react/src/html/constants.mjs +++ b/packages/react/src/html/constants.mjs @@ -26,6 +26,10 @@ export const JSX_IMPORTS = { name: 'CodeTabs', source: resolve(ROOT, './ui/components/CodeTabs'), }, + DocumentationIndex: { + name: 'DocumentationIndex', + source: resolve(ROOT, './ui/components/DocumentationIndex'), + }, MDXTooltip: { name: 'MDXTooltip', isDefaultExport: false, diff --git a/packages/react/src/html/ui/components/DocumentationIndex/index.jsx b/packages/react/src/html/ui/components/DocumentationIndex/index.jsx new file mode 100644 index 000000000..7b587a7a9 --- /dev/null +++ b/packages/react/src/html/ui/components/DocumentationIndex/index.jsx @@ -0,0 +1,56 @@ +import Badge from '@node-core/ui-components/Common/Badge'; + +import styles from './index.module.css'; +import { STABILITY_KINDS, STABILITY_LABELS } from '../constants.mjs'; + +/** + * @typedef {Object} DocumentationIndexEntry + * @property {string} api - Basename of the document, linked as `${api}.html` + * @property {string} name - Human-readable name from the document's heading + * @property {string} index - Stability index (e.g. `'2'` or `'1.1'`) + * @property {string} description - Stability description from the document + */ + +/** + * @param {DocumentationIndexEntry} props + */ +const IndexEntry = ({ api, name, index, description }) => { + const level = parseInt(index, 10); + const label = STABILITY_LABELS[level] ?? index; + + const summary = description + .replace(new RegExp(`^${label}[.:\\s-]*`, 'i'), '') + .split('. ')[0] + .replace(/\.$/, ''); + + return ( + + + {name} + + + {label} + + + + {api} + + {summary && {summary}} + + ); +}; + +/** + * @param {{ entries: Array }} props + */ +export default ({ entries = [] }) => ( + +); diff --git a/packages/react/src/html/ui/components/DocumentationIndex/index.module.css b/packages/react/src/html/ui/components/DocumentationIndex/index.module.css new file mode 100644 index 000000000..164e3b279 --- /dev/null +++ b/packages/react/src/html/ui/components/DocumentationIndex/index.module.css @@ -0,0 +1,72 @@ +.documentationIndex { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + gap: 1rem; + margin-block: 1.5rem; +} + +.entry { + display: flex; + flex-direction: column; + gap: 0.375rem; + padding: 1rem; + border: 1px solid var(--color-neutral-200); + border-radius: 0.75rem; + color: inherit; + text-decoration: none; + transition: + border-color 0.15s ease, + background-color 0.15s ease; +} + +.entry:hover, +.entry:focus-visible { + border-color: var(--color-neutral-400); + background-color: var(--color-neutral-100); +} + +:where([data-theme='dark'], [data-theme='dark'] *) .entry { + border-color: var(--color-neutral-900); +} + +:where([data-theme='dark'], [data-theme='dark'] *) .entry:hover, +:where([data-theme='dark'], [data-theme='dark'] *) .entry:focus-visible { + border-color: var(--color-neutral-700); + background-color: var(--color-neutral-950); +} + +.title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.name { + font-weight: 600; + color: var(--color-neutral-900); +} + +:where([data-theme='dark'], [data-theme='dark'] *) .name { + color: var(--color-white); +} + +.api { + align-self: flex-start; + font-family: var(--font-ibm-plex-mono); + font-size: 0.75rem; + color: var(--color-neutral-800); +} + +:where([data-theme='dark'], [data-theme='dark'] *) .api { + color: var(--color-neutral-600); +} + +.summary { + font-size: 0.875rem; + color: var(--color-neutral-800); +} + +:where([data-theme='dark'], [data-theme='dark'] *) .summary { + color: var(--color-neutral-600); +} diff --git a/packages/react/src/html/ui/components/MetaBar/index.jsx b/packages/react/src/html/ui/components/MetaBar/index.jsx index 5f705efd3..1a09aa8de 100644 --- a/packages/react/src/html/ui/components/MetaBar/index.jsx +++ b/packages/react/src/html/ui/components/MetaBar/index.jsx @@ -4,6 +4,7 @@ import MetaBar from '@node-core/ui-components/Containers/MetaBar'; import GitHubIcon from '@node-core/ui-components/Icons/Social/GitHub'; import styles from './index.module.css'; +import { STABILITY_KINDS, STABILITY_LABELS } from '../constants.mjs'; import { editURL } from '#theme/config'; @@ -12,10 +13,6 @@ const iconMap = { MD: DocumentIcon, }; -const STABILITY_KINDS = ['error', 'warning', null, 'info']; -const STABILITY_LABELS = ['D', 'E', null, 'L']; -const STABILITY_TOOLTIPS = ['Deprecated', 'Experimental', null, 'Legacy']; - /** * Renders a heading value with an optional stability badge * @param {{ value: string, stability: number }} props @@ -25,9 +22,7 @@ const HeadingValue = ({ value, stability }) => { return value; } - const ariaLabel = STABILITY_TOOLTIPS[stability] - ? `Stability: ${STABILITY_TOOLTIPS[stability]}` - : undefined; + const label = STABILITY_LABELS[stability]; return ( <> @@ -37,11 +32,11 @@ const HeadingValue = ({ value, stability }) => { size="small" className={styles.badge} kind={STABILITY_KINDS[stability]} - data-tooltip={STABILITY_TOOLTIPS[stability]} - aria-label={ariaLabel} + data-tooltip={label} + aria-label={label ? `Stability: ${label}` : undefined} tabIndex={0} > - {STABILITY_LABELS[stability]} + {label?.[0]} ); diff --git a/packages/react/src/html/ui/components/constants.mjs b/packages/react/src/html/ui/components/constants.mjs new file mode 100644 index 000000000..e6fccd249 --- /dev/null +++ b/packages/react/src/html/ui/components/constants.mjs @@ -0,0 +1,12 @@ +/** + * UI badge kinds and labels for Node.js API stability levels + * + * @see https://nodejs.org/api/documentation.html#stability-index + */ +export const STABILITY_KINDS = ['error', 'warning', 'default', 'info']; +export const STABILITY_LABELS = [ + 'Deprecated', + 'Experimental', + 'Stable', + 'Legacy', +]; diff --git a/packages/react/src/jsx-ast/__tests__/generate.test.mjs b/packages/react/src/jsx-ast/__tests__/generate.test.mjs index a3050b09e..7d09db400 100644 --- a/packages/react/src/jsx-ast/__tests__/generate.test.mjs +++ b/packages/react/src/jsx-ast/__tests__/generate.test.mjs @@ -94,50 +94,4 @@ describe('jsx-ast generate', () => { ['index', 'fs'] ); }); - - it('only generates an index page when an index document is an input', async () => { - await setConfig({ target: ['jsx-ast'] }); - - const jsxAstConfig = getConfig('jsx-ast'); - jsxAstConfig.generateAllPage = false; - jsxAstConfig.generateNotFoundPage = false; - - const seenItems = []; - await collect( - generate([createEntry('fs', 'File system')], createWorker(seenItems)) - ); - - assert.deepEqual( - seenItems.map(({ head }) => head.api), - ['fs'] - ); - }); - - it('places the stability overview at the DOCUMENTATION_INDEX comment', async () => { - await setConfig({ target: ['jsx-ast'] }); - - const jsxAstConfig = getConfig('jsx-ast'); - jsxAstConfig.generateAllPage = false; - jsxAstConfig.generateNotFoundPage = false; - - const index = createEntry('index', 'Index', { stabilityIndex: null }); - // The metadata parser turns a `` comment into - // this tag on the entry of the section containing it. - index.tags = ['DOCUMENTATION_INDEX']; - - const seenItems = []; - await collect( - generate( - [index, createEntry('fs', 'File system')], - createWorker(seenItems) - ) - ); - - const [{ entries }] = seenItems; - const table = entries[0].content.children.at(-1); - - assert.equal(table.tagName, 'table'); - const [row] = table.children.at(-1).children; - assert.equal(row.children[0].children[0].properties.href, 'fs.html'); - }); }); diff --git a/packages/react/src/jsx-ast/constants.mjs b/packages/react/src/jsx-ast/constants.mjs index ffeec38c8..26b5afcc1 100644 --- a/packages/react/src/jsx-ast/constants.mjs +++ b/packages/react/src/jsx-ast/constants.mjs @@ -189,6 +189,9 @@ export const AST_NODE_TYPES = { }, }; +// `` comment in a source document = a stability index +export const DOCUMENTATION_INDEX_TAG = 'DOCUMENTATION_INDEX'; + // These positions are explicity before anything else export const OVERRIDDEN_POSITIONS = [ 'index', // https://github.com/nodejs/node/blob/main/doc/api/index.md diff --git a/packages/react/src/jsx-ast/generate.mjs b/packages/react/src/jsx-ast/generate.mjs index 9f6969543..8423cb52d 100644 --- a/packages/react/src/jsx-ast/generate.mjs +++ b/packages/react/src/jsx-ast/generate.mjs @@ -2,12 +2,32 @@ import getConfig from '@doc-kit/core/utils/configuration/index.mjs'; import { groupNodesByModule } from '@doc-kit/core/utils/generators.mjs'; import { jsx, toJs } from 'estree-util-to-js'; +import { DOCUMENTATION_INDEX_TAG } from './constants.mjs'; +import { createJSXElement } from './utils/ast.mjs'; import buildContent from './utils/buildContent.mjs'; -import { injectDocumentationIndex } from './utils/documentationIndex.mjs'; import { getSortedHeadNodes } from './utils/getSortedHeadNodes.mjs'; +import { JSX_IMPORTS } from '../html/constants.mjs'; import { buildNotFoundPage } from './utils/synthetic/404.mjs'; import { buildAllPage } from './utils/synthetic/all.mjs'; +/** + * Builds the `` element + * + * @param {Array} moduleEntries + */ +const buildDocumentationIndex = moduleEntries => + createJSXElement(JSX_IMPORTS.DocumentationIndex.name, { + inline: false, + entries: getSortedHeadNodes(moduleEntries) + .filter(entry => entry.stability) + .map(({ api, heading, stability }) => ({ + api, + name: heading.data.name, + index: stability.data.index, + description: stability.data.description, + })), + }); + /** * Builds the `{ head, entries }` page descriptors for all configured synthetic * pages. The descriptors are cheap to build; the expensive `buildContent` step @@ -60,13 +80,16 @@ export async function processChunk(slicedInput, itemIndices) { */ export async function* generate(input, worker) { // The `index` page is only generated when an `index` document is part of - // the input; the module list for the synthetic pages and the stability - // overview excludes it. + // the input; the module list for the synthetic pages and the documentation + // index excludes it. const moduleInput = input.filter(entry => entry.api !== 'index'); - // Sections tagged with a `` comment (e.g. in - // the `index` document) receive the Stability Overview of all modules. - injectDocumentationIndex(input, moduleInput); + // Sections tagged with a `` build an index + for (const entry of input) { + if (entry.tags?.includes(DOCUMENTATION_INDEX_TAG)) { + entry.content.children.push(buildDocumentationIndex(moduleInput)); + } + } // Create sliced input: each item contains head + its module's entries // This avoids sending all 4700+ entries to every worker diff --git a/packages/react/src/jsx-ast/utils/__tests__/documentationIndex.test.mjs b/packages/react/src/jsx-ast/utils/__tests__/documentationIndex.test.mjs deleted file mode 100644 index 6817f6349..000000000 --- a/packages/react/src/jsx-ast/utils/__tests__/documentationIndex.test.mjs +++ /dev/null @@ -1,144 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -import { - buildStabilityOverview, - injectDocumentationIndex, -} from '../documentationIndex.mjs'; - -const fakeHead = (api, name, stabilityIndex, depth = 1) => ({ - api, - heading: { depth, data: { name, text: name, slug: api } }, - stability: - stabilityIndex == null - ? null - : { - data: { - index: String(stabilityIndex), - description: `${name} stable. Long-form description.`, - }, - }, -}); - -const findChild = (node, tagName) => - node.children.find(child => child.tagName === tagName); - -describe('injectDocumentationIndex', () => { - const createEntry = tags => ({ - ...fakeHead('index', 'Index', null), - tags, - content: { type: 'root', children: [] }, - }); - - it('appends the overview to entries tagged DOCUMENTATION_INDEX', () => { - const tagged = createEntry(['DOCUMENTATION_INDEX']); - const untagged = createEntry(undefined); - - injectDocumentationIndex( - [tagged, untagged], - [fakeHead('fs', 'fs', 2), fakeHead('assert', 'assert', 2)] - ); - - const table = findChild(tagged.content, 'table'); - assert.equal(findChild(table, 'tbody').children.length, 2); - assert.equal(untagged.content.children.length, 0); - }); - - it('sorts the stability overview rows alphabetically by API name', () => { - const entry = createEntry(['DOCUMENTATION_INDEX']); - - injectDocumentationIndex( - [entry], - [ - fakeHead('fs', 'fs', 2), - fakeHead('assert', 'assert', 2), - fakeHead('crypto', 'crypto', 2), - ] - ); - - const table = findChild(entry.content, 'table'); - const rows = findChild(table, 'tbody').children; - const names = rows.map( - row => row.children[0].children[0].children[0].value - ); - - assert.deepEqual(names, ['assert', 'crypto', 'fs']); - }); - - it('excludes module heads without a stability index', () => { - const entry = createEntry(['DOCUMENTATION_INDEX']); - - injectDocumentationIndex( - [entry], - [fakeHead('fs', 'fs', 2), fakeHead('synopsis', 'Usage', null)] - ); - - const table = findChild(entry.content, 'table'); - assert.equal(findChild(table, 'tbody').children.length, 1); - }); -}); - -describe('buildStabilityOverview', () => { - it('renders a header row and one body row per entry', () => { - const table = buildStabilityOverview([ - fakeHead('fs', 'fs', 2), - fakeHead('crypto', 'crypto', 1), - ]); - - assert.equal(table.tagName, 'table'); - const headerRow = findChild(findChild(table, 'thead'), 'tr'); - assert.deepEqual( - headerRow.children.map(c => c.children[0].value), - ['API', 'Stability'] - ); - - assert.equal(findChild(table, 'tbody').children.length, 2); - }); - - it('formats the stability cell with a colored badge and first sentence', () => { - const table = buildStabilityOverview([fakeHead('fs', 'fs', 1)]); - - const row = findChild(table, 'tbody').children[0]; - const stabilityCell = row.children[1]; - const badge = stabilityCell.children[0]; - - assert.equal(badge.name, 'Badge'); - assert.deepEqual( - badge.attributes.map(({ name, value }) => [name, value]), - [ - ['size', 'small'], - ['kind', 'warning'], - ['aria-label', 'Stability: 1'], - ] - ); - assert.equal(badge.children[0].value, '1'); - assert.equal(stabilityCell.children[1].value, ' fs stable'); - }); - - it('uses a default badge for stable entries', () => { - const table = buildStabilityOverview([fakeHead('fs', 'fs', 2)]); - - const row = findChild(table, 'tbody').children[0]; - const badge = row.children[1].children[0]; - const kind = badge.attributes.find(attr => attr.name === 'kind'); - - assert.equal(kind.value, 'default'); - }); - - it('builds a relative link to the module HTML page', () => { - const table = buildStabilityOverview([fakeHead('fs', 'fs', 2)]); - - const row = findChild(table, 'tbody').children[0]; - const link = row.children[0].children[0]; - - assert.equal(link.tagName, 'a'); - assert.equal(link.properties.href, 'fs.html'); - assert.equal(link.children[0].value, 'fs'); - }); - - it('renders an empty body when no entries are passed', () => { - const table = buildStabilityOverview([]); - - assert.equal(findChild(table, 'tbody').children.length, 0); - }); -}); diff --git a/packages/react/src/jsx-ast/utils/documentationIndex.mjs b/packages/react/src/jsx-ast/utils/documentationIndex.mjs deleted file mode 100644 index c12ce0bf7..000000000 --- a/packages/react/src/jsx-ast/utils/documentationIndex.mjs +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -import { h as createElement } from 'hastscript'; - -import { createJSXElement } from './ast.mjs'; -import { getSortedHeadNodes } from './getSortedHeadNodes.mjs'; -import { JSX_IMPORTS } from '../../html/constants.mjs'; - -// The metadata parser turns bare HTML comments into entry tags, so a -// `` comment in a source document surfaces as -// this tag on the entry for the section containing it. -export const DOCUMENTATION_INDEX_TAG = 'DOCUMENTATION_INDEX'; - -const STABILITY_BADGE_KINDS = [ - 'error', - 'warning', - 'default', - 'info', - 'neutral', - 'neutral', -]; - -/** - * Maps a Node.js stability index to a UI badge kind. - * - * @param {string} index - */ -const getStabilityBadgeKind = index => - STABILITY_BADGE_KINDS[parseInt(index, 10)] ?? 'neutral'; - -/** - * Builds the Stability Overview table from module heads that declare a - * top-level stability index, mirroring the `legacy-html-all` overview. - * - * @param {Array} headEntries - */ -export const buildStabilityOverview = headEntries => - createElement('table', [ - createElement('thead', [ - createElement('tr', [ - createElement('th', 'API'), - createElement('th', 'Stability'), - ]), - ]), - createElement( - 'tbody', - headEntries.map(({ heading, api, stability }) => - createElement('tr', [ - createElement( - 'td', - createElement('a', { href: `${api}.html` }, heading.data.name) - ), - createElement( - 'td', - createJSXElement(JSX_IMPORTS.Badge.name, { - size: 'small', - kind: getStabilityBadgeKind(stability.data.index), - 'aria-label': `Stability: ${stability.data.index}`, - children: stability.data.index, - }), - ` ${stability.data.description.split('. ')[0]}` - ), - ]) - ) - ), - ]); - -/** - * Places the Stability Overview into every entry whose source section - * contains a `` comment. The parser strips the - * comment itself, so the table lands at the end of the tagged section. - * - * @param {Array} entries - Entries to scan for the tag - * @param {Array} moduleEntries - Entries providing the module heads for the overview - */ -export const injectDocumentationIndex = (entries, moduleEntries) => { - const headEntries = getSortedHeadNodes(moduleEntries).filter( - entry => entry.stability - ); - - for (const entry of entries) { - if (entry.tags?.includes(DOCUMENTATION_INDEX_TAG)) { - entry.content.children.push(buildStabilityOverview(headEntries)); - } - } -}; From 9c18a58a615623b73dddab92dc8c784ec516dd7a Mon Sep 17 00:00:00 2001 From: avivkeller Date: Mon, 17 Aug 2026 10:45:31 -0700 Subject: [PATCH 2/5] chore: improved index page --- .../html/ui/components/DocumentationIndex/index.jsx | 10 +--------- .../ui/components/DocumentationIndex/index.module.css | 11 ----------- packages/react/src/jsx-ast/generate.mjs | 4 ++-- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/packages/react/src/html/ui/components/DocumentationIndex/index.jsx b/packages/react/src/html/ui/components/DocumentationIndex/index.jsx index 7b587a7a9..d82fae939 100644 --- a/packages/react/src/html/ui/components/DocumentationIndex/index.jsx +++ b/packages/react/src/html/ui/components/DocumentationIndex/index.jsx @@ -8,7 +8,6 @@ import { STABILITY_KINDS, STABILITY_LABELS } from '../constants.mjs'; * @property {string} api - Basename of the document, linked as `${api}.html` * @property {string} name - Human-readable name from the document's heading * @property {string} index - Stability index (e.g. `'2'` or `'1.1'`) - * @property {string} description - Stability description from the document */ /** @@ -18,11 +17,6 @@ const IndexEntry = ({ api, name, index, description }) => { const level = parseInt(index, 10); const label = STABILITY_LABELS[level] ?? index; - const summary = description - .replace(new RegExp(`^${label}[.:\\s-]*`, 'i'), '') - .split('. ')[0] - .replace(/\.$/, ''); - return ( @@ -37,9 +31,7 @@ const IndexEntry = ({ api, name, index, description }) => { - {api} - - {summary && {summary}} + {description && {description}} ); }; diff --git a/packages/react/src/html/ui/components/DocumentationIndex/index.module.css b/packages/react/src/html/ui/components/DocumentationIndex/index.module.css index 164e3b279..a9a29c8e8 100644 --- a/packages/react/src/html/ui/components/DocumentationIndex/index.module.css +++ b/packages/react/src/html/ui/components/DocumentationIndex/index.module.css @@ -51,17 +51,6 @@ color: var(--color-white); } -.api { - align-self: flex-start; - font-family: var(--font-ibm-plex-mono); - font-size: 0.75rem; - color: var(--color-neutral-800); -} - -:where([data-theme='dark'], [data-theme='dark'] *) .api { - color: var(--color-neutral-600); -} - .summary { font-size: 0.875rem; color: var(--color-neutral-800); diff --git a/packages/react/src/jsx-ast/generate.mjs b/packages/react/src/jsx-ast/generate.mjs index 8423cb52d..76bc41d0c 100644 --- a/packages/react/src/jsx-ast/generate.mjs +++ b/packages/react/src/jsx-ast/generate.mjs @@ -20,11 +20,11 @@ const buildDocumentationIndex = moduleEntries => inline: false, entries: getSortedHeadNodes(moduleEntries) .filter(entry => entry.stability) - .map(({ api, heading, stability }) => ({ + .map(({ api, heading, stability, llm_description }) => ({ api, name: heading.data.name, index: stability.data.index, - description: stability.data.description, + description: llm_description, })), }); From a661ff6acdfe91ef306f67e933eb3d46aa9b5e96 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Mon, 17 Aug 2026 12:03:59 -0700 Subject: [PATCH 3/5] chore: improved index page --- .../src/utils/__tests__/generators.test.mjs | 59 ++++++++++++++++++ packages/core/src/utils/generators.mjs | 30 ++++++++++ .../components/DocumentationIndex/index.jsx | 1 + packages/react/src/jsx-ast/generate.mjs | 15 +++-- .../utils/__tests__/buildApiDocLink.test.mjs | 60 +------------------ .../src/llms-txt/utils/buildApiDocLink.mjs | 30 +--------- 6 files changed, 101 insertions(+), 94 deletions(-) diff --git a/packages/core/src/utils/__tests__/generators.test.mjs b/packages/core/src/utils/__tests__/generators.test.mjs index d6ef025be..6fe290dba 100644 --- a/packages/core/src/utils/__tests__/generators.test.mjs +++ b/packages/core/src/utils/__tests__/generators.test.mjs @@ -2,12 +2,71 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { + getEntryDescription, groupNodesByModule, getVersionFromSemVer, coerceSemVer, getCompatibleVersions, } from '../generators.mjs'; +describe('getEntryDescription', () => { + it('returns llm_description when available', () => { + const entry = { + llm_description: 'LLM generated description', + content: { children: [] }, + }; + + const result = getEntryDescription(entry); + assert.equal(result, 'LLM generated description'); + }); + + it('extracts first paragraph when no llm_description', () => { + const entry = { + content: { + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'First paragraph' }], + }, + ], + }, + }; + + const result = getEntryDescription(entry); + assert.ok(result.length > 0); + }); + + it('returns empty string when no paragraph found', () => { + const entry = { + content: { + children: [ + { type: 'heading', children: [{ type: 'text', value: 'Title' }] }, + ], + }, + }; + + const result = getEntryDescription(entry); + assert.equal(result, ''); + }); + + it('removes newlines from description', () => { + const entry = { + content: { + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Line 1\nLine 2\r\nLine 3' }], + }, + ], + }, + }; + + const result = getEntryDescription(entry); + assert.equal(result.includes('\n'), false); + assert.equal(result.includes('\r'), false); + }); +}); + describe('groupNodesByModule', () => { it('groups nodes by api property', () => { const nodes = [ diff --git a/packages/core/src/utils/generators.mjs b/packages/core/src/utils/generators.mjs index ea2d97dfb..516113bca 100644 --- a/packages/core/src/utils/generators.mjs +++ b/packages/core/src/utils/generators.mjs @@ -2,6 +2,36 @@ import { coerce, major } from 'semver'; +import { transformNodeToString } from './unist.mjs'; + +/** + * Retrieves the description of a given API doc entry. It first checks whether + * the entry has a llm_description property. If not, it extracts the first + * paragraph from the entry's content. + * + * @param {import('../generators/metadata/types').MetadataEntry} entry + * @returns {string} + */ +export const getEntryDescription = entry => { + if (entry.llm_description) { + return entry.llm_description.trim(); + } + + const descriptionNode = entry.content.children.find( + child => child.type === 'paragraph' + ); + + if (!descriptionNode) { + return ''; + } + + return ( + transformNodeToString(descriptionNode) + // Remove newlines and extra spaces + .replace(/[\r\n]+/g, '') + ); +}; + /** * Groups all the API metadata nodes by module (`api` property) so that we can process each different file * based on the module it belongs to. diff --git a/packages/react/src/html/ui/components/DocumentationIndex/index.jsx b/packages/react/src/html/ui/components/DocumentationIndex/index.jsx index d82fae939..70d3d0d42 100644 --- a/packages/react/src/html/ui/components/DocumentationIndex/index.jsx +++ b/packages/react/src/html/ui/components/DocumentationIndex/index.jsx @@ -8,6 +8,7 @@ import { STABILITY_KINDS, STABILITY_LABELS } from '../constants.mjs'; * @property {string} api - Basename of the document, linked as `${api}.html` * @property {string} name - Human-readable name from the document's heading * @property {string} index - Stability index (e.g. `'2'` or `'1.1'`) + * @property {string} [description] - The document's `llm_description`, or its first paragraph */ /** diff --git a/packages/react/src/jsx-ast/generate.mjs b/packages/react/src/jsx-ast/generate.mjs index 76bc41d0c..6a1a2f733 100644 --- a/packages/react/src/jsx-ast/generate.mjs +++ b/packages/react/src/jsx-ast/generate.mjs @@ -1,5 +1,8 @@ import getConfig from '@doc-kit/core/utils/configuration/index.mjs'; -import { groupNodesByModule } from '@doc-kit/core/utils/generators.mjs'; +import { + getEntryDescription, + groupNodesByModule, +} from '@doc-kit/core/utils/generators.mjs'; import { jsx, toJs } from 'estree-util-to-js'; import { DOCUMENTATION_INDEX_TAG } from './constants.mjs'; @@ -20,11 +23,11 @@ const buildDocumentationIndex = moduleEntries => inline: false, entries: getSortedHeadNodes(moduleEntries) .filter(entry => entry.stability) - .map(({ api, heading, stability, llm_description }) => ({ - api, - name: heading.data.name, - index: stability.data.index, - description: llm_description, + .map(entry => ({ + api: entry.api, + name: entry.heading.data.name, + index: entry.stability.data.index, + description: getEntryDescription(entry), })), }); diff --git a/packages/react/src/llms-txt/utils/__tests__/buildApiDocLink.test.mjs b/packages/react/src/llms-txt/utils/__tests__/buildApiDocLink.test.mjs index 6120f93e1..0e8e4f202 100644 --- a/packages/react/src/llms-txt/utils/__tests__/buildApiDocLink.test.mjs +++ b/packages/react/src/llms-txt/utils/__tests__/buildApiDocLink.test.mjs @@ -1,65 +1,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { getEntryDescription, buildApiDocLink } from '../buildApiDocLink.mjs'; - -describe('getEntryDescription', () => { - it('returns llm_description when available', () => { - const entry = { - llm_description: 'LLM generated description', - content: { children: [] }, - }; - - const result = getEntryDescription(entry); - assert.equal(result, 'LLM generated description'); - }); - - it('extracts first paragraph when no llm_description', () => { - const entry = { - content: { - children: [ - { - type: 'paragraph', - children: [{ type: 'text', value: 'First paragraph' }], - }, - ], - }, - }; - - const result = getEntryDescription(entry); - assert.ok(result.length > 0); - }); - - it('returns empty string when no paragraph found', () => { - const entry = { - content: { - children: [ - { type: 'heading', children: [{ type: 'text', value: 'Title' }] }, - ], - }, - }; - - const result = getEntryDescription(entry); - assert.equal(result, ''); - }); - - it('removes newlines from description', () => { - const entry = { - content: { - children: [ - { - type: 'paragraph', - children: [{ type: 'text', value: 'Line 1\nLine 2\r\nLine 3' }], - }, - ], - }, - }; - - const result = getEntryDescription(entry); - assert.equal(result.includes('\n'), false); - assert.equal(result.includes('\r'), false); - }); -}); +import { buildApiDocLink } from '../buildApiDocLink.mjs'; describe('buildApiDocLink', () => { it('builds markdown link with description', () => { diff --git a/packages/react/src/llms-txt/utils/buildApiDocLink.mjs b/packages/react/src/llms-txt/utils/buildApiDocLink.mjs index 7d17d04f0..d81e84976 100644 --- a/packages/react/src/llms-txt/utils/buildApiDocLink.mjs +++ b/packages/react/src/llms-txt/utils/buildApiDocLink.mjs @@ -1,33 +1,5 @@ import { populate } from '@doc-kit/core/utils/configuration/templates.mjs'; -import { transformNodeToString } from '@doc-kit/core/utils/unist.mjs'; - -/** - * Retrieves the description of a given API doc entry. It first checks whether - * the entry has a llm_description property. If not, it extracts the first - * paragraph from the entry's content. - * - * @param {import('@doc-kit/core/generators/metadata/types').MetadataEntry} entry - * @returns {string} - */ -export const getEntryDescription = entry => { - if (entry.llm_description) { - return entry.llm_description.trim(); - } - - const descriptionNode = entry.content.children.find( - child => child.type === 'paragraph' - ); - - if (!descriptionNode) { - return ''; - } - - return ( - transformNodeToString(descriptionNode) - // Remove newlines and extra spaces - .replace(/[\r\n]+/g, '') - ); -}; +import { getEntryDescription } from '@doc-kit/core/utils/generators.mjs'; /** * Builds a markdown link for an API doc entry From 2756e3b534a84d3028ccdb30fddbb7dafc58123e Mon Sep 17 00:00:00 2001 From: avivkeller Date: Wed, 19 Aug 2026 14:37:37 -0700 Subject: [PATCH 4/5] fixup! --- .changeset/documentation-index-component.md | 2 +- .../mdx-degrades-in-string-pipelines.md | 5 ++ .../core/src/utils/__tests__/remark.test.mjs | 40 +++++++++++ packages/core/src/utils/remark.mjs | 43 ++++++++---- packages/react/src/html/README.md | 8 +++ .../components/DocumentationIndex/index.jsx | 9 ++- .../src/html/utils/__tests__/config.test.mjs | 68 ++++++++++++++++++- packages/react/src/html/utils/config.mjs | 24 ++++++- packages/react/src/jsx-ast/README.md | 6 +- packages/react/src/jsx-ast/constants.mjs | 3 - packages/react/src/jsx-ast/generate.mjs | 36 +--------- scripts/vercel-prepare.sh | 14 +++- 12 files changed, 194 insertions(+), 64 deletions(-) create mode 100644 .changeset/mdx-degrades-in-string-pipelines.md create mode 100644 packages/core/src/utils/__tests__/remark.test.mjs diff --git a/.changeset/documentation-index-component.md b/.changeset/documentation-index-component.md index ddfdf3ef7..efedf2986 100644 --- a/.changeset/documentation-index-component.md +++ b/.changeset/documentation-index-component.md @@ -2,4 +2,4 @@ '@doc-kit/generator-react': patch --- -Render `` with a new `DocumentationIndex` UI component +Add a `` MDX component that renders the stability overview of every module, backed by a new `documentationIndex` export on `#theme/config` (replaces the `` comment) diff --git a/.changeset/mdx-degrades-in-string-pipelines.md b/.changeset/mdx-degrades-in-string-pipelines.md new file mode 100644 index 000000000..df604754a --- /dev/null +++ b/.changeset/mdx-degrades-in-string-pipelines.md @@ -0,0 +1,5 @@ +--- +'@doc-kit/core': patch +--- + +MDX nodes in the HTML-string pipelines are now dropped, and do not crash. \ No newline at end of file diff --git a/packages/core/src/utils/__tests__/remark.test.mjs b/packages/core/src/utils/__tests__/remark.test.mjs new file mode 100644 index 000000000..40250885d --- /dev/null +++ b/packages/core/src/utils/__tests__/remark.test.mjs @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { getRemarkRehype } from '../remark.mjs'; + +describe('getRemarkRehype', () => { + it('degrades MDX nodes instead of crashing rehype-stringify', () => { + const processor = getRemarkRehype(); + + const tree = { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + { type: 'text', value: 'before ' }, + { + type: 'mdxJsxTextElement', + name: 'Tooltip', + attributes: [], + children: [{ type: 'text', value: 'inner' }], + }, + { type: 'mdxTextExpression', value: '1 + 1' }, + ], + }, + { + type: 'mdxJsxFlowElement', + name: 'DocumentationIndex', + attributes: [], + children: [], + }, + ], + }; + + const output = processor.stringify(processor.runSync(tree)); + + // JSX elements degrade to their children; expressions are dropped. + assert.equal(output, '

before inner

'); + }); +}); diff --git a/packages/core/src/utils/remark.mjs b/packages/core/src/utils/remark.mjs index 3935b7ea6..4fc3d9e9d 100644 --- a/packages/core/src/utils/remark.mjs +++ b/packages/core/src/utils/remark.mjs @@ -13,18 +13,33 @@ import { lazy } from './misc.mjs'; import { typeAnnotationToHast } from './type-annotations/hast.mjs'; import remarkTypeAnnotations from './type-annotations/remark.mjs'; -// MDX node types that may appear in trees parsed by `getRemarkMdx`; the -// rehype pipelines pass them through untouched. -const passThrough = [ - 'element', - 'mdxJsxTextElement', - 'mdxJsxFlowElement', - 'mdxJsxAttribute', - 'mdxJsxAttributeValueExpression', - 'mdxFlowExpression', - 'mdxTextExpression', - 'mdxjsEsm', -]; +// Nodes the rehype pipelines pass through untouched. +const passThrough = ['element']; + +/** + * Renders an MDX JSX element as just its children, so the surrounding prose + * still renders in HTML-string output. + * + * @param {import('mdast-util-to-hast').State} state + * @param {import('unist').Parent} node + */ +const mdxElementToChildren = (state, node) => state.all(node); + +/** + * Drops a node from HTML-string output. + */ +const dropNode = () => undefined; + +// The HTML-string pipelines cannot render MDX nodes (rendering those is the +// React generators' job): JSX elements degrade to their children so the +// surrounding prose still renders, and expressions/ESM are dropped. +const mdxToHastHandlers = { + mdxJsxTextElement: mdxElementToChildren, + mdxJsxFlowElement: mdxElementToChildren, + mdxFlowExpression: dropNode, + mdxTextExpression: dropNode, + mdxjsEsm: dropNode, +}; /** * Retrieves an instance of Remark configured to parse GFM (GitHub Flavored Markdown) @@ -64,7 +79,7 @@ export const getRemarkRehype = lazy(() => .use(remarkRehype, { allowDangerousHtml: true, passThrough, - handlers: { typeAnnotation: typeAnnotationToHast }, + handlers: { typeAnnotation: typeAnnotationToHast, ...mdxToHastHandlers }, }) // We allow dangerous HTML to be passed through, since we have HTML within our Markdown // and we trust the sources of the Markdown files @@ -86,7 +101,7 @@ export const getRemarkRehypeWithShiki = lazy(() => allowDangerousHtml: true, passThrough, // legacy-html gets the minimal (unhighlighted) type rendering - handlers: { typeAnnotation: typeAnnotationToHast }, + handlers: { typeAnnotation: typeAnnotationToHast, ...mdxToHastHandlers }, }) // This is a custom ad-hoc within the Shiki Rehype plugin, used to highlight code // and transform them into HAST nodes diff --git a/packages/react/src/html/README.md b/packages/react/src/html/README.md index 5dddd6529..54916a2d0 100644 --- a/packages/react/src/html/README.md +++ b/packages/react/src/html/README.md @@ -358,6 +358,11 @@ title: Welcome There are {stats.length} APIs documented. ``` +The built-in components are available without registration. Notably, +`` renders an index of every documented module with its +stability badge and description, sourced from the `documentationIndex` export +of [`#theme/config`](#themeconfig-virtual-module). + ## `#theme/config` virtual module The `html` 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. @@ -377,6 +382,9 @@ import { project, repository, editURL } from '#theme/config'; - `editURL` {string} Partially populated "edit this page" URL template (only `{path}` remains). - `pages` {Array} Sorted `[name, path]` tuples for sidebar navigation. +- `documentationIndex` {Array} Entries rendered by the built-in + `` component — every page with a stability index, each + `{ api, name, index, description }`. - `navigation` {Object} Mirrors the configured `navigation` (consumed by the built-in `SideBar` and `NavBar`). - `useAbsoluteURLs` {boolean} Whether internal links use absolute URLs (mirrors diff --git a/packages/react/src/html/ui/components/DocumentationIndex/index.jsx b/packages/react/src/html/ui/components/DocumentationIndex/index.jsx index 70d3d0d42..83b90b91a 100644 --- a/packages/react/src/html/ui/components/DocumentationIndex/index.jsx +++ b/packages/react/src/html/ui/components/DocumentationIndex/index.jsx @@ -3,6 +3,8 @@ import Badge from '@node-core/ui-components/Common/Badge'; import styles from './index.module.css'; import { STABILITY_KINDS, STABILITY_LABELS } from '../constants.mjs'; +import { documentationIndex } from '#theme/config'; + /** * @typedef {Object} DocumentationIndexEntry * @property {string} api - Basename of the document, linked as `${api}.html` @@ -37,12 +39,9 @@ const IndexEntry = ({ api, name, index, description }) => { ); }; -/** - * @param {{ entries: Array }} props - */ -export default ({ entries = [] }) => ( +export default () => ( diff --git a/packages/react/src/html/utils/__tests__/config.test.mjs b/packages/react/src/html/utils/__tests__/config.test.mjs index 98c8d678d..ba87f86c7 100644 --- a/packages/react/src/html/utils/__tests__/config.test.mjs +++ b/packages/react/src/html/utils/__tests__/config.test.mjs @@ -14,8 +14,12 @@ mock.module('@node-core/rehype-shiki', { }, }); -const { buildVersionEntries, buildPageList, buildLanguageDisplayNameMap } = - await import('../config.mjs'); +const { + buildVersionEntries, + buildPageList, + buildDocumentationIndex, + buildLanguageDisplayNameMap, +} = await import('../config.mjs'); await setConfig({ version: 'v22.0.0', @@ -123,6 +127,66 @@ describe('buildPageList', () => { }); }); +describe('buildDocumentationIndex', () => { + it('lists only pages with a stability index, with their descriptions', () => { + const input = [ + { + data: { + api: 'fs', + path: '/fs', + heading: { depth: 1, data: { name: 'File System' } }, + stability: { data: { index: '2' } }, + content: { + type: 'root', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'File system APIs.' }], + }, + ], + }, + }, + }, + { + data: { + api: 'index', + path: '/index', + heading: { depth: 1, data: { name: 'Index' } }, + stability: null, + content: { type: 'root', children: [] }, + }, + }, + { + data: { + api: 'quic', + path: '/quic', + heading: { depth: 1, data: { name: 'QUIC' } }, + stability: { data: { index: '1.1' } }, + llm_description: 'QUIC protocol support.', + content: { type: 'root', children: [] }, + }, + }, + ]; + + const result = buildDocumentationIndex(input); + + assert.deepStrictEqual(result, [ + { + api: 'fs', + name: 'File System', + index: '2', + description: 'File system APIs.', + }, + { + api: 'quic', + name: 'QUIC', + index: '1.1', + description: 'QUIC protocol support.', + }, + ]); + }); +}); + describe('buildLanguageDisplayNameMap', () => { it('returns entries suitable for constructing a Map', () => { const result = buildLanguageDisplayNameMap(); diff --git a/packages/react/src/html/utils/config.mjs b/packages/react/src/html/utils/config.mjs index 4a822b3a7..5df89a213 100644 --- a/packages/react/src/html/utils/config.mjs +++ b/packages/react/src/html/utils/config.mjs @@ -2,7 +2,10 @@ import getConfig from '@doc-kit/core/utils/configuration/index.mjs'; import { populate } from '@doc-kit/core/utils/configuration/templates.mjs'; -import { getVersionFromSemVer } from '@doc-kit/core/utils/generators.mjs'; +import { + getEntryDescription, + getVersionFromSemVer, +} from '@doc-kit/core/utils/generators.mjs'; import { omitKeys } from '@doc-kit/core/utils/misc.mjs'; import { LANGS } from '@node-core/rehype-shiki'; @@ -41,6 +44,24 @@ export function buildPageList(input) { return headNodes.map(node => [node.heading.data.name, node.path]); } +/** + * Pre-compute the entries rendered by the `` component: + * every page with a stability index, plus its description. + * + * @param {Array} input + * @returns {Array<{api: string, name: string, index: string, description: string}>} + */ +export function buildDocumentationIndex(input) { + return getSortedHeadNodes(input.map(e => e.data)) + .filter(entry => entry.stability) + .map(entry => ({ + api: entry.api, + name: entry.heading.data.name, + index: entry.stability.data.index, + description: getEntryDescription(entry), + })); +} + /** * Pre-compute Shiki language display name map entries. * @@ -107,6 +128,7 @@ export default function createConfigSource(input, server = false) { versions: buildVersionEntries(config.changelog, pageURL), editURL, pages: buildPageList(input), + documentationIndex: buildDocumentationIndex(input), server, }; diff --git a/packages/react/src/jsx-ast/README.md b/packages/react/src/jsx-ast/README.md index a8fbd040e..dcb504913 100644 --- a/packages/react/src/jsx-ast/README.md +++ b/packages/react/src/jsx-ast/README.md @@ -16,6 +16,6 @@ The `jsx-ast` generator converts MDAST (Markdown Abstract Syntax Tree) to JSX AS ## Index page `index.html` is generated when an `index` document is part of the input, and -is rendered from that document like any other page. A section containing a -`` comment additionally receives the Stability -Overview table of all modules. +is rendered from that document like any other page. An MDX `index` document +can render the stability overview of every module by using the built-in +`` component (see the `html` generator's README). diff --git a/packages/react/src/jsx-ast/constants.mjs b/packages/react/src/jsx-ast/constants.mjs index 26b5afcc1..ffeec38c8 100644 --- a/packages/react/src/jsx-ast/constants.mjs +++ b/packages/react/src/jsx-ast/constants.mjs @@ -189,9 +189,6 @@ export const AST_NODE_TYPES = { }, }; -// `` comment in a source document = a stability index -export const DOCUMENTATION_INDEX_TAG = 'DOCUMENTATION_INDEX'; - // These positions are explicity before anything else export const OVERRIDDEN_POSITIONS = [ 'index', // https://github.com/nodejs/node/blob/main/doc/api/index.md diff --git a/packages/react/src/jsx-ast/generate.mjs b/packages/react/src/jsx-ast/generate.mjs index 6a1a2f733..25d0c2b99 100644 --- a/packages/react/src/jsx-ast/generate.mjs +++ b/packages/react/src/jsx-ast/generate.mjs @@ -1,36 +1,12 @@ import getConfig from '@doc-kit/core/utils/configuration/index.mjs'; -import { - getEntryDescription, - groupNodesByModule, -} from '@doc-kit/core/utils/generators.mjs'; +import { groupNodesByModule } from '@doc-kit/core/utils/generators.mjs'; import { jsx, toJs } from 'estree-util-to-js'; -import { DOCUMENTATION_INDEX_TAG } from './constants.mjs'; -import { createJSXElement } from './utils/ast.mjs'; import buildContent from './utils/buildContent.mjs'; import { getSortedHeadNodes } from './utils/getSortedHeadNodes.mjs'; -import { JSX_IMPORTS } from '../html/constants.mjs'; import { buildNotFoundPage } from './utils/synthetic/404.mjs'; import { buildAllPage } from './utils/synthetic/all.mjs'; -/** - * Builds the `` element - * - * @param {Array} moduleEntries - */ -const buildDocumentationIndex = moduleEntries => - createJSXElement(JSX_IMPORTS.DocumentationIndex.name, { - inline: false, - entries: getSortedHeadNodes(moduleEntries) - .filter(entry => entry.stability) - .map(entry => ({ - api: entry.api, - name: entry.heading.data.name, - index: entry.stability.data.index, - description: getEntryDescription(entry), - })), - }); - /** * Builds the `{ head, entries }` page descriptors for all configured synthetic * pages. The descriptors are cheap to build; the expensive `buildContent` step @@ -83,17 +59,9 @@ export async function processChunk(slicedInput, itemIndices) { */ export async function* generate(input, worker) { // The `index` page is only generated when an `index` document is part of - // the input; the module list for the synthetic pages and the documentation - // index excludes it. + // the input; the module list for the synthetic pages excludes it. const moduleInput = input.filter(entry => entry.api !== 'index'); - // Sections tagged with a `` build an index - for (const entry of input) { - if (entry.tags?.includes(DOCUMENTATION_INDEX_TAG)) { - entry.content.children.push(buildDocumentationIndex(moduleInput)); - } - } - // Create sliced input: each item contains head + its module's entries // This avoids sending all 4700+ entries to every worker const groupedModules = groupNodesByModule(input); diff --git a/scripts/vercel-prepare.sh b/scripts/vercel-prepare.sh index 2fa3e1b5d..2c15fb9fa 100755 --- a/scripts/vercel-prepare.sh +++ b/scripts/vercel-prepare.sh @@ -26,7 +26,19 @@ cd node # Enable sparse checkout and specify the folder git sparse-checkout set lib doc . -sed 's/STABILITY_OVERVIEW_SLOT_BEGIN/DOCUMENTATION_INDEX/g' ./doc/api/documentation.md > ./doc/api/index.md +# TODO(@avivkeller): Remove this rewrite once nodejs/node embeds +# `` directly. +INTRODUCED_IN=$(sed -n 's/^$/\1/p' ./doc/api/documentation.md) + +{ + printf -- '---\nmdx: true\ntype: misc---\n' + sed \ + -e 's|||' \ + -e '/^$/d' \ + -e '/^$/d' \ + ./doc/api/documentation.md +} > ./doc/api/index.md + rm ./doc/api/documentation.md # Move back out From b1fe474533509889d1f55801dbd5a5daf6a46a6f Mon Sep 17 00:00:00 2001 From: avivkeller Date: Wed, 19 Aug 2026 15:26:44 -0700 Subject: [PATCH 5/5] fixup! --- scripts/vercel-prepare.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/vercel-prepare.sh b/scripts/vercel-prepare.sh index 2c15fb9fa..5726021a7 100755 --- a/scripts/vercel-prepare.sh +++ b/scripts/vercel-prepare.sh @@ -31,7 +31,11 @@ git sparse-checkout set lib doc . INTRODUCED_IN=$(sed -n 's/^$/\1/p' ./doc/api/documentation.md) { - printf -- '---\nmdx: true\ntype: misc---\n' + printf -- '---\nmdx: true\ntype: misc\n' + if [ -n "$INTRODUCED_IN" ]; then + printf -- 'introduced_in: %s\n' "$INTRODUCED_IN" + fi + printf -- '---\n' sed \ -e 's|||' \ -e '/^$/d' \