Improve transform-react-jsx typings (#13820)

* improve transform-react-jsx typings

* rename test fixtures

* remove unreachable sourceSelf check

* update test fixtures

* Update packages/babel-core/src/index.ts

* Update packages/babel-plugin-transform-react-jsx/src/create-plugin.ts

Co-authored-by: Henry Zhu <hi@henryzoo.com>

* Update packages/babel-plugin-transform-react-jsx/src/create-plugin.ts

Co-authored-by: Henry Zhu <hi@henryzoo.com>
This commit is contained in:
Huáng Jùnliàng 2021-10-07 14:01:50 -04:00 committed by GitHub
parent 41325645f8
commit c2f747c9b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 136 additions and 60 deletions

View File

@ -2,6 +2,7 @@ declare const PACKAGE_JSON: { name: string; version: string };
export const version = PACKAGE_JSON.version; export const version = PACKAGE_JSON.version;
export { default as File } from "./transformation/file/file"; export { default as File } from "./transformation/file/file";
export type { default as PluginPass } from "./transformation/plugin-pass";
export { default as buildExternalHelpers } from "./tools/build-external-helpers"; export { default as buildExternalHelpers } from "./tools/build-external-helpers";
export { resolvePlugin, resolvePreset } from "./config/files"; export { resolvePlugin, resolvePreset } from "./config/files";

View File

@ -0,0 +1 @@
var x = <div __self={self}></div>;

View File

@ -0,0 +1,4 @@
{
"throws": "Duplicate __self prop found. You are most likely using the deprecated transform-react-jsx-self Babel plugin. Both __source and __self are automatically set when using the automatic runtime. Please remove transform-react-jsx-source and transform-react-jsx-self from your Babel config.",
"plugins": ["transform-react-jsx-development"]
}

View File

@ -0,0 +1 @@
var x = <div __source={source}></div>;

View File

@ -0,0 +1,4 @@
{
"throws": "Duplicate __source prop found. You are most likely using the deprecated transform-react-jsx-source Babel plugin. Both __source and __self are automatically set when using the automatic runtime. Please remove transform-react-jsx-source and transform-react-jsx-self from your Babel config.",
"plugins": ["transform-react-jsx-development"]
}

View File

@ -1,8 +1,28 @@
import jsx from "@babel/plugin-syntax-jsx"; import jsx from "@babel/plugin-syntax-jsx";
import { declare } from "@babel/helper-plugin-utils"; import { declare } from "@babel/helper-plugin-utils";
import { types as t } from "@babel/core"; import { types as t } from "@babel/core";
import type { PluginPass } from "@babel/core";
import type { NodePath, Visitor } from "@babel/traverse";
import { addNamed, addNamespace, isModule } from "@babel/helper-module-imports"; import { addNamed, addNamespace, isModule } from "@babel/helper-module-imports";
import annotateAsPure from "@babel/helper-annotate-as-pure"; import annotateAsPure from "@babel/helper-annotate-as-pure";
import type {
ArrowFunctionExpression,
CallExpression,
Class,
Expression,
FunctionParent,
Identifier,
JSXAttribute,
JSXElement,
JSXFragment,
JSXOpeningElement,
JSXSpreadAttribute,
MemberExpression,
ObjectExpression,
Program,
} from "@babel/types";
type Diff<T, U> = T extends U ? never : T;
const DEFAULT = { const DEFAULT = {
importSource: "react", importSource: "react",
@ -17,8 +37,10 @@ const JSX_RUNTIME_ANNOTATION_REGEX = /\*?\s*@jsxRuntime\s+([^\s]+)/;
const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/; const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/;
const JSX_FRAG_ANNOTATION_REGEX = /\*?\s*@jsxFrag\s+([^\s]+)/; const JSX_FRAG_ANNOTATION_REGEX = /\*?\s*@jsxFrag\s+([^\s]+)/;
const get = (pass, name) => pass.get(`@babel/plugin-react-jsx/${name}`); const get = (pass: PluginPass, name: string) =>
const set = (pass, name, v) => pass.set(`@babel/plugin-react-jsx/${name}`, v); pass.get(`@babel/plugin-react-jsx/${name}`);
const set = (pass: PluginPass, name: string, v: any) =>
pass.set(`@babel/plugin-react-jsx/${name}`, v);
export default function createPlugin({ name, development }) { export default function createPlugin({ name, development }) {
return declare((api, options) => { return declare((api, options) => {
@ -90,19 +112,8 @@ export default function createPlugin({ name, development }) {
} }
} }
const injectMetaPropertiesVisitor = { const injectMetaPropertiesVisitor: Visitor<PluginPass> = {
JSXOpeningElement(path, state) { JSXOpeningElement(path, state) {
for (const attr of path.get("attributes")) {
if (!attr.isJSXElement()) continue;
const { name } = attr.node.name;
if (name === "__source" || name === "__self") {
throw path.buildCodeFrameError(
`__source and __self should not be defined in props and are reserved for internal usage.`,
);
}
}
const attributes = []; const attributes = [];
if (isThisAllowed(path)) { if (isThisAllowed(path)) {
attributes.push( attributes.push(
@ -144,11 +155,11 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
Program: { Program: {
enter(path, state) { enter(path, state) {
const { file } = state; const { file } = state;
let runtime = RUNTIME_DEFAULT; let runtime: string = RUNTIME_DEFAULT;
let source = IMPORT_SOURCE_DEFAULT; let source: string = IMPORT_SOURCE_DEFAULT;
let pragma = PRAGMA_DEFAULT; let pragma: string = PRAGMA_DEFAULT;
let pragmaFrag = PRAGMA_FRAG_DEFAULT; let pragmaFrag: string = PRAGMA_FRAG_DEFAULT;
let sourceSet = !!options.importSource; let sourceSet = !!options.importSource;
let pragmaSet = !!options.pragma; let pragmaSet = !!options.pragma;
@ -208,7 +219,7 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
); );
} }
const define = (name, id) => const define = (name: string, id: string) =>
set(state, name, createImportLazily(state, path, id, source)); set(state, name, createImportLazily(state, path, id, source));
define("id/jsx", development ? "jsxDEV" : "jsx"); define("id/jsx", development ? "jsxDEV" : "jsx");
@ -280,20 +291,23 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
path.node.value = t.jsxExpressionContainer(path.node.value); path.node.value = t.jsxExpressionContainer(path.node.value);
} }
}, },
}, } as Visitor<PluginPass>,
}; };
// Finds the closest parent function that provides `this`. Specifically, this looks for // Finds the closest parent function that provides `this`. Specifically, this looks for
// the first parent function that isn't an arrow function. // the first parent function that isn't an arrow function.
// //
// Derived from `Scope#getFunctionParent` // Derived from `Scope#getFunctionParent`
function getThisFunctionParent(path) { function getThisFunctionParent(
path: NodePath,
): NodePath<Diff<FunctionParent, ArrowFunctionExpression>> | null {
let scope = path.scope; let scope = path.scope;
do { do {
if ( if (
scope.path.isFunctionParent() && scope.path.isFunctionParent() &&
!scope.path.isArrowFunctionExpression() !scope.path.isArrowFunctionExpression()
) { ) {
// @ts-expect-error ts can not infer scope.path is Diff<FunctionParent, ArrowFunctionExpression>
return scope.path; return scope.path;
} }
} while ((scope = scope.parent)); } while ((scope = scope.parent));
@ -301,12 +315,12 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
} }
// Returns whether the class has specified a superclass. // Returns whether the class has specified a superclass.
function isDerivedClass(classPath) { function isDerivedClass(classPath: NodePath<Class>) {
return classPath.node.superClass !== null; return classPath.node.superClass !== null;
} }
// Returns whether `this` is allowed at given path. // Returns whether `this` is allowed at given path.
function isThisAllowed(path) { function isThisAllowed(path: NodePath<JSXOpeningElement>) {
// This specifically skips arrow functions as they do not rewrite `this`. // This specifically skips arrow functions as they do not rewrite `this`.
const parentMethodOrFunction = getThisFunctionParent(path); const parentMethodOrFunction = getThisFunctionParent(path);
if (parentMethodOrFunction === null) { if (parentMethodOrFunction === null) {
@ -323,10 +337,16 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
return true; return true;
} }
// Now we are in a constructor. If it is a derived class, we do not reference `this`. // Now we are in a constructor. If it is a derived class, we do not reference `this`.
return !isDerivedClass(parentMethodOrFunction.parentPath.parentPath); return !isDerivedClass(
parentMethodOrFunction.parentPath.parentPath as NodePath<Class>,
);
} }
function call(pass, name, args) { function call(
pass: PluginPass,
name: string,
args: CallExpression["arguments"],
) {
const node = t.callExpression(get(pass, `id/${name}`)(), args); const node = t.callExpression(get(pass, `id/${name}`)(), args);
if (PURE_ANNOTATION ?? get(pass, "defaultPure")) annotateAsPure(node); if (PURE_ANNOTATION ?? get(pass, "defaultPure")) annotateAsPure(node);
return node; return node;
@ -337,7 +357,7 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
// from <div key={key} {...props} />. This is an intermediary // from <div key={key} {...props} />. This is an intermediary
// step while we deprecate key spread from props. Afterwards, // step while we deprecate key spread from props. Afterwards,
// we will stop using createElement in the transform. // we will stop using createElement in the transform.
function shouldUseCreateElement(path) { function shouldUseCreateElement(path: NodePath<JSXElement>) {
const openingPath = path.get("openingElement"); const openingPath = path.get("openingElement");
const attributes = openingPath.node.attributes; const attributes = openingPath.node.attributes;
@ -391,7 +411,10 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
} }
} }
function accumulateAttribute(array, attribute) { function accumulateAttribute(
array: ObjectExpression["properties"],
attribute: NodePath<JSXAttribute | JSXSpreadAttribute>,
) {
if (t.isJSXSpreadAttribute(attribute.node)) { if (t.isJSXSpreadAttribute(attribute.node)) {
const arg = attribute.node.argument; const arg = attribute.node.argument;
// Collect properties into props array if spreading object expression // Collect properties into props array if spreading object expression
@ -426,27 +449,34 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
} }
if (t.isJSXNamespacedName(attribute.node.name)) { if (t.isJSXNamespacedName(attribute.node.name)) {
// @ts-expect-error mutating AST
attribute.node.name = t.stringLiteral( attribute.node.name = t.stringLiteral(
attribute.node.name.namespace.name + attribute.node.name.namespace.name +
":" + ":" +
attribute.node.name.name.name, attribute.node.name.name.name,
); );
} else if (t.isValidIdentifier(attribute.node.name.name, false)) { } else if (t.isValidIdentifier(attribute.node.name.name, false)) {
// @ts-expect-error mutating AST
attribute.node.name.type = "Identifier"; attribute.node.name.type = "Identifier";
} else { } else {
// @ts-expect-error mutating AST
attribute.node.name = t.stringLiteral(attribute.node.name.name); attribute.node.name = t.stringLiteral(attribute.node.name.name);
} }
array.push( array.push(
t.inherits( t.inherits(
t.objectProperty(attribute.node.name, value), t.objectProperty(
// @ts-expect-error The attribute.node.name is an Identifier now
attribute.node.name,
value,
),
attribute.node, attribute.node,
), ),
); );
return array; return array;
} }
function buildChildrenProperty(children) { function buildChildrenProperty(children: Expression[]) {
let childrenNode; let childrenNode;
if (children.length === 1) { if (children.length === 1) {
childrenNode = children[0]; childrenNode = children[0];
@ -462,7 +492,7 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
// Builds JSX into: // Builds JSX into:
// Production: React.jsx(type, arguments, key) // Production: React.jsx(type, arguments, key)
// Development: React.jsxDEV(type, arguments, key, isStaticChildren, source, self) // Development: React.jsxDEV(type, arguments, key, isStaticChildren, source, self)
function buildJSXElementCall(path, file) { function buildJSXElementCall(path: NodePath<JSXElement>, file: PluginPass) {
const openingPath = path.get("openingElement"); const openingPath = path.get("openingElement");
const args = [getTag(openingPath)]; const args = [getTag(openingPath)];
@ -507,7 +537,8 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
if (attribsArray.length || children.length) { if (attribsArray.length || children.length) {
attribs = buildJSXOpeningElementAttributes( attribs = buildJSXOpeningElementAttributes(
attribsArray, attribsArray,
file, //@ts-expect-error The children here contains JSXSpreadChild,
// which will be thrown later
children, children,
); );
} else { } else {
@ -536,7 +567,10 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
// Builds props for React.jsx. This function adds children into the props // Builds props for React.jsx. This function adds children into the props
// and ensures that props is always an object // and ensures that props is always an object
function buildJSXOpeningElementAttributes(attribs, file, children) { function buildJSXOpeningElementAttributes(
attribs: NodePath<JSXAttribute | JSXSpreadAttribute>[],
children: Expression[],
) {
const props = attribs.reduce(accumulateAttribute, []); const props = attribs.reduce(accumulateAttribute, []);
// In React.jsx, children is no longer a separate argument, but passed in // In React.jsx, children is no longer a separate argument, but passed in
@ -551,14 +585,25 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
// Builds JSX Fragment <></> into // Builds JSX Fragment <></> into
// Production: React.jsx(type, arguments) // Production: React.jsx(type, arguments)
// Development: React.jsxDEV(type, { children }) // Development: React.jsxDEV(type, { children })
function buildJSXFragmentCall(path, file) { function buildJSXFragmentCall(
path: NodePath<JSXFragment>,
file: PluginPass,
) {
const args = [get(file, "id/fragment")()]; const args = [get(file, "id/fragment")()];
const children = t.react.buildChildren(path.node); const children = t.react.buildChildren(path.node);
args.push( args.push(
t.objectExpression( t.objectExpression(
children.length > 0 ? [buildChildrenProperty(children)] : [], children.length > 0
? [
buildChildrenProperty(
//@ts-expect-error The children here contains JSXSpreadChild,
// which will be thrown later
children,
),
]
: [],
), ),
); );
@ -574,7 +619,10 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
// Builds JSX Fragment <></> into // Builds JSX Fragment <></> into
// React.createElement(React.Fragment, null, ...children) // React.createElement(React.Fragment, null, ...children)
function buildCreateElementFragmentCall(path, file) { function buildCreateElementFragmentCall(
path: NodePath<JSXFragment>,
file: PluginPass,
) {
if (filter && !filter(path.node, file)) return; if (filter && !filter(path.node, file)) return;
return call(file, "createElement", [ return call(file, "createElement", [
@ -587,7 +635,10 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
// Builds JSX into: // Builds JSX into:
// Production: React.createElement(type, arguments, children) // Production: React.createElement(type, arguments, children)
// Development: React.createElement(type, arguments, children, source, self) // Development: React.createElement(type, arguments, children, source, self)
function buildCreateElementCall(path, file) { function buildCreateElementCall(
path: NodePath<JSXElement>,
file: PluginPass,
) {
const openingPath = path.get("openingElement"); const openingPath = path.get("openingElement");
return call(file, "createElement", [ return call(file, "createElement", [
@ -601,7 +652,7 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
]); ]);
} }
function getTag(openingPath) { function getTag(openingPath: NodePath<JSXOpeningElement>) {
const tagExpr = convertJSXIdentifier( const tagExpr = convertJSXIdentifier(
openingPath.node.name, openingPath.node.name,
openingPath.node, openingPath.node,
@ -628,7 +679,11 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
* breaking on spreads, we then push a new object containing * breaking on spreads, we then push a new object containing
* all prior attributes to an array for later processing. * all prior attributes to an array for later processing.
*/ */
function buildCreateElementOpeningElementAttributes(file, path, attribs) { function buildCreateElementOpeningElementAttributes(
file: PluginPass,
path: NodePath<JSXElement>,
attribs: NodePath<JSXAttribute | JSXSpreadAttribute>[],
) {
const runtime = get(file, "runtime"); const runtime = get(file, "runtime");
if (!process.env.BABEL_8_BREAKING) { if (!process.env.BABEL_8_BREAKING) {
if (runtime !== "automatic") { if (runtime !== "automatic") {
@ -704,7 +759,7 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
} }
}); });
function getSource(source, importName) { function getSource(source: string, importName: string) {
switch (importName) { switch (importName) {
case "Fragment": case "Fragment":
return `${source}/${development ? "jsx-dev-runtime" : "jsx-runtime"}`; return `${source}/${development ? "jsx-dev-runtime" : "jsx-runtime"}`;
@ -718,7 +773,12 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
} }
} }
function createImportLazily(pass, path, importName, source) { function createImportLazily(
pass: PluginPass,
path: NodePath<Program>,
importName: string,
source: string,
): () => Identifier | MemberExpression {
return () => { return () => {
const actualSource = getSource(source, importName); const actualSource = getSource(source, importName);
if (isModule(path)) { if (isModule(path)) {
@ -749,20 +809,25 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`,
} }
} }
function toMemberExpression(id) { function toMemberExpression(id: string): Identifier | MemberExpression {
return id return (
.split(".") id
.map(name => t.identifier(name)) .split(".")
.reduce((object, property) => t.memberExpression(object, property)); .map(name => t.identifier(name))
// @ts-expect-error - The Array#reduce does not have a signature
// where the type of initialial value differs from callback return type
.reduce((object, property) => t.memberExpression(object, property))
);
} }
function makeSource(path, state) { function makeSource(path: NodePath, state: PluginPass) {
const location = path.node.loc; const location = path.node.loc;
if (!location) { if (!location) {
// the element was generated and doesn't have location information // the element was generated and doesn't have location information
return path.scope.buildUndefinedNode(); return path.scope.buildUndefinedNode();
} }
// @ts-expect-error todo: avoid mutating PluginPass
if (!state.fileNameIdentifier) { if (!state.fileNameIdentifier) {
const { filename = "" } = state; const { filename = "" } = state;
@ -774,17 +839,25 @@ function makeSource(path, state) {
init: t.stringLiteral(filename), init: t.stringLiteral(filename),
}); });
} }
// @ts-expect-error todo: avoid mutating PluginPass
state.fileNameIdentifier = fileNameIdentifier; state.fileNameIdentifier = fileNameIdentifier;
} }
return makeTrace( return makeTrace(
t.cloneNode(state.fileNameIdentifier), t.cloneNode(
// @ts-expect-error todo: avoid mutating PluginPass
state.fileNameIdentifier,
),
location.start.line, location.start.line,
location.start.column, location.start.column,
); );
} }
function makeTrace(fileNameIdentifier, lineNumber, column0Based) { function makeTrace(
fileNameIdentifier: Identifier,
lineNumber?: number,
column0Based?: number,
) {
const fileLineLiteral = const fileLineLiteral =
lineNumber != null ? t.numericLiteral(lineNumber) : t.nullLiteral(); lineNumber != null ? t.numericLiteral(lineNumber) : t.nullLiteral();
@ -810,7 +883,7 @@ function makeTrace(fileNameIdentifier, lineNumber, column0Based) {
]); ]);
} }
function sourceSelfError(path, name) { function sourceSelfError(path: NodePath, name: string) {
const pluginName = `transform-react-jsx-${name.slice(2)}`; const pluginName = `transform-react-jsx-${name.slice(2)}`;
return path.buildCodeFrameError( return path.buildCodeFrameError(

View File

@ -7,22 +7,14 @@ import cleanJSXElementLiteralChild from "../../utils/react/cleanJSXElementLitera
import type * as t from "../.."; import type * as t from "../..";
type ReturnedChild = type ReturnedChild =
| t.JSXExpressionContainer
| t.JSXSpreadChild | t.JSXSpreadChild
| t.JSXElement | t.JSXElement
| t.JSXFragment | t.JSXFragment
| t.Expression; | t.Expression;
export default function buildChildren(node: { export default function buildChildren(
children: ReadonlyArray< node: t.JSXElement | t.JSXFragment,
| t.JSXText ): ReturnedChild[] {
| t.JSXExpressionContainer
| t.JSXSpreadChild
| t.JSXElement
| t.JSXFragment
| t.JSXEmptyExpression
>;
}): ReturnedChild[] {
const elements = []; const elements = [];
for (let i = 0; i < node.children.length; i++) { for (let i = 0; i < node.children.length; i++) {