-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Add JSX Fragment syntax support #6552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bbf0de5
aad7721
a9e52c5
c5b549a
18de9af
eef5e94
8770a56
587649c
b815338
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| <></>; | ||
|
|
||
| < | ||
| > | ||
| text | ||
| </>; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| <></>; | ||
| <> | ||
| text | ||
| </>; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| { "plugins": ["jsx" ] } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct. IIRC, I just stuck this in there so it would throw an error if you're trying to use fragments with the |
||
| }); | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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.", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we do a peerDep on react @babel/react? (maybe for the preset)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you mean by a peerDep on react @babel/react?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean should
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, good question. Would non-React library authors use
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yeah they may just use the specific transform itself, not the preset |
||
| ); | ||
| } | ||
| 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); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ export default function({ types: t }) { | |
| ); | ||
| } | ||
| }, | ||
| compat: true, | ||
| }), | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This probably just throws in React 15.4 🤔
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that something to address in this PR?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, not really, was just thinking out loud 🤔 |
||
|
|
||
| ### `pragmaFrag` | ||
|
|
||
| `string`, defaults to `React.Fragment`. | ||
|
|
||
| Replace the component used when compiling JSX fragments. | ||
|
|
||
| ### `useBuiltIns` | ||
|
|
||
| `boolean`, defaults to `false`. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)); | ||
| }; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks suspiciously like (we really need to document that in handbook 🤔)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, nevermind, it's its inverse 😆 |
||
|
|
||
| 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 pragma = PRAGMA_DEFAULT; | ||
| let pragmaFrag = PRAGMA_FRAG_DEFAULT; | ||
| let pragmaSet = !!options.pragma; | ||
| let pragmaFragSet = !!options.pragmaFrag; | ||
|
|
||
| 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; | ||
| 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) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <div> | ||
| < > | ||
| <> | ||
| <span>Hello</span> | ||
| <span>world</span> | ||
| </> | ||
| <> | ||
| <span>Goodbye</span> | ||
| <span>world</span> | ||
| </> | ||
| </> | ||
| </div> |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| /** @jsx dom */ | ||
|
|
||
| <div>no fragment is used</div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| /** @jsx dom */ | ||
| dom("div", null, "no fragment is used"); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| /** @jsx dom */ | ||
| /** @jsxFrag DomFrag */ | ||
|
|
||
| <></> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| /** @jsx dom */ | ||
|
|
||
| /** @jsxFrag DomFrag */ | ||
| dom(DomFrag, null); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| /** @jsx dom */ | ||
|
|
||
| <></> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| /** @jsx dom */ | ||
| dom(React.Fragment, null); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "throws": "transform-react-jsx: pragma has been set but pragmafrag has not been set" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should have a test for printing too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added printing test in this commit: eef5e94
Wondering why the test folders are prefixed with XJS instead of JSX. 🤔
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
funny, 😄 that used to be a thing, can ask the team - we should rename in another pr
https://github.com/facebook/jsx/pull/26/files