Skip to content

Commit

Permalink
Add codemod for files that do not support the new React JSX transform (
Browse files Browse the repository at this point in the history
…vercel#21281)

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'`
  • Loading branch information
timneutkens authored Jan 18, 2021
1 parent 699a7ae commit c5b5c43
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 1 deletion.
31 changes: 30 additions & 1 deletion docs/advanced-features/codemods.md
Original file line number Diff line number Diff line change
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
4 changes: 4 additions & 0 deletions docs/upgrading.md
Original file line number Diff line number Diff line change
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
5 changes: 5 additions & 0 deletions packages/next-codemod/bin/cli.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default class Home extends React.Component {
render() {
return <div>Hello World</div>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react'
export default class Home extends React.Component {
render() {
return <div>Hello World</div>
}
}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit c5b5c43

Please sign in to comment.