diff --git a/.vscode/settings.json b/.vscode/settings.json
index f0fc53bd6cd74..7b4b1e6a9bfb5 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -57,6 +57,8 @@
"RUST_BACKTRACE": "0"
},
"cSpell.words": [
+ "codemod",
+ "codemods",
"Destructuring",
"Entrypoints",
"jscodeshift",
diff --git a/packages/next-codemod/bin/cli.ts b/packages/next-codemod/bin/cli.ts
deleted file mode 100644
index c93eb018fb61b..0000000000000
--- a/packages/next-codemod/bin/cli.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-/**
- * Copyright 2015-present, Facebook, Inc.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- */
-// Based on https://github.com/reactjs/react-codemod/blob/dd8671c9a470a2c342b221ec903c574cf31e9f57/bin/cli.js
-// @next/codemod optional-name-of-transform optional/path/to/src [...options]
-
-import globby from 'globby'
-import inquirer from 'inquirer'
-import meow from 'meow'
-import path from 'path'
-import execa from 'execa'
-import { yellow } from 'picocolors'
-import isGitClean from 'is-git-clean'
-import { installPackage, uninstallPackage } from '../lib/handle-package'
-import { runUpgrade } from './upgrade'
-
-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 {
- clean = isGitClean.sync(process.cwd())
- errorMessage = 'Git directory is not clean'
- } catch (err) {
- if (err && err.stderr && err.stderr.includes('Not a git repository')) {
- clean = true
- }
- }
-
- if (!clean) {
- if (force) {
- console.log(`WARNING: ${errorMessage}. Forcibly continuing.`)
- } else {
- console.log('Thank you for using @next/codemod!')
- console.log(
- yellow(
- '\nBut before we continue, please stash or commit your git changes.'
- )
- )
- console.log(
- '\nYou may use the --force flag to override this safety check.'
- )
- process.exit(1)
- }
- }
-}
-
-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, runInBand } = flags
-
- if (dry) {
- args.push('--dry')
- }
- if (print) {
- args.push('--print')
- }
- if (runInBand) {
- args.push('--run-in-band')
- }
-
- args.push('--verbose=2')
-
- args.push('--ignore-pattern=**/node_modules/**')
- args.push('--ignore-pattern=**/.next/**')
-
- args.push('--extensions=tsx,ts,jsx,js')
-
- args = args.concat(['--transform', transformerPath])
-
- if (flags.jscodeshift) {
- args = args.concat(flags.jscodeshift)
- }
-
- args = args.concat(files)
-
- console.log(`Executing command: jscodeshift ${args.join(' ')}`)
-
- const result = execa.sync(jscodeshiftExecutable, args, {
- stdio: 'inherit',
- stripFinalNewline: false,
- })
-
- if (result.failed) {
- throw new Error(`jscodeshift exited with code ${result.exitCode}`)
- }
-
- if (!dry && transformer === 'built-in-next-font') {
- console.log('Uninstalling `@next/font`')
- try {
- uninstallPackage('@next/font')
- } catch {
- console.error(
- "Couldn't uninstall `@next/font`, please uninstall it manually"
- )
- }
- }
-
- if (!dry && transformer === 'next-request-geo-ip') {
- console.log('Installing `@vercel/functions`...')
- installPackage('@vercel/functions')
- }
-}
-
-const TRANSFORMER_INQUIRER_CHOICES = [
- {
- name: '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',
- value: 'withamp-to-config',
- },
- {
- name: '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',
- },
- {
- name: 'new-link: Ensures your usage is backwards compatible.',
- value: 'new-link',
- },
- {
- name: 'next-og-import: Transforms imports from `next/server` to `next/og` for usage of Dynamic OG Image Generation.',
- value: 'next-og-import',
- },
- {
- name: 'metadata-to-viewport-export: Migrates certain viewport related metadata from the `metadata` export to a new `viewport` export.',
- value: 'metadata-to-viewport-export',
- },
- {
- name: 'next-dynamic-access-named-export: Transforms dynamic imports that return the named export itself to a module like object.',
- value: 'next-dynamic-access-named-export',
- },
- {
- name: 'next-image-to-legacy-image: safely migrate Next.js 10, 11, 12 applications importing `next/image` to the renamed `next/legacy/image` import in Next.js 13',
- value: 'next-image-to-legacy-image',
- },
- {
- name: 'next-image-experimental (experimental): dangerously migrates from `next/legacy/image` to the new `next/image` by adding inline styles and removing unused props',
- value: 'next-image-experimental',
- },
- {
- name: 'built-in-next-font: Uninstall `@next/font` and transform imports to `next/font`',
- value: 'built-in-next-font',
- },
- {
- name: 'next-async-request-api: Transforms usage of Next.js async Request APIs',
- value: 'next-async-request-api',
- },
- {
- name: 'next-request-geo-ip: Install `@vercel/functions` to replace `geo` and `ip` properties on `NextRequest`',
- value: 'next-request-geo-ip',
- },
-]
-
-function expandFilePathsIfNeeded(filesBeforeExpansion) {
- const shouldExpandFiles = filesBeforeExpansion.some((file) =>
- file.includes('*')
- )
- return shouldExpandFiles
- ? globby.sync(filesBeforeExpansion)
- : filesBeforeExpansion
-}
-
-export function run() {
- const cli = meow({
- description: 'Codemods for updating Next.js apps.',
- help: `
- Usage
- $ npx @next/codemod <...options>
- transform One of the choices from https://github.com/vercel/next.js/tree/canary/packages/next-codemod
- path Files or directory to transform. Can be a glob like pages/**.js
- Options
- --force Bypass Git safety checks and forcibly run codemods
- --dry Dry run (no changes are made to files)
- --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)
-
- if (!cli.flags.dry) {
- checkGitStatus(cli.flags.force)
- }
-
- const isUpgrade = cli.input[0] === 'upgrade' || cli.input[0] === 'up'
-
- if (isUpgrade) {
- return runUpgrade().catch(console.error)
- }
-
- if (
- cli.input[0] &&
- !TRANSFORMER_INQUIRER_CHOICES.find((x) => x.value === cli.input[0])
- ) {
- console.error('Invalid transform choice, pick one of:')
- console.error(
- TRANSFORMER_INQUIRER_CHOICES.map((x) => '- ' + x.value).join('\n')
- )
- process.exit(1)
- }
-
- inquirer
- .prompt([
- {
- type: 'input',
- name: 'files',
- message: 'On which files or directory should the codemods be applied?',
- when: !cli.input[1],
- default: '.',
- // validate: () =>
- filter: (files) => files.trim(),
- },
- {
- type: 'list',
- name: 'transformer',
- message: 'Which transform would you like to apply?',
- when: !cli.input[0],
- pageSize: TRANSFORMER_INQUIRER_CHOICES.length,
- choices: TRANSFORMER_INQUIRER_CHOICES,
- },
- ])
- .then((answers) => {
- const { files, transformer } = answers
-
- const filesBeforeExpansion = cli.input[1] || files
- const filesExpanded = expandFilePathsIfNeeded([filesBeforeExpansion])
-
- const selectedTransformer = cli.input[0] || transformer
-
- if (!filesExpanded.length) {
- console.log(`No files found matching ${filesBeforeExpansion.join(' ')}`)
- return null
- }
-
- return runTransform({
- files: filesExpanded,
- flags: cli.flags,
- transformer: selectedTransformer,
- })
- })
-}
diff --git a/packages/next-codemod/bin/next-codemod.ts b/packages/next-codemod/bin/next-codemod.ts
index 8268a4de769b0..d47e596a481c5 100644
--- a/packages/next-codemod/bin/next-codemod.ts
+++ b/packages/next-codemod/bin/next-codemod.ts
@@ -1,5 +1,4 @@
#!/usr/bin/env node
-
/**
* Copyright 2015-present, Facebook, Inc.
*
@@ -7,7 +6,47 @@
* LICENSE file in the root directory of this source tree.
*
*/
-// Based on https://github.com/reactjs/react-codemod/blob/dd8671c9a470a2c342b221ec903c574cf31e9f57/bin/react-codemod.js
-// next-codemod optional-name-of-transform optional/path/to/src [...options]
+// Based on https://github.com/reactjs/react-codemod/blob/dd8671c9a470a2c342b221ec903c574cf31e9f57/bin/cli.js
+// @next/codemod optional-name-of-transform optional/path/to/src [...options]
+
+import { Command } from 'commander'
+import { runUpgrade } from './upgrade'
+import { runTransform } from './transform'
+
+const packageJson = require('../package.json')
+const program = new Command(packageJson.name)
+ .description('Codemods for updating Next.js apps.')
+ .version(
+ packageJson.version,
+ '-v, --version',
+ 'Output the current version of @next/codemod.'
+ )
+ .argument(
+ '[codemod]',
+ 'Codemod slug to run. See "https://github.com/vercel/next.js/tree/canary/packages/next-codemod".'
+ )
+ .argument(
+ '[source]',
+ 'Path to source files or directory to transform including glob patterns.'
+ )
+ .usage('[codemod] [source] [options]')
+ .helpOption('-h, --help', 'Display this help message.')
+ .option('-f, --force', 'Bypass Git safety checks and forcibly run codemods')
+ .option('-d, --dry', 'Dry run (no changes are made to files)')
+ .option('-p, --print', 'Print transformed files to your terminal')
+ .option(
+ '-j, --jscodeshift',
+ '(Advanced) Pass options directly to jscodeshift'
+ )
+ .action(runTransform)
+ .allowUnknownOption()
+
+program
+ .command('upgrade')
+ .description(
+ 'Upgrade Next.js apps to desired versions with a single command.'
+ )
+ .usage('[options]')
+ .action(runUpgrade)
-require('./cli').run()
+program.parse(process.argv)
diff --git a/packages/next-codemod/bin/transform.ts b/packages/next-codemod/bin/transform.ts
new file mode 100644
index 0000000000000..4ef9f14017c87
--- /dev/null
+++ b/packages/next-codemod/bin/transform.ts
@@ -0,0 +1,133 @@
+import execa from 'execa'
+import globby from 'globby'
+import prompts from 'prompts'
+import { join } from 'node:path'
+import { installPackage, uninstallPackage } from '../lib/handle-package'
+import { checkGitStatus, TRANSFORMER_INQUIRER_CHOICES } from '../lib/utils'
+
+function expandFilePathsIfNeeded(filesBeforeExpansion) {
+ const shouldExpandFiles = filesBeforeExpansion.some((file) =>
+ file.includes('*')
+ )
+ return shouldExpandFiles
+ ? globby.sync(filesBeforeExpansion)
+ : filesBeforeExpansion
+}
+
+export const jscodeshiftExecutable = require.resolve('.bin/jscodeshift')
+export const transformerDirectory = join(__dirname, '../', 'transforms')
+
+export async function runTransform(
+ transform: string,
+ path: string,
+ options: any
+) {
+ let transformer = transform
+ let directory = path
+
+ if (!options.dry) {
+ checkGitStatus(options.force)
+ }
+
+ if (
+ transform &&
+ !TRANSFORMER_INQUIRER_CHOICES.find((x) => x.value === transform)
+ ) {
+ console.error('Invalid transform choice, pick one of:')
+ console.error(
+ TRANSFORMER_INQUIRER_CHOICES.map((x) => '- ' + x.value).join('\n')
+ )
+ process.exit(1)
+ }
+
+ if (!path) {
+ const res = await prompts({
+ type: 'text',
+ name: 'path',
+ message: 'On which files or directory should the codemods be applied?',
+ default: '.',
+ })
+
+ directory = res.path
+ }
+ if (!transform) {
+ const res = await prompts({
+ type: 'select',
+ name: 'transformer',
+ message: 'Which transform would you like to apply?',
+ choices: TRANSFORMER_INQUIRER_CHOICES,
+ })
+
+ transformer = res.transformer
+ }
+
+ const filesExpanded = expandFilePathsIfNeeded([directory])
+
+ if (!filesExpanded.length) {
+ console.log(`No files found matching "${directory}"`)
+ return null
+ }
+
+ const transformerPath = join(transformerDirectory, `${transformer}.js`)
+
+ if (transformer === 'cra-to-next') {
+ // cra-to-next transform doesn't use jscodeshift directly
+ return require(transformerPath).default(filesExpanded, options)
+ }
+
+ let args = []
+
+ const { dry, print, runInBand, jscodeshift } = options
+
+ if (dry) {
+ args.push('--dry')
+ }
+ if (print) {
+ args.push('--print')
+ }
+ if (runInBand) {
+ args.push('--run-in-band')
+ }
+
+ args.push('--verbose=2')
+
+ args.push('--ignore-pattern=**/node_modules/**')
+ args.push('--ignore-pattern=**/.next/**')
+
+ args.push('--extensions=tsx,ts,jsx,js')
+
+ args = args.concat(['--transform', transformerPath])
+
+ if (jscodeshift) {
+ args = args.concat(jscodeshift)
+ }
+
+ args = args.concat(filesExpanded)
+
+ console.log(`Executing command: jscodeshift ${args.join(' ')}`)
+
+ const result = execa.sync(jscodeshiftExecutable, args, {
+ stdio: 'inherit',
+ stripFinalNewline: false,
+ })
+
+ if (result.failed) {
+ throw new Error(`jscodeshift exited with code ${result.exitCode}`)
+ }
+
+ if (!dry && transformer === 'built-in-next-font') {
+ console.log('Uninstalling `@next/font`')
+ try {
+ uninstallPackage('@next/font')
+ } catch {
+ console.error(
+ "Couldn't uninstall `@next/font`, please uninstall it manually"
+ )
+ }
+ }
+
+ if (!dry && transformer === 'next-request-geo-ip') {
+ console.log('Installing `@vercel/functions`...')
+ installPackage('@vercel/functions')
+ }
+}
diff --git a/packages/next-codemod/lib/utils.ts b/packages/next-codemod/lib/utils.ts
new file mode 100644
index 0000000000000..38f5bf4e86aa7
--- /dev/null
+++ b/packages/next-codemod/lib/utils.ts
@@ -0,0 +1,104 @@
+import { yellow } from 'picocolors'
+import isGitClean from 'is-git-clean'
+
+export function checkGitStatus(force) {
+ let clean = false
+ let errorMessage = 'Unable to determine if git directory is clean'
+ try {
+ clean = isGitClean.sync(process.cwd())
+ errorMessage = 'Git directory is not clean'
+ } catch (err) {
+ if (err && err.stderr && err.stderr.includes('Not a git repository')) {
+ clean = true
+ }
+ }
+
+ if (!clean) {
+ if (force) {
+ console.log(`WARNING: ${errorMessage}. Forcibly continuing.`)
+ } else {
+ console.log('Thank you for using @next/codemod!')
+ console.log(
+ yellow(
+ '\nBut before we continue, please stash or commit your git changes.'
+ )
+ )
+ console.log(
+ '\nYou may use the --force flag to override this safety check.'
+ )
+ process.exit(1)
+ }
+ }
+}
+
+export const TRANSFORMER_INQUIRER_CHOICES = [
+ {
+ title:
+ 'name-default-component: Transforms anonymous components into named components to make sure they work with Fast Refresh',
+ value: 'name-default-component',
+ },
+ {
+ title:
+ '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',
+ },
+ {
+ title:
+ 'withamp-to-config: Transforms the withAmp HOC into Next.js 9 page configuration',
+ value: 'withamp-to-config',
+ },
+ {
+ title:
+ 'url-to-withrouter: Transforms the deprecated automatically injected url property on top level pages to using withRouter',
+ value: 'url-to-withrouter',
+ },
+ {
+ title:
+ 'cra-to-next (experimental): automatically migrates a Create React App project to Next.js',
+ value: 'cra-to-next',
+ },
+ {
+ title: 'new-link: Ensures your usage is backwards compatible.',
+ value: 'new-link',
+ },
+ {
+ title:
+ 'next-og-import: Transforms imports from `next/server` to `next/og` for usage of Dynamic OG Image Generation.',
+ value: 'next-og-import',
+ },
+ {
+ title:
+ 'metadata-to-viewport-export: Migrates certain viewport related metadata from the `metadata` export to a new `viewport` export.',
+ value: 'metadata-to-viewport-export',
+ },
+ {
+ title:
+ 'next-dynamic-access-named-export: Transforms dynamic imports that return the named export itself to a module like object.',
+ value: 'next-dynamic-access-named-export',
+ },
+ {
+ title:
+ 'next-image-to-legacy-image: safely migrate Next.js 10, 11, 12 applications importing `next/image` to the renamed `next/legacy/image` import in Next.js 13',
+ value: 'next-image-to-legacy-image',
+ },
+ {
+ title:
+ 'next-image-experimental (experimental): dangerously migrates from `next/legacy/image` to the new `next/image` by adding inline styles and removing unused props',
+ value: 'next-image-experimental',
+ },
+ {
+ title:
+ 'built-in-next-font: Uninstall `@next/font` and transform imports to `next/font`',
+ value: 'built-in-next-font',
+ },
+ {
+ title:
+ 'next-async-request-api: Transforms usage of Next.js async Request APIs',
+ value: 'next-async-request-api',
+ },
+ {
+ title:
+ 'next-request-geo-ip: Install `@vercel/functions` to replace `geo` and `ip` properties on `NextRequest`',
+ value: 'next-request-geo-ip',
+ },
+]
diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json
index 39eb6be78a3b7..431e1abe8cab5 100644
--- a/packages/next-codemod/package.json
+++ b/packages/next-codemod/package.json
@@ -10,13 +10,12 @@
"dependencies": {
"chalk": "4.1.2",
"cheerio": "1.0.0-rc.9",
+ "commander": "12.1.0",
"compare-versions": "6.1.1",
"execa": "4.0.3",
"globby": "11.0.1",
- "inquirer": "7.3.3",
"is-git-clean": "1.1.0",
"jscodeshift": "17.0.0",
- "meow": "7.0.1",
"picocolors": "1.0.0",
"prompts": "2.4.2"
},
diff --git a/packages/next-codemod/tsconfig.json b/packages/next-codemod/tsconfig.json
index 8f5a4bf31b1e1..f54bd265b1843 100644
--- a/packages/next-codemod/tsconfig.json
+++ b/packages/next-codemod/tsconfig.json
@@ -3,7 +3,7 @@
"module": "commonjs",
"sourceMap": true,
"esModuleInterop": true,
- "target": "es2015",
+ "target": "ES2022",
"downlevelIteration": true,
"preserveWatchOutput": true
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3a2bcd5d5645a..cf4144e34b405 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1502,6 +1502,9 @@ importers:
cheerio:
specifier: 1.0.0-rc.9
version: 1.0.0-rc.9
+ commander:
+ specifier: 12.1.0
+ version: 12.1.0
compare-versions:
specifier: 6.1.1
version: 6.1.1
@@ -1511,18 +1514,12 @@ importers:
globby:
specifier: 11.0.1
version: 11.0.1
- inquirer:
- specifier: 7.3.3
- version: 7.3.3
is-git-clean:
specifier: 1.1.0
version: 1.1.0
jscodeshift:
specifier: 17.0.0
version: 17.0.0(@babel/preset-env@7.24.8(@babel/core@7.22.5))
- meow:
- specifier: 7.0.1
- version: 7.0.1
picocolors:
specifier: 1.0.0
version: 1.0.0
@@ -5117,9 +5114,6 @@ packages:
'@types/minimatch@3.0.3':
resolution: {integrity: sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==}
- '@types/minimist@1.2.0':
- resolution: {integrity: sha512-BsF2gEVEIOcbQCSwXR6V14fGD6QLLT0yQBK6RpblkxVYP9x8ANNThpxMUxV7h4KKjqMDR8qELlcnqrEoyvsohw==}
-
'@types/minimist@1.2.5':
resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
@@ -10488,10 +10482,6 @@ packages:
resolution: {integrity: sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==}
engines: {node: '>=0.10.0'}
- meow@7.0.1:
- resolution: {integrity: sha512-tBKIQqVrAHqwit0vfuFPY3LlzJYkEOFyKa3bPgxzNl6q/RtN8KQ+ALYEASYuFayzSAsjlhXj/JZ10rH85Q6TUw==}
- engines: {node: '>=10'}
-
meow@7.1.1:
resolution: {integrity: sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==}
engines: {node: '>=10'}
@@ -19702,8 +19692,6 @@ snapshots:
'@types/minimatch@3.0.3': {}
- '@types/minimist@1.2.0': {}
-
'@types/minimist@1.2.5': {}
'@types/mute-stream@0.0.4':
@@ -26576,22 +26564,6 @@ snapshots:
redent: 1.0.0
trim-newlines: 1.0.0
- meow@7.0.1:
- dependencies:
- '@types/minimist': 1.2.0
- arrify: 2.0.1
- camelcase: 6.2.0
- camelcase-keys: 6.2.2
- decamelize-keys: 1.1.0
- hard-rejection: 2.1.0
- minimist-options: 4.1.0
- normalize-package-data: 2.5.0
- read-pkg-up: 7.0.1
- redent: 3.0.0
- trim-newlines: 3.0.0
- type-fest: 0.13.1
- yargs-parser: 18.1.3
-
meow@7.1.1:
dependencies:
'@types/minimist': 1.2.5