Add JSX Fragment syntax support (#6552)

* Add JSX Fragments to babel-types

* Support JSX fragments in the transform-react-jsx plugin

* Add tests JSX fragments

* Update helper-builder and transform plugin documentations for jsx fragment

* Add generator for jsx fragments

* Add test for jsx fragment generator

* Split jsx transform example into normal and fragment examples

* Remove unnecessary fields from ElementState in babel-helper-builder-react-jsx

* inline [skip ci]
This commit is contained in:
Clement Hoang 2017-11-03 07:43:48 -07:00 committed by Henry Zhu
parent 9e2828322e
commit 1a7194a22f
20 changed files with 322 additions and 29 deletions

View File

@ -92,3 +92,25 @@ export function JSXClosingElement(node: Object) {
}
export function JSXEmptyExpression() {}
export function JSXFragment(node: Object) {
this.print(node.openingFragment, node);
this.indent();
for (const child of (node.children: Array<Object>)) {
this.print(child, node);
}
this.dedent();
this.print(node.closingFragment, node);
}
export function JSXOpeningFragment() {
this.token("<");
this.token(">");
}
export function JSXClosingFragment() {
this.token("</");
this.token(">");
}

View File

@ -0,0 +1,6 @@
<></>;
<
>
text
</>;

View File

@ -0,0 +1,4 @@
<></>;
<>
text
</>;

View File

@ -0,0 +1 @@
{ "plugins": ["jsx" ] }

View File

@ -8,8 +8,6 @@ type ElementState = {
tagName: string; // raw string tag name
args: Array<Object>; // array of call arguments
call?: Object; // optional call property that can be set to override the call expression returned
pre?: Function; // function called with (state: ElementState) before building attribs
post?: Function; // function called with (state: ElementState) after building attribs
};
require("@babel/helper-builder-react-jsx")({
@ -18,11 +16,13 @@ require("@babel/helper-builder-react-jsx")({
},
pre: function (state: ElementState) {
// called before building the element
// function called with (state: ElementState) before building attribs
},
post: function (state: ElementState) {
// called after building the element
}
// function called with (state: ElementState) after building attribs
},
compat?: boolean // true if React is in compat mode
});
```

View File

@ -3,11 +3,9 @@ import * as t from "@babel/types";
type ElementState = {
tagExpr: Object, // tag node
tagName: string, // raw string tag name
tagName: ?string, // raw string tag name
args: Array<Object>, // array of call arguments
call?: Object, // optional call property that can be set to override the call expression returned
pre?: Function, // function called with (state: ElementState) before building attribs
post?: Function, // function called with (state: ElementState) after building attribs
};
export default function(opts) {
@ -30,6 +28,20 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`,
},
};
visitor.JSXFragment = {
exit(path, file) {
if (opts.compat) {
throw path.buildCodeFrameError(
"Fragment tags are only supported in React 16 and up.",
);
}
const callExpr = buildFragmentCall(path, file);
if (callExpr) {
path.replaceWith(t.inherits(callExpr, path.node));
}
},
};
return visitor;
function convertJSXIdentifier(node, parent) {
@ -188,4 +200,35 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`,
return attribs;
}
function buildFragmentCall(path, file) {
if (opts.filter && !opts.filter(path.node, file)) return;
const openingPath = path.get("openingElement");
openingPath.parent.children = t.react.buildChildren(openingPath.parent);
const args = [];
const tagName = null;
const tagExpr = file.get("jsxFragIdentifier")();
const state: ElementState = {
tagExpr: tagExpr,
tagName: tagName,
args: args,
};
if (opts.pre) {
opts.pre(state, file);
}
// no attributes are allowed with <> syntax
args.push(t.nullLiteral(), ...path.node.children);
if (opts.post) {
opts.post(state, file);
}
file.set("usedFragment", true);
return state.call || t.callExpression(state.callee, args);
}
}

View File

@ -23,6 +23,7 @@ export default function({ types: t }) {
);
}
},
compat: true,
}),
};
}

View File

@ -46,12 +46,76 @@ var profile = <div>
var dom = require("deku").dom;
var profile = dom( "div", null,
var profile = dom("div", null,
dom("img", { src: "avatar.png", className: "profile" }),
dom("h3", null, [user.firstName, user.lastName].join(" "))
);
```
### Fragments
Fragments are a feature available in React 16.2.0+.
#### React
**In**
```javascript
var descriptions = items.map(item => (
<>
<dt>{item.name}</dt>
<dd>{item.value}</dd>
</>
));
```
**Out**
```javascript
var descriptions = items.map(item => React.createElement(
React.Fragment,
null,
React.createElement("dt", null, item.name),
React.createElement("dd", null, item.value)
));
```
#### Custom
**In**
```javascript
/** @jsx dom */
/** @jsxFrag DomFrag */
var { dom, DomFrag } = require("deku"); // DomFrag is fictional!
var descriptions = items.map(item => (
<>
<dt>{item.name}</dt>
<dd>{item.value}</dd>
</>
));
```
**Out**
```javascript
/** @jsx dom */
/** @jsxFrag DomFrag */
var { dom, DomFrag } = require("deku"); // DomFrag is fictional!
var descriptions = items.map(item => dom(
DomFrag,
null,
dom("dt", null, item.name),
dom("dd", null, item.value)
));
```
Note that if a custom pragma is specified, then a custom fragment pragma must also be specified if the `<></>` is used. Otherwise, an error will be thrown.
## Installation
```sh
@ -79,6 +143,7 @@ With options:
"plugins": [
["@babel/transform-react-jsx", {
"pragma": "dom", // default pragma is React.createElement
"pragmaFrag": "DomFrag", // default is React.Fragment
"throwIfNamespace": false // defaults to true
}]
]
@ -109,6 +174,12 @@ Replace the function used when compiling JSX expressions.
Note that the `@jsx React.DOM` pragma has been deprecated as of React v0.12
### `pragmaFrag`
`string`, defaults to `React.Fragment`.
Replace the component used when compiling JSX fragments.
### `useBuiltIns`
`boolean`, defaults to `false`.

View File

@ -2,11 +2,23 @@ import jsx from "@babel/plugin-syntax-jsx";
import helper from "@babel/helper-builder-react-jsx";
export default function({ types: t }, options) {
const pragma = options.pragma || "React.createElement";
const throwIfNamespace =
const THROW_IF_NAMESPACE =
options.throwIfNamespace === undefined ? true : !!options.throwIfNamespace;
const PRAGMA_DEFAULT = options.pragma || "React.createElement";
const PRAGMA_FRAG_DEFAULT = options.pragmaFrag || "React.Fragment";
const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/;
const JSX_FRAG_ANNOTATION_REGEX = /\*?\s*@jsxFrag\s+([^\s]+)/;
// returns a closure that returns an identifier or memberExpression node
// based on the given id
const createIdentifierParser = (id: string) => () => {
return id
.split(".")
.map(name => t.identifier(name))
.reduce((object, property) => t.memberExpression(object, property));
};
const visitor = helper({
pre(state) {
@ -23,27 +35,49 @@ export default function({ types: t }, options) {
state.callee = pass.get("jsxIdentifier")();
},
throwIfNamespace,
throwIfNamespace: THROW_IF_NAMESPACE,
});
visitor.Program = function(path, state) {
const { file } = state;
visitor.Program = {
enter(path, state) {
const { file } = state;
let id = pragma;
for (const comment of (file.ast.comments: Array<Object>)) {
const matches = JSX_ANNOTATION_REGEX.exec(comment.value);
if (matches) {
id = matches[1];
break;
let pragma = PRAGMA_DEFAULT;
let pragmaFrag = PRAGMA_FRAG_DEFAULT;
let pragmaSet = !!options.pragma;
let pragmaFragSet = !!options.pragmaFrag;
for (const comment of (file.ast.comments: Array<Object>)) {
const jsxMatches = JSX_ANNOTATION_REGEX.exec(comment.value);
if (jsxMatches) {
pragma = jsxMatches[1];
pragmaSet = true;
}
const jsxFragMatches = JSX_FRAG_ANNOTATION_REGEX.exec(comment.value);
if (jsxFragMatches) {
pragmaFrag = jsxFragMatches[1];
pragmaFragSet = true;
}
}
}
state.set("jsxIdentifier", () =>
id
.split(".")
.map(name => t.identifier(name))
.reduce((object, property) => t.memberExpression(object, property)),
);
state.set("jsxIdentifier", createIdentifierParser(pragma));
state.set("jsxFragIdentifier", createIdentifierParser(pragmaFrag));
state.set("usedFragment", false);
state.set("pragmaSet", pragmaSet);
state.set("pragmaFragSet", pragmaFragSet);
},
exit(path, state) {
if (
state.get("pragmaSet") &&
state.get("usedFragment") &&
!state.get("pragmaFragSet")
) {
throw new Error(
"transform-react-jsx: pragma has been set but " +
"pragmafrag has not been set",
);
}
},
};
visitor.JSXAttribute = function(path) {

View File

@ -0,0 +1,12 @@
<div>
< >
<>
<span>Hello</span>
<span>world</span>
</>
<>
<span>Goodbye</span>
<span>world</span>
</>
</>
</div>

View File

@ -0,0 +1 @@
React.createElement("div", null, React.createElement(React.Fragment, null, React.createElement(React.Fragment, null, React.createElement("span", null, "Hello"), React.createElement("span", null, "world")), React.createElement(React.Fragment, null, React.createElement("span", null, "Goodbye"), React.createElement("span", null, "world"))));

View File

@ -0,0 +1,3 @@
/** @jsx dom */
<div>no fragment is used</div>

View File

@ -0,0 +1,2 @@
/** @jsx dom */
dom("div", null, "no fragment is used");

View File

@ -0,0 +1,4 @@
/** @jsx dom */
/** @jsxFrag DomFrag */
<></>

View File

@ -0,0 +1,4 @@
/** @jsx dom */
/** @jsxFrag DomFrag */
dom(DomFrag, null);

View File

@ -0,0 +1,2 @@
/** @jsx dom */
dom(React.Fragment, null);

View File

@ -0,0 +1,3 @@
{
"throws": "transform-react-jsx: pragma has been set but pragmafrag has not been set"
}

View File

@ -1059,7 +1059,7 @@ See also `t.isJSXAttribute(node, opts)` and `t.assertJSXAttribute(node, opts)`.
Aliases: `JSX`, `Immutable`
- `name`: `JSXIdentifier | JSXNamespacedName` (required)
- `value`: `JSXElement | StringLiteral | JSXExpressionContainer` (default: `null`)
- `value`: `JSXElement | JSXFragment | StringLiteral | JSXExpressionContainer` (default: `null`)
---
@ -1074,6 +1074,18 @@ Aliases: `JSX`, `Immutable`
- `name`: `JSXIdentifier | JSXMemberExpression` (required)
---
### jSXClosingFragment
```javascript
t.jSXClosingFragment()
```
See also `t.isJSXClosingFragment(node, opts)` and `t.assertJSXClosingFragment(node, opts)`.
Aliases: `JSX`, `Immutable`
---
### jSXElement
@ -1087,7 +1099,7 @@ Aliases: `JSX`, `Immutable`, `Expression`
- `openingElement`: `JSXOpeningElement` (required)
- `closingElement`: `JSXClosingElement` (default: `null`)
- `children`: `Array<JSXText | JSXExpressionContainer | JSXSpreadChild | JSXElement>` (required)
- `children`: `Array<JSXText | JSXExpressionContainer | JSXSpreadChild | JSXElement | JSXFragment>` (required)
- `selfClosing` (required)
---
@ -1117,6 +1129,21 @@ Aliases: `JSX`, `Immutable`
---
### jSXFragment
```javascript
t.jSXFragment(openingFragment, closingFragment, children)
```
See also `t.isJSXFragment(node, opts)` and `t.assertJSXFragment(node, opts)`.
Aliases: `JSX`, `Immutable`, `Expression`
- `openingFragment`: `JSXOpeningFragment` (required)
- `closingFragment`: `JSXClosingFragment` (required)
- `children`: `Array<JSXText | JSXExpressionContainer | JSXSpreadChild | JSXElement | JSXFragment>` (required)
---
### jSXIdentifier
```javascript
t.jSXIdentifier(name)
@ -1171,6 +1198,18 @@ Aliases: `JSX`, `Immutable`
- `attributes`: `Array<JSXAttribute | JSXSpreadAttribute>` (required)
- `selfClosing`: `boolean` (default: `false`)
---
### jSXOpeningFragment
```javascript
t.jSXOpeningFragment()
```
See also `t.isJSXOpeningFragment(node, opts)` and `t.assertJSXOpeningFragment(node, opts)`.
Aliases: `JSX`, `Immutable`
---
### jSXSpreadAttribute

View File

@ -16,6 +16,7 @@ defineType("JSXAttribute", {
optional: true,
validate: assertNodeType(
"JSXElement",
"JSXFragment",
"StringLiteral",
"JSXExpressionContainer",
),
@ -54,6 +55,7 @@ defineType("JSXElement", {
"JSXExpressionContainer",
"JSXSpreadChild",
"JSXElement",
"JSXFragment",
),
),
),
@ -161,3 +163,39 @@ defineType("JSXText", {
},
},
});
defineType("JSXFragment", {
builder: ["openingFragment", "closingFragment", "children"],
visitor: ["openingFragment", "children", "closingFragment"],
aliases: ["JSX", "Immutable", "Expression"],
fields: {
openingFragment: {
validate: assertNodeType("JSXOpeningFragment"),
},
closingFragment: {
validate: assertNodeType("JSXClosingFragment"),
},
children: {
validate: chain(
assertValueType("array"),
assertEach(
assertNodeType(
"JSXText",
"JSXExpressionContainer",
"JSXSpreadChild",
"JSXElement",
"JSXFragment",
),
),
),
},
},
});
defineType("JSXOpeningFragment", {
aliases: ["JSX", "Immutable"],
});
defineType("JSXClosingFragment", {
aliases: ["JSX", "Immutable"],
});