diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..b2aaf22780a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 00000000000..5b6046c8e85 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,29 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [10.x, 12.x, 14.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2.1.1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run build --if-present + - run: npm test diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..95f2ac9f212 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :white_check_mark: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :white_check_mark: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/azure.yml b/azure.yml new file mode 100644 index 00000000000..d6783e2188e --- /dev/null +++ b/azure.yml @@ -0,0 +1,33 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + # Runs a single command using the runners shell + - name: Run a one-line script + run: echo Hello, world! + + # Runs a set of commands using the runners shell + - name: Run a multi-line script + run: | + echo Add other actions to build, + echo test, and deploy your project. diff --git a/docusaurus/website/yarn.lock b/docusaurus/website/yarn.lock index dee1365ea76..a86ca2bfee1 100644 --- a/docusaurus/website/yarn.lock +++ b/docusaurus/website/yarn.lock @@ -1748,9 +1748,9 @@ bluebird@^3.5.5, bluebird@^3.7.1: integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== + version "4.11.9" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" + integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== body-parser@1.19.0: version "1.19.0" @@ -3150,9 +3150,9 @@ electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.322: integrity sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA== elliptic@^6.0.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" - integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== dependencies: bn.js "^4.4.0" brorand "^1.0.1" @@ -5219,9 +5219,9 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0: integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== loglevel@^1.6.4: version "1.6.6" @@ -9137,9 +9137,9 @@ websocket-driver@>=0.5.1: websocket-extensions ">=0.1.1" websocket-extensions@>=0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" - integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== whatwg-url@^7.0.0: version "7.1.0" diff --git a/package.json b/package.json index 4f3c9bee078..5e9492a1e2b 100644 --- a/package.json +++ b/package.json @@ -30,17 +30,17 @@ "get-port": "^5.1.1", "globby": "^11.0.0", "husky": "^4.2.5", - "jest": "26.1.0", - "lerna": "3.20.2", - "lerna-changelog": "~0.8.2", + "jest": "26.2.2", + "lerna": "3.22.1", + "lerna-changelog": "~1.0.1", "lint-staged": "^10.2.2", - "meow": "^6.1.1", + "meow": "^7.0.1", "multimatch": "^4.0.0", "prettier": "2.0.5", - "puppeteer": "^3.0.2", + "puppeteer": "^5.2.1", "strip-ansi": "^6.0.0", "svg-term-cli": "^2.1.1", - "tempy": "^0.2.1", + "tempy": "^0.6.0", "wait-for-localhost": "^3.1.0", "web-vitals": "^0.2.2" }, diff --git a/packages/babel-plugin-named-asset-import/package.json b/packages/babel-plugin-named-asset-import/package.json index 7991d324d68..0dc988c2cc5 100644 --- a/packages/babel-plugin-named-asset-import/package.json +++ b/packages/babel-plugin-named-asset-import/package.json @@ -19,8 +19,8 @@ "@babel/core": "^7.1.0" }, "devDependencies": { - "babel-plugin-tester": "^8.0.1", - "jest": "26.1.0" + "babel-plugin-tester": "^9.2.0", + "jest": "26.2.2" }, "scripts": { "test": "jest" diff --git a/packages/babel-plugin-optimize-react/.gitignore b/packages/babel-plugin-optimize-react/.gitignore new file mode 100644 index 00000000000..588380aa8ac --- /dev/null +++ b/packages/babel-plugin-optimize-react/.gitignore @@ -0,0 +1 @@ +sandbox.js \ No newline at end of file diff --git a/packages/babel-plugin-optimize-react/LICENSE b/packages/babel-plugin-optimize-react/LICENSE new file mode 100644 index 00000000000..188fb2b0bd8 --- /dev/null +++ b/packages/babel-plugin-optimize-react/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-present, Facebook, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/babel-plugin-optimize-react/README.md b/packages/babel-plugin-optimize-react/README.md new file mode 100644 index 00000000000..222e9c364fd --- /dev/null +++ b/packages/babel-plugin-optimize-react/README.md @@ -0,0 +1,58 @@ +# babel-plugin-optimize-react + +This Babel 7 plugin optimizes React hooks by transforming common patterns into more effecient output when using with tools such as [Create React App](https://github.com/facebook/create-react-app). For example, with this plugin the following output is optimized as shown: + +```js +// Original +var _useState = Object(react__WEBPACK_IMPORTED_MODULE_1_["useState"])(Math.random()), + _State2 = Object(_Users_gaearon_p_create_rreact_app_node_modules_babel_runtime_helpers_esm_sliceToArray_WEBPACK_IMPORTED_MODULE_0__["default"])(_useState, 1), + value = _useState2[0]; + +// With this plugin +var useState = react__WEBPACK_IMPORTED_MODULE_1_.useState; +var __ref__0 = useState(Math.random()); +var value = __ref__0[0]; +``` + +## Named imports for React get transformed + +```js +// Original +import React, {memo, useState} from 'react'; + +// With this plugin +import React from 'react'; +const {memo, useState} = React; +``` + +## Array destructuring transform for React's built-in hooks + +```js +// Original +const [counter, setCounter] = useState(0); + +// With this plugin +const __ref__0 = useState(0); +const counter = __ref__0[0]; +const setCounter = __ref__0[1]; +``` + +## React.createElement becomes a hoisted constant + +```js +// Original +import React from 'react'; + +function MyComponent() { + return React.createElement('div', null, 'Hello world'); +} + +// With this plugin +import React from 'react'; +const __reactCreateElement__ = React.createElement; + +function MyComponent() { + return __reactCreateElement__('div', null, 'Hello world'); +} +``` + diff --git a/packages/babel-plugin-optimize-react/__tests__/__snapshots__/createElement-test.js.snap b/packages/babel-plugin-optimize-react/__tests__/__snapshots__/createElement-test.js.snap new file mode 100644 index 00000000000..a8f86c88946 --- /dev/null +++ b/packages/babel-plugin-optimize-react/__tests__/__snapshots__/createElement-test.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`React createElement transforms should transform React.createElement calls #2 1`] = ` +"const React = require(\\"react\\"); + +const __reactCreateElement__ = React.createElement; +export function MyComponent() { + return __reactCreateElement__(\\"div\\", null, __reactCreateElement__(\\"span\\", null, \\"Hello world!\\")); +}" +`; + +exports[`React createElement transforms should transform React.createElement calls #3 1`] = ` +"const React = require(\\"react\\"); + +const __reactCreateElement__ = React.createElement; + +const node = __reactCreateElement__(\\"div\\", null, __reactCreateElement__(\\"span\\", null, \\"Hello world!\\")); + +export function MyComponent() { + return node; +}" +`; + +exports[`React createElement transforms should transform React.createElement calls #4 1`] = ` +"import * as React from \\"react\\"; +const __reactCreateElement__ = React.createElement; + +const node = __reactCreateElement__(\\"div\\", null, __reactCreateElement__(\\"span\\", null, \\"Hello world!\\")); + +export function MyComponent() { + return node; +}" +`; + +exports[`React createElement transforms should transform React.createElement calls 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +export function MyComponent() { + return __reactCreateElement__(\\"div\\"); +}" +`; diff --git a/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap b/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap new file mode 100644 index 00000000000..efe5d68521a --- /dev/null +++ b/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap @@ -0,0 +1,145 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`React hook transforms should support destructuring hooks from imports #2 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const { + useState +} = React; +export function MyComponent() { + const _ref_0 = useState(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`React hook transforms should support destructuring hooks from imports #3 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const useState = React.useState; +export function MyComponent() { + const _ref_0 = useState(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`React hook transforms should support destructuring hooks from imports #4 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const foo = React.useState; +export function MyComponent() { + const _ref_0 = foo(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`React hook transforms should support destructuring hooks from imports #5 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const { + useState: foo +} = React; +export function MyComponent() { + const _ref_0 = foo(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`React hook transforms should support destructuring hooks from imports 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const { + useState +} = React; +export function MyComponent() { + const _ref_0 = useState(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`React hook transforms should support destructuring hooks from require calls 1`] = ` +"const React = require(\\"react\\"); + +const __reactCreateElement__ = React.createElement; +const { + useState +} = React; +export function MyComponent() { + const _ref_0 = useState(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`React hook transforms should support hook CJS require with no default 1`] = ` +"const { + useState +} = require(\\"react\\");" +`; + +exports[`React hook transforms should support hook imports with aliasing 1`] = ` +"import React from \\"react\\"; +const { + useState: foo +} = React;" +`; + +exports[`React hook transforms should support hook imports with no default 1`] = ` +"import React from \\"react\\"; +const { + useState +} = React;" +`; + +exports[`React hook transforms should support mixed hook imports 1`] = ` +"import React from \\"react\\"; +import \\"react\\"; +const { + memo, + useState +} = React;" +`; + +exports[`React hook transforms should support mixed hook imports with no default #2 1`] = ` +"import React from \\"react\\"; +const { + memo, + useRef, + useState +} = React; +export const Portal = memo(() => null);" +`; + +exports[`React hook transforms should support mixed hook imports with no default 1`] = ` +"import React from \\"react\\"; +const { + useState +} = React; +import \\"react\\"; +const { + memo +} = React;" +`; + +exports[`React hook transforms should support transform hook imports 1`] = ` +"import React from \\"react\\"; +const { + useState +} = React;" +`; diff --git a/packages/babel-plugin-optimize-react/__tests__/createElement-test.js b/packages/babel-plugin-optimize-react/__tests__/createElement-test.js new file mode 100644 index 00000000000..c7d452068ad --- /dev/null +++ b/packages/babel-plugin-optimize-react/__tests__/createElement-test.js @@ -0,0 +1,64 @@ +'use strict'; + +const plugin = require('../index.js'); +const babel = require('@babel/core'); + +function transform(code) { + return babel.transform(code, { + plugins: [plugin], + }).code; +} + +describe('React createElement transforms', () => { + it('should transform React.createElement calls', () => { + const test = ` + import React from "react"; + + export function MyComponent() { + return React.createElement("div"); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should transform React.createElement calls #2', () => { + const test = ` + const React = require("react"); + + export function MyComponent() { + return React.createElement("div", null, React.createElement("span", null, "Hello world!")); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should transform React.createElement calls #3', () => { + const test = ` + const React = require("react"); + + const node = React.createElement("div", null, React.createElement("span", null, "Hello world!")); + + export function MyComponent() { + return node; + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should transform React.createElement calls #4', () => { + const test = ` + import * as React from "react"; + + const node = React.createElement("div", null, React.createElement("span", null, "Hello world!")); + + export function MyComponent() { + return node; + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); +}); diff --git a/packages/babel-plugin-optimize-react/__tests__/hooks-test.js b/packages/babel-plugin-optimize-react/__tests__/hooks-test.js new file mode 100644 index 00000000000..6bc7887b111 --- /dev/null +++ b/packages/babel-plugin-optimize-react/__tests__/hooks-test.js @@ -0,0 +1,160 @@ +'use strict'; + +const plugin = require('../index.js'); +const babel = require('@babel/core'); + +function transform(code) { + return babel.transform(code, { + plugins: [plugin], + }).code; +} + +describe('React hook transforms', () => { + it('should support transform hook imports', () => { + const test = ` + import React, {useState} from "react"; + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support hook imports with aliasing', () => { + const test = ` + import React, {useState as foo} from "react"; + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support destructuring hooks from imports', () => { + const test = ` + import React, {useState} from "react"; + + export function MyComponent() { + const [counter, setCounter] = useState(0); + + return React.createElement("div", null, counter); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support destructuring hooks from imports #2', () => { + const test = ` + import React from "react"; + const {useState} = React; + + export function MyComponent() { + const [counter, setCounter] = useState(0); + + return React.createElement("div", null, counter); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support destructuring hooks from imports #3', () => { + const test = ` + import React from "react"; + const useState = React.useState; + + export function MyComponent() { + const [counter, setCounter] = useState(0); + + return React.createElement("div", null, counter); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support destructuring hooks from imports #4', () => { + const test = ` + import React from "react"; + const foo = React.useState; + + export function MyComponent() { + const [counter, setCounter] = foo(0); + + return React.createElement("div", null, counter); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support destructuring hooks from imports #5', () => { + const test = ` + import React, {useState as foo} from "react"; + + export function MyComponent() { + const [counter, setCounter] = foo(0); + + return React.createElement("div", null, counter); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support destructuring hooks from require calls', () => { + const test = ` + const React = require("react"); + const {useState} = React; + + export function MyComponent() { + const [counter, setCounter] = useState(0); + + return React.createElement("div", null, counter); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support hook imports with no default', () => { + const test = ` + import {useState} from "react"; + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support hook CJS require with no default', () => { + const test = ` + const {useState} = require("react"); + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support mixed hook imports', () => { + const test = ` + import React from "react"; + import {memo, useState} from "react"; + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support mixed hook imports with no default', () => { + const test = ` + import {useState} from "react"; + import {memo} from "react"; + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support mixed hook imports with no default #2', () => { + const test = ` + import {memo, useRef, useState} from "react"; + + export const Portal = memo(() => null); + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); +}); diff --git a/packages/babel-plugin-optimize-react/index.js b/packages/babel-plugin-optimize-react/index.js new file mode 100644 index 00000000000..ceafa288e19 --- /dev/null +++ b/packages/babel-plugin-optimize-react/index.js @@ -0,0 +1,347 @@ +'use strict'; + +const reactHooks = new Set([ + 'useCallback', + 'useContext', + 'useDebugValue', + 'useEffect', + 'useImperativeHandle', + 'useLayoutEffect', + 'useMemo', + 'useReducer', + 'useRef', + 'useState', +]); + +const reactNamedImports = new Set([ + 'Children', + 'cloneElement', + 'Component', + 'ConcurrentMode', + 'createContext', + 'createElement', + 'createFactory', + 'forwardRef', + 'Fragment', + 'isValidElement', + 'lazy', + 'memo', + 'Profiler', + 'PureComponent', + 'StrictMode', + 'Suspense', + 'useCallback', + 'useContext', + 'useDebugValue', + 'useEffect', + 'useImperativeHandle', + 'useLayoutEffect', + 'useMemo', + 'useReducer', + 'useRef', + 'useState', + 'version', +]); + +module.exports = function(babel) { + const { types: t } = babel; + + // Collects named imports from the "react" package + function collectAllReactImportalsAndRemoveTheirNamedImports(path, state) { + const node = path.node; + const hooks = []; + if (t.isStringLiteral(node.source) && node.source.value === 'react') { + const specifiers = path.get('specifiers'); + if (state.hasDefaultOrNamespaceSpecifier === undefined) { + state.hasDefaultOrNamespaceSpecifier = false; + } + + for (let specifier of specifiers) { + if (t.isImportSpecifier(specifier)) { + const importedNode = specifier.node.imported; + const localNode = specifier.node.local; + + if (t.isIdentifier(importedNode) && t.isIdentifier(localNode)) { + if (reactNamedImports.has(importedNode.name)) { + hooks.push({ + imported: importedNode.name, + local: localNode.name, + }); + specifier.remove(); + } + } + } else if (t.isImportDefaultSpecifier(specifier)) { + const local = specifier.get('local'); + + if (t.isIdentifier(local) && local.node.name === 'React') { + state.hasDefaultOrNamespaceSpecifier = true; + } + } else if (t.isImportNamespaceSpecifier(specifier)) { + const local = specifier.get('local'); + + if (t.isIdentifier(local) && local.node.name === 'React') { + state.hasDefaultOrNamespaceSpecifier = true; + } + } + } + // If there is no default specifier for React, add one + if ( + state.hasDefaultOrNamespaceSpecifier === false && + specifiers.length > 0 + ) { + const defaultSpecifierNode = t.importDefaultSpecifier( + t.identifier('React') + ); + + // We unshift so it goes to the beginning + path.unshiftContainer('specifiers', defaultSpecifierNode); + state.hasDefaultOrNamespaceSpecifier = true; + // Make sure we register the binding, so tracking continues to work + path.scope.registerDeclaration(path); + } + } + return hooks; + } + + function isReactImport(path) { + if (t.isIdentifier(path)) { + const identifierName = path.node.name; + const binding = path.scope.getBinding(identifierName); + + if (binding !== undefined) { + const bindingPath = binding.path; + + if ( + t.isImportDefaultSpecifier(bindingPath) || + t.isImportNamespaceSpecifier(bindingPath) + ) { + const parentPath = bindingPath.parentPath; + + if ( + t.isImportDeclaration(parentPath) && + t.isStringLiteral(parentPath.node.source) && + parentPath.node.source.value === 'react' + ) { + return true; + } + } else if (t.isVariableDeclarator(bindingPath)) { + const init = bindingPath.get('init'); + + if ( + t.isCallExpression(init) && + t.isIdentifier(init.node.callee) && + init.node.callee.name === 'require' && + init.node.arguments.length === 1 && + t.isStringLiteral(init.node.arguments[0]) && + init.node.arguments[0].value === 'react' + ) { + return true; + } + } + } + } + return false; + } + + function isReferencingReactHook(path) { + if (t.isIdentifier(path)) { + const identifierName = path.node.name; + const binding = path.scope.getBinding(identifierName); + + if (binding !== undefined) { + const bindingPath = binding.path; + + if (t.isVariableDeclarator(bindingPath)) { + const init = bindingPath.get('init'); + const bindingId = binding.identifier; + + if (t.isIdentifier(init) && isReactImport(init)) { + if (reactHooks.has(bindingId.name)) { + return true; + } + const id = bindingPath.get('id'); + + if (t.isObjectPattern(id)) { + const properties = id.get('properties'); + + for (let property of properties) { + if ( + t.isObjectProperty(property) && + property.node.value === bindingId && + t.isIdentifier(property.node.key) && + reactHooks.has(property.node.key.name) + ) { + return true; + } + } + } + } else if (t.isMemberExpression(init)) { + const object = init.get('object'); + const property = init.get('property'); + + if ( + isReactImport(object) && + t.isIdentifier(property) && + reactHooks.has(property.node.name) + ) { + return true; + } + } + } + } + } + return false; + } + + function isUsingDestructuredArray(path) { + const parentPath = path.parentPath; + + if (t.isVariableDeclarator(parentPath)) { + const id = parentPath.get('id'); + return t.isArrayPattern(id); + } + return false; + } + + function isCreateReactElementCall(path) { + if (t.isCallExpression(path)) { + const callee = path.get('callee'); + + if (t.isMemberExpression(callee)) { + const object = callee.get('object'); + const property = callee.get('property'); + + if ( + isReactImport(object) && + t.isIdentifier(property) && + property.node.name === 'createElement' + ) { + return true; + } + } + } + } + + function createConstantCreateElementReference(reactReferencePath) { + const identifierName = reactReferencePath.node.name; + const binding = reactReferencePath.scope.getBinding(identifierName); + const createElementReference = t.identifier('__reactCreateElement__'); + const createElementDeclaration = t.variableDeclaration('const', [ + t.variableDeclarator( + createElementReference, + t.memberExpression(t.identifier('React'), t.identifier('createElement')) + ), + ]); + const bindingPath = binding.path; + + if ( + t.isImportDefaultSpecifier(bindingPath) || + t.isImportNamespaceSpecifier(bindingPath) || + t.isVariableDeclarator(bindingPath) + ) { + bindingPath.parentPath.insertAfter(createElementDeclaration); + // Make sure we declare our new now so scope tracking continues to work + const reactElementDeclarationPath = bindingPath.parentPath.getNextSibling(); + reactReferencePath.scope.registerDeclaration(reactElementDeclarationPath); + } + return createElementReference; + } + + return { + name: 'babel-plugin-optimize-react', + visitor: { + ImportDeclaration(path, state) { + // Collect all hooks that are named imports from the React package. i.e.: + // import React, {useState} from "react"; + // As we collection them, we also remove the imports from the declaration. + + const reactNamedImports = collectAllReactImportalsAndRemoveTheirNamedImports( + path, + state + ); + if (reactNamedImports.length > 0) { + // Create a destructured variable declaration. i.e.: + // const {memo, useEffect, useState} = React; + // Then insert it below the import declaration node. + + const declarations = t.variableDeclarator( + t.objectPattern( + reactNamedImports.map(({ imported, local }) => + t.objectProperty( + t.identifier(imported), + t.identifier(local), + false, + imported === local + ) + ) + ), + t.identifier('React') + ); + const hookDeclarationNode = t.variableDeclaration('const', [ + declarations, + ]); + path.insertAfter(hookDeclarationNode); + // Make sure we declare our new now so scope tracking continues to work + const hookDeclarationPath = path.getNextSibling(); + path.scope.registerDeclaration(hookDeclarationPath); + } + }, + CallExpression(path, state) { + if (state.destructuredCounter === undefined) { + state.destructuredCounter = 0; + } + const calleePath = path.get('callee'); + + // Ensure we found a primitive React hook that is using a destructuring array pattern + if ( + isUsingDestructuredArray(path) && + isReferencingReactHook(calleePath) + ) { + const parentPath = path.parentPath; + + if (t.isVariableDeclarator(parentPath)) { + const id = parentPath.get('id'); + const elements = id.get('elements'); + const kind = parentPath.parentPath.node.kind; + // Replace the array destructure pattern with a reference node. + + const referenceNode = t.identifier( + '_ref_' + state.destructuredCounter++ + ); + id.replaceWith(referenceNode); + // Now insert references to the reference node, i.e.: + // const counter = __ref__[0]; + + let arrayIndex = 0; + for (let element of elements) { + const arrayAccessNode = t.variableDeclaration(kind, [ + t.variableDeclarator( + element.node, + t.memberExpression( + referenceNode, + t.numericLiteral(arrayIndex++), + true + ) + ), + ]); + parentPath.parentPath.insertAfter(arrayAccessNode); + // Make sure we declare our new now so scope tracking continues to work + const arrayAccessPath = path.getNextSibling(); + path.scope.registerDeclaration(arrayAccessPath); + } + } + } else if (isCreateReactElementCall(path)) { + const callee = path.get('callee'); + const reactReferencePath = callee.get('object'); + + if (state.createElementReference === undefined) { + state.createElementReference = createConstantCreateElementReference( + reactReferencePath + ); + } + callee.replaceWith(state.createElementReference); + } + }, + }, + }; +}; diff --git a/packages/babel-plugin-optimize-react/package.json b/packages/babel-plugin-optimize-react/package.json new file mode 100644 index 00000000000..ce4a1fad3cb --- /dev/null +++ b/packages/babel-plugin-optimize-react/package.json @@ -0,0 +1,24 @@ +{ + "name": "babel-plugin-optimize-react", + "version": "0.0.4", + "description": "Babel plugin for optimizing common React patterns", + "repository": "facebookincubator/create-react-app", + "license": "MIT", + "bugs": { + "url": "https://github.com/facebookincubator/create-react-app/issues" + }, + "main": "index.js", + "files": [ + "index.js" + ], + "peerDependencies": { + "@babel/core": "^7.1.0" + }, + "devDependencies": { + "jest": "^26.2.2", + "prettier": "^1.15.3" + }, + "scripts": { + "test": "jest" + } +} diff --git a/packages/babel-preset-react-app/package.json b/packages/babel-preset-react-app/package.json index 7bcff4e136f..cf22e77e0d0 100644 --- a/packages/babel-preset-react-app/package.json +++ b/packages/babel-preset-react-app/package.json @@ -21,19 +21,19 @@ "test.js" ], "dependencies": { - "@babel/core": "7.10.5", + "@babel/core": "7.11.0", "@babel/plugin-proposal-class-properties": "7.10.4", "@babel/plugin-proposal-decorators": "7.10.5", "@babel/plugin-proposal-nullish-coalescing-operator": "7.10.4", "@babel/plugin-proposal-numeric-separator": "7.10.4", - "@babel/plugin-proposal-optional-chaining": "7.10.4", + "@babel/plugin-proposal-optional-chaining": "7.11.0", "@babel/plugin-transform-flow-strip-types": "7.10.4", "@babel/plugin-transform-react-display-name": "7.10.4", - "@babel/plugin-transform-runtime": "7.10.5", - "@babel/preset-env": "7.10.4", + "@babel/plugin-transform-runtime": "7.11.0", + "@babel/preset-env": "7.11.0", "@babel/preset-react": "7.10.4", "@babel/preset-typescript": "7.10.4", - "@babel/runtime": "7.10.5", + "@babel/runtime": "7.11.0", "babel-plugin-macros": "2.8.0", "babel-plugin-transform-react-remove-prop-types": "0.4.24" } diff --git a/packages/confusing-browser-globals/package.json b/packages/confusing-browser-globals/package.json index c25da3f44a4..e199252a50f 100644 --- a/packages/confusing-browser-globals/package.json +++ b/packages/confusing-browser-globals/package.json @@ -20,6 +20,6 @@ "index.js" ], "devDependencies": { - "jest": "26.1.0" + "jest": "26.2.2" } } diff --git a/packages/create-react-app/package.json b/packages/create-react-app/package.json index 013968b725d..b397f443fdc 100644 --- a/packages/create-react-app/package.json +++ b/packages/create-react-app/package.json @@ -27,12 +27,12 @@ }, "dependencies": { "chalk": "4.1.0", - "commander": "4.1.0", + "commander": "6.0.0", "cross-spawn": "7.0.3", - "envinfo": "7.5.1", + "envinfo": "7.7.2", "fs-extra": "9.0.1", "hyperquest": "2.1.3", - "inquirer": "7.3.2", + "inquirer": "7.3.3", "semver": "7.3.2", "tar-pack": "3.4.1", "tmp": "0.2.1", diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 33919e60b42..dda1bff7390 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -59,15 +59,15 @@ "chalk": "2.4.2", "cross-spawn": "7.0.3", "detect-port-alt": "1.1.6", - "escape-string-regexp": "2.0.0", + "escape-string-regexp": "4.0.0", "filesize": "6.1.0", "find-up": "4.1.0", - "fork-ts-checker-webpack-plugin": "4.1.6", + "fork-ts-checker-webpack-plugin": "5.0.13", "global-modules": "2.0.0", "globby": "11.0.1", "gzip-size": "5.1.1", - "immer": "1.10.0", - "inquirer": "7.3.2", + "immer": "7.0.7", + "inquirer": "7.3.3", "is-root": "2.1.0", "loader-utils": "2.0.0", "open": "^7.0.2", @@ -80,7 +80,7 @@ }, "devDependencies": { "cross-env": "^7.0.2", - "jest": "26.1.0" + "jest": "26.2.2" }, "scripts": { "test": "cross-env FORCE_COLOR=true jest" diff --git a/packages/react-error-overlay/package.json b/packages/react-error-overlay/package.json index 14bf3f49b99..a9c7bff7bfe 100644 --- a/packages/react-error-overlay/package.json +++ b/packages/react-error-overlay/package.json @@ -35,7 +35,7 @@ ], "devDependencies": { "@babel/code-frame": "7.10.4", - "@babel/core": "7.10.5", + "@babel/core": "7.11.0", "anser": "1.4.9", "babel-eslint": "^10.1.0", "babel-jest": "^26.0.1", @@ -51,10 +51,10 @@ "eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^4.0.8", - "flow-bin": "^0.116.0", + "flow-bin": "^0.130.0", "html-entities": "1.3.1", - "jest": "26.1.0", - "jest-fetch-mock": "2.1.2", + "jest": "26.2.2", + "jest-fetch-mock": "3.0.3", "object-assign": "4.1.1", "promise": "8.1.0", "raw-loader": "^4.0.1", @@ -63,7 +63,7 @@ "react-dom": "^16.12.0", "rimraf": "^3.0.0", "settle-promise": "1.0.0", - "source-map": "0.5.6", + "source-map": "0.7.3", "terser-webpack-plugin": "3.0.7", "webpack": "^4.35.0" }, diff --git a/packages/react-scripts/bin/react-scripts.js b/packages/react-scripts/bin/react-scripts.js index 09604f6a03f..baa36eab3e4 100755 --- a/packages/react-scripts/bin/react-scripts.js +++ b/packages/react-scripts/bin/react-scripts.js @@ -19,12 +19,17 @@ const spawn = require('react-dev-utils/crossSpawn'); const args = process.argv.slice(2); const scriptIndex = args.findIndex( - x => x === 'build' || x === 'eject' || x === 'start' || x === 'test' + x => + x === 'build' || + x === 'eject' || + x === 'start' || + x === 'test' || + x === 'audit' ); const script = scriptIndex === -1 ? args[0] : args[scriptIndex]; const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : []; -if (['build', 'eject', 'start', 'test'].includes(script)) { +if (['build', 'eject', 'start', 'test', 'audit'].includes(script)) { const result = spawn.sync( process.execPath, nodeArgs diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 28b22715349..380b4132cd6 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -28,7 +28,7 @@ }, "types": "./lib/react-app.d.ts", "dependencies": { - "@babel/core": "7.10.5", + "@babel/core": "7.11.0", "@pmmmwh/react-refresh-webpack-plugin": "0.4.1", "@svgr/webpack": "5.4.0", "@typescript-eslint/eslint-plugin": "^3.3.0", @@ -41,7 +41,7 @@ "bfj": "^7.0.2", "camelcase": "^6.0.0", "case-sensitive-paths-webpack-plugin": "2.3.0", - "css-loader": "3.6.0", + "css-loader": "4.2.0", "dotenv": "8.2.0", "dotenv-expand": "5.1.0", "eslint": "^7.5.0", @@ -56,16 +56,18 @@ "fs-extra": "^9.0.0", "html-webpack-plugin": "4.3.0", "identity-obj-proxy": "3.0.0", - "jest": "26.1.0", - "jest-circus": "26.1.0", - "jest-resolve": "26.1.0", + "jest-environment-jsdom-fourteen": "1.0.1", + "lighthouse": "^6.1.1", + "jest": "26.2.2", + "jest-resolve": "26.2.2", + "jest-circus": "26.2.2", "jest-watch-typeahead": "0.6.0", "mini-css-extract-plugin": "0.9.0", "optimize-css-assets-webpack-plugin": "5.0.3", "pnp-webpack-plugin": "1.6.4", "postcss-flexbugs-fixes": "4.2.1", "postcss-loader": "3.0.0", - "postcss-normalize": "8.0.1", + "postcss-normalize": "9.0.0", "postcss-preset-env": "6.7.0", "postcss-safe-parser": "4.0.2", "react-app-polyfill": "^1.0.6", @@ -73,13 +75,13 @@ "react-refresh": "^0.8.3", "resolve": "1.17.0", "resolve-url-loader": "3.1.1", - "sass-loader": "8.0.2", + "sass-loader": "9.0.2", "semver": "7.3.2", "style-loader": "1.2.1", "terser-webpack-plugin": "3.0.7", "ts-pnp": "1.2.0", "url-loader": "4.1.0", - "webpack": "4.43.0", + "webpack": "4.44.1", "webpack-dev-server": "3.11.0", "webpack-manifest-plugin": "2.2.0", "workbox-webpack-plugin": "5.1.3" diff --git a/packages/react-scripts/scripts/audit.js b/packages/react-scripts/scripts/audit.js new file mode 100644 index 00000000000..9eefb4a51b2 --- /dev/null +++ b/packages/react-scripts/scripts/audit.js @@ -0,0 +1,75 @@ +// @remove-file-on-eject +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +'use strict'; + +const { createServer } = require('http'); +const { writeFileSync } = require('fs'); +const { join } = require('path'); +const { choosePort } = require('react-dev-utils/WebpackDevServerUtils'); +const open = require('open'); +const handler = require('serve-handler'); +const lighthouse = require('lighthouse'); +const chromeLauncher = require('chrome-launcher'); +const paths = require('../config/paths'); + +const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; +const HOST = process.env.HOST || '0.0.0.0'; + +// https://github.com/GoogleChrome/lighthouse/blob/master/docs/readme.md#using-programmatically +const launchChromeAndRunLighthouse = (url, opts) => { + return chromeLauncher + .launch({ chromeFlags: opts.chromeFlags }) + .then(chrome => { + opts.port = chrome.port; + return lighthouse(url, opts).then(results => { + return chrome.kill().then(() => results.report); + }); + }); +}; + +const server = createServer((request, response) => + handler(request, response, { + renderSingle: true, + public: paths.appBuild, + }) +); + +choosePort(HOST, DEFAULT_PORT) + .then(() => choosePort(HOST, DEFAULT_PORT)) + .then(port => { + if (port == null) { + console.log('Unable to find a free port'); + process.exit(1); + } + + server.listen(port); + + console.log('Server started, beginning audit...'); + + return launchChromeAndRunLighthouse(`http://${HOST}:${port}`, { + output: 'html', + }); + }) + .then(report => { + console.log('Audit finished, writing report...'); + + const reportPath = join(paths.appPath, 'lighthouse-audit.html'); + writeFileSync(reportPath, report); + + console.log('Opening report in browser...'); + + open(reportPath, { url: true }); + + console.log('Exiting...'); + + server.close(); + }) + .catch(() => { + console.log('Something went wrong, exiting...'); + server.close(); + }); diff --git a/packages/react-scripts/scripts/init.js b/packages/react-scripts/scripts/init.js index 7b9958e8148..b5138b39f45 100644 --- a/packages/react-scripts/scripts/init.js +++ b/packages/react-scripts/scripts/init.js @@ -188,17 +188,15 @@ module.exports = function ( appPackage.dependencies = appPackage.dependencies || {}; // Setup the script rules - const templateScripts = templatePackage.scripts || {}; - appPackage.scripts = Object.assign( - { - start: 'react-scripts start', - build: 'react-scripts build', - test: 'react-scripts test', - eject: 'react-scripts eject', - }, - templateScripts - ); + appPackage.scripts = { + start: 'react-scripts start', + build: 'react-scripts build', + test: 'react-scripts test', + eject: 'react-scripts eject', + lighthouse: 'react-scripts audit', + }; + // Update scripts for Yarn users if (useYarn) { appPackage.scripts = Object.entries(appPackage.scripts).reduce( @@ -415,4 +413,4 @@ function isReactInstalled(appPackage) { typeof dependencies.react !== 'undefined' && typeof dependencies['react-dom'] !== 'undefined' ); -} +} \ No newline at end of file