-
Notifications
You must be signed in to change notification settings - Fork 10
feat(jsx): add jsx
generator
#273
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
Merged
+1,180
−52
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
/** | ||
* UI classes for Node.js API stability levels | ||
* | ||
* @see https://nodejs.org/api/documentation.html#stability-index | ||
*/ | ||
export const STABILITY_LEVELS = [ | ||
'danger', // (0) Deprecated | ||
'warning', // (1) Experimental | ||
'success', // (2) Stable | ||
'info', // (3) Legacy | ||
]; | ||
|
||
/** | ||
* HTML tag to UI component mappings | ||
*/ | ||
export const TAG_TRANSFORMS = { | ||
pre: 'CodeBox', | ||
blockquote: 'Blockquote', | ||
}; | ||
|
||
/** | ||
* @see transformer.mjs's TODO comment | ||
*/ | ||
export const TYPE_TRANSFORMS = { | ||
raw: 'text', | ||
}; | ||
|
||
/** | ||
* API type icon configurations | ||
*/ | ||
export const API_ICONS = { | ||
event: { symbol: 'E', color: 'red' }, | ||
method: { symbol: 'M', color: 'red' }, | ||
property: { symbol: 'P', color: 'red' }, | ||
class: { symbol: 'C', color: 'red' }, | ||
module: { symbol: 'M', color: 'red' }, | ||
classMethod: { symbol: 'S', color: 'red' }, | ||
ctor: { symbol: 'C', color: 'red' }, | ||
}; | ||
|
||
/** | ||
* API lifecycle change labels | ||
*/ | ||
export const LIFECYCLE_LABELS = { | ||
added_in: 'Added in', | ||
deprecated_in: 'Deprecated in', | ||
removed_in: 'Removed in', | ||
introduced_in: 'Introduced in', | ||
}; | ||
|
||
// TODO(@avivkeller): These should be inherited from @node-core/website-i18n | ||
export const INTERNATIONALIZABLE = { | ||
sourceCode: 'Source Code: ', | ||
}; | ||
|
||
/** | ||
* Abstract Syntax Tree node type constants | ||
*/ | ||
export const AST_NODE_TYPES = { | ||
MDX: { | ||
/** | ||
* Text-level JSX element | ||
* | ||
* @see https://github.com/syntax-tree/mdast-util-mdx-jsx#mdxjsxtextelement | ||
*/ | ||
JSX_INLINE_ELEMENT: 'mdxJsxTextElement', | ||
|
||
/** | ||
* Block-level JSX element | ||
* | ||
* @see https://github.com/syntax-tree/mdast-util-mdx-jsx#mdxjsxflowelement | ||
*/ | ||
JSX_BLOCK_ELEMENT: 'mdxJsxFlowElement', | ||
|
||
/** | ||
* JSX attribute | ||
* | ||
* @see https://github.com/syntax-tree/mdast-util-mdx-jsx#mdxjsxattribute | ||
*/ | ||
JSX_ATTRIBUTE: 'mdxJsxAttribute', | ||
|
||
/** | ||
* JSX expression attribute | ||
* | ||
* @see https://github.com/syntax-tree/mdast-util-mdx-jsx#mdxjsxattributevalueexpression | ||
*/ | ||
JSX_ATTRIBUTE_EXPRESSION: 'mdxJsxAttributeValueExpression', | ||
}, | ||
ESTREE: { | ||
/** | ||
* AST Program node | ||
* | ||
* @see https://github.com/estree/estree/blob/master/es5.md#programs | ||
*/ | ||
PROGRAM: 'Program', | ||
|
||
/** | ||
* Expression statement | ||
* | ||
* @see https://github.com/estree/estree/blob/master/es5.md#expressionstatement | ||
*/ | ||
EXPRESSION_STATEMENT: 'ExpressionStatement', | ||
}, | ||
// TODO(@avivkeller): These should be inherited from the elements themselves | ||
JSX: { | ||
ALERT_BOX: 'AlertBox', | ||
CHANGE_HISTORY: 'ChangeHistory', | ||
CIRCULAR_ICON: 'CircularIcon', | ||
NAV_BAR: 'NavBar', | ||
ARTICLE: 'Article', | ||
SIDE_BAR: 'SideBar', | ||
META_BAR: 'MetaBar', | ||
FOOTER: 'Footer', | ||
}, | ||
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { | ||
getCompatibleVersions, | ||
groupNodesByModule, | ||
} from '../../utils/generators.mjs'; | ||
import buildContent from './utils/buildContent.mjs'; | ||
import { getRemarkRecma } from '../../utils/remark.mjs'; | ||
import { buildSideBarDocPages } from './utils/buildBarProps.mjs'; | ||
|
||
/** | ||
* This generator generates a JSX AST from an input MDAST | ||
* | ||
* @typedef {Array<ApiDocMetadataEntry>} Input | ||
* | ||
* @type {GeneratorMetadata<Input, string>} | ||
*/ | ||
export default { | ||
name: 'jsx-ast', | ||
version: '1.0.0', | ||
description: 'Generates JSX AST from the input MDAST', | ||
dependsOn: 'ast', | ||
|
||
/** | ||
* Generates a JSX AST | ||
* | ||
* @param {Input} entries | ||
* @param {Partial<GeneratorOptions>} options | ||
* @returns {Promise<Array<string>>} Array of generated content | ||
*/ | ||
async generate(entries, { releases, version }) { | ||
const remarkRecma = getRemarkRecma(); | ||
const groupedModules = groupNodesByModule(entries); | ||
|
||
// Get sorted primary heading nodes | ||
const headNodes = entries | ||
.filter(node => node.heading.depth === 1) | ||
.sort((a, b) => a.heading.data.name.localeCompare(b.heading.data.name)); | ||
|
||
// Generate table of contents | ||
const docPages = buildSideBarDocPages(groupedModules, headNodes); | ||
|
||
// Process each head node and build content | ||
const results = await Promise.all( | ||
headNodes.map(entry => { | ||
const versions = getCompatibleVersions( | ||
entry.introduced_in, | ||
releases, | ||
true | ||
); | ||
|
||
const sideBarProps = { | ||
versions: versions.map(({ version }) => `v${version.version}`), | ||
currentVersion: `v${version.version}`, | ||
currentPage: `${entry.api}.html`, | ||
docPages, | ||
}; | ||
|
||
return buildContent( | ||
groupedModules.get(entry.api), | ||
entry, | ||
sideBarProps, | ||
remarkRecma | ||
); | ||
}) | ||
); | ||
|
||
return results; | ||
}, | ||
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import test from 'node:test'; | ||
import assert from 'node:assert/strict'; | ||
import { | ||
buildSideBarDocPages, | ||
buildMetaBarProps, | ||
} from '../utils/buildBarProps.mjs'; | ||
import buildContent from '../utils/buildContent.mjs'; | ||
import { createJSXElement } from '../utils/ast.mjs'; | ||
import { AST_NODE_TYPES } from '../constants.mjs'; | ||
import { unified } from 'unified'; | ||
import remarkParse from 'remark-parse'; | ||
import remarkStringify from 'remark-stringify'; | ||
|
||
const sampleEntry = { | ||
api: 'sample-api', | ||
heading: { | ||
depth: 2, | ||
data: { name: 'SampleFunc', slug: 'sample-func', type: 'function' }, | ||
}, | ||
content: { | ||
type: 'root', | ||
children: [ | ||
{ type: 'text', value: 'Example text for testing reading time.' }, | ||
], | ||
}, | ||
added_in: 'v1.0.0', | ||
source_link: '/src/index.js', | ||
changes: [ | ||
{ | ||
version: 'v1.1.0', | ||
description: 'Improved performance', | ||
'pr-url': 'https://github.com/org/repo/pull/123', | ||
}, | ||
], | ||
}; | ||
|
||
test('buildSideBarDocPages returns expected format', () => { | ||
const grouped = new Map([['sample-api', [sampleEntry]]]); | ||
const result = buildSideBarDocPages(grouped, [sampleEntry]); | ||
|
||
assert.equal(result.length, 1); | ||
assert.equal(result[0].title, 'SampleFunc'); | ||
assert.equal(result[0].doc, 'sample-api.html'); | ||
assert.deepEqual(result[0].headings, [['SampleFunc', '#sample-func']]); | ||
}); | ||
|
||
test('buildMetaBarProps includes expected fields', () => { | ||
const result = buildMetaBarProps(sampleEntry, [sampleEntry]); | ||
|
||
assert.equal(result.addedIn, 'v1.0.0'); | ||
assert.deepEqual(result.viewAs, [['JSON', 'sample-api.json']]); | ||
assert.ok(result.readingTime.startsWith('1 min')); | ||
assert.ok(result.editThisPage.endsWith('sample-api.md')); | ||
assert.deepEqual(result.headings, [{ depth: 2, value: 'SampleFunc' }]); | ||
}); | ||
|
||
test('createJSXElement builds correct JSX tree', () => { | ||
const el = createJSXElement('TestComponent', { | ||
inline: false, | ||
children: 'Some content', | ||
dataAttr: { test: true }, | ||
}); | ||
|
||
assert.equal(el.type, AST_NODE_TYPES.MDX.JSX_BLOCK_ELEMENT); | ||
assert.equal(el.name, 'TestComponent'); | ||
assert.ok(Array.isArray(el.children)); | ||
assert.ok(el.attributes.some(attr => attr.name === 'dataAttr')); | ||
}); | ||
|
||
test('buildContent processes entries and includes JSX wrapper elements', () => { | ||
const processor = unified().use(remarkParse).use(remarkStringify); | ||
const tree = buildContent([sampleEntry], sampleEntry, {}, processor); | ||
|
||
const article = tree.children.find( | ||
child => child.name === AST_NODE_TYPES.JSX.ARTICLE | ||
); | ||
assert.ok(article); | ||
assert.ok(article.children.some(c => c.name === AST_NODE_TYPES.JSX.SIDE_BAR)); | ||
assert.ok(article.children.some(c => c.name === AST_NODE_TYPES.JSX.FOOTER)); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
'use strict'; | ||
|
||
import { u as createTree } from 'unist-builder'; | ||
import { valueToEstree } from 'estree-util-value-to-estree'; | ||
import { AST_NODE_TYPES } from '../constants.mjs'; | ||
|
||
/** | ||
* @typedef {Object} JSXOptions | ||
* @property {boolean} [inline] - Whether the element is inline | ||
* @property {(string | Array<import('unist').Node>)} [children] - Child content or nodes | ||
*/ | ||
|
||
/** | ||
* Creates an MDX JSX element with support for complex attribute values. | ||
* | ||
* @param {string} name - The name of the JSX element | ||
* @param {JSXOptions & Record<string, any>} [options={}] - Options including type, children, and JSX attributes | ||
* @returns {import('unist').Node} The created MDX JSX element node | ||
*/ | ||
export const createJSXElement = ( | ||
name, | ||
{ inline = true, children = [], ...attributes } = {} | ||
) => { | ||
// Convert string children to text node or use array directly | ||
const processedChildren = | ||
typeof children === 'string' | ||
? [createTree('text', { value: children })] | ||
: children; | ||
|
||
const elementType = inline | ||
? AST_NODE_TYPES.MDX.JSX_INLINE_ELEMENT | ||
: AST_NODE_TYPES.MDX.JSX_BLOCK_ELEMENT; | ||
|
||
const attrs = Object.entries(attributes).map(([key, value]) => | ||
createAttributeNode(key, value) | ||
); | ||
|
||
return createTree(elementType, { | ||
name, | ||
attributes: attrs, | ||
children: processedChildren, | ||
}); | ||
}; | ||
|
||
/** | ||
* Creates an MDX JSX attribute node based on the value type. | ||
* | ||
* @param {string} name - The attribute name | ||
* @param {any} value - The attribute value | ||
* @returns {import('unist').Node} The MDX JSX attribute node | ||
*/ | ||
function createAttributeNode(name, value) { | ||
// Use expression for objects and arrays | ||
if (value !== null && typeof value === 'object') { | ||
return createTree(AST_NODE_TYPES.MDX.JSX_ATTRIBUTE, { | ||
name, | ||
value: createTree(AST_NODE_TYPES.MDX.JSX_ATTRIBUTE_EXPRESSION, { | ||
data: { | ||
estree: { | ||
type: AST_NODE_TYPES.ESTREE.PROGRAM, | ||
body: [ | ||
{ | ||
type: AST_NODE_TYPES.ESTREE.EXPRESSION_STATEMENT, | ||
expression: valueToEstree(value), | ||
}, | ||
], | ||
}, | ||
}, | ||
}), | ||
}); | ||
} | ||
|
||
// For primitives, use simple string conversion. | ||
// If undefined, pass nothing. | ||
return createTree(AST_NODE_TYPES.MDX.JSX_ATTRIBUTE, { | ||
name, | ||
value: value == null ? value : String(value), | ||
}); | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.