Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add codemod for files that do not support the new React JSX transform #21281

Merged
merged 6 commits into from Jan 18, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 30 additions & 1 deletion docs/advanced-features/codemods.md
Expand Up @@ -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 <div>Hello World</div>
}
}
```

Transforms into:

```jsx
// my-component.js
import React from 'react'
export default class Home extends React.Component {
render() {
return <div>Hello World</div>
}
}
```

## 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
Expand Down
28 changes: 16 additions & 12 deletions docs/upgrading.md
Expand Up @@ -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.
Expand Down Expand Up @@ -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!*
timneutkens marked this conversation as resolved.
Show resolved Hide resolved
static async getInitialProps({ *Component*, *ctx* }) {
let pageProps = {}

if (Component.getInitialProps) {
Expand All @@ -48,7 +52,7 @@ class MyApp extends App {
}

render() {
// ... etc
*// ... etc*
}
}
```
Expand Down Expand Up @@ -129,7 +133,7 @@ function Home() {
}

export default withAmp(Home)
// or
*// or*
export default withAmp(Home, { hybrid: true })
```

Expand All @@ -142,7 +146,7 @@ export default function Home() {

export const config = {
amp: true,
// or
*// or*
amp: 'hybrid',
}
```
Expand All @@ -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,
}
Expand All @@ -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* }) => (
<div>
<h1>{props.title}</h1>
<Hello1 />
Expand All @@ -197,7 +201,7 @@ const HelloBundle = dynamic({
})

function DynamicBundle() {
return <HelloBundle title="Dynamic Bundle" />
return <HelloBundle *title*="Dynamic Bundle" />
}

export default DynamicBundle
Expand All @@ -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 (
<div>
<h1>{title}</h1>
Expand All @@ -222,7 +226,7 @@ function HelloBundle({ title }) {
}

function DynamicBundle() {
return <HelloBundle title="Dynamic Bundle" />
return <HelloBundle *title*="Dynamic Bundle" />
}

export default DynamicBundle
Expand Down
5 changes: 5 additions & 0 deletions packages/next-codemod/bin/cli.ts
Expand Up @@ -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',
Expand Down
@@ -0,0 +1,5 @@
export default class Home extends React.Component {
render() {
return <div>Hello World</div>
}
}
@@ -0,0 +1,6 @@
import React from 'react'
export default class Home extends React.Component {
render() {
return <div>Hello World</div>
}
}
@@ -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;
@@ -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;
@@ -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}`
)
}
77 changes: 77 additions & 0 deletions 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`
timneutkens marked this conversation as resolved.
Show resolved Hide resolved
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)
}