Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 50a494e

Browse files
ijjktimneutkenskodiakhq[bot]
authored
Add experimental cra-to-next transform in codemod cli (vercel#24969)
Co-authored-by: Tim Neutkens <[email protected]> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 791eeb9 commit 50a494e

File tree

17 files changed

+1727
-69
lines changed

17 files changed

+1727
-69
lines changed

packages/next-codemod/bin/cli.ts

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88
// Based on https://github.com/reactjs/react-codemod/blob/dd8671c9a470a2c342b221ec903c574cf31e9f57/bin/cli.js
99
// @next/codemod optional-name-of-transform optional/path/to/src [...options]
1010

11-
const globby = require('globby')
12-
const inquirer = require('inquirer')
13-
const meow = require('meow')
14-
const path = require('path')
15-
const execa = require('execa')
16-
const chalk = require('chalk')
17-
const isGitClean = require('is-git-clean')
18-
19-
const transformerDirectory = path.join(__dirname, '../', 'transforms')
20-
const jscodeshiftExecutable = require.resolve('.bin/jscodeshift')
21-
22-
function checkGitStatus(force) {
11+
import globby from 'globby'
12+
import inquirer from 'inquirer'
13+
import meow from 'meow'
14+
import path from 'path'
15+
import execa from 'execa'
16+
import chalk from 'chalk'
17+
import isGitClean from 'is-git-clean'
18+
19+
export const jscodeshiftExecutable = require.resolve('.bin/jscodeshift')
20+
export const transformerDirectory = path.join(__dirname, '../', 'transforms')
21+
22+
export function checkGitStatus(force) {
2323
let clean = false
2424
let errorMessage = 'Unable to determine if git directory is clean'
2525
try {
@@ -49,19 +49,27 @@ function checkGitStatus(force) {
4949
}
5050
}
5151

52-
function runTransform({ files, flags, transformer }) {
52+
export function runTransform({ files, flags, transformer }) {
5353
const transformerPath = path.join(transformerDirectory, `${transformer}.js`)
5454

55+
if (transformer === 'cra-to-next') {
56+
// cra-to-next transform doesn't use jscodeshift directly
57+
return require(transformerPath).default(files, flags)
58+
}
59+
5560
let args = []
5661

57-
const { dry, print } = flags
62+
const { dry, print, runInBand } = flags
5863

5964
if (dry) {
6065
args.push('--dry')
6166
}
6267
if (print) {
6368
args.push('--print')
6469
}
70+
if (runInBand) {
71+
args.push('--run-in-band')
72+
}
6573

6674
args.push('--verbose=2')
6775

@@ -83,11 +91,11 @@ function runTransform({ files, flags, transformer }) {
8391

8492
const result = execa.sync(jscodeshiftExecutable, args, {
8593
stdio: 'inherit',
86-
stripEof: false,
94+
stripFinalNewline: false,
8795
})
8896

89-
if (result.error) {
90-
throw result.error
97+
if (result.failed) {
98+
throw new Error(`jscodeshift exited with code ${result.exitCode}`)
9199
}
92100
}
93101

@@ -112,6 +120,11 @@ const TRANSFORMER_INQUIRER_CHOICES = [
112120
'url-to-withrouter: Transforms the deprecated automatically injected url property on top level pages to using withRouter',
113121
value: 'url-to-withrouter',
114122
},
123+
{
124+
name:
125+
'cra-to-next (experimental): automatically migrates a Create React App project to Next.js',
126+
value: 'cra-to-next',
127+
},
115128
]
116129

117130
function expandFilePathsIfNeeded(filesBeforeExpansion) {
@@ -123,11 +136,10 @@ function expandFilePathsIfNeeded(filesBeforeExpansion) {
123136
: filesBeforeExpansion
124137
}
125138

126-
function run() {
127-
const cli = meow(
128-
{
129-
description: 'Codemods for updating Next.js apps.',
130-
help: `
139+
export function run() {
140+
const cli = meow({
141+
description: 'Codemods for updating Next.js apps.',
142+
help: `
131143
Usage
132144
$ npx @next/codemod <transform> <path> <...options>
133145
transform One of the choices from https://github.com/vercel/next.js/tree/canary/packages/next-codemod
@@ -138,15 +150,14 @@ function run() {
138150
--print Print transformed files to your terminal
139151
--jscodeshift (Advanced) Pass options directly to jscodeshift
140152
`,
141-
},
142-
{
153+
flags: {
143154
boolean: ['force', 'dry', 'print', 'help'],
144155
string: ['_'],
145156
alias: {
146157
h: 'help',
147158
},
148-
}
149-
)
159+
},
160+
} as meow.Options<meow.AnyFlags>)
150161

151162
if (!cli.flags.dry) {
152163
checkGitStatus(cli.flags.force)
@@ -203,11 +214,3 @@ function run() {
203214
})
204215
})
205216
}
206-
207-
module.exports = {
208-
run: run,
209-
runTransform: runTransform,
210-
checkGitStatus: checkGitStatus,
211-
jscodeshiftExecutable: jscodeshiftExecutable,
212-
transformerDirectory: transformerDirectory,
213-
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# local env files
28+
.env.local
29+
.env.development.local
30+
.env.test.local
31+
.env.production.local
32+
33+
# vercel
34+
.vercel
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import nodePath from 'path'
2+
import { API, FileInfo, Options } from 'jscodeshift'
3+
4+
export const globalCssContext = {
5+
cssImports: new Set<string>(),
6+
reactSvgImports: new Set<string>(),
7+
}
8+
const globalStylesRegex = /(?<!\.module)\.(css|scss|sass)$/i
9+
10+
export default function transformer(
11+
file: FileInfo,
12+
api: API,
13+
options: Options
14+
) {
15+
const j = api.jscodeshift
16+
const root = j(file.source)
17+
let hasModifications = false
18+
19+
root
20+
.find(j.ImportDeclaration)
21+
.filter((path) => {
22+
const {
23+
node: {
24+
source: { value },
25+
},
26+
} = path
27+
28+
if (typeof value === 'string') {
29+
if (globalStylesRegex.test(value)) {
30+
let resolvedPath = value
31+
32+
if (value.startsWith('.')) {
33+
resolvedPath = nodePath.resolve(nodePath.dirname(file.path), value)
34+
}
35+
globalCssContext.cssImports.add(resolvedPath)
36+
37+
const { start, end } = path.node as any
38+
39+
if (!path.parentPath.node.comments) {
40+
path.parentPath.node.comments = []
41+
}
42+
43+
path.parentPath.node.comments = [
44+
j.commentLine(' ' + file.source.substring(start, end)),
45+
]
46+
hasModifications = true
47+
return true
48+
} else if (value.endsWith('.svg')) {
49+
const isComponentImport = path.node.specifiers.some((specifier) => {
50+
return (specifier as any).imported?.name === 'ReactComponent'
51+
})
52+
53+
if (isComponentImport) {
54+
globalCssContext.reactSvgImports.add(file.path)
55+
}
56+
}
57+
}
58+
return false
59+
})
60+
.remove()
61+
62+
return hasModifications && globalCssContext.reactSvgImports.size === 0
63+
? root.toSource(options)
64+
: null
65+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { API, FileInfo, JSXElement, Options } from 'jscodeshift'
2+
3+
export const indexContext = {
4+
multipleRenderRoots: false,
5+
nestedRender: false,
6+
}
7+
8+
export default function transformer(
9+
file: FileInfo,
10+
api: API,
11+
options: Options
12+
) {
13+
const j = api.jscodeshift
14+
const root = j(file.source)
15+
let hasModifications = false
16+
let foundReactRender = 0
17+
let hasRenderImport = false
18+
let defaultReactDomImport: string | undefined
19+
20+
root.find(j.ImportDeclaration).forEach((path) => {
21+
if (path.node.source.value === 'react-dom') {
22+
return path.node.specifiers.forEach((specifier) => {
23+
if (specifier.local.name === 'render') {
24+
hasRenderImport = true
25+
}
26+
if (specifier.type === 'ImportDefaultSpecifier') {
27+
defaultReactDomImport = specifier.local.name
28+
}
29+
})
30+
}
31+
return false
32+
})
33+
34+
root
35+
.find(j.CallExpression)
36+
.filter((path) => {
37+
const { node } = path
38+
let found = false
39+
40+
if (
41+
defaultReactDomImport &&
42+
node.callee.type === 'MemberExpression' &&
43+
(node.callee.object as any).name === defaultReactDomImport &&
44+
(node.callee.property as any).name === 'render'
45+
) {
46+
found = true
47+
}
48+
49+
if (hasRenderImport && (node.callee as any).name === 'render') {
50+
found = true
51+
}
52+
53+
if (found) {
54+
foundReactRender++
55+
hasModifications = true
56+
57+
if (!Array.isArray(path.parentPath?.parentPath?.value)) {
58+
indexContext.nestedRender = true
59+
return false
60+
}
61+
62+
const newNode = j.exportDefaultDeclaration(
63+
j.functionDeclaration(
64+
j.identifier('NextIndexWrapper'),
65+
[],
66+
j.blockStatement([
67+
j.returnStatement(
68+
// TODO: remove React.StrictMode wrapper and use
69+
// next.config.js option instead?
70+
path.node.arguments.find(
71+
(a) => a.type === 'JSXElement'
72+
) as JSXElement
73+
),
74+
])
75+
)
76+
)
77+
78+
path.parentPath.insertBefore(newNode)
79+
return true
80+
}
81+
return false
82+
})
83+
.remove()
84+
85+
indexContext.multipleRenderRoots = foundReactRender > 1
86+
hasModifications =
87+
hasModifications &&
88+
!indexContext.nestedRender &&
89+
!indexContext.multipleRenderRoots
90+
91+
// TODO: move function passed to reportWebVitals if present to
92+
// _app reportWebVitals and massage values to expected shape
93+
94+
// root.find(j.CallExpression, {
95+
// callee: {
96+
// name: 'reportWebVitals'
97+
// }
98+
// }).remove()
99+
100+
return hasModifications ? root.toSource(options) : null
101+
}

0 commit comments

Comments
 (0)