Skip to content

Commit

Permalink
Add experimental cra-to-next transform in codemod cli (#24969)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Jun 9, 2021
1 parent 7530865 commit dde9ad4
Show file tree
Hide file tree
Showing 17 changed files with 1,727 additions and 69 deletions.
71 changes: 37 additions & 34 deletions packages/next-codemod/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@
// Based on https://github.com/reactjs/react-codemod/blob/dd8671c9a470a2c342b221ec903c574cf31e9f57/bin/cli.js
// @next/codemod optional-name-of-transform optional/path/to/src [...options]

const globby = require('globby')
const inquirer = require('inquirer')
const meow = require('meow')
const path = require('path')
const execa = require('execa')
const chalk = require('chalk')
const isGitClean = require('is-git-clean')

const transformerDirectory = path.join(__dirname, '../', 'transforms')
const jscodeshiftExecutable = require.resolve('.bin/jscodeshift')

function checkGitStatus(force) {
import globby from 'globby'
import inquirer from 'inquirer'
import meow from 'meow'
import path from 'path'
import execa from 'execa'
import chalk from 'chalk'
import isGitClean from 'is-git-clean'

export const jscodeshiftExecutable = require.resolve('.bin/jscodeshift')
export const transformerDirectory = path.join(__dirname, '../', 'transforms')

export function checkGitStatus(force) {
let clean = false
let errorMessage = 'Unable to determine if git directory is clean'
try {
Expand Down Expand Up @@ -49,19 +49,27 @@ function checkGitStatus(force) {
}
}

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

if (transformer === 'cra-to-next') {
// cra-to-next transform doesn't use jscodeshift directly
return require(transformerPath).default(files, flags)
}

let args = []

const { dry, print } = flags
const { dry, print, runInBand } = flags

if (dry) {
args.push('--dry')
}
if (print) {
args.push('--print')
}
if (runInBand) {
args.push('--run-in-band')
}

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

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

const result = execa.sync(jscodeshiftExecutable, args, {
stdio: 'inherit',
stripEof: false,
stripFinalNewline: false,
})

if (result.error) {
throw result.error
if (result.failed) {
throw new Error(`jscodeshift exited with code ${result.exitCode}`)
}
}

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

function expandFilePathsIfNeeded(filesBeforeExpansion) {
Expand All @@ -123,11 +136,10 @@ function expandFilePathsIfNeeded(filesBeforeExpansion) {
: filesBeforeExpansion
}

function run() {
const cli = meow(
{
description: 'Codemods for updating Next.js apps.',
help: `
export function run() {
const cli = meow({
description: 'Codemods for updating Next.js apps.',
help: `
Usage
$ npx @next/codemod <transform> <path> <...options>
transform One of the choices from https://github.com/vercel/next.js/tree/canary/packages/next-codemod
Expand All @@ -138,15 +150,14 @@ function run() {
--print Print transformed files to your terminal
--jscodeshift (Advanced) Pass options directly to jscodeshift
`,
},
{
flags: {
boolean: ['force', 'dry', 'print', 'help'],
string: ['_'],
alias: {
h: 'help',
},
}
)
},
} as meow.Options<meow.AnyFlags>)

if (!cli.flags.dry) {
checkGitStatus(cli.flags.force)
Expand Down Expand Up @@ -203,11 +214,3 @@ function run() {
})
})
}

module.exports = {
run: run,
runTransform: runTransform,
checkGitStatus: checkGitStatus,
jscodeshiftExecutable: jscodeshiftExecutable,
transformerDirectory: transformerDirectory,
}
34 changes: 34 additions & 0 deletions packages/next-codemod/lib/cra-to-next/gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel
65 changes: 65 additions & 0 deletions packages/next-codemod/lib/cra-to-next/global-css-transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import nodePath from 'path'
import { API, FileInfo, Options } from 'jscodeshift'

export const globalCssContext = {
cssImports: new Set<string>(),
reactSvgImports: new Set<string>(),
}
const globalStylesRegex = /(?<!\.module)\.(css|scss|sass)$/i

export default function transformer(
file: FileInfo,
api: API,
options: Options
) {
const j = api.jscodeshift
const root = j(file.source)
let hasModifications = false

root
.find(j.ImportDeclaration)
.filter((path) => {
const {
node: {
source: { value },
},
} = path

if (typeof value === 'string') {
if (globalStylesRegex.test(value)) {
let resolvedPath = value

if (value.startsWith('.')) {
resolvedPath = nodePath.resolve(nodePath.dirname(file.path), value)
}
globalCssContext.cssImports.add(resolvedPath)

const { start, end } = path.node as any

if (!path.parentPath.node.comments) {
path.parentPath.node.comments = []
}

path.parentPath.node.comments = [
j.commentLine(' ' + file.source.substring(start, end)),
]
hasModifications = true
return true
} else if (value.endsWith('.svg')) {
const isComponentImport = path.node.specifiers.some((specifier) => {
return (specifier as any).imported?.name === 'ReactComponent'
})

if (isComponentImport) {
globalCssContext.reactSvgImports.add(file.path)
}
}
}
return false
})
.remove()

return hasModifications && globalCssContext.reactSvgImports.size === 0
? root.toSource(options)
: null
}
101 changes: 101 additions & 0 deletions packages/next-codemod/lib/cra-to-next/index-to-component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { API, FileInfo, JSXElement, Options } from 'jscodeshift'

export const indexContext = {
multipleRenderRoots: false,
nestedRender: false,
}

export default function transformer(
file: FileInfo,
api: API,
options: Options
) {
const j = api.jscodeshift
const root = j(file.source)
let hasModifications = false
let foundReactRender = 0
let hasRenderImport = false
let defaultReactDomImport: string | undefined

root.find(j.ImportDeclaration).forEach((path) => {
if (path.node.source.value === 'react-dom') {
return path.node.specifiers.forEach((specifier) => {
if (specifier.local.name === 'render') {
hasRenderImport = true
}
if (specifier.type === 'ImportDefaultSpecifier') {
defaultReactDomImport = specifier.local.name
}
})
}
return false
})

root
.find(j.CallExpression)
.filter((path) => {
const { node } = path
let found = false

if (
defaultReactDomImport &&
node.callee.type === 'MemberExpression' &&
(node.callee.object as any).name === defaultReactDomImport &&
(node.callee.property as any).name === 'render'
) {
found = true
}

if (hasRenderImport && (node.callee as any).name === 'render') {
found = true
}

if (found) {
foundReactRender++
hasModifications = true

if (!Array.isArray(path.parentPath?.parentPath?.value)) {
indexContext.nestedRender = true
return false
}

const newNode = j.exportDefaultDeclaration(
j.functionDeclaration(
j.identifier('NextIndexWrapper'),
[],
j.blockStatement([
j.returnStatement(
// TODO: remove React.StrictMode wrapper and use
// next.config.js option instead?
path.node.arguments.find(
(a) => a.type === 'JSXElement'
) as JSXElement
),
])
)
)

path.parentPath.insertBefore(newNode)
return true
}
return false
})
.remove()

indexContext.multipleRenderRoots = foundReactRender > 1
hasModifications =
hasModifications &&
!indexContext.nestedRender &&
!indexContext.multipleRenderRoots

// TODO: move function passed to reportWebVitals if present to
// _app reportWebVitals and massage values to expected shape

// root.find(j.CallExpression, {
// callee: {
// name: 'reportWebVitals'
// }
// }).remove()

return hasModifications ? root.toSource(options) : null
}
Loading

0 comments on commit dde9ad4

Please sign in to comment.