From 2378af09aca84e3b0946b482ba2147b2ec7a3fd6 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 18 Jan 2021 14:14:57 +0100 Subject: [PATCH 1/5] Add codemod for files that do not support the new React JSX transform Previously our automatic React injection approach injected `import React from 'react'` automatically whenever JSX was detected. The new official JSX transform solves this by enforcing importing `React` when it is used. This codemod automatically converted files that are using a "global React variable" to use `import React from 'react'` --- docs/advanced-features/codemods.md | 31 +++++++- .../class-component.input.js | 5 ++ .../class-component.output.js | 6 ++ ...missing-react-import-in-component.input.js | 16 ++++ ...issing-react-import-in-component.output.js | 16 ++++ .../__tests__/add-missing-react-import.js | 16 ++++ .../transforms/add-missing-react-import.ts | 77 +++++++++++++++++++ 7 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.output.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.output.js create mode 100644 packages/next-codemod/transforms/__tests__/add-missing-react-import.js create mode 100644 packages/next-codemod/transforms/add-missing-react-import.ts diff --git a/docs/advanced-features/codemods.md b/docs/advanced-features/codemods.md index 5c42d4551bb9..d20824316e58 100644 --- a/docs/advanced-features/codemods.md +++ b/docs/advanced-features/codemods.md @@ -17,13 +17,42 @@ Codemods are transformations that run on your codebase programmatically. This al - `--dry` Do a dry-run, no code will be edited - `--print` Prints the changed output for comparison +## Next.js 10 + +### `add-missing-react-import` + +Transforms files that do not import `React` to include the import in order for the new [React JSX transform](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) to work. + +For example: + +```jsx +// my-component.js +export default class Home extends React.Component { + render() { + return
Hello World
+ } +} +``` + +Transforms into: + +```jsx +// my-component.js +import React from 'react' +export default class Home extends React.Component { + render() { + return
Hello World
+ } +} +``` + ## Next.js 9 ### `name-default-component` Transforms anonymous components into named components to make sure they work with [Fast Refresh](https://nextjs.org/blog/next-9-4#fast-refresh). -For example +For example: ```jsx // my-component.js diff --git a/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.input.js b/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.input.js new file mode 100644 index 000000000000..f78adb630b08 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.input.js @@ -0,0 +1,5 @@ +export default class Home extends React.Component { + render() { + return
Hello World
+ } +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.output.js b/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.output.js new file mode 100644 index 000000000000..06c84a885d99 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.output.js @@ -0,0 +1,6 @@ +import React from 'react' +export default class Home extends React.Component { + render() { + return
Hello World
+ } +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.input.js b/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.input.js new file mode 100644 index 000000000000..5ba85aa61068 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.input.js @@ -0,0 +1,16 @@ +import { Children, isValidElement } from 'react'; + +function Heading(props) { + const { component, className, children, ...rest } = props; + return React.cloneElement( + component, + { + className: [className, component.props.className || ''].join(' '), + ...rest + }, + children + ); +} + + +export default Heading; diff --git a/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.output.js b/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.output.js new file mode 100644 index 000000000000..7b15a0ad4b14 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.output.js @@ -0,0 +1,16 @@ +import React, { Children, isValidElement } from 'react'; + +function Heading(props) { + const { component, className, children, ...rest } = props; + return React.cloneElement( + component, + { + className: [className, component.props.className || ''].join(' '), + ...rest + }, + children + ); +} + + +export default Heading; diff --git a/packages/next-codemod/transforms/__tests__/add-missing-react-import.js b/packages/next-codemod/transforms/__tests__/add-missing-react-import.js new file mode 100644 index 000000000000..11342ad49e61 --- /dev/null +++ b/packages/next-codemod/transforms/__tests__/add-missing-react-import.js @@ -0,0 +1,16 @@ +/* global jest */ +jest.autoMockOff() +const defineTest = require('jscodeshift/dist/testUtils').defineTest + +const fixtures = [ + 'missing-react-import-in-component' +] + +for (const fixture of fixtures) { + defineTest( + __dirname, + 'add-missing-react-import', + null, + `add-missing-react-import/${fixture}` + ) +} diff --git a/packages/next-codemod/transforms/add-missing-react-import.ts b/packages/next-codemod/transforms/add-missing-react-import.ts new file mode 100644 index 000000000000..a75ccc820f2c --- /dev/null +++ b/packages/next-codemod/transforms/add-missing-react-import.ts @@ -0,0 +1,77 @@ +function addReactImport(j, root) { + // We create an import specifier, this is the value of an import, eg: + // import {withRouter} from 'next/router + // The specifier would be `withRouter` + const ReactDefaultSpecifier = j.importDefaultSpecifier(j.identifier('React')) + + // Check if this file is already importing `react` + // so that we can attach `React` to the existing import instead of creating a new `import` node + const originalReactImport = root.find(j.ImportDeclaration, { + source: { + value: 'react', + }, + }) + if (originalReactImport.length > 0) { + // Check if `React` is already imported. In that case we don't have to do anything + if (originalReactImport.find(j.ImportDefaultSpecifier).length > 0) { + return + } + + // Attach `React` to the existing `react` import node + originalReactImport.forEach((node) => { + node.value.specifiers.unshift(ReactDefaultSpecifier) + }) + return + } + + // Create import node + // import React from 'react' + const ReactImport = j.importDeclaration( + [ReactDefaultSpecifier], + j.stringLiteral('react') + ) + + // Find the Program, this is the top level AST node + const Program = root.find(j.Program) + // Attach the import at the top of the body + Program.forEach((node) => { + node.value.body.unshift(ReactImport) + }) +} + +export default function transformer(file, api, options) { + const j = api.jscodeshift + const root = j(file.source) + + const hasReactImport = (r) => { + return ( + r.find(j.ImportDefaultSpecifier, { + local: { + type: 'Identifier', + name: 'React', + }, + }).length > 0 + ) + } + + const hasReactVariableUsage = (r) => { + return ( + r.find(j.MemberExpression, { + object: { + type: 'Identifier', + name: 'React', + }, + }).length > 0 + ) + } + + if (hasReactImport(root)) { + return + } + + if (hasReactVariableUsage(root)) { + addReactImport(j, root) + } + + return root.toSource(options) +} From 1ffd8a2ee5abfaec92ffb74d6b6391f076d12ac3 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 18 Jan 2021 14:23:21 +0100 Subject: [PATCH 2/5] Add option --- packages/next-codemod/bin/cli.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/next-codemod/bin/cli.ts b/packages/next-codemod/bin/cli.ts index c3552bc79740..76278a7bafaf 100644 --- a/packages/next-codemod/bin/cli.ts +++ b/packages/next-codemod/bin/cli.ts @@ -97,6 +97,11 @@ const TRANSFORMER_INQUIRER_CHOICES = [ 'name-default-component: Transforms anonymous components into named components to make sure they work with Fast Refresh', value: 'name-default-component', }, + { + name: + 'add-missing-react-import: Transforms files that do not import `React` to include the import in order for the new React JSX transform', + value: 'add-missing-react-import', + }, { name: 'withamp-to-config: Transforms the withAmp HOC into Next.js 9 page configuration', From 5255f1611e993828bca307383ec4c06bd831c855 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 18 Jan 2021 15:22:13 +0100 Subject: [PATCH 3/5] Add section to upgrading guide --- docs/upgrading.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index 4c1c029fc663..dc0e025a1a47 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -4,6 +4,10 @@ description: Learn how to upgrade Next.js. # Upgrade Guide +## React 16 to 17 + +React 17 introduced a new [JSX Transform](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) that brings a long-time Next.js feature to the wider React ecosystem: Not having to `import React from 'react'` when using JSX. When using React 17 Next.js will automatically use the new transform. This transform does not make the `React` variable global, which was an unintended side-effect of the previous Next.js implementation. A [codemod is available](/docs/advanced-features/codemods#add-missing-react-import) to automatically fix cases where you accidentally used `React` without importing it. + ## Upgrading from version 9 to 10 There were no breaking changes between version 9 and 10. @@ -36,8 +40,8 @@ The following `getInitialProps` does nothing and may be removed: ```js class MyApp extends App { - // Remove me, I do nothing! - static async getInitialProps({ Component, ctx }) { + *// Remove me, I do nothing!* + static async getInitialProps({ *Component*, *ctx* }) { let pageProps = {} if (Component.getInitialProps) { @@ -48,7 +52,7 @@ class MyApp extends App { } render() { - // ... etc + *// ... etc* } } ``` @@ -129,7 +133,7 @@ function Home() { } export default withAmp(Home) -// or +*// or* export default withAmp(Home, { hybrid: true }) ``` @@ -142,7 +146,7 @@ export default function Home() { export const config = { amp: true, - // or + *// or* amp: 'hybrid', } ``` @@ -154,7 +158,7 @@ Previously, exporting `pages/about.js` would result in `out/about/index.html`. T You can revert to the previous behavior by creating a `next.config.js` with the following content: ```js -// next.config.js +*// next.config.js* module.exports = { trailingSlash: true, } @@ -181,13 +185,13 @@ import dynamic from 'next/dynamic' const HelloBundle = dynamic({ modules: () => { const components = { - Hello1: () => import('../components/hello1').then((m) => m.default), - Hello2: () => import('../components/hello2').then((m) => m.default), + Hello1: () => import('../components/hello1').then((*m*) => m.default), + Hello2: () => import('../components/hello2').then((*m*) => m.default), } return components }, - render: (props, { Hello1, Hello2 }) => ( + render: (*props*, { *Hello1*, *Hello2* }) => (

{props.title}

@@ -197,7 +201,7 @@ const HelloBundle = dynamic({ }) function DynamicBundle() { - return + return } export default DynamicBundle @@ -211,7 +215,7 @@ import dynamic from 'next/dynamic' const Hello1 = dynamic(() => import('../components/hello1')) const Hello2 = dynamic(() => import('../components/hello2')) -function HelloBundle({ title }) { +function HelloBundle({ *title* }) { return (

{title}

@@ -222,7 +226,7 @@ function HelloBundle({ title }) { } function DynamicBundle() { - return + return } export default DynamicBundle From 526c73cbb6b5403d073b173a79feea3c12e73d19 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 18 Jan 2021 15:46:19 +0100 Subject: [PATCH 4/5] Fix upgrading guide --- docs/upgrading.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/upgrading.md b/docs/upgrading.md index dc0e025a1a47..80762f8b7124 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -40,8 +40,8 @@ The following `getInitialProps` does nothing and may be removed: ```js class MyApp extends App { - *// Remove me, I do nothing!* - static async getInitialProps({ *Component*, *ctx* }) { + // Remove me, I do nothing! + static async getInitialProps({ Component, ctx }) { let pageProps = {} if (Component.getInitialProps) { @@ -52,7 +52,7 @@ class MyApp extends App { } render() { - *// ... etc* + // ... etc } } ``` @@ -133,7 +133,7 @@ function Home() { } export default withAmp(Home) -*// or* +// or export default withAmp(Home, { hybrid: true }) ``` @@ -146,7 +146,7 @@ export default function Home() { export const config = { amp: true, - *// or* + // or amp: 'hybrid', } ``` @@ -158,7 +158,7 @@ Previously, exporting `pages/about.js` would result in `out/about/index.html`. T You can revert to the previous behavior by creating a `next.config.js` with the following content: ```js -*// next.config.js* +// next.config.js module.exports = { trailingSlash: true, } @@ -185,13 +185,13 @@ import dynamic from 'next/dynamic' const HelloBundle = dynamic({ modules: () => { const components = { - Hello1: () => import('../components/hello1').then((*m*) => m.default), - Hello2: () => import('../components/hello2').then((*m*) => m.default), + Hello1: () => import('../components/hello1').then((m) => m.default), + Hello2: () => import('../components/hello2').then((m) => m.default), } return components }, - render: (*props*, { *Hello1*, *Hello2* }) => ( + render: (props, { Hello1, Hello2 }) => (

{props.title}

@@ -201,7 +201,7 @@ const HelloBundle = dynamic({ }) function DynamicBundle() { - return + return } export default DynamicBundle @@ -215,7 +215,7 @@ import dynamic from 'next/dynamic' const Hello1 = dynamic(() => import('../components/hello1')) const Hello2 = dynamic(() => import('../components/hello2')) -function HelloBundle({ *title* }) { +function HelloBundle({ title }) { return (

{title}

@@ -226,7 +226,7 @@ function HelloBundle({ *title* }) { } function DynamicBundle() { - return + return } export default DynamicBundle From cdeec64a71bd49db4931f76fc0f7380f0a89914f Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 18 Jan 2021 15:48:53 +0100 Subject: [PATCH 5/5] update comment --- packages/next-codemod/transforms/add-missing-react-import.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next-codemod/transforms/add-missing-react-import.ts b/packages/next-codemod/transforms/add-missing-react-import.ts index a75ccc820f2c..f545778001d8 100644 --- a/packages/next-codemod/transforms/add-missing-react-import.ts +++ b/packages/next-codemod/transforms/add-missing-react-import.ts @@ -1,7 +1,7 @@ function addReactImport(j, root) { // We create an import specifier, this is the value of an import, eg: - // import {withRouter} from 'next/router - // The specifier would be `withRouter` + // import React from 'react' + // The specifier would be `React` const ReactDefaultSpecifier = j.importDefaultSpecifier(j.identifier('React')) // Check if this file is already importing `react`