Skip to content

Commit 24c5620

Browse files
eryue0220ariya
authored andcommitted
Support JSX fragment (#2046)
1 parent 977b1f8 commit 24c5620

14 files changed

+1193
-10
lines changed

src/jsx-nodes.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,23 @@ export class JSXClosingElement {
1616
}
1717
}
1818

19+
export class JSXClosingFragment {
20+
readonly type: string;
21+
constructor() {
22+
this.type = JSXSyntax.JSXClosingFragment;
23+
}
24+
}
25+
1926
export class JSXElement {
2027
readonly type: string;
21-
readonly openingElement: JSXOpeningElement;
28+
readonly openingElement: JSXOpeningElement | JSXOpeningFragment;
2229
readonly children: JSXChild[];
23-
readonly closingElement: JSXClosingElement | null;
24-
constructor(openingElement: JSXOpeningElement, children: JSXChild[], closingElement: JSXClosingElement | null) {
30+
readonly closingElement: JSXClosingElement | JSXClosingFragment | null;
31+
constructor(
32+
openingElement: JSXOpeningElement | JSXOpeningFragment,
33+
children: JSXChild[],
34+
closingElement: JSXClosingElement | JSXClosingFragment | null
35+
) {
2536
this.type = JSXSyntax.JSXElement;
2637
this.openingElement = openingElement;
2738
this.children = children;
@@ -100,6 +111,15 @@ export class JSXOpeningElement {
100111
}
101112
}
102113

114+
export class JSXOpeningFragment {
115+
readonly type: string;
116+
readonly selfClosing: boolean;
117+
constructor(selfClosing: boolean) {
118+
this.type = JSXSyntax.JSXOpeningFragment;
119+
this.selfClosing = selfClosing;
120+
}
121+
}
122+
103123
export class JSXSpreadAttribute {
104124
readonly type: string;
105125
readonly argument: Node.Expression;

src/jsx-parser.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { XHTMLEntities } from './xhtml-entities';
88

99
interface MetaJSXElement {
1010
node: Marker;
11-
opening: JSXNode.JSXOpeningElement;
12-
closing: JSXNode.JSXClosingElement | null;
11+
opening: JSXNode.JSXOpeningElement | JSXNode.JSXOpeningFragment;
12+
closing: JSXNode.JSXClosingElement | JSXNode.JSXClosingFragment | null;
1313
children: JSXNode.JSXChild[];
1414
}
1515

@@ -463,10 +463,15 @@ export class JSXParser extends Parser {
463463
return attributes;
464464
}
465465

466-
parseJSXOpeningElement(): JSXNode.JSXOpeningElement {
466+
parseJSXOpeningElement(): JSXNode.JSXOpeningElement | JSXNode.JSXOpeningFragment {
467467
const node = this.createJSXNode();
468468

469469
this.expectJSX('<');
470+
if (this.matchJSX('>')) {
471+
this.expectJSX('>');
472+
return this.finalize(node, new JSXNode.JSXOpeningFragment(false));
473+
}
474+
470475
const name = this.parseJSXElementName();
471476
const attributes = this.parseJSXAttributes();
472477
const selfClosing = this.matchJSX('/');
@@ -478,12 +483,16 @@ export class JSXParser extends Parser {
478483
return this.finalize(node, new JSXNode.JSXOpeningElement(name, selfClosing, attributes));
479484
}
480485

481-
parseJSXBoundaryElement(): JSXNode.JSXOpeningElement | JSXNode.JSXClosingElement {
486+
parseJSXBoundaryElement(): JSXNode.JSXOpeningElement | JSXNode.JSXClosingElement | JSXNode.JSXOpeningFragment | JSXNode.JSXClosingFragment {
482487
const node = this.createJSXNode();
483488

484489
this.expectJSX('<');
485490
if (this.matchJSX('/')) {
486491
this.expectJSX('/');
492+
if (this.matchJSX('>')) {
493+
this.expectJSX('>');
494+
return this.finalize(node, new JSXNode.JSXClosingFragment());
495+
}
487496
const elementName = this.parseJSXElementName();
488497
this.expectJSX('>');
489498
return this.finalize(node, new JSXNode.JSXClosingElement(elementName));
@@ -567,8 +576,8 @@ export class JSXParser extends Parser {
567576
}
568577
if (element.type === JSXSyntax.JSXClosingElement) {
569578
el.closing = element as JSXNode.JSXClosingElement;
570-
const open = getQualifiedElementName(el.opening.name);
571-
const close = getQualifiedElementName(el.closing.name);
579+
const open = getQualifiedElementName((el.opening as JSXNode.JSXOpeningElement).name);
580+
const close = getQualifiedElementName((el.closing as JSXNode.JSXClosingElement).name);
572581
if (open !== close) {
573582
this.tolerateError('Expected corresponding JSX closing tag for %0', open);
574583
}
@@ -581,6 +590,14 @@ export class JSXParser extends Parser {
581590
break;
582591
}
583592
}
593+
if (element.type === JSXSyntax.JSXClosingFragment) {
594+
el.closing = element as JSXNode.JSXClosingFragment;
595+
if (el.opening.type !== JSXSyntax.JSXOpeningFragment) {
596+
this.tolerateError('Expected corresponding JSX closing tag for jsx fragment');
597+
} else {
598+
break;
599+
}
600+
}
584601
}
585602

586603
return el;
@@ -591,7 +608,7 @@ export class JSXParser extends Parser {
591608

592609
const opening = this.parseJSXOpeningElement();
593610
let children: JSXNode.JSXChild[] = [];
594-
let closing: JSXNode.JSXClosingElement | null = null;
611+
let closing: JSXNode.JSXClosingElement | JSXNode.JSXClosingFragment | null = null;
595612

596613
if (!opening.selfClosing) {
597614
const el = this.parseComplexJSXElement({ node, opening, closing, children });

src/jsx-syntax.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
export const JSXSyntax = {
22
JSXAttribute: 'JSXAttribute',
33
JSXClosingElement: 'JSXClosingElement',
4+
JSXClosingFragment: 'JSXClosingFragment',
45
JSXElement: 'JSXElement',
56
JSXEmptyExpression: 'JSXEmptyExpression',
67
JSXExpressionContainer: 'JSXExpressionContainer',
78
JSXIdentifier: 'JSXIdentifier',
89
JSXMemberExpression: 'JSXMemberExpression',
910
JSXNamespacedName: 'JSXNamespacedName',
1011
JSXOpeningElement: 'JSXOpeningElement',
12+
JSXOpeningFragment: 'JSXOpeningFragment',
1113
JSXSpreadAttribute: 'JSXSpreadAttribute',
1214
JSXText: 'JSXText'
1315
};

test/api-tests.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,32 @@ describe('esprima.parse', function () {
262262
assert.deepEqual(expression.closingElement, null);
263263
});
264264

265+
it('should understand JSX fragment', function () {
266+
assert.doesNotThrow(function () {
267+
esprima.parse('<></>', { jsx: true });
268+
});
269+
});
270+
271+
it('should understand JSX fragment syntax', function () {
272+
var ast = esprima.parse('<></>', { jsx: true });
273+
var statement = ast.body[0];
274+
var expression = statement.expression;
275+
276+
assert.deepEqual(expression.type, 'JSXElement');
277+
assert.deepEqual(expression.openingElement.type, 'JSXOpeningFragment');
278+
assert.deepEqual(expression.closingElement.type, 'JSXClosingFragment');
279+
});
280+
281+
it('should understand JSX fragment syntax', function () {
282+
var ast = esprima.parse('<></>', { jsx: true });
283+
var statement = ast.body[0];
284+
var expression = statement.expression;
285+
286+
assert.deepEqual(expression.type, 'JSXElement');
287+
assert.deepEqual(expression.openingElement.type, 'JSXOpeningFragment');
288+
assert.deepEqual(expression.closingElement.type, 'JSXClosingFragment');
289+
});
290+
265291
it('should never produce shallow copied nodes', function () {
266292
var ast, pattern, expr;
267293
ast = esprima.parse('let {a, b} = {x, y, z}');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<><div /></>

0 commit comments

Comments
 (0)