Skip to content

Commit 7dfd6c7

Browse files
Merge pull request #19249 from uniqueiniquity/jsxFragment
Add support for JSX fragment syntax
2 parents 239a039 + 3ebb2e8 commit 7dfd6c7

36 files changed

+1323
-132
lines changed

src/compiler/binder.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3296,6 +3296,9 @@ namespace ts {
32963296
case SyntaxKind.JsxOpeningElement:
32973297
case SyntaxKind.JsxText:
32983298
case SyntaxKind.JsxClosingElement:
3299+
case SyntaxKind.JsxFragment:
3300+
case SyntaxKind.JsxOpeningFragment:
3301+
case SyntaxKind.JsxClosingFragment:
32993302
case SyntaxKind.JsxAttribute:
33003303
case SyntaxKind.JsxAttributes:
33013304
case SyntaxKind.JsxSpreadAttribute:

src/compiler/checker.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13597,7 +13597,13 @@ namespace ts {
1359713597
// JSX expression can appear in two position : JSX Element's children or JSX attribute
1359813598
const jsxAttributes = isJsxAttributeLike(node.parent) ?
1359913599
node.parent.parent :
13600-
node.parent.openingElement.attributes; // node.parent is JsxElement
13600+
isJsxElement(node.parent) ?
13601+
node.parent.openingElement.attributes :
13602+
undefined; // node.parent is JsxFragment with no attributes
13603+
13604+
if (!jsxAttributes) {
13605+
return undefined; // don't check children of a fragment
13606+
}
1360113607

1360213608
// When we trying to resolve JsxOpeningLikeElement as a stateless function element, we will already give its attributes a contextual type
1360313609
// which is a type of the parameter of the signature we are trying out.
@@ -14177,13 +14183,13 @@ namespace ts {
1417714183
}
1417814184

1417914185
function checkJsxSelfClosingElement(node: JsxSelfClosingElement): Type {
14180-
checkJsxOpeningLikeElement(node);
14186+
checkJsxOpeningLikeElementOrOpeningFragment(node);
1418114187
return getJsxGlobalElementType() || anyType;
1418214188
}
1418314189

1418414190
function checkJsxElement(node: JsxElement): Type {
1418514191
// Check attributes
14186-
checkJsxOpeningLikeElement(node.openingElement);
14192+
checkJsxOpeningLikeElementOrOpeningFragment(node.openingElement);
1418714193

1418814194
// Perform resolution on the closing tag so that rename/go to definition/etc work
1418914195
if (isJsxIntrinsicIdentifier(node.closingElement.tagName)) {
@@ -14196,6 +14202,16 @@ namespace ts {
1419614202
return getJsxGlobalElementType() || anyType;
1419714203
}
1419814204

14205+
function checkJsxFragment(node: JsxFragment): Type {
14206+
checkJsxOpeningLikeElementOrOpeningFragment(node.openingFragment);
14207+
14208+
if (compilerOptions.jsx === JsxEmit.React && compilerOptions.jsxFactory) {
14209+
error(node, Diagnostics.JSX_fragment_is_not_supported_when_using_jsxFactory);
14210+
}
14211+
14212+
return getJsxGlobalElementType() || anyType;
14213+
}
14214+
1419914215
/**
1420014216
* Returns true iff the JSX element name would be a valid JS identifier, ignoring restrictions about keywords not being identifiers
1420114217
*/
@@ -14859,14 +14875,19 @@ namespace ts {
1485914875
}
1486014876
}
1486114877

14862-
function checkJsxOpeningLikeElement(node: JsxOpeningLikeElement) {
14863-
checkGrammarJsxElement(node);
14878+
function checkJsxOpeningLikeElementOrOpeningFragment(node: JsxOpeningLikeElement | JsxOpeningFragment) {
14879+
const isNodeOpeningLikeElement = isJsxOpeningLikeElement(node);
14880+
14881+
if (isNodeOpeningLikeElement) {
14882+
checkGrammarJsxElement(<JsxOpeningLikeElement>node);
14883+
}
1486414884
checkJsxPreconditions(node);
1486514885
// The reactNamespace/jsxFactory's root symbol should be marked as 'used' so we don't incorrectly elide its import.
1486614886
// And if there is no reactNamespace/jsxFactory's symbol in scope when targeting React emit, we should issue an error.
1486714887
const reactRefErr = diagnostics && compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined;
1486814888
const reactNamespace = getJsxNamespace();
14869-
const reactSym = resolveName(node.tagName, reactNamespace, SymbolFlags.Value, reactRefErr, reactNamespace, /*isUse*/ true);
14889+
const reactLocation = isNodeOpeningLikeElement ? (<JsxOpeningLikeElement>node).tagName : node;
14890+
const reactSym = resolveName(reactLocation, reactNamespace, SymbolFlags.Value, reactRefErr, reactNamespace, /*isUse*/ true);
1487014891
if (reactSym) {
1487114892
// Mark local symbol as referenced here because it might not have been marked
1487214893
// if jsx emit was not react as there wont be error being emitted
@@ -14878,7 +14899,9 @@ namespace ts {
1487814899
}
1487914900
}
1488014901

14881-
checkJsxAttributesAssignableToTagNameAttributes(node);
14902+
if (isNodeOpeningLikeElement) {
14903+
checkJsxAttributesAssignableToTagNameAttributes(<JsxOpeningLikeElement>node);
14904+
}
1488214905
}
1488314906

1488414907
/**
@@ -18655,6 +18678,8 @@ namespace ts {
1865518678
return checkJsxElement(<JsxElement>node);
1865618679
case SyntaxKind.JsxSelfClosingElement:
1865718680
return checkJsxSelfClosingElement(<JsxSelfClosingElement>node);
18681+
case SyntaxKind.JsxFragment:
18682+
return checkJsxFragment(<JsxFragment>node);
1865818683
case SyntaxKind.JsxAttributes:
1865918684
return checkJsxAttributes(<JsxAttributes>node, checkMode);
1866018685
case SyntaxKind.JsxOpeningElement:

src/compiler/diagnosticMessages.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3615,6 +3615,18 @@
36153615
"category": "Error",
36163616
"code": 17013
36173617
},
3618+
"JSX fragment has no corresponding closing tag.": {
3619+
"category": "Error",
3620+
"code": 17014
3621+
},
3622+
"Expected corresponding closing tag for JSX fragment.": {
3623+
"category": "Error",
3624+
"code": 17015
3625+
},
3626+
"JSX fragment is not supported when using --jsxFactory": {
3627+
"category": "Error",
3628+
"code":17016
3629+
},
36183630

36193631
"Circularity detected while resolving configuration: {0}": {
36203632
"category": "Error",

src/compiler/emitter.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -699,9 +699,11 @@ namespace ts {
699699
case SyntaxKind.JsxText:
700700
return emitJsxText(<JsxText>node);
701701
case SyntaxKind.JsxOpeningElement:
702-
return emitJsxOpeningElement(<JsxOpeningElement>node);
702+
case SyntaxKind.JsxOpeningFragment:
703+
return emitJsxOpeningElementOrFragment(<JsxOpeningElement>node);
703704
case SyntaxKind.JsxClosingElement:
704-
return emitJsxClosingElement(<JsxClosingElement>node);
705+
case SyntaxKind.JsxClosingFragment:
706+
return emitJsxClosingElementOrFragment(<JsxClosingElement>node);
705707
case SyntaxKind.JsxAttribute:
706708
return emitJsxAttribute(<JsxAttribute>node);
707709
case SyntaxKind.JsxAttributes:
@@ -836,6 +838,8 @@ namespace ts {
836838
return emitJsxElement(<JsxElement>node);
837839
case SyntaxKind.JsxSelfClosingElement:
838840
return emitJsxSelfClosingElement(<JsxSelfClosingElement>node);
841+
case SyntaxKind.JsxFragment:
842+
return emitJsxFragment(<JsxFragment>node);
839843

840844
// Transformation nodes
841845
case SyntaxKind.PartiallyEmittedExpression:
@@ -2060,7 +2064,7 @@ namespace ts {
20602064

20612065
function emitJsxElement(node: JsxElement) {
20622066
emit(node.openingElement);
2063-
emitList(node, node.children, ListFormat.JsxElementChildren);
2067+
emitList(node, node.children, ListFormat.JsxElementOrFragmentChildren);
20642068
emit(node.closingElement);
20652069
}
20662070

@@ -2075,24 +2079,36 @@ namespace ts {
20752079
write("/>");
20762080
}
20772081

2078-
function emitJsxOpeningElement(node: JsxOpeningElement) {
2082+
function emitJsxFragment(node: JsxFragment) {
2083+
emit(node.openingFragment);
2084+
emitList(node, node.children, ListFormat.JsxElementOrFragmentChildren);
2085+
emit(node.closingFragment);
2086+
}
2087+
2088+
function emitJsxOpeningElementOrFragment(node: JsxOpeningElement | JsxOpeningFragment) {
20792089
write("<");
2080-
emitJsxTagName(node.tagName);
2081-
writeIfAny(node.attributes.properties, " ");
2082-
// We are checking here so we won't re-enter the emitting pipeline and emit extra sourcemap
2083-
if (node.attributes.properties && node.attributes.properties.length > 0) {
2084-
emit(node.attributes);
2090+
2091+
if (isJsxOpeningElement(node)) {
2092+
emitJsxTagName(node.tagName);
2093+
// We are checking here so we won't re-enter the emitting pipeline and emit extra sourcemap
2094+
if (node.attributes.properties && node.attributes.properties.length > 0) {
2095+
write(" ");
2096+
emit(node.attributes);
2097+
}
20852098
}
2099+
20862100
write(">");
20872101
}
20882102

20892103
function emitJsxText(node: JsxText) {
20902104
writer.writeLiteral(getTextOfNode(node, /*includeTrivia*/ true));
20912105
}
20922106

2093-
function emitJsxClosingElement(node: JsxClosingElement) {
2107+
function emitJsxClosingElementOrFragment(node: JsxClosingElement | JsxClosingFragment) {
20942108
write("</");
2095-
emitJsxTagName(node.tagName);
2109+
if (isJsxClosingElement(node)) {
2110+
emitJsxTagName(node.tagName);
2111+
}
20962112
write(">");
20972113
}
20982114

@@ -2611,12 +2627,6 @@ namespace ts {
26112627
writer.decreaseIndent();
26122628
}
26132629

2614-
function writeIfAny(nodes: NodeArray<Node>, text: string) {
2615-
if (some(nodes)) {
2616-
write(text);
2617-
}
2618-
}
2619-
26202630
function writeToken(token: SyntaxKind, pos: number, contextNode?: Node) {
26212631
return onEmitSourceMapOfToken
26222632
? onEmitSourceMapOfToken(contextNode, token, pos, writeTokenText)
@@ -3176,7 +3186,7 @@ namespace ts {
31763186
EnumMembers = CommaDelimited | Indented | MultiLine,
31773187
CaseBlockClauses = Indented | MultiLine,
31783188
NamedImportsOrExportsElements = CommaDelimited | SpaceBetweenSiblings | AllowTrailingComma | SingleLine | SpaceBetweenBraces,
3179-
JsxElementChildren = SingleLine | NoInterveningComments,
3189+
JsxElementOrFragmentChildren = SingleLine | NoInterveningComments,
31803190
JsxElementAttributes = SingleLine | SpaceBetweenSiblings | NoInterveningComments,
31813191
CaseOrDefaultClauseStatements = Indented | MultiLine | NoTrailingNewLine | OptionalIfEmpty,
31823192
HeritageClauseTypes = CommaDelimited | SpaceBetweenSiblings | SingleLine,

src/compiler/factory.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,6 +2115,22 @@ namespace ts {
21152115
: node;
21162116
}
21172117

2118+
export function createJsxFragment(openingFragment: JsxOpeningFragment, children: ReadonlyArray<JsxChild>, closingFragment: JsxClosingFragment) {
2119+
const node = <JsxFragment>createSynthesizedNode(SyntaxKind.JsxFragment);
2120+
node.openingFragment = openingFragment;
2121+
node.children = createNodeArray(children);
2122+
node.closingFragment = closingFragment;
2123+
return node;
2124+
}
2125+
2126+
export function updateJsxFragment(node: JsxFragment, openingFragment: JsxOpeningFragment, children: ReadonlyArray<JsxChild>, closingFragment: JsxClosingFragment) {
2127+
return node.openingFragment !== openingFragment
2128+
|| node.children !== children
2129+
|| node.closingFragment !== closingFragment
2130+
? updateNode(createJsxFragment(openingFragment, children, closingFragment), node)
2131+
: node;
2132+
}
2133+
21182134
export function createJsxAttribute(name: Identifier, initializer: StringLiteral | JsxExpression) {
21192135
const node = <JsxAttribute>createSynthesizedNode(SyntaxKind.JsxAttribute);
21202136
node.name = name;
@@ -2951,7 +2967,7 @@ namespace ts {
29512967
);
29522968
}
29532969

2954-
function createReactNamespace(reactNamespace: string, parent: JsxOpeningLikeElement) {
2970+
function createReactNamespace(reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment) {
29552971
// To ensure the emit resolver can properly resolve the namespace, we need to
29562972
// treat this identifier as if it were a source tree node by clearing the `Synthesized`
29572973
// flag and setting a parent node.
@@ -2963,7 +2979,7 @@ namespace ts {
29632979
return react;
29642980
}
29652981

2966-
function createJsxFactoryExpressionFromEntityName(jsxFactory: EntityName, parent: JsxOpeningLikeElement): Expression {
2982+
function createJsxFactoryExpressionFromEntityName(jsxFactory: EntityName, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression {
29672983
if (isQualifiedName(jsxFactory)) {
29682984
const left = createJsxFactoryExpressionFromEntityName(jsxFactory.left, parent);
29692985
const right = createIdentifier(idText(jsxFactory.right));
@@ -2975,7 +2991,7 @@ namespace ts {
29752991
}
29762992
}
29772993

2978-
function createJsxFactoryExpression(jsxFactoryEntity: EntityName, reactNamespace: string, parent: JsxOpeningLikeElement): Expression {
2994+
function createJsxFactoryExpression(jsxFactoryEntity: EntityName, reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression {
29792995
return jsxFactoryEntity ?
29802996
createJsxFactoryExpressionFromEntityName(jsxFactoryEntity, parent) :
29812997
createPropertyAccess(
@@ -3016,6 +3032,37 @@ namespace ts {
30163032
);
30173033
}
30183034

3035+
export function createExpressionForJsxFragment(jsxFactoryEntity: EntityName, reactNamespace: string, children: Expression[], parentElement: JsxOpeningFragment, location: TextRange): LeftHandSideExpression {
3036+
const tagName = createPropertyAccess(
3037+
createReactNamespace(reactNamespace, parentElement),
3038+
"Fragment"
3039+
);
3040+
3041+
const argumentsList = [<Expression>tagName];
3042+
argumentsList.push(createNull());
3043+
3044+
if (children && children.length > 0) {
3045+
if (children.length > 1) {
3046+
for (const child of children) {
3047+
child.startsOnNewLine = true;
3048+
argumentsList.push(child);
3049+
}
3050+
}
3051+
else {
3052+
argumentsList.push(children[0]);
3053+
}
3054+
}
3055+
3056+
return setTextRange(
3057+
createCall(
3058+
createJsxFactoryExpression(jsxFactoryEntity, reactNamespace, parentElement),
3059+
/*typeArguments*/ undefined,
3060+
argumentsList
3061+
),
3062+
location
3063+
);
3064+
}
3065+
30193066
// Helpers
30203067

30213068
export function getHelperName(name: string) {

0 commit comments

Comments
 (0)