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:
parent
9e2828322e
commit
1a7194a22f
@ -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(">");
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
<></>;
|
||||
|
||||
<
|
||||
>
|
||||
text
|
||||
</>;
|
||||
@ -0,0 +1,4 @@
|
||||
<></>;
|
||||
<>
|
||||
text
|
||||
</>;
|
||||
@ -0,0 +1 @@
|
||||
{ "plugins": ["jsx" ] }
|
||||
@ -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
|
||||
});
|
||||
```
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ export default function({ types: t }) {
|
||||
);
|
||||
}
|
||||
},
|
||||
compat: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
<div>
|
||||
< >
|
||||
<>
|
||||
<span>Hello</span>
|
||||
<span>world</span>
|
||||
</>
|
||||
<>
|
||||
<span>Goodbye</span>
|
||||
<span>world</span>
|
||||
</>
|
||||
</>
|
||||
</div>
|
||||
@ -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"))));
|
||||
@ -0,0 +1,3 @@
|
||||
/** @jsx dom */
|
||||
|
||||
<div>no fragment is used</div>
|
||||
@ -0,0 +1,2 @@
|
||||
/** @jsx dom */
|
||||
dom("div", null, "no fragment is used");
|
||||
@ -0,0 +1,4 @@
|
||||
/** @jsx dom */
|
||||
/** @jsxFrag DomFrag */
|
||||
|
||||
<></>
|
||||
@ -0,0 +1,4 @@
|
||||
/** @jsx dom */
|
||||
|
||||
/** @jsxFrag DomFrag */
|
||||
dom(DomFrag, null);
|
||||
@ -0,0 +1,3 @@
|
||||
/** @jsx dom */
|
||||
|
||||
<></>
|
||||
@ -0,0 +1,2 @@
|
||||
/** @jsx dom */
|
||||
dom(React.Fragment, null);
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
"throws": "transform-react-jsx: pragma has been set but pragmafrag has not been set"
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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"],
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user