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/docs/upgrading.md b/docs/upgrading.md index 4c1c029fc663..80762f8b7124 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. 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', 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..f545778001d8 --- /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 React from 'react' + // The specifier would be `React` + 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) +}