Skip to content

Commit 987899c

Browse files
authored
JSX support (#1052)
* JSX support * Add jsx and jsxFrag pragma support * Remove unneeded flattenSpreadExpressions call. Typescript doesn't actually "spread out" jsx spread children, even though the syntax is supported. * Changes for PR. * Add license for functions copied from sucrase * Remove jsx snapshot test Co-authored-by: Benjamin Ye <24237065+enjoydambience@users.noreply.github.com>
1 parent f034d9f commit 987899c

File tree

9 files changed

+976
-62
lines changed

9 files changed

+976
-62
lines changed

src/CompilerOptions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as ts from "typescript";
2+
import { JsxEmit } from "typescript";
23
import * as diagnosticFactories from "./transpilation/diagnostics";
34

45
type OmitIndexSignature<T> = {
@@ -74,5 +75,9 @@ export function validateOptions(options: CompilerOptions): ts.Diagnostic[] {
7475
diagnostics.push(diagnosticFactories.cannotBundleLibrary());
7576
}
7677

78+
if (options.jsx && options.jsx !== JsxEmit.React) {
79+
diagnostics.push(diagnosticFactories.unsupportedJsxEmit());
80+
}
81+
7782
return diagnostics;
7883
}

src/transformation/utils/annotations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export enum AnnotationKind {
1717
NoSelfInFile = "noSelfInFile",
1818
Vararg = "vararg",
1919
ForRange = "forRange",
20+
Jsx = "jsx",
21+
JsxFrag = "jsxFrag",
2022
}
2123

2224
export interface Annotation {

src/transformation/visitors/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { transformTypeOfExpression } from "./typeof";
4040
import { typescriptVisitors } from "./typescript";
4141
import { transformPostfixUnaryExpression, transformPrefixUnaryExpression } from "./unary-expression";
4242
import { transformVariableStatement } from "./variable-declaration";
43+
import { jsxVisitors } from "./jsx/jsx";
4344

4445
const transformEmptyStatement: FunctionVisitor<ts.EmptyStatement> = () => undefined;
4546
const transformParenthesizedExpression: FunctionVisitor<ts.ParenthesizedExpression> = (node, context) =>
@@ -48,6 +49,7 @@ const transformParenthesizedExpression: FunctionVisitor<ts.ParenthesizedExpressi
4849
export const standardVisitors: Visitors = {
4950
...literalVisitors,
5051
...typescriptVisitors,
52+
...jsxVisitors,
5153
[ts.SyntaxKind.ArrowFunction]: transformFunctionLikeDeclaration,
5254
[ts.SyntaxKind.BinaryExpression]: transformBinaryExpression,
5355
[ts.SyntaxKind.Block]: transformBlock,
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import * as ts from "typescript";
2+
import * as lua from "../../../LuaAST";
3+
import { FunctionVisitor, TransformationContext, Visitors } from "../../context";
4+
import { transformJsxAttributes } from "../literal";
5+
import { XHTMLEntities } from "./xhtml";
6+
import { AnnotationKind, getFileAnnotations } from "../../utils/annotations";
7+
8+
function findAnnotationByType(node: ts.Node, fileAnnotation: AnnotationKind): string | undefined {
9+
const annotation = getFileAnnotations(node.getSourceFile()).get(fileAnnotation);
10+
return annotation?.args[0];
11+
}
12+
13+
function getExpressionFromOption(option: string): lua.Expression {
14+
const [first, second] = option.split(".");
15+
return second === undefined
16+
? lua.createIdentifier(first)
17+
: lua.createTableIndexExpression(lua.createIdentifier(first), lua.createStringLiteral(second));
18+
}
19+
20+
function getJsxFactory(node: ts.Node, context: TransformationContext): lua.Expression {
21+
const option =
22+
findAnnotationByType(node, AnnotationKind.Jsx) ?? context.options.jsxFactory ?? "React.createElement";
23+
return getExpressionFromOption(option);
24+
}
25+
26+
function getJsxFragmentName(node: ts.Node, context: TransformationContext): lua.Expression {
27+
const option =
28+
findAnnotationByType(node, AnnotationKind.JsxFrag) ?? context.options.jsxFragmentFactory ?? "React.Fragment";
29+
return getExpressionFromOption(option);
30+
}
31+
32+
/*
33+
The following 3 functions for jsx text processing modified from sucrase (https://github.com/alangpierce/sucrase), which
34+
is published with the MIT licence:
35+
36+
The MIT License (MIT)
37+
38+
Copyright (c) 2012-2018 various contributors (see AUTHORS)
39+
40+
Permission is hereby granted, free of charge, to any person obtaining a copy
41+
of this software and associated documentation files (the "Software"), to deal
42+
in the Software without restriction, including without limitation the rights
43+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
44+
copies of the Software, and to permit persons to whom the Software is
45+
furnished to do so, subject to the following conditions:
46+
47+
The above copyright notice and this permission notice shall be included in all
48+
copies or substantial portions of the Software.
49+
50+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
51+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
52+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
53+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
54+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
55+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
56+
SOFTWARE.
57+
*/
58+
59+
const HEX_NUMBER = /^[\da-fA-F]+$/;
60+
const DECIMAL_NUMBER = /^\d+$/;
61+
62+
/**
63+
* Turn the given jsxText string into a JS string literal. Leading and trailing
64+
* whitespace on lines is removed, except immediately after the open-tag and
65+
* before the close-tag. Empty lines are completely removed, and spaces are
66+
* added between lines after that.
67+
*
68+
* We trim the start and end of each line and remove blank lines.
69+
*/
70+
function formatJSXTextLiteral(text: string): string {
71+
let result = "";
72+
let whitespace = "";
73+
74+
let isInInitialLineWhitespace = false;
75+
let seenNonWhitespace = false;
76+
for (let i = 0; i < text.length; i++) {
77+
const c = text[i];
78+
if (c === " " || c === "\t" || c === "\r") {
79+
if (!isInInitialLineWhitespace) {
80+
whitespace += c;
81+
}
82+
} else if (c === "\n") {
83+
whitespace = "";
84+
isInInitialLineWhitespace = true;
85+
} else {
86+
if (seenNonWhitespace && isInInitialLineWhitespace) {
87+
result += " ";
88+
}
89+
result += whitespace;
90+
whitespace = "";
91+
if (c === "&") {
92+
const { entity, newI } = processEntity(text, i + 1);
93+
i = newI - 1;
94+
result += entity;
95+
} else {
96+
result += c;
97+
}
98+
seenNonWhitespace = true;
99+
isInInitialLineWhitespace = false;
100+
}
101+
}
102+
if (!isInInitialLineWhitespace) {
103+
result += whitespace;
104+
}
105+
return result;
106+
}
107+
108+
/**
109+
* Format a string in the value position of a JSX prop.
110+
*
111+
* Use the same implementation as convertAttribute from
112+
* babel-helper-builder-react-jsx.
113+
*/
114+
// changes from sucrase: no multi-line flattening of prop strings in typescript.
115+
export function formatJSXStringValueLiteral(text: string): string {
116+
let result = "";
117+
for (let i = 0; i < text.length; i++) {
118+
const c = text[i];
119+
if (c === "&") {
120+
const { entity, newI } = processEntity(text, i + 1);
121+
result += entity;
122+
i = newI - 1;
123+
} else {
124+
result += c;
125+
}
126+
}
127+
return result;
128+
}
129+
130+
/**
131+
* Modified from jsxReadString in Babylon.
132+
*/
133+
function processEntity(text: string, indexAfterAmpersand: number): { entity: string; newI: number } {
134+
let str = "";
135+
let count = 0;
136+
let entity;
137+
let i = indexAfterAmpersand;
138+
139+
while (i < text.length && count++ < 10) {
140+
const ch = text[i];
141+
i++;
142+
if (ch === ";") {
143+
if (str.startsWith("#")) {
144+
if (str[1] === "x") {
145+
str = str.substr(2);
146+
if (HEX_NUMBER.test(str)) {
147+
entity = String.fromCodePoint(Number.parseInt(str, 16));
148+
}
149+
} else {
150+
str = str.substr(1);
151+
if (DECIMAL_NUMBER.test(str)) {
152+
entity = String.fromCodePoint(Number.parseInt(str, 10));
153+
}
154+
}
155+
} else {
156+
entity = XHTMLEntities[str];
157+
}
158+
break;
159+
}
160+
str += ch;
161+
}
162+
if (!entity) {
163+
return { entity: "&", newI: indexAfterAmpersand };
164+
}
165+
return { entity, newI: i };
166+
}
167+
168+
// end functions copied from sucrase
169+
170+
function processJsxText(jsxText: ts.JsxText): ts.StringLiteral | undefined {
171+
const text = formatJSXTextLiteral(jsxText.text);
172+
if (text === "") return undefined;
173+
return ts.factory.createStringLiteral(text);
174+
}
175+
176+
const charCodes = {
177+
a: 0x61,
178+
z: 0x7a,
179+
};
180+
181+
// how typescript does it
182+
function isIntrinsicJsxName(escapedName: ts.__String): boolean {
183+
const name = escapedName as string;
184+
const ch = name.charCodeAt(0);
185+
return (ch >= charCodes.a && ch <= charCodes.z) || name.includes("-") || name.includes(":");
186+
}
187+
188+
function transformTagName(name: ts.JsxTagNameExpression, context: TransformationContext): lua.Expression {
189+
if (ts.isIdentifier(name) && isIntrinsicJsxName(name.escapedText)) {
190+
return lua.createStringLiteral(ts.idText(name), name);
191+
} else {
192+
return context.transformExpression(name);
193+
}
194+
}
195+
196+
function transformJsxChildren(
197+
children: ts.NodeArray<ts.JsxChild> | undefined,
198+
context: TransformationContext
199+
): lua.Expression[] | undefined {
200+
if (!children) return undefined;
201+
202+
return children
203+
.map(child => {
204+
if (ts.isJsxText(child)) {
205+
return processJsxText(child);
206+
}
207+
if (ts.isJsxExpression(child)) {
208+
return child.expression;
209+
}
210+
return child;
211+
})
212+
.filter(child => child !== undefined)
213+
.map(child => context.transformExpression(child!));
214+
}
215+
216+
function createJsxFactoryCall(
217+
tagName: lua.Expression,
218+
props: lua.Expression | undefined,
219+
tsChildren: ts.NodeArray<ts.JsxChild> | undefined,
220+
tsOriginal: ts.Node,
221+
context: TransformationContext
222+
): lua.Expression {
223+
const transformedChildren = transformJsxChildren(tsChildren, context);
224+
const jsxFactory = getJsxFactory(tsOriginal, context);
225+
226+
const args = [tagName];
227+
if (props) {
228+
args.push(props);
229+
}
230+
if (transformedChildren && transformedChildren.length > 0) {
231+
if (!props) {
232+
args.push(lua.createNilLiteral());
233+
}
234+
args.push(...transformedChildren);
235+
}
236+
return lua.createCallExpression(jsxFactory, args, tsOriginal);
237+
}
238+
239+
function transformJsxOpeningLikeElement(
240+
node: ts.JsxOpeningLikeElement,
241+
children: ts.NodeArray<ts.JsxChild> | undefined,
242+
context: TransformationContext
243+
): lua.Expression {
244+
const tagName = transformTagName(node.tagName, context);
245+
const props =
246+
node.attributes.properties.length !== 0 ? transformJsxAttributes(node.attributes, context) : undefined;
247+
248+
return createJsxFactoryCall(tagName, props, children, node, context);
249+
}
250+
251+
const transformJsxElement: FunctionVisitor<ts.JsxElement> = (node, context) =>
252+
transformJsxOpeningLikeElement(node.openingElement, node.children, context);
253+
const transformSelfClosingJsxElement: FunctionVisitor<ts.JsxSelfClosingElement> = (node, context) =>
254+
transformJsxOpeningLikeElement(node, undefined, context);
255+
const transformJsxFragment: FunctionVisitor<ts.JsxFragment> = (node, context) => {
256+
const tagName = getJsxFragmentName(node, context);
257+
return createJsxFactoryCall(tagName, undefined, node.children, node, context);
258+
};
259+
260+
export const jsxVisitors: Visitors = {
261+
[ts.SyntaxKind.JsxElement]: transformJsxElement,
262+
[ts.SyntaxKind.JsxSelfClosingElement]: transformSelfClosingJsxElement,
263+
[ts.SyntaxKind.JsxFragment]: transformJsxFragment,
264+
};

0 commit comments

Comments
 (0)