diff --git a/packages/babel-generator/src/generators/jsx.js b/packages/babel-generator/src/generators/jsx.js
index 24865630c0..89312add92 100644
--- a/packages/babel-generator/src/generators/jsx.js
+++ b/packages/babel-generator/src/generators/jsx.js
@@ -73,6 +73,7 @@ function spaceSeparator() {
export function JSXOpeningElement(node: Object) {
this.token("<");
this.print(node.name, node);
+ this.print(node.typeParameters, node); // TS
if (node.attributes.length > 0) {
this.space();
this.printJoin(node.attributes, node, { separator: spaceSeparator });
diff --git a/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/input.js b/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/input.js
new file mode 100644
index 0000000000..520e35ee90
--- /dev/null
+++ b/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/input.js
@@ -0,0 +1,2 @@
+>;
+/>;
diff --git a/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/options.json b/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/options.json
new file mode 100644
index 0000000000..2993d04859
--- /dev/null
+++ b/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/options.json
@@ -0,0 +1,4 @@
+{
+ "plugins": ["jsx", "typescript"],
+ "sourceType": "module"
+}
\ No newline at end of file
diff --git a/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/output.js b/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/output.js
new file mode 100644
index 0000000000..535f6a651d
--- /dev/null
+++ b/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/output.js
@@ -0,0 +1,2 @@
+>;
+ />;
\ No newline at end of file
diff --git a/packages/babel-parser/src/parser/index.js b/packages/babel-parser/src/parser/index.js
index 8042410ff6..6d6d9cfd8b 100644
--- a/packages/babel-parser/src/parser/index.js
+++ b/packages/babel-parser/src/parser/index.js
@@ -1,7 +1,7 @@
// @flow
import type { Options } from "../options";
-import type { File } from "../types";
+import type { File, JSXOpeningElement } from "../types";
import type { PluginList } from "../plugin-utils";
import { getOptions } from "../options";
import StatementParser from "./statement";
@@ -11,6 +11,11 @@ export type PluginsMap = {
};
export default class Parser extends StatementParser {
+ // Forward-declaration so typescript plugin can override jsx plugin
+ +jsxParseOpeningElementAfterName: (
+ node: JSXOpeningElement,
+ ) => JSXOpeningElement;
+
constructor(options: ?Options, input: string) {
options = getOptions(options);
super(options, input);
diff --git a/packages/babel-parser/src/plugins/jsx/index.js b/packages/babel-parser/src/plugins/jsx/index.js
index 6ac5204377..0d32ea98d1 100644
--- a/packages/babel-parser/src/plugins/jsx/index.js
+++ b/packages/babel-parser/src/plugins/jsx/index.js
@@ -358,11 +358,18 @@ export default (superClass: Class): Class =>
this.expect(tt.jsxTagEnd);
return this.finishNode(node, "JSXOpeningFragment");
}
- node.attributes = [];
node.name = this.jsxParseElementName();
+ return this.jsxParseOpeningElementAfterName(node);
+ }
+
+ jsxParseOpeningElementAfterName(
+ node: N.JSXOpeningElement,
+ ): N.JSXOpeningElement {
+ const attributes: N.JSXAttribute[] = [];
while (!this.match(tt.slash) && !this.match(tt.jsxTagEnd)) {
- node.attributes.push(this.jsxParseAttribute());
+ attributes.push(this.jsxParseAttribute());
}
+ node.attributes = attributes;
node.selfClosing = this.eat(tt.slash);
this.expect(tt.jsxTagEnd);
return this.finishNode(node, "JSXOpeningElement");
diff --git a/packages/babel-parser/src/plugins/typescript.js b/packages/babel-parser/src/plugins/typescript.js
index e9abe7276f..c1fc563479 100644
--- a/packages/babel-parser/src/plugins/typescript.js
+++ b/packages/babel-parser/src/plugins/typescript.js
@@ -836,10 +836,12 @@ export default (superClass: Class): Class =>
return this.finishNode(node, "TSTypeAssertion");
}
- tsTryParseTypeArgumentsInExpression(): ?N.TsTypeParameterInstantiation {
+ tsTryParseTypeArgumentsInExpression(
+ eatNextToken: boolean,
+ ): ?N.TsTypeParameterInstantiation {
return this.tsTryParseAndCatch(() => {
const res = this.tsParseTypeArguments();
- this.expect(tt.parenL);
+ if (eatNextToken) this.expect(tt.parenL);
return res;
});
}
@@ -887,6 +889,16 @@ export default (superClass: Class): Class =>
return this.finishNode(node, "TSTypeAliasDeclaration");
}
+ tsInNoContext(cb: () => T): T {
+ const oldContext = this.state.context;
+ this.state.context = [oldContext[0]];
+ try {
+ return cb();
+ } finally {
+ this.state.context = oldContext;
+ }
+ }
+
/**
* Runs `cb` in a type context.
* This should be called one token *before* the first type token,
@@ -1241,13 +1253,19 @@ export default (superClass: Class): Class =>
tsParseTypeArguments(): N.TsTypeParameterInstantiation {
const node = this.startNode();
- node.params = this.tsInType(() => {
- this.expectRelational("<");
- return this.tsParseDelimitedList(
- "TypeParametersOrArguments",
- this.tsParseType.bind(this),
- );
- });
+ node.params = this.tsInType(() =>
+ // Temporarily remove a JSX parsing context, which makes us scan different tokens.
+ this.tsInNoContext(() => {
+ this.expectRelational("<");
+ return this.tsParseDelimitedList(
+ "TypeParametersOrArguments",
+ this.tsParseType.bind(this),
+ );
+ }),
+ );
+ // This reads the next token after the `>` too, so do this in the enclosing context.
+ // But be sure not to parse a regex in the jsx expression ` />`, so set exprAllowed = false
+ this.state.exprAllowed = false;
this.expectRelational(">");
return this.finishNode(node, "TSTypeParameterInstantiation");
}
@@ -1375,7 +1393,10 @@ export default (superClass: Class): Class =>
node.callee = base;
// May be passing type arguments. But may just be the `<` operator.
- const typeArguments = this.tsTryParseTypeArgumentsInExpression(); // Also eats the "("
+ // Note: With `/*eatNextToken*/ true` this also eats the `(` following the type arguments
+ const typeArguments = this.tsTryParseTypeArgumentsInExpression(
+ /*eatNextToken*/ true,
+ );
if (typeArguments) {
// possibleAsync always false here, because we would have handled it above.
// $FlowIgnore (won't be any undefined arguments)
@@ -2102,4 +2123,14 @@ export default (superClass: Class): Class =>
// Avoid unnecessary lookahead in checking for abstract class unless needed!
return super.canHaveLeadingDecorator() || this.isAbstractClass();
}
+
+ jsxParseOpeningElementAfterName(
+ node: N.JSXOpeningElement,
+ ): N.JSXOpeningElement {
+ const typeArguments = this.tsTryParseTypeArgumentsInExpression(
+ /*eatNextToken*/ false,
+ );
+ if (typeArguments) node.typeParameters = typeArguments;
+ return super.jsxParseOpeningElementAfterName(node);
+ }
};
diff --git a/packages/babel-parser/src/tokenizer/index.js b/packages/babel-parser/src/tokenizer/index.js
index a5655cadb4..2043832b1c 100644
--- a/packages/babel-parser/src/tokenizer/index.js
+++ b/packages/babel-parser/src/tokenizer/index.js
@@ -423,7 +423,7 @@ export default class Tokenizer extends LocationParser {
readToken_slash(): void {
// '/'
- if (this.state.exprAllowed) {
+ if (this.state.exprAllowed && !this.state.inType) {
++this.state.pos;
this.readRegexp();
return;
diff --git a/packages/babel-parser/src/types.js b/packages/babel-parser/src/types.js
index 0a7d2d0f52..92b6aaba09 100644
--- a/packages/babel-parser/src/types.js
+++ b/packages/babel-parser/src/types.js
@@ -573,7 +573,7 @@ export type TemplateLiteral = NodeBase & {
expressions: $ReadOnlyArray,
};
-export type TaggedTmplateExpression = NodeBase & {
+export type TaggedTemplateExpression = NodeBase & {
type: "TaggedTemplateExpression",
tag: Expression,
quasi: TemplateLiteral,
@@ -820,7 +820,13 @@ export type JSXEmptyExpression = Node;
export type JSXSpreadChild = Node;
export type JSXExpressionContainer = Node;
export type JSXAttribute = Node;
-export type JSXOpeningElement = Node;
+export type JSXOpeningElement = NodeBase & {
+ type: "JSXOpeningElement",
+ name: JSXNamespacedName | JSXMemberExpression,
+ typeParameters?: ?TypeParameterInstantiationBase, // TODO: Not in spec
+ attributes: $ReadOnlyArray,
+ selfClosing: boolean,
+};
export type JSXClosingElement = Node;
export type JSXElement = Node;
export type JSXOpeningFragment = Node;
diff --git a/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/input.js b/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/input.js
new file mode 100644
index 0000000000..520e35ee90
--- /dev/null
+++ b/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/input.js
@@ -0,0 +1,2 @@
+>;
+/>;
diff --git a/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/options.json b/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/options.json
new file mode 100644
index 0000000000..b2578f32c4
--- /dev/null
+++ b/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/options.json
@@ -0,0 +1,3 @@
+{
+ "plugins": ["jsx", "typescript"]
+}
\ No newline at end of file
diff --git a/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/output.json b/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/output.json
new file mode 100644
index 0000000000..15f1e7ebde
--- /dev/null
+++ b/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/output.json
@@ -0,0 +1,259 @@
+{
+ "type": "File",
+ "start": 0,
+ "end": 30,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 0
+ },
+ "end": {
+ "line": 2,
+ "column": 13
+ }
+ },
+ "program": {
+ "type": "Program",
+ "start": 0,
+ "end": 30,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 0
+ },
+ "end": {
+ "line": 2,
+ "column": 13
+ }
+ },
+ "sourceType": "module",
+ "interpreter": null,
+ "body": [
+ {
+ "type": "ExpressionStatement",
+ "start": 0,
+ "end": 16,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 0
+ },
+ "end": {
+ "line": 1,
+ "column": 16
+ }
+ },
+ "expression": {
+ "type": "JSXElement",
+ "start": 0,
+ "end": 15,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 0
+ },
+ "end": {
+ "line": 1,
+ "column": 15
+ }
+ },
+ "openingElement": {
+ "type": "JSXOpeningElement",
+ "start": 0,
+ "end": 11,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 0
+ },
+ "end": {
+ "line": 1,
+ "column": 11
+ }
+ },
+ "name": {
+ "type": "JSXIdentifier",
+ "start": 1,
+ "end": 2,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 1
+ },
+ "end": {
+ "line": 1,
+ "column": 2
+ }
+ },
+ "name": "C"
+ },
+ "typeParameters": {
+ "type": "TSTypeParameterInstantiation",
+ "start": 2,
+ "end": 10,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 2
+ },
+ "end": {
+ "line": 1,
+ "column": 10
+ }
+ },
+ "params": [
+ {
+ "type": "TSNumberKeyword",
+ "start": 3,
+ "end": 9,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 3
+ },
+ "end": {
+ "line": 1,
+ "column": 9
+ }
+ }
+ }
+ ]
+ },
+ "attributes": [],
+ "selfClosing": false
+ },
+ "closingElement": {
+ "type": "JSXClosingElement",
+ "start": 11,
+ "end": 15,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 11
+ },
+ "end": {
+ "line": 1,
+ "column": 15
+ }
+ },
+ "name": {
+ "type": "JSXIdentifier",
+ "start": 13,
+ "end": 14,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 13
+ },
+ "end": {
+ "line": 1,
+ "column": 14
+ }
+ },
+ "name": "C"
+ }
+ },
+ "children": []
+ }
+ },
+ {
+ "type": "ExpressionStatement",
+ "start": 17,
+ "end": 30,
+ "loc": {
+ "start": {
+ "line": 2,
+ "column": 0
+ },
+ "end": {
+ "line": 2,
+ "column": 13
+ }
+ },
+ "expression": {
+ "type": "JSXElement",
+ "start": 17,
+ "end": 29,
+ "loc": {
+ "start": {
+ "line": 2,
+ "column": 0
+ },
+ "end": {
+ "line": 2,
+ "column": 12
+ }
+ },
+ "openingElement": {
+ "type": "JSXOpeningElement",
+ "start": 17,
+ "end": 29,
+ "loc": {
+ "start": {
+ "line": 2,
+ "column": 0
+ },
+ "end": {
+ "line": 2,
+ "column": 12
+ }
+ },
+ "name": {
+ "type": "JSXIdentifier",
+ "start": 18,
+ "end": 19,
+ "loc": {
+ "start": {
+ "line": 2,
+ "column": 1
+ },
+ "end": {
+ "line": 2,
+ "column": 2
+ }
+ },
+ "name": "C"
+ },
+ "typeParameters": {
+ "type": "TSTypeParameterInstantiation",
+ "start": 19,
+ "end": 27,
+ "loc": {
+ "start": {
+ "line": 2,
+ "column": 2
+ },
+ "end": {
+ "line": 2,
+ "column": 10
+ }
+ },
+ "params": [
+ {
+ "type": "TSNumberKeyword",
+ "start": 20,
+ "end": 26,
+ "loc": {
+ "start": {
+ "line": 2,
+ "column": 3
+ },
+ "end": {
+ "line": 2,
+ "column": 9
+ }
+ }
+ }
+ ]
+ },
+ "attributes": [],
+ "selfClosing": true
+ },
+ "closingElement": null,
+ "children": []
+ }
+ }
+ ],
+ "directives": []
+ }
+}
\ No newline at end of file
diff --git a/packages/babel-plugin-transform-typescript/src/index.js b/packages/babel-plugin-transform-typescript/src/index.js
index f7f2b304f6..220529a773 100644
--- a/packages/babel-plugin-transform-typescript/src/index.js
+++ b/packages/babel-plugin-transform-typescript/src/index.js
@@ -264,6 +264,10 @@ export default declare((api, { jsxPragma = "React" }) => {
NewExpression(path) {
path.node.typeParameters = null;
},
+
+ JSXOpeningElement(path) {
+ path.node.typeParameters = null;
+ },
},
};
diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/input.js b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/input.js
new file mode 100644
index 0000000000..520e35ee90
--- /dev/null
+++ b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/input.js
@@ -0,0 +1,2 @@
+>;
+/>;
diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/options.json b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/options.json
new file mode 100644
index 0000000000..2c7aa7bce2
--- /dev/null
+++ b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/options.json
@@ -0,0 +1,3 @@
+{
+ "plugins": [["transform-typescript", { "isTSX": true }]]
+}
diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/output.js b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/output.js
new file mode 100644
index 0000000000..c2d492ae05
--- /dev/null
+++ b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/output.js
@@ -0,0 +1,2 @@
+;
+;
diff --git a/packages/babel-types/src/definitions/jsx.js b/packages/babel-types/src/definitions/jsx.js
index c039450e95..4404273956 100644
--- a/packages/babel-types/src/definitions/jsx.js
+++ b/packages/babel-types/src/definitions/jsx.js
@@ -142,6 +142,13 @@ defineType("JSXOpeningElement", {
assertEach(assertNodeType("JSXAttribute", "JSXSpreadAttribute")),
),
},
+ typeParameters: {
+ validate: assertNodeType(
+ "TypeParameterInstantiation",
+ "TSTypeParameterInstantiation",
+ ),
+ optional: true,
+ },
},
});