diff --git a/packages/babel-preset-gatsby/.babelrc b/packages/babel-preset-gatsby/.babelrc index ac0ad292bb087..841d89afa9a35 100644 --- a/packages/babel-preset-gatsby/.babelrc +++ b/packages/babel-preset-gatsby/.babelrc @@ -1,3 +1,9 @@ { - "presets": [["babel-preset-gatsby-package"]] + "presets": [["babel-preset-gatsby-package"]], + "overrides": [ + { + "test": ["**/*.ts", "**/*.tsx"], + "plugins": [["@babel/plugin-transform-typescript", { "isTSX": true }]] + } + ] } diff --git a/packages/babel-preset-gatsby/README.md b/packages/babel-preset-gatsby/README.md index 7de679dfb9f64..e5f47e8d417c2 100644 --- a/packages/babel-preset-gatsby/README.md +++ b/packages/babel-preset-gatsby/README.md @@ -15,6 +15,7 @@ For more information on how to customize the Babel configuration of your Gatsby - [`babel-plugin-transform-react-remove-prop-types`](https://github.com/oliviertassinari/babel-plugin-transform-react-remove-prop-types) - [`@babel/plugin-proposal-nullish-coalescing-operator`](https://babeljs.io/docs/en/babel-plugin-proposal-nullish-coalescing-operator) - [`@babel/plugin-proposal-optional-chaining`](https://babeljs.io/docs/en/babel-plugin-proposal-optional-chaining) +- [`babel-plugin-optimize-hook-destructuring`](https://www.github.com/gatsbyjs/gatsby/packages/babel-plugin-optimize-hook-destructring) ## Usage diff --git a/packages/babel-preset-gatsby/src/__tests__/__snapshots__/index.js.snap b/packages/babel-preset-gatsby/src/__tests__/__snapshots__/index.js.snap index bf872b28d82e9..13accfb93ac9e 100644 --- a/packages/babel-preset-gatsby/src/__tests__/__snapshots__/index.js.snap +++ b/packages/babel-preset-gatsby/src/__tests__/__snapshots__/index.js.snap @@ -3,6 +3,12 @@ exports[`babel-preset-gatsby should specify proper presets and plugins when stage is build-html 1`] = ` Object { "plugins": Array [ + Array [ + "/packages/babel-preset-gatsby/src/optimize-hook-destructuring.ts", + Object { + "lib": true, + }, + ], Array [ "/node_modules/@babel/plugin-proposal-class-properties/lib/index.js", Object { @@ -66,6 +72,12 @@ Object { exports[`babel-preset-gatsby should specify proper presets and plugins when stage is build-javascript 1`] = ` Object { "plugins": Array [ + Array [ + "/packages/babel-preset-gatsby/src/optimize-hook-destructuring.ts", + Object { + "lib": true, + }, + ], Array [ "/node_modules/@babel/plugin-proposal-class-properties/lib/index.js", Object { @@ -133,6 +145,12 @@ Object { exports[`babel-preset-gatsby should specify proper presets and plugins when stage is build-stage 1`] = ` Object { "plugins": Array [ + Array [ + "/packages/babel-preset-gatsby/src/optimize-hook-destructuring.ts", + Object { + "lib": true, + }, + ], Array [ "/node_modules/@babel/plugin-proposal-class-properties/lib/index.js", Object { @@ -194,6 +212,12 @@ Object { exports[`babel-preset-gatsby should specify proper presets and plugins when stage is develop 1`] = ` Object { "plugins": Array [ + Array [ + "/packages/babel-preset-gatsby/src/optimize-hook-destructuring.ts", + Object { + "lib": true, + }, + ], Array [ "/node_modules/@babel/plugin-proposal-class-properties/lib/index.js", Object { diff --git a/packages/babel-preset-gatsby/src/__tests__/optimize-hook-destructuring.js b/packages/babel-preset-gatsby/src/__tests__/optimize-hook-destructuring.js new file mode 100644 index 0000000000000..a28360cb5bc1b --- /dev/null +++ b/packages/babel-preset-gatsby/src/__tests__/optimize-hook-destructuring.js @@ -0,0 +1,39 @@ +import { transform } from "@babel/core" +import preset from "babel-preset-gatsby" +import plugin from "../optimize-hook-destructuring" + +const trim = s => s.join(`\n`).trim().replace(/^\s+/gm, ``) + +const babel = code => + transform(code, { + filename: `noop.js`, + presets: [preset], + plugins: [plugin], + babelrc: false, + configFile: false, + sourceType: `module`, + compact: true, + caller: { + name: `tests`, + supportsStaticESM: true, + }, + }).code + +describe(`optimize-hook-destructuring`, () => { + it(`should transform Array-destructured hook return values use object destructuring`, () => { + const output = babel( + trim` + import { useState } from 'react'; + const [count, setCount] = useState(0); + ` + ) + + expect(output).toMatch(trim` + \"use strict\";var _react=require(\"react\");const{0:count,1:setCount}=(0,_react.useState)(0); + `) + + expect(output).toMatchInlineSnapshot( + `"\\"use strict\\";var _react=require(\\"react\\");const{0:count,1:setCount}=(0,_react.useState)(0);"` + ) + }) +}) diff --git a/packages/babel-preset-gatsby/src/index.js b/packages/babel-preset-gatsby/src/index.js index 578a8b7847ac6..fa92e82efa7e7 100644 --- a/packages/babel-preset-gatsby/src/index.js +++ b/packages/babel-preset-gatsby/src/index.js @@ -70,6 +70,12 @@ module.exports = function preset(_, options = {}) { ], ], plugins: [ + [ + resolve(`./optimize-hook-destructuring`), + { + lib: true, + }, + ], [ resolve(`@babel/plugin-proposal-class-properties`), { diff --git a/packages/babel-preset-gatsby/src/optimize-hook-destructuring.ts b/packages/babel-preset-gatsby/src/optimize-hook-destructuring.ts new file mode 100644 index 0000000000000..eb997133eec68 --- /dev/null +++ b/packages/babel-preset-gatsby/src/optimize-hook-destructuring.ts @@ -0,0 +1,71 @@ +import { NodePath, PluginObj, Visitor } from "@babel/core" +import * as BabelTypes from "@babel/types" +import { Program } from "@babel/types" + +// matches any hook-like (the default) +const isHook = /^use[A-Z]/ + +// matches only built-in hooks provided by React et al +const isBuiltInHook = /^use(Callback|Context|DebugValue|Effect|ImperativeHandle|LayoutEffect|Memo|Reducer|Ref|State)$/ + +interface IState { + opts?: { + onlyBuiltIns?: boolean + lib?: boolean + } +} + +export default function ({ + types: t, +}: { + types: typeof BabelTypes +}): PluginObj { + const visitor: Visitor = { + CallExpression(path, state: IState): void { + const onlyBuiltIns = state.opts?.onlyBuiltIns || false + + // if specified, options.lib is a list of libraries that provide hook functions + const libs = + state.opts?.lib === true ? [`react`, `preact/hooks`] : [state.opts!.lib] + + // skip function calls that are not the init of a variable declaration: + if (!t.isVariableDeclarator(path.parent)) return + + // skip function calls where the return value is not Array-destructured: + if (!t.isArrayPattern(path.parent.id)) return + + // name of the (hook) function being called: + const hookName = (path.node.callee as BabelTypes.Identifier).name + + if (libs) { + const binding = path.scope.getBinding(hookName) + // not an import + if (!binding || binding.kind !== `module`) return + + const specifier = (binding.path.parent as BabelTypes.ImportDeclaration) + .source.value + // not a match + if (!libs.some(lib => lib === specifier)) return + } + + // only match function calls with names that look like a hook + if (!(onlyBuiltIns ? isBuiltInHook : isHook).test(hookName)) return + + path.parent.id = t.objectPattern( + path.parent.id.elements.map((element, i) => + t.objectProperty(t.numericLiteral(i), element!) + ) + ) + }, + } + + return { + name: `optimize-hook-destructuring`, + visitor: { + // this is a workaround to run before preset-env destroys destructured assignments + Program(path: NodePath, state: any): void { + path.traverse(visitor, state) + }, + }, + } +} diff --git a/packages/gatsby-dev-cli/src/__tests__/watch.js b/packages/gatsby-dev-cli/src/__tests__/watch.js index 68326929526c1..f991be64add81 100644 --- a/packages/gatsby-dev-cli/src/__tests__/watch.js +++ b/packages/gatsby-dev-cli/src/__tests__/watch.js @@ -183,6 +183,7 @@ describe(`watching`, () => { }) const monoRepoPackages = [ + `babel-plugin-optimize-hook-destructuring`, `babel-plugin-remove-graphql-queries`, `babel-preset-gatsby`, `babel-preset-gatsby-package`,