Skip to content

Commit 60e76e5

Browse files
committed
feat(jsx): add jsx generator
1 parent 6f70de4 commit 60e76e5

File tree

18 files changed

+1395
-66
lines changed

18 files changed

+1395
-66
lines changed

package-lock.json

Lines changed: 833 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"test:watch": "node --test --watch",
1616
"prepare": "husky",
1717
"run": "node bin/cli.mjs",
18-
"watch": "node --watch bin/cli.mjs"
18+
"watch": "node --watch bin/cli.mjs",
19+
"postinstall": "git-deps install"
1920
},
2021
"main": "./src/index.mjs",
2122
"bin": {
@@ -28,6 +29,7 @@
2829
"eslint": "^9.23.0",
2930
"eslint-config-prettier": "^10.1.1",
3031
"eslint-plugin-jsdoc": "^50.6.9",
32+
"git-deps": "^1.0.0",
3133
"globals": "^16.0.0",
3234
"husky": "^9.1.7",
3335
"lint-staged": "^15.5.0",
@@ -41,12 +43,16 @@
4143
"acorn": "^8.14.1",
4244
"commander": "^13.1.0",
4345
"dedent": "^1.5.3",
46+
"estree-util-value-to-estree": "^3.4.0",
4447
"estree-util-visit": "^2.0.0",
4548
"github-slugger": "^2.0.0",
4649
"glob": "^11.0.1",
4750
"hast-util-to-string": "^3.0.1",
4851
"hastscript": "^9.0.1",
4952
"html-minifier-terser": "^7.2.0",
53+
"reading-time": "^1.5.0",
54+
"recma-jsx": "^1.0.0",
55+
"rehype-recma": "^1.0.0",
5056
"rehype-stringify": "^10.0.1",
5157
"remark-gfm": "^4.0.1",
5258
"remark-parse": "^11.0.0",
@@ -63,5 +69,8 @@
6369
"unist-util-visit": "^5.0.0",
6470
"vfile": "^6.0.3",
6571
"yaml": "^2.7.1"
72+
},
73+
"gitDependencies": {
74+
"@node-core/rehype-shiki": "https://github.com/nodejs/nodejs.org#path:/packages/rehype-shiki"
6675
}
6776
}

src/constants.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,14 @@ export const DOC_NODE_CHANGELOG_URL =
99

1010
// The base URL for the Node.js website
1111
export const BASE_URL = 'https://nodejs.org/';
12+
13+
// This is the Node.js Base URL for viewing a file within GitHub UI
14+
export const DOC_NODE_BLOB_BASE_URL =
15+
'https://github.com/nodejs/node/blob/HEAD/';
16+
17+
// This is the Node.js API docs base URL for editing a file on GitHub UI
18+
export const DOC_API_BLOB_EDIT_BASE_URL =
19+
'https://github.com/nodejs/node/edit/main/doc/api/';
20+
21+
// Base URL for a specific Node.js version within the Node.js API docs
22+
export const DOC_API_BASE_URL_VERSION = 'https://nodejs.org/docs/latest-v';

src/generators/index.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import apiLinks from './api-links/index.mjs';
1111
import oramaDb from './orama-db/index.mjs';
1212
import astJs from './ast-js/index.mjs';
1313
import llmsTxt from './llms-txt/index.mjs';
14+
import jsx from './jsx/index.mjs';
1415

