From 113fbbe0e7cee1b0269510e7830e1d889fa05e33 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:14:25 +0100 Subject: [PATCH 1/4] feat(`web`): introduce stability overview --- src/generators/jsx-ast/generate.mjs | 27 ++++++++- src/generators/jsx-ast/utils/buildContent.mjs | 32 +++++++++-- src/generators/web/constants.mjs | 4 ++ src/generators/web/generate.mjs | 4 +- .../ui/components/StabilityOverview/index.jsx | 56 +++++++++++++++++++ .../StabilityOverview/index.module.css | 10 ++++ 6 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 src/generators/web/ui/components/StabilityOverview/index.jsx create mode 100644 src/generators/web/ui/components/StabilityOverview/index.module.css diff --git a/src/generators/jsx-ast/generate.mjs b/src/generators/jsx-ast/generate.mjs index ea55af76..81c86736 100644 --- a/src/generators/jsx-ast/generate.mjs +++ b/src/generators/jsx-ast/generate.mjs @@ -16,7 +16,11 @@ const remarkRecma = getRemarkRecma(); * * @type {import('./types').Generator['processChunk']} */ -export async function processChunk(slicedInput, itemIndices, docPages) { +export async function processChunk( + slicedInput, + itemIndices, + { docPages, stabilityOverviewEntries } +) { const results = []; for (const idx of itemIndices) { @@ -28,7 +32,8 @@ export async function processChunk(slicedInput, itemIndices, docPages) { entries, head, sideBarProps, - remarkRecma + remarkRecma, + stabilityOverviewEntries ); results.push(content); @@ -54,6 +59,19 @@ export async function* generate(input, worker) { ? config.index.map(({ section, api }) => [section, `${api}.html`]) : headNodes.map(node => [node.heading.data.name, `${node.api}.html`]); + // Pre-compute stability overview data once — avoid serialising full AST nodes to workers + const stabilityOverviewEntries = headNodes + .filter(node => node.stability?.children?.length) + .map(({ api, heading, stability }) => { + const [{ data }] = stability.children; + return { + api, + name: heading.data.name, + stabilityIndex: parseInt(data.index, 10), + stabilityDescription: data.description.split('. ')[0], + }; + }); + // 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 => ({ @@ -61,7 +79,10 @@ export async function* generate(input, worker) { entries: groupedModules.get(head.api), })); - for await (const chunkResult of worker.stream(entries, entries, docPages)) { + for await (const chunkResult of worker.stream(entries, entries, { + docPages, + stabilityOverviewEntries, + })) { yield chunkResult; } } diff --git a/src/generators/jsx-ast/utils/buildContent.mjs b/src/generators/jsx-ast/utils/buildContent.mjs index 363d1829..f7867068 100644 --- a/src/generators/jsx-ast/utils/buildContent.mjs +++ b/src/generators/jsx-ast/utils/buildContent.mjs @@ -253,7 +253,7 @@ export const transformHeadingNode = async ( * @param {ApiDocMetadataEntry} entry - The API metadata entry to process * @param {import('unified').Processor} remark - The remark processor */ -export const processEntry = (entry, remark) => { +export const processEntry = (entry, remark, stabilityOverviewEntries = []) => { // Deep copy content to avoid mutations on original const content = structuredClone(entry.content); @@ -272,6 +272,18 @@ export const processEntry = (entry, remark) => { (node, idx, parent) => (parent.children[idx] = createPropertyTable(node)) ); + // Inject the stability overview table where the slot tag is present + if ( + stabilityOverviewEntries.length && + entry.tags.includes('STABILITY_OVERVIEW_SLOT_BEGIN') + ) { + content.children.push( + createJSXElement(JSX_IMPORTS.StabilityOverview.name, { + entries: stabilityOverviewEntries, + }) + ); + } + return content; }; @@ -286,7 +298,8 @@ export const createDocumentLayout = ( entries, sideBarProps, metaBarProps, - remark + remark, + stabilityOverviewEntries = [] ) => createTree('root', [ createJSXElement(JSX_IMPORTS.NavBar.name), @@ -302,7 +315,9 @@ export const createDocumentLayout = ( createElement('br'), createElement( 'main', - entries.map(entry => processEntry(entry, remark)) + entries.map(entry => + processEntry(entry, remark, stabilityOverviewEntries) + ) ), ]), createJSXElement(JSX_IMPORTS.MetaBar.name, metaBarProps), @@ -321,7 +336,13 @@ export const createDocumentLayout = ( * @param {import('unified').Processor} remark - Remark processor instance for markdown processing * @returns {Promise} */ -const buildContent = async (metadataEntries, head, sideBarProps, remark) => { +const buildContent = async ( + metadataEntries, + head, + sideBarProps, + remark, + stabilityOverviewEntries = [] +) => { // Build props for the MetaBar from head and entries const metaBarProps = buildMetaBarProps(head, metadataEntries); @@ -330,7 +351,8 @@ const buildContent = async (metadataEntries, head, sideBarProps, remark) => { metadataEntries, sideBarProps, metaBarProps, - remark + remark, + stabilityOverviewEntries ); // Run remark processor to transform AST (parse markdown, plugins, etc.) diff --git a/src/generators/web/constants.mjs b/src/generators/web/constants.mjs index 44efdabb..ba2ec054 100644 --- a/src/generators/web/constants.mjs +++ b/src/generators/web/constants.mjs @@ -86,6 +86,10 @@ export const JSX_IMPORTS = { name: 'ArrowUpRightIcon', source: '@heroicons/react/24/solid/ArrowUpRightIcon', }, + StabilityOverview: { + name: 'StabilityOverview', + source: resolve(ROOT, './ui/components/StabilityOverview'), + }, }; /** diff --git a/src/generators/web/generate.mjs b/src/generators/web/generate.mjs index 1679a5a8..969afa6d 100644 --- a/src/generators/web/generate.mjs +++ b/src/generators/web/generate.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { readFile, writeFile } from 'node:fs/promises'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { join } from 'node:path'; @@ -35,6 +35,8 @@ export async function generate(input) { // Process all entries together (required for code-split bundles) if (config.output) { + await mkdir(config.output, { recursive: true }); + // Write HTML files for (const { html, api } of results) { await writeFile(join(config.output, `${api}.html`), html, 'utf-8'); diff --git a/src/generators/web/ui/components/StabilityOverview/index.jsx b/src/generators/web/ui/components/StabilityOverview/index.jsx new file mode 100644 index 00000000..ed5bb8e3 --- /dev/null +++ b/src/generators/web/ui/components/StabilityOverview/index.jsx @@ -0,0 +1,56 @@ +import Badge from '@node-core/ui-components/Common/Badge'; + +import styles from './index.module.css'; + +const STABILITY_KINDS = ['error', 'warning', 'success', 'info']; +const STABILITY_TOOLTIPS = ['Deprecated', 'Experimental', 'Stable', 'Legacy']; + +/** + * @typedef StabilityOverviewEntry + * @property {string} api - The API identifier (basename, e.g. "fs") + * @property {string} name - The human-readable display name of the API module + * @property {number} stabilityIndex - The stability level index (0–3) + * @property {string} stabilityDescription - First sentence of the stability description + */ + +/** + * Renders a table summarising the stability level of each API module. + * + * @param {{ entries: Array }} props + */ +export default ({ entries = [] }) => { + if (!entries.length) { + return null; + } + + return ( + + + + + + + + + {entries.map(({ api, name, stabilityIndex, stabilityDescription }) => ( + + + + + ))} + +
APIStability
+ {name} + + + {stabilityIndex} + + + {` ${stabilityDescription}`} +
+ ); +}; diff --git a/src/generators/web/ui/components/StabilityOverview/index.module.css b/src/generators/web/ui/components/StabilityOverview/index.module.css new file mode 100644 index 00000000..8c28dd74 --- /dev/null +++ b/src/generators/web/ui/components/StabilityOverview/index.module.css @@ -0,0 +1,10 @@ +.table { + width: 100%; +} + +.stabilityCell { + display: flex; + align-items: center; + gap: 0.4rem; + flex-wrap: wrap; +} From df3c7a5f6895f259e27800c60b159710f5732ae9 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:54:42 +0100 Subject: [PATCH 2/4] Update generate.mjs --- src/generators/web/generate.mjs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/generators/web/generate.mjs b/src/generators/web/generate.mjs index 969afa6d..1679a5a8 100644 --- a/src/generators/web/generate.mjs +++ b/src/generators/web/generate.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { readFile, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { join } from 'node:path'; @@ -35,8 +35,6 @@ export async function generate(input) { // Process all entries together (required for code-split bundles) if (config.output) { - await mkdir(config.output, { recursive: true }); - // Write HTML files for (const { html, api } of results) { await writeFile(join(config.output, `${api}.html`), html, 'utf-8'); From 95229d16f411fed4e1d604c8c623ace2965d1729 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:32:32 +0100 Subject: [PATCH 3/4] update --- src/generators/jsx-ast/utils/buildContent.mjs | 7 +-- .../jsx-ast/utils/buildStabilityOverview.mjs | 49 ++++++++++++++++ src/generators/web/constants.mjs | 8 +-- .../ui/components/StabilityOverview/index.jsx | 56 ------------------- .../StabilityOverview/index.module.css | 10 ---- 5 files changed, 55 insertions(+), 75 deletions(-) create mode 100644 src/generators/jsx-ast/utils/buildStabilityOverview.mjs delete mode 100644 src/generators/web/ui/components/StabilityOverview/index.jsx delete mode 100644 src/generators/web/ui/components/StabilityOverview/index.module.css diff --git a/src/generators/jsx-ast/utils/buildContent.mjs b/src/generators/jsx-ast/utils/buildContent.mjs index f7867068..88807bd4 100644 --- a/src/generators/jsx-ast/utils/buildContent.mjs +++ b/src/generators/jsx-ast/utils/buildContent.mjs @@ -8,6 +8,7 @@ import { SKIP, visit } from 'unist-util-visit'; import { createJSXElement } from './ast.mjs'; import { buildMetaBarProps } from './buildBarProps.mjs'; import createPropertyTable from './buildPropertyTable.mjs'; +import buildStabilityOverview from './buildStabilityOverview.mjs'; import { enforceArray } from '../../../utils/array.mjs'; import createQueries from '../../../utils/queries/index.mjs'; import { JSX_IMPORTS } from '../../web/constants.mjs'; @@ -277,11 +278,7 @@ export const processEntry = (entry, remark, stabilityOverviewEntries = []) => { stabilityOverviewEntries.length && entry.tags.includes('STABILITY_OVERVIEW_SLOT_BEGIN') ) { - content.children.push( - createJSXElement(JSX_IMPORTS.StabilityOverview.name, { - entries: stabilityOverviewEntries, - }) - ); + content.children.push(buildStabilityOverview(stabilityOverviewEntries)); } return content; diff --git a/src/generators/jsx-ast/utils/buildStabilityOverview.mjs b/src/generators/jsx-ast/utils/buildStabilityOverview.mjs new file mode 100644 index 00000000..1503aaca --- /dev/null +++ b/src/generators/jsx-ast/utils/buildStabilityOverview.mjs @@ -0,0 +1,49 @@ +import { h as createElement } from 'hastscript'; + +import { createJSXElement } from './ast.mjs'; +import { JSX_IMPORTS } from '../../web/constants.mjs'; + +const STABILITY_KINDS = ['error', 'warning', 'default', 'info']; + +/** + * + */ +const createBadge = (stabilityIndex, stabilityDescription) => { + const kind = STABILITY_KINDS[stabilityIndex] ?? 'success'; + + return createJSXElement(JSX_IMPORTS.BadgeGroup.name, { + as: 'span', + size: 'small', + kind, + badgeText: stabilityIndex, + children: stabilityDescription, + }); +}; + +/** + * Builds a static Stability Overview table. + * + * @param {Array<{ api: string, name: string, stabilityIndex: number, stabilityDescription: string }>} entries + * @returns {import('hast').Element} + */ +const buildStabilityOverview = entries => { + const rows = entries.map( + ({ api, name, stabilityIndex, stabilityDescription }) => + createElement('tr', [ + createElement('td', createElement('a', { href: `${api}.html` }, name)), + createElement('td', createBadge(stabilityIndex, stabilityDescription)), + ]) + ); + + return createElement('table', [ + createElement('thead', [ + createElement('tr', [ + createElement('th', 'API'), + createElement('th', 'Stability'), + ]), + ]), + createElement('tbody', rows), + ]); +}; + +export default buildStabilityOverview; diff --git a/src/generators/web/constants.mjs b/src/generators/web/constants.mjs index ba2ec054..89f00f11 100644 --- a/src/generators/web/constants.mjs +++ b/src/generators/web/constants.mjs @@ -62,6 +62,10 @@ export const JSX_IMPORTS = { name: 'TableOfContents', source: '@node-core/ui-components/Common/TableOfContents', }, + BadgeGroup: { + name: 'BadgeGroup', + source: '@node-core/ui-components/Common/BadgeGroup', + }, ChangeHistory: { name: 'ChangeHistory', source: '@node-core/ui-components/Common/ChangeHistory', @@ -86,10 +90,6 @@ export const JSX_IMPORTS = { name: 'ArrowUpRightIcon', source: '@heroicons/react/24/solid/ArrowUpRightIcon', }, - StabilityOverview: { - name: 'StabilityOverview', - source: resolve(ROOT, './ui/components/StabilityOverview'), - }, }; /** diff --git a/src/generators/web/ui/components/StabilityOverview/index.jsx b/src/generators/web/ui/components/StabilityOverview/index.jsx deleted file mode 100644 index ed5bb8e3..00000000 --- a/src/generators/web/ui/components/StabilityOverview/index.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import Badge from '@node-core/ui-components/Common/Badge'; - -import styles from './index.module.css'; - -const STABILITY_KINDS = ['error', 'warning', 'success', 'info']; -const STABILITY_TOOLTIPS = ['Deprecated', 'Experimental', 'Stable', 'Legacy']; - -/** - * @typedef StabilityOverviewEntry - * @property {string} api - The API identifier (basename, e.g. "fs") - * @property {string} name - The human-readable display name of the API module - * @property {number} stabilityIndex - The stability level index (0–3) - * @property {string} stabilityDescription - First sentence of the stability description - */ - -/** - * Renders a table summarising the stability level of each API module. - * - * @param {{ entries: Array }} props - */ -export default ({ entries = [] }) => { - if (!entries.length) { - return null; - } - - return ( - - - - - - - - - {entries.map(({ api, name, stabilityIndex, stabilityDescription }) => ( - - - - - ))} - -
APIStability
- {name} - - - {stabilityIndex} - - - {` ${stabilityDescription}`} -
- ); -}; diff --git a/src/generators/web/ui/components/StabilityOverview/index.module.css b/src/generators/web/ui/components/StabilityOverview/index.module.css deleted file mode 100644 index 8c28dd74..00000000 --- a/src/generators/web/ui/components/StabilityOverview/index.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.table { - width: 100%; -} - -.stabilityCell { - display: flex; - align-items: center; - gap: 0.4rem; - flex-wrap: wrap; -} From f2b9366d2a4aa07e11ec44ac975add064531cced Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:42:21 +0100 Subject: [PATCH 4/4] Create buildStabilityOverview.test.mjs --- .../__tests__/buildStabilityOverview.test.mjs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/generators/jsx-ast/utils/__tests__/buildStabilityOverview.test.mjs diff --git a/src/generators/jsx-ast/utils/__tests__/buildStabilityOverview.test.mjs b/src/generators/jsx-ast/utils/__tests__/buildStabilityOverview.test.mjs new file mode 100644 index 00000000..a39627e2 --- /dev/null +++ b/src/generators/jsx-ast/utils/__tests__/buildStabilityOverview.test.mjs @@ -0,0 +1,82 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import buildStabilityOverview from '../buildStabilityOverview.mjs'; + +const getAttribute = (node, name) => + node.attributes.find(attribute => attribute.name === name)?.value; + +describe('buildStabilityOverview', () => { + it('builds a table with expected headings and rows', () => { + const entries = [ + { + api: 'fs', + name: 'File system', + stabilityIndex: 2, + stabilityDescription: 'Stable', + }, + { + api: 'async_context', + name: 'Async context', + stabilityIndex: 1, + stabilityDescription: 'Experimental', + }, + ]; + + const result = buildStabilityOverview(entries); + + assert.equal(result.tagName, 'table'); + + const thead = result.children[0]; + const tbody = result.children[1]; + + assert.equal(thead.tagName, 'thead'); + assert.equal(tbody.tagName, 'tbody'); + assert.equal(tbody.children.length, 2); + + const headerCells = thead.children[0].children; + assert.equal(headerCells[0].children[0].value, 'API'); + assert.equal(headerCells[1].children[0].value, 'Stability'); + }); + + it('creates links and BadgeGroup cells with mapped props', () => { + const [row] = buildStabilityOverview([ + { + api: 'fs', + name: 'File system', + stabilityIndex: 0, + stabilityDescription: 'Deprecated: use fs/promises', + }, + ]).children[1].children; + + const link = row.children[0].children[0]; + const badgeGroup = row.children[1].children[0]; + + assert.equal(link.tagName, 'a'); + assert.equal(link.properties.href, 'fs.html'); + assert.equal(link.children[0].value, 'File system'); + + assert.equal(badgeGroup.name, 'BadgeGroup'); + assert.equal(getAttribute(badgeGroup, 'as'), 'span'); + assert.equal(getAttribute(badgeGroup, 'size'), 'small'); + assert.equal(getAttribute(badgeGroup, 'kind'), 'error'); + assert.equal(getAttribute(badgeGroup, 'badgeText'), '0'); + assert.equal(badgeGroup.children[0].value, 'Deprecated: use fs/promises'); + }); + + it('falls back to success kind for unknown stability index', () => { + const [row] = buildStabilityOverview([ + { + api: 'custom', + name: 'Custom API', + stabilityIndex: 9, + stabilityDescription: 'Unknown status', + }, + ]).children[1].children; + + const badgeGroup = row.children[1].children[0]; + + assert.equal(getAttribute(badgeGroup, 'kind'), 'success'); + assert.equal(getAttribute(badgeGroup, 'badgeText'), '9'); + }); +});