Skip to content
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

improve jsx packages typings #14607

Merged
merged 1 commit into from May 27, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
Comment on lines +18 to +20
Copy link
Member

Choose a reason for hiding this comment

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

Is this just to help inference, or is it a bugfix? (If it's a bugfix, please add a test).

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