1516
export const publicGenerators = {
1617
'json-simple': jsonSimple,
@@ -23,6 +24,7 @@ export const publicGenerators = {
2324
'api-links': apiLinks,
2425
'orama-db': oramaDb,
2526
'llms-txt': llmsTxt,
27+
jsx,
2628
};
2729

2830
export const allGenerators = {

src/generators/jsx/constants.mjs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Maps the the stability index (0-3) to the level used by @node-core/ui-components
2+
export const STABILITY_LEVELS = [
3+
'danger', // (0) Deprecated
4+
'warning', // (1) Experimental
5+
'success', // (2) Stable
6+
'info', // (3) Legacy
7+
];
8+
9+
// Maps HTML tags to corresponding component names in @node-core/ui-components
10+
export const TAG_TRANSFORMS = {
11+
pre: 'CodeBox',
12+
blockquote: 'Blockquote',
13+
};
14+
15+
// Maps types to icon symbols
16+
export const ICON_SYMBOL_MAP = {
17+
event: { symbol: 'E', color: 'red' },
18+
method: { symbol: 'M', color: 'red' },
19+
property: { symbol: 'P', color: 'red' },
20+
class: { symbol: 'C', color: 'red' },
21+
module: { symbol: 'M', color: 'red' },
22+
classMethod: { symbol: 'S', color: 'red' },
23+
ctor: { symbol: 'C', color: 'red' },
24+
};
25+
26+
export const CHANGE_TYPES = {
27+
added_in: 'Added in',
28+
deprecated_in: 'Deprecated in',
29+
removed_in: 'Removed in',
30+
introduced_in: 'Introduced in',
31+
};

src/generators/jsx/index.mjs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {
2+
coerceSemVer,
3+
getCompatibleVersions,
4+
groupNodesByModule,
5+
} from '../../utils/generators.mjs';
6+
import buildContent from './utils/buildContent.mjs';
7+
import { getRemarkRecma } from '../../utils/remark.mjs';
8+
import { major } from 'semver';
9+
10+
/**
11+
* This generator generates a JSX AST from an input MDAST
12+
*
13+
* @typedef {Array<ApiDocMetadataEntry>} Input
14+
*
15+
* @type {GeneratorMetadata<Input, string>}
16+
*/
17+
export default {
18+
name: 'jsx',
19+
version: '1.0.0',
20+
description: 'Generates JSX from the input AST',
21+
dependsOn: 'ast',
22+
23+
/**
24+
* Generates a JSX AST
25+
*
26+
* @param {Input} entries
27+
* @param {Partial<GeneratorOptions>} options
28+
* @returns {Promise<string[]>} Array of generated content
29+
*/
30+
async generate(entries, { releases, version }) {
31+
const remarkRecma = getRemarkRecma();
32+
const groupedModules = groupNodesByModule(entries);
33+
34+
// Get sorted primary heading nodes
35+
const headNodes = entries
36+
.filter(node => node.heading.depth === 1)
37+
.sort((a, b) => a.heading.data.name.localeCompare(b.heading.data.name));
38+
39+
// Generate table of contents
40+
const docPages = headNodes.map(node => [
41+
node.heading.data.name,
42+
`${node.api}.html`,
43+
]);
44+
45+
// Process each head node and build content
46+
const results = await Promise.all(
47+
headNodes.map(entry => {
48+
const coercedMajor = major(coerceSemVer(entry.introduced_in));
49+
const otherVersions = getCompatibleVersions(
50+
entry.introduced_in,
51+
releases
52+
).filter(({ version }) => version.major != coercedMajor);
53+
54+
const sideBarProps = {
55+
otherVersions: otherVersions.map(
56+
({ version }) => `v${version.major}.x`
57+
),
58+
currentVersion: `v${version.version}`,
59+
currentPage: `${entry.api}.html`,
60+
docPages,
61+
};
62+
63+
return buildContent(
64+
groupedModules.get(entry.api),
65+
entry,
66+
sideBarProps,
67+
remarkRecma
68+
);
69+
})
70+
);
71+
72+
return results;
73+
},
74+
};

src/generators/jsx/utils/ast.mjs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use strict';
2+
3+
import { u as createTree } from 'unist-builder';
4+
import { valueToEstree } from 'estree-util-value-to-estree';
5+
6+
/**
7+
* Creates an MDX JSX element with support for complex attribute values.
8+
*
9+
* @param {string} name - The name of the JSX element
10+
* @param {{
11+
* inline?: boolean,
12+
* children?: string | import('unist').Node[],
13+
* [key: string]: any
14+
* }} [options={}] - Options including type, children, and JSX attributes
15+
* @returns {import('unist').Node} The created MDX JSX element node
16+
*/
17+
export const createJSXElement = (
18+
name,
19+
{ inline = true, children = [], ...attributes } = {}
20+
) => {
21+
// Process children: convert string to text node or use array as is
22+
const processedChildren =
23+
typeof children === 'string'
24+
? [createTree('text', { value: children })]
25+
: (children ?? []);
26+
27+
// Create attribute nodes, handling complex objects and primitive values differently
28+
const attrs = Object.entries(attributes).map(([key, value]) =>
29+
createAttributeNode(key, value)
30+
);
31+
32+
// Create and return the appropriate JSX element type
33+
return createTree(inline ? 'mdxJsxTextElement' : 'mdxJsxFlowElement', {
34+
name,
35+
attributes: attrs,
36+
children: processedChildren,
37+
});
38+
};
39+
40+
/**
41+
* Creates an MDX JSX attribute node from the input.
42+
*
43+
* @param {string} name - The attribute name
44+
* @param {any} value - The attribute value (can be any valid JS value)
45+
* @returns {import('unist').Node} The MDX JSX attribute node
46+
*/
47+
function createAttributeNode(name, value) {
48+
// For objects and arrays, create expression nodes to preserve structure
49+
if (value !== null && typeof value === 'object') {
50+
return createTree('mdxJsxAttribute', {
51+
name,
52+
value: createTree('mdxJsxAttributeValueExpression', {
53+
data: {
54+
estree: {
55+
type: 'Program',
56+
body: [
57+
{
58+
type: 'ExpressionStatement',
59+
expression: valueToEstree(value),
60+
},
61+
],
62+
sourceType: 'module',
63+
},
64+
},
65+
}),
66+
});
67+
}
68+
69+
// For primitives, use simple string conversion
70+
return createTree('mdxJsxAttribute', {
71+
name,
72+
value: String(value),
73+
});
74+
}

0 commit comments

Comments
 (0)