Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/babel-generator/src/generators/jsx.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,25 @@ export function JSXClosingElement(node: Object) {
}

export function JSXEmptyExpression() {}

export function JSXFragment(node: Object) {
Copy link
Copy Markdown
Member

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

Copy link
Copy Markdown
Contributor Author

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. 🤔

Copy link
Copy Markdown
Member

@hzoo hzoo Oct 30, 2017

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

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(">");
}
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" ] }
10 changes: 5 additions & 5 deletions packages/babel-helper-builder-react-jsx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")({
Expand All @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true if transforming in React < 0.12 compat mode?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 plugin-transform-react-jsx-compat transform.

});
```
49 changes: 46 additions & 3 deletions packages/babel-helper-builder-react-jsx/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Fragment name is also only exported in 16.1, even though it's technically supported in 16.0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by a peerDep on react @babel/react?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean should @babel/preset-react have a peerDep on react or react-dom?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, good question. Would non-React library authors use @babel/preset-react? It seems like enabling @babel/preset-react and providing pragmas allows Babel to compile to alternative JSX rendering libraries.

Copy link
Copy Markdown
Member

@hzoo hzoo Oct 30, 2017

Choose a reason for hiding this comment

The 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) {
Expand Down Expand Up @@ -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
Expand Up @@ -23,6 +23,7 @@ export default function({ types: t }) {
);
}
},
compat: true,
}),
};
}
73 changes: 72 additions & 1 deletion packages/babel-plugin-transform-react-jsx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}]
]
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably just throws in React 15.4 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that something to address in this PR?

Copy link
Copy Markdown
Member

@Jessidhia Jessidhia Nov 2, 2017

Choose a reason for hiding this comment

The 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`.
Expand Down
70 changes: 52 additions & 18 deletions packages/babel-plugin-transform-react-jsx/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks suspiciously like t.buildMatchMemberExpression

(we really need to document that in handbook 🤔)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, nevermind, it's its inverse 😆


const visitor = helper({
pre(state) {
Expand All @@ -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) {
Expand Down
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"
}
Loading