Skip to content

Commit c0e11ef

Browse files
committed
JSX: Add JSX support
1 parent 2fd1b68 commit c0e11ef

File tree

7 files changed

+188
-10
lines changed

7 files changed

+188
-10
lines changed

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@
1515
".": {
1616
"types": "./types/index.d.ts",
1717
"default": "./src/index.js"
18+
},
19+
"./modules/jsx": {
20+
"default": "./src/modules/jsx.js"
21+
},
22+
"./modules/ecmascript": {
23+
"default": "./src/modules/ecmascript.js"
24+
},
25+
"./modules/typescript": {
26+
"default": "./src/modules/typescript.js"
1827
}
1928
},
2029
"types": "./types/index.d.ts",

src/modules/jsx.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/** @import { Handlers } from '../types' */
2+
3+
import { handle, indent, dedent, newline } from '../handlers';
4+
5+
/** @type {Handlers} */
6+
export default {
7+
JSXElement(node, state) {
8+
handle(node.openingElement, state);
9+
10+
if (node.children.length > 0) {
11+
state.commands.push(indent);
12+
}
13+
for (const child of node.children) {
14+
handle(child, state);
15+
if (child !== node.children.at(-1)) {
16+
state.commands.push(newline)
17+
}
18+
}
19+
if (node.children.length > 0) {
20+
state.commands.push(dedent);
21+
state.commands.push(newline);
22+
}
23+
24+
if (node.closingElement) {
25+
handle(node.closingElement, state);
26+
}
27+
},
28+
JSXOpeningElement(node, state) {
29+
state.commands.push('<');
30+
31+
handle(node.name, state);
32+
33+
for (const attribute of node.attributes) {
34+
state.commands.push(' ');
35+
handle(attribute, state);
36+
}
37+
38+
if (node.selfClosing) {
39+
state.commands.push(' /');
40+
}
41+
42+
state.commands.push('>');
43+
},
44+
JSXClosingElement(node, state) {
45+
state.commands.push('</');
46+
47+
handle(node.name, state);
48+
49+
state.commands.push('>');
50+
},
51+
JSXNamespacedName(node, state) {
52+
handle(node.namespace, state);
53+
state.commands.push(':');
54+
handle(node.name, state);
55+
},
56+
JSXIdentifier(node, state) {
57+
state.commands.push(node.name);
58+
},
59+
JSXMemberExpression(node, state) {
60+
handle(node.object, state);
61+
state.commands.push('.');
62+
handle(node.property, state);
63+
},
64+
JSXText(node, state) {
65+
state.commands.push(node.value);
66+
},
67+
JSXAttribute(node, state) {
68+
handle(node.name, state);
69+
if (node.value) {
70+
state.commands.push('=');
71+
handle(node.value, state);
72+
}
73+
},
74+
JSXEmptyExpression(node, state) {},
75+
JSXFragment(node, state) {
76+
handle(node.openingFragment, state);
77+
78+
if (node.children.length > 0) {
79+
state.commands.push(indent);
80+
}
81+
for (const child of node.children) {
82+
handle(child, state);
83+
84+
if (child !== node.children.at(-1)) {
85+
state.commands.push(newline);
86+
}
87+
}
88+
if (node.children.length > 0) {
89+
state.commands.push(dedent);
90+
}
91+
92+
handle(node.closingFragment, state);
93+
},
94+
JSXOpeningFragment(node, state) {
95+
state.commands.push('<>');
96+
},
97+
JSXClosingFragment(node, state) {
98+
state.commands.push('</>');
99+
},
100+
JSXExpressionContainer(node, state) {
101+
state.commands.push('{');
102+
103+
handle(node.expression, state);
104+
105+
state.commands.push('}');
106+
},
107+
JSXSpreadChild(node, state) {
108+
state.commands.push('{...');
109+
110+
handle(node.expression, state);
111+
112+
state.commands.push('}');
113+
},
114+
JSXSpreadAttribute(node, state) {
115+
state.commands.push('{...');
116+
117+
handle(node.argument, state);
118+
119+
state.commands.push('}');
120+
}
121+
};

test/common.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ import { walk } from 'zimmerframe';
99

