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/__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'); + }); +}); diff --git a/src/generators/jsx-ast/utils/buildContent.mjs b/src/generators/jsx-ast/utils/buildContent.mjs index 62487631..8aa3736c 100644 --- a/src/generators/jsx-ast/utils/buildContent.mjs +++ b/src/generators/jsx-ast/utils/buildContent.mjs @@ -7,6 +7,7 @@ import { SKIP, visit } from 'unist-util-visit'; import { createJSXElement } from './ast.mjs'; import { buildMetaBarProps } from './buildBarProps.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'; @@ -256,7 +257,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); @@ -276,6 +277,14 @@ export const processEntry = (entry, remark) => { (parent.children[idx] = createSignatureTable(node, remark)) ); + // Inject the stability overview table where the slot tag is present + if ( + stabilityOverviewEntries.length && + entry.tags.includes('STABILITY_OVERVIEW_SLOT_BEGIN') + ) { + content.children.push(buildStabilityOverview(stabilityOverviewEntries)); + } + return content; }; @@ -290,7 +299,8 @@ export const createDocumentLayout = ( entries, sideBarProps, metaBarProps, - remark + remark, + stabilityOverviewEntries = [] ) => createTree('root', [ createJSXElement(JSX_IMPORTS.NavBar.name), @@ -306,7 +316,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), @@ -325,7 +337,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); @@ -334,7 +352,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/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 82632a3a..c719ad71 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',