Skip to content

Commit 60922b4

Browse files
committed
feat(generator): add jsx generator
1 parent 6f70de4 commit 60922b4

14 files changed

+941
-320
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ npm-debug.log
44

55
# Default Output Directory
66
out
7+
dist

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
# Tests files
22
src/generators/api-links/test/fixtures/
33
*.snapshot
4+
5+
dist

declarations.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module "*.module.css" {
2+
const classes: { [key: string]: string };
3+
export default classes;
4+
}

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import jsdoc from 'eslint-plugin-jsdoc';
44
import globals from 'globals';
55

66
export default [
7+
{ ignores: ['dist', 'out'] },
78
// @see https://eslint.org/docs/latest/use/configure/configuration-files#specifying-files-and-ignores
89
{
910
files: ['src/**/*.mjs', 'bin/**/*.mjs'],

package-lock.json

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

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@
4747
"hast-util-to-string": "^3.0.1",
4848
"hastscript": "^9.0.1",
4949
"html-minifier-terser": "^7.2.0",
50+
"recma-build-jsx": "^1.0.0",
51+
"recma-jsx": "^1.0.0",
52+
"recma-stringify": "^1.0.0",
53+
"rehype-recma": "^1.0.0",
5054
"rehype-stringify": "^10.0.1",
5155
"remark-gfm": "^4.0.1",
5256
"remark-parse": "^11.0.0",

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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const STABILITY_LEVELS = [
2+
'danger', // Deprecated
3+
'warning', // Experimental
4+
'success', // Stable
5+
'info', // Legacy
6+
];

src/generators/jsx/index.mjs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { groupNodesByModule } from '../../utils/generators.mjs';
2+
import buildContent from './utils/buildContent.mjs';
3+
import { getRemarkRecma } from '../../utils/remark.mjs';
4+
5+
/**
6+
* This generator generates an JSX AST from an input MDAST
7+
*
8+
* @typedef {Array<ApiDocMetadataEntry>} Input
9+
*
10+
* @type {GeneratorMetadata<Input, string>}
11+
*/
12+
export default {
13+
name: 'jsx',
14+
version: '1.0.0',
15+
description: 'Generates JSX from the input AST',
16+
dependsOn: 'ast',
17+
18+
/**
19+
* Generates a JSX AST
20+
*
21+
* @param {Input} entries
22+
* @returns {Promise<string[]>} Array of generated content
23+
*/
24+
async generate(entries) {
25+
const remarkRecma = getRemarkRecma();
26+
27+
const results = await Promise.all(
28+
Array.from(groupNodesByModule(entries).values()).map(entry =>
29+
buildContent(entry, remarkRecma)
30+
)
31+
);
32+
33+
console.log(results);
34+
return results;
35+
},
36+
};

src/generators/jsx/utils/ast.mjs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
import { u as createTree } from 'unist-builder';
4+
5+
/**
6+
* Creates an MDX JSX element.
7+
*
8+
* @param {string} name - The name of the JSX element
9+
* @param {{
10+
* inline?: boolean,
11+
* children?: string | import('unist').Node[],
12+
* [key: string]: string
13+
* }} [options={}] - Options including type, children, and JSX attributes
14+
* @returns {import('unist').Node} The created MDX JSX element node
15+
*/
16+
export const createJSXElement = (
17+
name,
18+
{ inline = true, children = [], ...attributes } = {}
19+
) => {
20+
const processedChildren =
21+
typeof children === 'string'
22+
? [createTree('text', { value: children })]
23+
: (children ?? []);
24+
25+
const attrs = Object.entries(attributes).map(([key, value]) =>
26+
createTree('mdxJsxAttribute', { name: key, value: String(value) })
27+
);
28+
29+
return createTree(inline ? 'mdxJsxTextElement' : 'mdxJsxFlowElement', {
30+
name,
31+
attributes: attrs,
32+
children: processedChildren,
33+
});
34+
};
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use strict';
2+
3+
// External dependencies
4+
import { u as createTree } from 'unist-builder';
5+
import { SKIP, visit } from 'unist-util-visit';
6+
7+
// Internal dependencies
8+
import createQueries from '../../../utils/queries/index.mjs';
9+
import { createJSXElement } from './ast.mjs';
10+
import { STABILITY_LEVELS } from '../constants.mjs';
11+
12+
/**
13+
* Transforms a stability node into an AlertBox JSX element
14+
*
15+
* @param {Object} node - The stability node to transform
16+
* @param {number} index - The index of the node in its parent's children array
17+
* @param {Object} parent - The parent node containing the stability node
18+
* @returns {Array} - Returns [SKIP] to indicate this node should be skipped in further traversal
19+
*/
20+
function transformStabilityNode(node, index, parent) {
21+
parent.children.splice(
22+
index,
23+
1,
24+
createTree('paragraph', {
25+
children: [
26+
createJSXElement('AlertBox', {
27+
children: node.data.description,
28+
level: STABILITY_LEVELS[node.data.index],
29+
title: node.data.index,
30+
}),
31+
],
32+
})
33+
);
34+
return [SKIP];
35+
}
36+
37+
/**
38+
* Processes content by transforming stability nodes
39+
*
40+
* @param {Object} content - The content object to process
41+
* @returns {Object} - The processed content with stability nodes transformed
42+
*/
43+
function processContent(content) {
44+
const contentCopy = { ...content };
45+
visit(
46+
contentCopy,
47+
createQueries.UNIST.isStabilityNode,
48+
transformStabilityNode
49+
);
50+
return contentCopy;
51+
}
52+
53+
/**
54+
* Transforms API metadata entries into processed MDX content
55+
*
56+
* @param {Array} metadataEntries - API documentation metadata entries
57+
* @param {import('unified').Processor} remark - Remark processor instance for markdown processing
58+
* @returns {React.ReactElement} - Evaluated React element from processed MDX, ready for rendering
59+
*/
60+
export default function transformMetadataToMdx(metadataEntries, remark) {
61+
// Create and process the root node with all entries
62+
const rootNode = createTree(
63+
'root',
64+
metadataEntries.map(entry => processContent(entry.content))
65+
);
66+
67+
// Process with remark and convert to string
68+
const processedNodes = remark.runSync(rootNode);
69+
const mdxString = remark.stringify(processedNodes);
70+
71+
// Evaluate MDX and return React element
72+
return mdxString;
73+
}

src/generators/legacy-html/utils/buildContent.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ export default (headNodes, metadataEntries, remark) => {
209209
// Parses the metadata pieces of each node and the content
210210
metadataEntries.map(entry => {
211211
// Deep clones the content nodes to avoid affecting upstream nodes
212-
const content = JSON.parse(JSON.stringify(entry.content));
212+
const content = structuredClone(entry.content);
213213

214214
// Parses the Heading nodes into Heading elements
215215
visit(content, createQueries.UNIST.isHeading, buildHeading);

src/threading/index.mjs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,9 @@ export default class WorkerPool {
4444
this.changeActiveThreadCount(1);
4545

4646
// Create and start the worker thread
47-
const worker = new Worker(
48-
new URL(import.meta.resolve('./worker.mjs')),
49-
{
50-
workerData: { name, dependencyOutput, extra },
51-
}
52-
);
47+
const worker = new Worker(new URL('./worker.mjs', import.meta.url), {
48+
workerData: { name, dependencyOutput, extra },
49+
});
5350

5451
// Handle worker thread messages (result or error)
5552
worker.on('message', result => {

src/utils/remark.mjs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ import remarkParse from 'remark-parse';
77
import remarkRehype from 'remark-rehype';
88
import remarkStringify from 'remark-stringify';
99

10+
import rehypeRecma from 'rehype-recma';
1011
import rehypeStringify from 'rehype-stringify';
1112

13+
import recmaJsx from 'recma-jsx';
14+
import recmaBuildJsx from 'recma-build-jsx';
15+
import recmaStringify from 'recma-stringify';
16+
1217
import syntaxHighlighter from './highlighter.mjs';
1318

1419
/**
@@ -37,3 +42,32 @@ export const getRemarkRehype = () =>
3742
// We allow dangerous HTML to be passed through, since we have HTML within our Markdown
3843
// and we trust the sources of the Markdown files
3944
.use(rehypeStringify, { allowDangerousHtml: true });
45+
46+
/**
47+
* Retrieves an instance of Remark configured to output JSX code.
48+
* including parsing Code Boxes with syntax highlighting
49+
*/
50+
export const getRemarkRecma = () =>
51+
unified()
52+
.use(remarkParse)
53+
// TODO(@avivkeller): @node-core/mdx Remark plugins
54+
// We make Rehype ignore existing HTML nodes, and JSX
55+
// as these are nodes we manually created during the generation process
56+
// We also allow dangerous HTML to be passed through, since we have HTML within our Markdown
57+
// and we trust the sources of the Markdown files
58+
.use(remarkRehype, {
59+
allowDangerousHtml: true,
60+
passThrough: [
61+
'element',
62+
'mdxFlowExpression',
63+
'mdxJsxFlowElement',
64+
'mdxJsxTextElement',
65+
'mdxTextExpression',
66+
'mdxjsEsm',
67+
],
68+
})
69+
// TODO(@avivkeller): @node-core/mdx Rehype plugins
70+
.use(rehypeRecma)
71+
.use(recmaJsx)
72+
.use(recmaBuildJsx)
73+
.use(recmaStringify);

0 commit comments

Comments
 (0)