-
Notifications
You must be signed in to change notification settings - Fork 28
feat(web): introduce stability overview
#644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
113fbbe
df3c7a5
95229d1
f2b9366
83203ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,14 +59,30 @@ 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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this map fn be moved to a dedicated function and be unit tested? Thanks!
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seem reasonable |
||
| 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 => ({ | ||
| head, | ||
| 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 ( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder how optimal this is... I wonder if a visit statement is better here, I also kinda dislike this method of inserting the stability index. |
||
| 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<JSXContent>} | ||
| */ | ||
| 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.) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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']; | ||
|
|
||
| /** | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: remove this empty doc
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we remove it es-lint ask for it but it's doesn't require any jsdoc |
||
| * | ||
| */ | ||
| 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; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh 🤓 that's smart