1010
// @ts-expect-error
1111
export const acornTs = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }));
12-
13-
/** @param {string} input */
14-
export function load(input) {
12+
export const acornTsx = acorn.Parser.extend(tsPlugin({ allowSatisfies: true, jsx: true }));
13+
14+
/** @param {string} input
15+
* @param {{ jsx?: boolean }} opts
16+
*/
17+
export function load(input, opts = {}) {
18+
const jsx = opts.jsx ?? false;
1519
/** @type {any[]} */
1620
const comments = [];
1721

18-
const ast = acornTs.parse(input, {
22+
const ast = (jsx ? acornTsx : acornTs).parse(input, {
1923
ecmaVersion: 'latest',
2024
sourceType: 'module',
2125
locations: true,

test/esrap.test.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import fs from 'node:fs';
55
import { expect, test } from 'vitest';
66
import { walk } from 'zimmerframe';
77
import { print } from '../src/index.js';
8-
import { acornTs, load } from './common.js';
8+
import { acornTs, acornTsx, load } from './common.js';
9+
import ecmascript from '../src/modules/ecmascript.js';
10+
import typescript from '../src/modules/typescript.js';
11+
import jsx from "../src/modules/jsx.js";
912

1013
/** @param {TSESTree.Node} ast */
1114
function clean(ast) {
@@ -53,8 +56,9 @@ function clean(ast) {
5356

5457
for (const dir of fs.readdirSync(`${__dirname}/samples`)) {
5558
if (dir[0] === '.') continue;
56-
const tsMode = dir.startsWith('ts-');
57-
const fileExtension = tsMode ? 'ts' : 'js';
59+
const tsMode = dir.startsWith('ts-') || dir.startsWith('tsx-');
60+
const jsxMode = dir.startsWith('jsx-') || dir.startsWith('tsx-');
61+
const fileExtension = (tsMode ? 'ts' : 'js') + (jsxMode ? "x" : "");
5862

5963
test(dir, async () => {
6064
let input_js = '';
@@ -77,13 +81,23 @@ for (const dir of fs.readdirSync(`${__dirname}/samples`)) {
7781
opts = {};
7882
} else {
7983
const content = input_js;
80-
ast = load(content);
84+
ast = load(content, { jsx: true });
8185
opts = {
8286
sourceMapSource: 'input.js',
8387
sourceMapContent: content
8488
};
8589
}
8690

91+
opts.handlers = { ...ecmascript };
92+
93+
if (tsMode) {
94+
opts.handlers = {...opts.handlers, ...typescript};
95+
}
96+
97+
if (jsxMode) {
98+
opts.handlers = {...opts.handlers, ...jsx};
99+
}
100+
87101
const { code, map } = print(ast, opts);
88102

89103
fs.writeFileSync(`${__dirname}/samples/${dir}/_actual.${fileExtension}`, code);
@@ -92,10 +106,10 @@ for (const dir of fs.readdirSync(`${__dirname}/samples`)) {
92106
JSON.stringify(map, null, '\t')
93107
);
94108

95-
const parsed = acornTs.parse(code, {
109+
const parsed = (jsxMode ? acornTsx : acornTs).parse(code, {
96110
ecmaVersion: 'latest',
97111
sourceType: input_json.length > 0 ? 'script' : 'module',
98-
locations: true
112+
locations: true,
99113
});
100114

101115
fs.writeFileSync(

test/samples/jsx-basic/expected.jsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
console.log(<>
2+
3+
<div>
4+
5+
<img src="cat.png" alt={"An image of a flying cat"} />
6+
7+
Time since last cat incident:
8+
{"5 days"}
9+
10+
11+
</div>
12+
13+
</>);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"version": 3,
3+
"names": [],
4+
"sources": [
5+
"input.js"
6+
],
7+
"sourcesContent": [
8+
"console.log(<>\n <div>\n <img src=\"cat.png\" alt={\"An image of a flying cat\"} />\n Time since last cat incident: {\"5 days\"}\n </div>\n</>)"
9+
],
10+
"mappings": "AAAA,OAAO,CAAC,GAAG;;;;WAEM,SAAS,MAAM,0BAA0B;;;GACnB,QAAQ;;;;;"
11+
}

test/samples/jsx-basic/input.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
console.log(<>
2+
<div>
3+
<img src="cat.png" alt={"An image of a flying cat"} />
4+
Time since last cat incident: {"5 days"}
5+
</div>
6+
</>)

0 commit comments

Comments
 (0)