diff --git a/src/generators/jsx-ast/generate.mjs b/src/generators/jsx-ast/generate.mjs index 160971fd..d63fe817 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) { @@ -36,7 +40,8 @@ export async function processChunk(slicedInput, itemIndices, docPages) { entries, head, sideBarProps, - remarkRecma + remarkRecma, + stabilityOverviewEntries ); results.push(content); @@ -57,6 +62,18 @@ export async function* generate(input, worker) { const docPages = headNodes.map(node => [node.heading.data.name, node.path]); + // Pre-compute stability overview data once — avoid serialising full AST nodes to workers + const stabilityOverviewEntries = headNodes + .filter(node => node.stability) + .map(({ api, heading, stability }) => { + return { + api, + name: heading.data.name, + stabilityIndex: parseInt(stability.data.index, 10), + stabilityDescription: stability.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 => ({ @@ -64,7 +81,10 @@ 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, { + 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..4a46a395 --- /dev/null +++ b/src/generators/jsx-ast/utils/__tests__/buildStabilityOverview.test.mjs @@ -0,0 +1,67 @@ +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; + +const getAttributeExpression = (node, name) => + getAttribute(node, name)?.data?.estree?.body?.[0]?.expression; + +describe('buildStabilityOverview', () => { + it('builds a StabilityOverview JSX block element', () => { + 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.type, 'mdxJsxFlowElement'); + assert.equal(result.name, 'StabilityOverview'); + }); + + it('serializes entries into the entries prop', () => { + const result = buildStabilityOverview([ + { + api: 'fs', + name: 'File system', + stabilityIndex: 0, + stabilityDescription: 'Deprecated: use fs/promises', + }, + { + api: 'timers', + name: 'Timers', + stabilityIndex: 2, + stabilityDescription: 'Stable', + }, + ]); + + const entriesExpression = getAttributeExpression(result, 'entries'); + + assert.equal(entriesExpression.type, 'ArrayExpression'); + assert.equal(entriesExpression.elements.length, 2); + + const firstEntry = entriesExpression.elements[0]; + const firstApi = firstEntry.properties.find( + ({ key }) => key.name === 'api' + ); + const firstStabilityIndex = firstEntry.properties.find( + ({ key }) => key.name === 'stabilityIndex' + ); + + assert.equal(firstApi.value.value, 'fs'); + assert.equal(firstStabilityIndex.value.value, 0); + }); +}); diff --git a/src/generators/jsx-ast/utils/__tests__/processEntry.test.mjs b/src/generators/jsx-ast/utils/__tests__/processEntry.test.mjs new file mode 100644 index 00000000..44b74331 --- /dev/null +++ b/src/generators/jsx-ast/utils/__tests__/processEntry.test.mjs @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { processEntry } from '../buildContent.mjs'; + +describe('processEntry', () => { + it('does not throw when tags are missing', () => { + const entry = { + content: { + type: 'root', + children: [], + }, + }; + + assert.doesNotThrow(() => + processEntry(entry, null, [ + { + api: 'fs', + name: 'File system', + stabilityIndex: 2, + stabilityDescription: 'Stable', + }, + ]) + ); + }); +}); diff --git a/src/generators/jsx-ast/utils/buildContent.mjs b/src/generators/jsx-ast/utils/buildContent.mjs index 3091e80d..adb6d2c3 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 { JSX_IMPORTS } from '../../web/constants.mjs'; import { @@ -256,7 +257,7 @@ export const transformHeadingNode = async ( * @param {import('../../metadata/types').MetadataEntry} 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,13 +299,16 @@ export const createDocumentLayout = ( entries, sideBarProps, metaBarProps, - remark + remark, + stabilityOverviewEntries = [] ) => createTree('root', [ createJSXElement(JSX_IMPORTS.Layout.name, { sideBarProps, metaBarProps, - children: entries.map(entry => processEntry(entry, remark)), + children: entries.map(entry => + processEntry(entry, remark, stabilityOverviewEntries) + ), }), ]); @@ -310,7 +322,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); @@ -319,7 +337,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..7ec30431 --- /dev/null +++ b/src/generators/jsx-ast/utils/buildStabilityOverview.mjs @@ -0,0 +1,17 @@ +import { createJSXElement } from './ast.mjs'; +// TODO(@avivkeller): JSX imports belong in the JSX generator +import { JSX_IMPORTS } from '../../web/constants.mjs'; + +/** + * Builds the Stability Overview component. + * + * @param {Array<{ api: string, name: string, stabilityIndex: number, stabilityDescription: string }>} entries + * @returns {import('unist').Node} + */ +const buildStabilityOverview = entries => + createJSXElement(JSX_IMPORTS.StabilityOverview.name, { + inline: false, + entries, + }); + +export default buildStabilityOverview; diff --git a/src/generators/web/constants.mjs b/src/generators/web/constants.mjs index 5cbfe760..cccbe303 100644 --- a/src/generators/web/constants.mjs +++ b/src/generators/web/constants.mjs @@ -22,6 +22,10 @@ export const JSX_IMPORTS = { name: 'CodeBox', source: resolve(ROOT, './ui/components/CodeBox'), }, + StabilityOverview: { + name: 'StabilityOverview', + source: resolve(ROOT, './ui/components/StabilityOverview'), + }, CodeTabs: { name: 'CodeTabs', source: '@node-core/ui-components/MDX/CodeTabs', @@ -41,6 +45,14 @@ export const JSX_IMPORTS = { isDefaultExport: false, source: '@node-core/ui-components/MDX/Tooltip', }, + TableOfContents: { + 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', diff --git a/src/generators/web/ui/components/StabilityOverview/constants.mjs b/src/generators/web/ui/components/StabilityOverview/constants.mjs new file mode 100644 index 00000000..09d82fd5 --- /dev/null +++ b/src/generators/web/ui/components/StabilityOverview/constants.mjs @@ -0,0 +1 @@ +export const STABILITY_KINDS = ['error', 'warning', 'default', 'info']; 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..b0e78ff6 --- /dev/null +++ b/src/generators/web/ui/components/StabilityOverview/index.jsx @@ -0,0 +1,37 @@ +import BadgeGroup from '@node-core/ui-components/Common/BadgeGroup'; + +import { STABILITY_KINDS } from './constants.mjs'; + +/** + * Renders the module stability overview table. + * @param {{ entries: Array<{ api: string, name: string, stabilityIndex: number, stabilityDescription: string }> }} props + */ +export default ({ entries = [] }) => ( + + + + + + + + + {entries.map(({ api, name, stabilityIndex, stabilityDescription }) => ( + + + + + ))} + +
APIStability
+ {name} + + + {stabilityDescription} + +
+);