mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
f512537754
This babel transform is a fork of the @babel/plugin-transform-react-jsx transform and is for experimentation purposes only. We don't plan to own this code in the future, and we will upstream this to Babel at some point once we've proven out the concept.
As per the RFC to simplify element creation, we want to change the JSX transform from targeting React.createElement(type, props, children) to React.jsx(type, props, key). This modifies the existing @babel/plugin-transform-react-jsx (and helper) babel plugin to support React.jsx and React.jsxDEV.
The main differences between React.jsx/React.jsxDEV and React.createElement are:
1.) key is now passed as an explicit argument rather than through props
3.) children are now passed through props rather than as an explicit argument
4.) props must always be an object
5.) __source and and __self are now passed as separate arguments into React.jsxDEV rather than through props
Part of the rationale for this change is that we want to deprecate key spread through props because this is an expensive dynamic comparison operation. We want users instead always explicitly pass key as a prop. However, in the interim, we need a way to distinguish between <div {...props} key={foo} /> and <div key={foo} {...props} />. Therefore, until we completely deprecate key spreading, we will use React.createElement to transform <div {...props} key="Hi" /> and React.jsx to transform everything else.
612 lines
17 KiB
JavaScript
612 lines
17 KiB
JavaScript
// MIT License
|
|
|
|
// Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
|
|
|
// Permission is hereby granted, free of charge, to any person obtaining
|
|
// a copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
|
// permit persons to whom the Software is furnished to do so, subject to
|
|
// the following conditions:
|
|
|
|
// The above copyright notice and this permission notice shall be
|
|
// included in all copies or substantial portions of the Software.
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
// © 2019 GitHub, Inc.
|
|
'use strict';
|
|
|
|
const esutils = require('esutils');
|
|
|
|
function helper(babel, opts) {
|
|
const {types: t} = babel;
|
|
|
|
const visitor = {};
|
|
|
|
visitor.JSXNamespacedName = function(path, state) {
|
|
const throwIfNamespace =
|
|
state.opts.throwIfNamespace === undefined
|
|
? true
|
|
: !!state.opts.throwIfNamespace;
|
|
if (throwIfNamespace) {
|
|
throw path.buildCodeFrameError(
|
|
`Namespace tags are not supported by default. React's JSX doesn't support namespace tags. \
|
|
You can turn on the 'throwIfNamespace' flag to bypass this warning.`,
|
|
);
|
|
}
|
|
};
|
|
|
|
visitor.JSXSpreadChild = function(path) {
|
|
throw path.buildCodeFrameError(
|
|
'Spread children are not supported in React.',
|
|
);
|
|
};
|
|
|
|
visitor.JSXElement = {
|
|
exit(path, file) {
|
|
let callExpr;
|
|
if (file.opts.useCreateElement || shouldUseCreateElement(path)) {
|
|
callExpr = buildCreateElementCall(path, file);
|
|
} else {
|
|
callExpr = buildJSXElementCall(path, file);
|
|
}
|
|
|
|
if (callExpr) {
|
|
path.replaceWith(t.inherits(callExpr, path.node));
|
|
}
|
|
},
|
|
};
|
|
|
|
visitor.JSXFragment = {
|
|
exit(path, file) {
|
|
if (opts.compat) {
|
|
throw path.buildCodeFrameError(
|
|
'Fragment tags are only supported in React 16 and up.',
|
|
);
|
|
}
|
|
let callExpr;
|
|
if (file.opts.useCreateElement) {
|
|
callExpr = buildCreateElementFragmentCall(path, file);
|
|
} else {
|
|
callExpr = buildJSXFragmentCall(path, file);
|
|
}
|
|
|
|
if (callExpr) {
|
|
path.replaceWith(t.inherits(callExpr, path.node));
|
|
}
|
|
},
|
|
};
|
|
|
|
return visitor;
|
|
|
|
function convertJSXIdentifier(node, parent) {
|
|
if (t.isJSXIdentifier(node)) {
|
|
if (node.name === 'this' && t.isReferenced(node, parent)) {
|
|
return t.thisExpression();
|
|
} else if (esutils.keyword.isIdentifierNameES6(node.name)) {
|
|
node.type = 'Identifier';
|
|
} else {
|
|
return t.stringLiteral(node.name);
|
|
}
|
|
} else if (t.isJSXMemberExpression(node)) {
|
|
return t.memberExpression(
|
|
convertJSXIdentifier(node.object, node),
|
|
convertJSXIdentifier(node.property, node),
|
|
);
|
|
} else if (t.isJSXNamespacedName(node)) {
|
|
/**
|
|
* If there is flag "throwIfNamespace"
|
|
* print XMLNamespace like string literal
|
|
*/
|
|
return t.stringLiteral(`${node.namespace.name}:${node.name.name}`);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
function convertAttributeValue(node) {
|
|
if (t.isJSXExpressionContainer(node)) {
|
|
return node.expression;
|
|
} else {
|
|
return node;
|
|
}
|
|
}
|
|
|
|
function convertAttribute(node) {
|
|
const value = convertAttributeValue(node.value || t.booleanLiteral(true));
|
|
|
|
if (t.isStringLiteral(value) && !t.isJSXExpressionContainer(node.value)) {
|
|
value.value = value.value.replace(/\n\s+/g, ' ');
|
|
|
|
// "raw" JSXText should not be used from a StringLiteral because it needs to be escaped.
|
|
if (value.extra && value.extra.raw) {
|
|
delete value.extra.raw;
|
|
}
|
|
}
|
|
|
|
if (t.isJSXNamespacedName(node.name)) {
|
|
node.name = t.stringLiteral(
|
|
node.name.namespace.name + ':' + node.name.name.name,
|
|
);
|
|
} else if (esutils.keyword.isIdentifierNameES6(node.name.name)) {
|
|
node.name.type = 'Identifier';
|
|
} else {
|
|
node.name = t.stringLiteral(node.name.name);
|
|
}
|
|
|
|
return t.inherits(t.objectProperty(node.name, value), node);
|
|
}
|
|
|
|
// We want to use React.createElement, even in the case of
|
|
// jsx, for <div {...props} key={key} /> to distinguish it
|
|
// from <div key={key} {...props} />. This is an intermediary
|
|
// step while we deprecate key spread from props. Afterwards,
|
|
// we will remove createElement entirely
|
|
function shouldUseCreateElement(path) {
|
|
const openingPath = path.get('openingElement');
|
|
const attributes = openingPath.node.attributes;
|
|
|
|
let seenPropsSpread = false;
|
|
for (let i = 0; i < attributes.length; i++) {
|
|
const attr = attributes[i];
|
|
if (
|
|
seenPropsSpread &&
|
|
t.isJSXAttribute(attr) &&
|
|
attr.name.name === 'key'
|
|
) {
|
|
return true;
|
|
} else if (t.isJSXSpreadAttribute(attr)) {
|
|
seenPropsSpread = true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Builds JSX into:
|
|
// Production: React.jsx(type, arguments, key)
|
|
// Development: React.jsxDEV(type, arguments, key, isStaticChildren, source, self)
|
|
function buildJSXElementCall(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 tagExpr = convertJSXIdentifier(
|
|
openingPath.node.name,
|
|
openingPath.node,
|
|
);
|
|
const args = [];
|
|
|
|
let tagName;
|
|
if (t.isIdentifier(tagExpr)) {
|
|
tagName = tagExpr.name;
|
|
} else if (t.isLiteral(tagExpr)) {
|
|
tagName = tagExpr.value;
|
|
}
|
|
|
|
const state = {
|
|
tagExpr: tagExpr,
|
|
tagName: tagName,
|
|
args: args,
|
|
};
|
|
|
|
if (opts.pre) {
|
|
opts.pre(state, file);
|
|
}
|
|
|
|
let attribs = [];
|
|
let key;
|
|
let source;
|
|
let self;
|
|
|
|
// for React.jsx, key, __source (dev), and __self (dev) is passed in as
|
|
// a separate argument rather than in the args object. We go through the
|
|
// props and filter out these three keywords so we can pass them in
|
|
// as separate arguments later
|
|
for (let i = 0; i < openingPath.node.attributes.length; i++) {
|
|
const attr = openingPath.node.attributes[i];
|
|
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
|
|
if (attr.name.name === 'key') {
|
|
key = convertAttribute(attr).value;
|
|
} else if (attr.name.name === '__source') {
|
|
source = convertAttribute(attr).value;
|
|
} else if (attr.name.name === '__self') {
|
|
self = convertAttribute(attr).value;
|
|
} else {
|
|
attribs.push(attr);
|
|
}
|
|
} else {
|
|
attribs.push(attr);
|
|
}
|
|
}
|
|
|
|
if (attribs.length || path.node.children.length) {
|
|
attribs = buildJSXOpeningElementAttributes(
|
|
attribs,
|
|
file,
|
|
path.node.children,
|
|
);
|
|
} else {
|
|
// attributes should never be null
|
|
attribs = t.objectExpression([]);
|
|
}
|
|
|
|
args.push(attribs);
|
|
|
|
if (!file.opts.development) {
|
|
if (key !== undefined) {
|
|
args.push(key);
|
|
}
|
|
} else {
|
|
// isStaticChildren, __source, and __self are only used in development
|
|
args.push(
|
|
key === undefined ? t.identifier('undefined') : key,
|
|
t.booleanLiteral(path.node.children.length > 1),
|
|
source === undefined ? t.identifier('undefined') : source,
|
|
self === undefined ? t.identifier('undefined') : self,
|
|
);
|
|
}
|
|
|
|
if (opts.post) {
|
|
opts.post(state, file);
|
|
}
|
|
return (
|
|
state.call ||
|
|
t.callExpression(
|
|
path.node.children.length > 1 ? state.staticCallee : state.callee,
|
|
args,
|
|
)
|
|
);
|
|
}
|
|
|
|
// Builds props for React.jsx. This function adds children into the props
|
|
// and ensures that props is always an object
|
|
function buildJSXOpeningElementAttributes(attribs, file, children) {
|
|
let _props = [];
|
|
const objs = [];
|
|
|
|
const useBuiltIns = file.opts.useBuiltIns || false;
|
|
if (typeof useBuiltIns !== 'boolean') {
|
|
throw new Error(
|
|
'transform-react-jsx currently only accepts a boolean option for ' +
|
|
'useBuiltIns (defaults to false)',
|
|
);
|
|
}
|
|
|
|
while (attribs.length) {
|
|
const prop = attribs.shift();
|
|
if (t.isJSXSpreadAttribute(prop)) {
|
|
_props = pushProps(_props, objs);
|
|
objs.push(prop.argument);
|
|
} else {
|
|
_props.push(convertAttribute(prop));
|
|
}
|
|
}
|
|
|
|
// In React.JSX, children is no longer a separate argument, but passed in
|
|
// through the argument object
|
|
if (children && children.length > 0) {
|
|
if (children.length === 1) {
|
|
_props.push(t.objectProperty(t.identifier('children'), children[0]));
|
|
} else {
|
|
_props.push(
|
|
t.objectProperty(
|
|
t.identifier('children'),
|
|
t.arrayExpression(children),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
pushProps(_props, objs);
|
|
|
|
if (objs.length === 1) {
|
|
// only one object
|
|
if (!t.isObjectExpression(objs[0])) {
|
|
// if the prop object isn't an object, use Object.assign or _extends
|
|
// to ensure that the prop will always be an object (as opposed to a variable
|
|
// that could be null at some point)
|
|
const expressionHelper = useBuiltIns
|
|
? t.memberExpression(t.identifier('Object'), t.identifier('assign'))
|
|
: file.addHelper('extends');
|
|
|
|
attribs = t.callExpression(expressionHelper, [
|
|
t.objectExpression([]),
|
|
objs[0],
|
|
]);
|
|
} else {
|
|
attribs = objs[0];
|
|
}
|
|
} else {
|
|
// looks like we have multiple objects
|
|
if (!t.isObjectExpression(objs[0])) {
|
|
objs.unshift(t.objectExpression([]));
|
|
}
|
|
|
|
const expressionHelper = useBuiltIns
|
|
? t.memberExpression(t.identifier('Object'), t.identifier('assign'))
|
|
: file.addHelper('extends');
|
|
|
|
// spread it
|
|
attribs = t.callExpression(expressionHelper, objs);
|
|
}
|
|
|
|
return attribs;
|
|
}
|
|
|
|
// Builds JSX Fragment <></> into
|
|
// Production: React.jsx(type, arguments)
|
|
// Development: React.jsxDEV(type, { children})
|
|
function buildJSXFragmentCall(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 = {
|
|
tagExpr: tagExpr,
|
|
tagName: tagName,
|
|
args: args,
|
|
};
|
|
|
|
if (opts.pre) {
|
|
opts.pre(state, file);
|
|
}
|
|
|
|
let childrenNode;
|
|
if (path.node.children.length > 0) {
|
|
if (path.node.children.length === 1) {
|
|
childrenNode = path.node.children[0];
|
|
} else {
|
|
childrenNode = t.arrayExpression(path.node.children);
|
|
}
|
|
}
|
|
|
|
args.push(
|
|
t.objectExpression(
|
|
childrenNode !== undefined
|
|
? [t.objectProperty(t.identifier('children'), childrenNode)]
|
|
: [],
|
|
),
|
|
);
|
|
|
|
if (file.opts.development) {
|
|
args.push(
|
|
t.identifier('undefined'),
|
|
t.booleanLiteral(path.node.children.length > 1),
|
|
);
|
|
}
|
|
|
|
if (opts.post) {
|
|
opts.post(state, file);
|
|
}
|
|
|
|
return (
|
|
state.call ||
|
|
t.callExpression(
|
|
path.node.children.length > 1 ? state.staticCallee : state.callee,
|
|
args,
|
|
)
|
|
);
|
|
}
|
|
|
|
// Builds JSX into:
|
|
// Production: React.createElement(type, arguments, children)
|
|
// Development: React.createElement(type, arguments, children, source, self)
|
|
function buildCreateElementCall(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 tagExpr = convertJSXIdentifier(
|
|
openingPath.node.name,
|
|
openingPath.node,
|
|
);
|
|
const args = [];
|
|
|
|
let tagName;
|
|
if (t.isIdentifier(tagExpr)) {
|
|
tagName = tagExpr.name;
|
|
} else if (t.isLiteral(tagExpr)) {
|
|
tagName = tagExpr.value;
|
|
}
|
|
|
|
const state = {
|
|
tagExpr: tagExpr,
|
|
tagName: tagName,
|
|
args: args,
|
|
};
|
|
|
|
if (opts.pre) {
|
|
opts.pre(state, file);
|
|
}
|
|
|
|
let attribs = openingPath.node.attributes;
|
|
if (attribs.length) {
|
|
attribs = buildCreateElementOpeningElementAttributes(attribs, file);
|
|
} else {
|
|
attribs = t.nullLiteral();
|
|
}
|
|
|
|
args.push(attribs, ...path.node.children);
|
|
|
|
if (opts.post) {
|
|
opts.post(state, file);
|
|
}
|
|
|
|
return state.call || t.callExpression(state.oldCallee, args);
|
|
}
|
|
|
|
function pushProps(_props, objs) {
|
|
if (!_props.length) {
|
|
return _props;
|
|
}
|
|
|
|
objs.push(t.objectExpression(_props));
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* The logic for this is quite terse. It's because we need to
|
|
* support spread elements. We loop over all attributes,
|
|
* breaking on spreads, we then push a new object containing
|
|
* all prior attributes to an array for later processing.
|
|
*/
|
|
function buildCreateElementOpeningElementAttributes(attribs, file) {
|
|
let _props = [];
|
|
const objs = [];
|
|
|
|
const useBuiltIns = file.opts.useBuiltIns || false;
|
|
if (typeof useBuiltIns !== 'boolean') {
|
|
throw new Error(
|
|
'transform-react-jsx currently only accepts a boolean option for ' +
|
|
'useBuiltIns (defaults to false)',
|
|
);
|
|
}
|
|
|
|
while (attribs.length) {
|
|
const prop = attribs.shift();
|
|
if (t.isJSXSpreadAttribute(prop)) {
|
|
_props = pushProps(_props, objs);
|
|
objs.push(prop.argument);
|
|
} else {
|
|
const attr = convertAttribute(prop);
|
|
_props.push(attr);
|
|
}
|
|
}
|
|
|
|
pushProps(_props, objs);
|
|
|
|
if (objs.length === 1) {
|
|
// only one object
|
|
attribs = objs[0];
|
|
} else {
|
|
// looks like we have multiple objects
|
|
if (!t.isObjectExpression(objs[0])) {
|
|
objs.unshift(t.objectExpression([]));
|
|
}
|
|
|
|
const expressionHelper = useBuiltIns
|
|
? t.memberExpression(t.identifier('Object'), t.identifier('assign'))
|
|
: file.addHelper('extends');
|
|
|
|
// spread it
|
|
attribs = t.callExpression(expressionHelper, objs);
|
|
}
|
|
|
|
return attribs;
|
|
}
|
|
|
|
function buildCreateElementFragmentCall(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 = {
|
|
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);
|
|
}
|
|
|
|
return state.call || t.callExpression(state.oldCallee, args);
|
|
}
|
|
}
|
|
|
|
module.exports = function(babel) {
|
|
const {types: t} = babel;
|
|
|
|
const createIdentifierParser = id => () => {
|
|
return id
|
|
.split('.')
|
|
.map(name => t.identifier(name))
|
|
.reduce((object, property) => t.memberExpression(object, property));
|
|
};
|
|
|
|
const visitor = helper(babel, {
|
|
pre(state) {
|
|
const tagName = state.tagName;
|
|
const args = state.args;
|
|
if (t.react.isCompatTag(tagName)) {
|
|
args.push(t.stringLiteral(tagName));
|
|
} else {
|
|
args.push(state.tagExpr);
|
|
}
|
|
},
|
|
|
|
post(state, pass) {
|
|
state.callee = pass.get('jsxIdentifier')();
|
|
state.staticCallee = pass.get('jsxStaticIdentifier')();
|
|
state.oldCallee = pass.get('oldJSXIdentifier')();
|
|
},
|
|
});
|
|
|
|
visitor.Program = {
|
|
enter(path, state) {
|
|
state.set(
|
|
'oldJSXIdentifier',
|
|
createIdentifierParser('React.createElement'),
|
|
);
|
|
state.set(
|
|
'jsxIdentifier',
|
|
createIdentifierParser(
|
|
state.opts.development ? 'React.jsxDEV' : 'React.jsx',
|
|
),
|
|
);
|
|
state.set(
|
|
'jsxStaticIdentifier',
|
|
createIdentifierParser(
|
|
state.opts.development ? 'React.jsxDEV' : 'React.jsxs',
|
|
),
|
|
);
|
|
state.set('jsxFragIdentifier', createIdentifierParser('React.Fragment'));
|
|
},
|
|
};
|
|
|
|
visitor.JSXAttribute = function(path) {
|
|
if (t.isJSXElement(path.node.value)) {
|
|
path.node.value = t.jsxExpressionContainer(path.node.value);
|
|
}
|
|
};
|
|
|
|
return {
|
|
name: 'transform-react-jsx',
|
|
visitor,
|
|
};
|
|
};
|