Skip to content

Commit

Permalink
jsx (#14607)
Browse files Browse the repository at this point in the history
  • Loading branch information
JLHwung committed Jun 21, 2022
1 parent 0c0741d commit 7623b47
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 66 deletions.
119 changes: 80 additions & 39 deletions packages/babel-helper-builder-react-jsx/src/index.ts
Expand Up @@ -9,7 +9,6 @@ import {
isJSXMemberExpression,
isJSXNamespacedName,
isJSXSpreadAttribute,
isLiteral,
isObjectExpression,
isReferenced,
isStringLiteral,
Expand All @@ -24,19 +23,30 @@ import {
thisExpression,
} from "@babel/types";
import annotateAsPure from "@babel/helper-annotate-as-pure";
import type { Visitor } from "@babel/traverse";
import type { NodePath, Visitor } from "@babel/traverse";
import type { PluginPass, File } from "@babel/core";
import type * as t from "@babel/types";

type ElementState = {
tagExpr: any; // tag node,
tagExpr: t.Expression; // tag node,
tagName: string | undefined | null; // raw string tag name,
args: Array<any>; // array of call arguments,
call?: any; // optional call property that can be set to override the call expression returned,
pure: boolean; // true if the element can be marked with a #__PURE__ annotation
callee?: any;
};

export default function (opts) {
const visitor: Visitor = {};
export interface Options {
filter?: (node: t.Node, file: File) => boolean;
pre?: (state: ElementState, file: File) => void;
post?: (state: ElementState, file: File) => void;
compat?: boolean;
pure?: string;
throwIfNamespace?: boolean;
}

export default function (opts: Options) {
const visitor: Visitor<PluginPass> = {};

visitor.JSXNamespacedName = function (path) {
if (opts.throwIfNamespace) {
Expand All @@ -54,22 +64,22 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
};

visitor.JSXElement = {
exit(path, file) {
const callExpr = buildElementCall(path, file);
exit(path, state) {
const callExpr = buildElementCall(path, state.file);
if (callExpr) {
path.replaceWith(inherits(callExpr, path.node));
}
},
};

visitor.JSXFragment = {
exit(path, file) {
exit(path, state) {
if (opts.compat) {
throw path.buildCodeFrameError(
"Fragment tags are only supported in React 16 and up.",
);
}
const callExpr = buildFragmentCall(path, file);
const callExpr = buildFragmentCall(path, state.file);
if (callExpr) {
path.replaceWith(inherits(callExpr, path.node));
}
Expand All @@ -78,7 +88,10 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,

return visitor;

function convertJSXIdentifier(node, parent) {
function convertJSXIdentifier(
node: t.JSXIdentifier | t.JSXMemberExpression | t.JSXNamespacedName,
parent: t.JSXOpeningElement | t.JSXMemberExpression,
): t.ThisExpression | t.StringLiteral | t.MemberExpression | t.Identifier {
if (isJSXIdentifier(node)) {
if (node.name === "this" && isReferenced(node, parent)) {
return thisExpression();
Expand All @@ -101,23 +114,25 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
return stringLiteral(`${node.namespace.name}:${node.name.name}`);
}

// @ts-expect-error
return node;
}

function convertAttributeValue(node) {
function convertAttributeValue(
node: t.JSXAttribute["value"] | t.BooleanLiteral,
) {
if (isJSXExpressionContainer(node)) {
return node.expression;
} else {
return node;
}
}

function convertAttribute(node) {
const value = convertAttributeValue(node.value || booleanLiteral(true));

function convertAttribute(node: t.JSXAttribute | t.JSXSpreadAttribute) {
if (isJSXSpreadAttribute(node)) {
return spreadElement(node.argument);
}
const value = convertAttributeValue(node.value || booleanLiteral(true));

if (isStringLiteral(value) && !isJSXExpressionContainer(node.value)) {
value.value = value.value.replace(/\n\s+/g, " ");
Expand All @@ -127,35 +142,45 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
}

if (isJSXNamespacedName(node.name)) {
// @ts-expect-error Mutating AST nodes
node.name = stringLiteral(
node.name.namespace.name + ":" + node.name.name.name,
);
} else if (isValidIdentifier(node.name.name, false)) {
// @ts-expect-error Mutating AST nodes
node.name.type = "Identifier";
} else {
// @ts-expect-error Mutating AST nodes
node.name = stringLiteral(node.name.name);
}

return inherits(objectProperty(node.name, value), node);
return inherits(
objectProperty(
// @ts-expect-error Mutating AST nodes
node.name,
value,
),
node,
);
}

function buildElementCall(path, file) {
function buildElementCall(path: NodePath<t.JSXElement>, file: File) {
if (opts.filter && !opts.filter(path.node, file)) return;

const openingPath = path.get("openingElement");
openingPath.parent.children = react.buildChildren(openingPath.parent);
// @ts-expect-error mutating AST nodes
path.node.children = react.buildChildren(path.node);

const tagExpr = convertJSXIdentifier(
openingPath.node.name,
openingPath.node,
);
const args = [];
const args: (t.Expression | t.JSXElement | t.JSXFragment)[] = [];

let tagName;
let tagName: string;
if (isIdentifier(tagExpr)) {
tagName = tagExpr.name;
} else if (isLiteral(tagExpr)) {
// @ts-expect-error todo(flow->ts) NullLiteral
} else if (isStringLiteral(tagExpr)) {
tagName = tagExpr.value;
}

Expand All @@ -170,18 +195,23 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
opts.pre(state, file);
}

let attribs = openingPath.node.attributes;
const attribs = openingPath.node.attributes;
let convertedAttributes: t.Expression;
if (attribs.length) {
if (process.env.BABEL_8_BREAKING) {
attribs = objectExpression(attribs.map(convertAttribute));
convertedAttributes = objectExpression(attribs.map(convertAttribute));
} else {
attribs = buildOpeningElementAttributes(attribs, file);
convertedAttributes = buildOpeningElementAttributes(attribs, file);
}
} else {
attribs = nullLiteral();
convertedAttributes = nullLiteral();
}

args.push(attribs, ...path.node.children);
args.push(
convertedAttributes,
// @ts-expect-error JSXExpressionContainer has been transformed by convertAttributeValue
...path.node.children,
);

if (opts.post) {
opts.post(state, file);
Expand All @@ -193,7 +223,10 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
return call;
}

function pushProps(_props, objs) {
function pushProps(
_props: (t.ObjectProperty | t.SpreadElement)[],
objs: t.Expression[],
) {
if (!_props.length) return _props;

objs.push(objectExpression(_props));
Expand All @@ -207,9 +240,12 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
* all prior attributes to an array for later processing.
*/

function buildOpeningElementAttributes(attribs, file) {
let _props = [];
const objs = [];
function buildOpeningElementAttributes(
attribs: (t.JSXAttribute | t.JSXSpreadAttribute)[],
file: File,
): t.Expression {
let _props: (t.ObjectProperty | t.SpreadElement)[] = [];
const objs: t.Expression[] = [];

const { useSpread = false } = file.opts;
if (typeof useSpread !== "boolean") {
Expand Down Expand Up @@ -250,10 +286,11 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
}

pushProps(_props, objs);
let convertedAttribs: t.Expression;

if (objs.length === 1) {
// only one object
attribs = objs[0];
convertedAttribs = objs[0];
} else {
// looks like we have multiple objects
if (!isObjectExpression(objs[0])) {
Expand All @@ -265,20 +302,20 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
: file.addHelper("extends");

// spread it
attribs = callExpression(helper, objs);
convertedAttribs = callExpression(helper, objs);
}

return attribs;
return convertedAttribs;
}

function buildFragmentCall(path, file) {
function buildFragmentCall(path: NodePath<t.JSXFragment>, file: File) {
if (opts.filter && !opts.filter(path.node, file)) return;

const openingPath = path.get("openingElement");
openingPath.parent.children = react.buildChildren(openingPath.parent);
// @ts-expect-error mutating AST nodes
path.node.children = react.buildChildren(path.node);

const args = [];
const tagName = null;
const args: t.Expression[] = [];
const tagName: null = null;
const tagExpr = file.get("jsxFragIdentifier")();

const state: ElementState = {
Expand All @@ -293,7 +330,11 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
}

// no attributes are allowed with <> syntax
args.push(nullLiteral(), ...path.node.children);
args.push(
nullLiteral(),
// @ts-expect-error JSXExpressionContainer has been transformed by convertAttributeValue
...path.node.children,
);

if (opts.post) {
opts.post(state, file);
Expand Down
18 changes: 13 additions & 5 deletions packages/babel-plugin-transform-react-display-name/src/index.ts
Expand Up @@ -2,17 +2,24 @@ import { declare } from "@babel/helper-plugin-utils";
import path from "path";
import { types as t } from "@babel/core";

type ReactCreateClassCall = t.CallExpression & {
arguments: [t.ObjectExpression];
};

export default declare(api => {
api.assertVersion(7);

function addDisplayName(id, call) {
function addDisplayName(id: string, call: ReactCreateClassCall) {
const props = call.arguments[0].properties;
let safe = true;

for (let i = 0; i < props.length; i++) {
const prop = props[i];
if (t.isSpreadElement(prop)) {
continue;
}
const key = t.toComputedKey(prop);
if (t.isLiteral(key, { value: "displayName" })) {
if (t.isStringLiteral(key, { value: "displayName" })) {
safe = false;
break;
}
Expand All @@ -27,9 +34,10 @@ export default declare(api => {

const isCreateClassCallExpression =
t.buildMatchMemberExpression("React.createClass");
const isCreateClassAddon = callee => callee.name === "createReactClass";
const isCreateClassAddon = (callee: t.CallExpression["callee"]) =>
t.isIdentifier(callee, { name: "createReactClass" });

function isCreateClass(node) {
function isCreateClass(node?: t.Node): node is ReactCreateClassCall {
if (!node || !t.isCallExpression(node)) return false;

// not createReactClass nor React.createClass call member object
Expand Down Expand Up @@ -74,7 +82,7 @@ export default declare(api => {
const { node } = path;
if (!isCreateClass(node)) return;

let id;
let id: t.LVal | t.Expression | t.PrivateName | null;

// crawl up the ancestry looking for possible candidates for displayName inference
path.find(function (path) {
Expand Down
Expand Up @@ -5,7 +5,7 @@ import { types as t } from "@babel/core";
export default declare(api => {
api.assertVersion(7);

function hasRefOrSpread(attrs) {
function hasRefOrSpread(attrs: t.JSXOpeningElement["attributes"]) {
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i];
if (t.isJSXSpreadAttribute(attr)) return true;
Expand All @@ -14,7 +14,7 @@ export default declare(api => {
return false;
}

function isJSXAttributeOfName(attr, name) {
function isJSXAttributeOfName(attr: t.JSXAttribute, name: string) {
return (
t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: name })
);
Expand All @@ -23,9 +23,8 @@ export default declare(api => {
const visitor = helper({
filter(node) {
return (
// Regular JSX nodes have an `openingElement`. JSX fragments, however, don't have an
// `openingElement` which causes `node.openingElement.attributes` to throw.
node.openingElement && !hasRefOrSpread(node.openingElement.attributes)
node.type === "JSXElement" &&
!hasRefOrSpread(node.openingElement.attributes)
);
},
pre(state) {
Expand Down
Expand Up @@ -8,7 +8,7 @@ export default declare(api => {
return {
name: "transform-react-jsx-compat",

manipulateOptions(opts, parserOpts) {
manipulateOptions(_, parserOpts) {
parserOpts.plugins.push("jsx");
},

Expand Down

0 comments on commit 7623b47

Please sign in to comment.