diff --git a/packages/babel-core/src/index.ts b/packages/babel-core/src/index.ts index f938b3c7d3d4..8abd7cc17489 100644 --- a/packages/babel-core/src/index.ts +++ b/packages/babel-core/src/index.ts @@ -2,6 +2,7 @@ declare const PACKAGE_JSON: { name: string; version: string }; export const version = PACKAGE_JSON.version; 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 { resolvePlugin, resolvePreset } from "./config/files"; diff --git a/packages/babel-plugin-transform-react-jsx-development/test/fixtures/cross-platform/disallow-__self-as-jsx-attribute/input.js b/packages/babel-plugin-transform-react-jsx-development/test/fixtures/cross-platform/disallow-__self-as-jsx-attribute/input.js new file mode 100644 index 000000000000..b7438e2124c8 --- /dev/null +++ b/packages/babel-plugin-transform-react-jsx-development/test/fixtures/cross-platform/disallow-__self-as-jsx-attribute/input.js @@ -0,0 +1 @@ +var x =
; diff --git a/packages/babel-plugin-transform-react-jsx-development/test/fixtures/cross-platform/disallow-__self-as-jsx-attribute/options.json b/packages/babel-plugin-transform-react-jsx-development/test/fixtures/cross-platform/disallow-__self-as-jsx-attribute/options.json new file mode 100644 index 000000000000..09c46c7f6b2e --- /dev/null +++ b/packages/babel-plugin-transform-react-jsx-development/test/fixtures/cross-platform/disallow-__self-as-jsx-attribute/options.json @@ -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"] +} diff --git a/packages/babel-plugin-transform-react-jsx-development/test/fixtures/cross-platform/disallow-__source-as-jsx-attribute/input.js b/packages/babel-plugin-transform-react-jsx-development/test/fixtures/cross-platform/disallow-__source-as-jsx-attribute/input.js new file mode 100644 index 000000000000..6c7b703aa861 --- /dev/null +++ b/packages/babel-plugin-transform-react-jsx-development/test/fixtures/cross-platform/disallow-__source-as-jsx-attribute/input.js @@ -0,0 +1 @@ +var x =
; diff --git a/packages/babel-plugin-transform-react-jsx-development/test/fixtures/cross-platform/disallow-__source-as-jsx-attribute/options.json b/packages/babel-plugin-transform-react-jsx-development/test/fixtures/cross-platform/disallow-__source-as-jsx-attribute/options.json new file mode 100644 index 000000000000..1fcae672734d --- /dev/null +++ b/packages/babel-plugin-transform-react-jsx-development/test/fixtures/cross-platform/disallow-__source-as-jsx-attribute/options.json @@ -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"] +} diff --git a/packages/babel-plugin-transform-react-jsx/src/create-plugin.ts b/packages/babel-plugin-transform-react-jsx/src/create-plugin.ts index 40974d740bc8..d4afe577f25c 100644 --- a/packages/babel-plugin-transform-react-jsx/src/create-plugin.ts +++ b/packages/babel-plugin-transform-react-jsx/src/create-plugin.ts @@ -1,8 +1,28 @@ import jsx from "@babel/plugin-syntax-jsx"; import { declare } from "@babel/helper-plugin-utils"; 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 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 extends U ? never : T; const DEFAULT = { 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_FRAG_ANNOTATION_REGEX = /\*?\s*@jsxFrag\s+([^\s]+)/; -const get = (pass, name) => pass.get(`@babel/plugin-react-jsx/${name}`); -const set = (pass, name, v) => pass.set(`@babel/plugin-react-jsx/${name}`, v); +const get = (pass: PluginPass, name: string) => + 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 }) { return declare((api, options) => { @@ -90,19 +112,8 @@ export default function createPlugin({ name, development }) { } } - const injectMetaPropertiesVisitor = { + const injectMetaPropertiesVisitor: Visitor = { 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 = []; if (isThisAllowed(path)) { attributes.push( @@ -144,11 +155,11 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`, Program: { enter(path, state) { const { file } = state; - let runtime = RUNTIME_DEFAULT; + let runtime: string = RUNTIME_DEFAULT; - let source = IMPORT_SOURCE_DEFAULT; - let pragma = PRAGMA_DEFAULT; - let pragmaFrag = PRAGMA_FRAG_DEFAULT; + let source: string = IMPORT_SOURCE_DEFAULT; + let pragma: string = PRAGMA_DEFAULT; + let pragmaFrag: string = PRAGMA_FRAG_DEFAULT; let sourceSet = !!options.importSource; 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)); 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); } }, - }, + } as Visitor, }; // Finds the closest parent function that provides `this`. Specifically, this looks for // the first parent function that isn't an arrow function. // // Derived from `Scope#getFunctionParent` - function getThisFunctionParent(path) { + function getThisFunctionParent( + path: NodePath, + ): NodePath> | null { let scope = path.scope; do { if ( scope.path.isFunctionParent() && !scope.path.isArrowFunctionExpression() ) { + // @ts-expect-error ts can not infer scope.path is Diff return scope.path; } } 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. - function isDerivedClass(classPath) { + function isDerivedClass(classPath: NodePath) { return classPath.node.superClass !== null; } // Returns whether `this` is allowed at given path. - function isThisAllowed(path) { + function isThisAllowed(path: NodePath) { // This specifically skips arrow functions as they do not rewrite `this`. const parentMethodOrFunction = getThisFunctionParent(path); if (parentMethodOrFunction === null) { @@ -323,10 +337,16 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`, return true; } // 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, + ); } - function call(pass, name, args) { + function call( + pass: PluginPass, + name: string, + args: CallExpression["arguments"], + ) { const node = t.callExpression(get(pass, `id/${name}`)(), args); if (PURE_ANNOTATION ?? get(pass, "defaultPure")) annotateAsPure(node); return node; @@ -337,7 +357,7 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`, // from
. This is an intermediary // step while we deprecate key spread from props. Afterwards, // we will stop using createElement in the transform. - function shouldUseCreateElement(path) { + function shouldUseCreateElement(path: NodePath) { const openingPath = path.get("openingElement"); 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, + ) { if (t.isJSXSpreadAttribute(attribute.node)) { const arg = attribute.node.argument; // 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)) { + // @ts-expect-error mutating AST attribute.node.name = t.stringLiteral( attribute.node.name.namespace.name + ":" + attribute.node.name.name.name, ); } else if (t.isValidIdentifier(attribute.node.name.name, false)) { + // @ts-expect-error mutating AST attribute.node.name.type = "Identifier"; } else { + // @ts-expect-error mutating AST attribute.node.name = t.stringLiteral(attribute.node.name.name); } array.push( 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, ), ); return array; } - function buildChildrenProperty(children) { + function buildChildrenProperty(children: Expression[]) { let childrenNode; if (children.length === 1) { childrenNode = children[0]; @@ -462,7 +492,7 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`, // Builds JSX into: // Production: React.jsx(type, arguments, key) // Development: React.jsxDEV(type, arguments, key, isStaticChildren, source, self) - function buildJSXElementCall(path, file) { + function buildJSXElementCall(path: NodePath, file: PluginPass) { const openingPath = path.get("openingElement"); const args = [getTag(openingPath)]; @@ -507,7 +537,8 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`, if (attribsArray.length || children.length) { attribs = buildJSXOpeningElementAttributes( attribsArray, - file, + //@ts-expect-error The children here contains JSXSpreadChild, + // which will be thrown later children, ); } 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 // and ensures that props is always an object - function buildJSXOpeningElementAttributes(attribs, file, children) { + function buildJSXOpeningElementAttributes( + attribs: NodePath[], + children: Expression[], + ) { const props = attribs.reduce(accumulateAttribute, []); // 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 // Production: React.jsx(type, arguments) // Development: React.jsxDEV(type, { children }) - function buildJSXFragmentCall(path, file) { + function buildJSXFragmentCall( + path: NodePath, + file: PluginPass, + ) { const args = [get(file, "id/fragment")()]; const children = t.react.buildChildren(path.node); args.push( 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 // React.createElement(React.Fragment, null, ...children) - function buildCreateElementFragmentCall(path, file) { + function buildCreateElementFragmentCall( + path: NodePath, + file: PluginPass, + ) { if (filter && !filter(path.node, file)) return; return call(file, "createElement", [ @@ -587,7 +635,10 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`, // Builds JSX into: // Production: React.createElement(type, arguments, children) // Development: React.createElement(type, arguments, children, source, self) - function buildCreateElementCall(path, file) { + function buildCreateElementCall( + path: NodePath, + file: PluginPass, + ) { const openingPath = path.get("openingElement"); return call(file, "createElement", [ @@ -601,7 +652,7 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`, ]); } - function getTag(openingPath) { + function getTag(openingPath: NodePath) { const tagExpr = convertJSXIdentifier( openingPath.node.name, 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 * all prior attributes to an array for later processing. */ - function buildCreateElementOpeningElementAttributes(file, path, attribs) { + function buildCreateElementOpeningElementAttributes( + file: PluginPass, + path: NodePath, + attribs: NodePath[], + ) { const runtime = get(file, "runtime"); if (!process.env.BABEL_8_BREAKING) { 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) { case "Fragment": 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, + importName: string, + source: string, + ): () => Identifier | MemberExpression { return () => { const actualSource = getSource(source, importName); if (isModule(path)) { @@ -749,20 +809,25 @@ You can set \`throwIfNamespace: false\` to bypass this warning.`, } } -function toMemberExpression(id) { - return id - .split(".") - .map(name => t.identifier(name)) - .reduce((object, property) => t.memberExpression(object, property)); +function toMemberExpression(id: string): Identifier | MemberExpression { + return ( + id + .split(".") + .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; if (!location) { // the element was generated and doesn't have location information return path.scope.buildUndefinedNode(); } + // @ts-expect-error todo: avoid mutating PluginPass if (!state.fileNameIdentifier) { const { filename = "" } = state; @@ -774,17 +839,25 @@ function makeSource(path, state) { init: t.stringLiteral(filename), }); } + // @ts-expect-error todo: avoid mutating PluginPass state.fileNameIdentifier = fileNameIdentifier; } return makeTrace( - t.cloneNode(state.fileNameIdentifier), + t.cloneNode( + // @ts-expect-error todo: avoid mutating PluginPass + state.fileNameIdentifier, + ), location.start.line, location.start.column, ); } -function makeTrace(fileNameIdentifier, lineNumber, column0Based) { +function makeTrace( + fileNameIdentifier: Identifier, + lineNumber?: number, + column0Based?: number, +) { const fileLineLiteral = 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)}`; return path.buildCodeFrameError( diff --git a/packages/babel-types/src/builders/react/buildChildren.ts b/packages/babel-types/src/builders/react/buildChildren.ts index 0893a8bb301d..90f9328853e6 100644 --- a/packages/babel-types/src/builders/react/buildChildren.ts +++ b/packages/babel-types/src/builders/react/buildChildren.ts @@ -7,22 +7,14 @@ import cleanJSXElementLiteralChild from "../../utils/react/cleanJSXElementLitera import type * as t from "../.."; type ReturnedChild = - | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXElement | t.JSXFragment | t.Expression; -export default function buildChildren(node: { - children: ReadonlyArray< - | t.JSXText - | t.JSXExpressionContainer - | t.JSXSpreadChild - | t.JSXElement - | t.JSXFragment - | t.JSXEmptyExpression - >; -}): ReturnedChild[] { +export default function buildChildren( + node: t.JSXElement | t.JSXFragment, +): ReturnedChild[] { const elements = []; for (let i = 0; i < node.children.length; i++) {