From a100bd37ef4b890d03924c76c221da22ce887610 Mon Sep 17 00:00:00 2001 From: Will Binns-Smith Date: Mon, 28 Jul 2025 11:21:45 -0700 Subject: [PATCH] `@next/codemod`: Add `experimental.turbo` to `turbopack` coded for Next.js configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a new transform to `@next/codemod` that updates `next.config.{js,ts}` that updates the deprecated `experimental.turbo` property to the new top-level `turbopack` property. Details: - Only operates on files called next.config.js or next.config.ts, in the case the codemod tool is run on a large codebase. - Updates _any_ object literal with a `experimental.turbo` property to use `turbopack`. Since this transform is constrained to Next.js configs, there shouldn’t be many false positives. - Much of this change was written by Copilot. I simplified it, and refactored it to make it more readable and safe. Test Plan: - `pnpm run build && pnpm test -- -t next-experimental-turbo-to-turbopack` in the codemod directory - Tested on several publicly available next configs that used the deprecated property --- packages/next-codemod/bin/transform.ts | 2 +- packages/next-codemod/lib/utils.ts | 5 + .../commonjs-var.input.js | 22 ++ .../commonjs-var.output.js | 24 ++ .../esm.input.js | 19 ++ .../esm.output.js | 21 ++ .../mixed-config.input.js | 23 ++ .../mixed-config.output.js | 25 ++ .../modified-var.input.js | 22 ++ .../modified-var.output.js | 23 ++ .../no-change.input.js | 1 + .../no-change.output.js | 1 + .../property-assignment.input.js | 23 ++ .../property-assignment.output.js | 24 ++ .../typescript-as-const.input.ts | 22 ++ .../typescript-as-const.output.ts | 24 ++ .../typescript-satisfies-wrapped.input.ts | 22 ++ .../typescript-satisfies-wrapped.output.ts | 24 ++ .../typescript-satisfies.input.ts | 21 ++ .../typescript-satisfies.output.ts | 23 ++ .../typescript.input.ts | 23 ++ .../typescript.output.ts | 26 ++ .../wrapped-function.input.js | 23 ++ .../wrapped-function.output.js | 25 ++ ...xt-experimental-turbo-to-turbopack.test.js | 17 + packages/next-codemod/transforms/lib/utils.ts | 12 + .../next-experimental-turbo-to-turbopack.ts | 314 ++++++++++++++++++ .../transforms/next-image-experimental.ts | 13 +- packages/next/src/server/config.ts | 2 +- 29 files changed, 816 insertions(+), 10 deletions(-) create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/commonjs-var.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/commonjs-var.output.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/esm.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/esm.output.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/mixed-config.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/mixed-config.output.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/modified-var.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/modified-var.output.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/no-change.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/no-change.output.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/property-assignment.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/property-assignment.output.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-as-const.input.ts create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-as-const.output.ts create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies-wrapped.input.ts create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies-wrapped.output.ts create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies.input.ts create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies.output.ts create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript.input.ts create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript.output.ts create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/wrapped-function.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/wrapped-function.output.js create mode 100644 packages/next-codemod/transforms/__tests__/next-experimental-turbo-to-turbopack.test.js create mode 100644 packages/next-codemod/transforms/lib/utils.ts create mode 100644 packages/next-codemod/transforms/next-experimental-turbo-to-turbopack.ts diff --git a/packages/next-codemod/bin/transform.ts b/packages/next-codemod/bin/transform.ts index 8fdaa90e12ff..e2096e53390c 100644 --- a/packages/next-codemod/bin/transform.ts +++ b/packages/next-codemod/bin/transform.ts @@ -129,7 +129,7 @@ export async function runTransform( if (verbose) { args.push('--verbose=2') } - args.push('--no-babel') + args.push('--parser=tsx') args.push('--ignore-pattern=**/node_modules/**') args.push('--ignore-pattern=**/.next/**') diff --git a/packages/next-codemod/lib/utils.ts b/packages/next-codemod/lib/utils.ts index 5baf18c31c2e..d0923ffe1c27 100644 --- a/packages/next-codemod/lib/utils.ts +++ b/packages/next-codemod/lib/utils.ts @@ -116,4 +116,9 @@ export const TRANSFORMER_INQUIRER_CHOICES = [ value: 'app-dir-runtime-config-experimental-edge', version: '15.0.0-canary.179', }, + { + title: 'Updates `next.config.js` to use the new `turbopack` configuration', + value: 'next-experimental-turbo-to-turbopack', + version: '10.0.0', + }, ] diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/commonjs-var.input.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/commonjs-var.input.js new file mode 100644 index 000000000000..a61082de15c8 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/commonjs-var.input.js @@ -0,0 +1,22 @@ +// CommonJS configuration with variable declaration first +const config = { + experimental: { + turbo: { + resolveAlias: { + underscore: 'lodash', + mocha: { browser: 'mocha/browser-entry.js' }, + }, + memoryLimit: 4096, + minify: true, + treeShaking: false, + sourceMaps: true + }, + serverActions: true, + typedRoutes: false + }, + images: { + formats: ['image/avif', 'image/webp'] + } +}; + +module.exports = config; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/commonjs-var.output.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/commonjs-var.output.js new file mode 100644 index 000000000000..39c9d19708d4 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/commonjs-var.output.js @@ -0,0 +1,24 @@ +// CommonJS configuration with variable declaration first +const config = { + experimental: { + serverActions: true, + typedRoutes: false, + turbopackMemoryLimit: 4096, + turbopackMinify: true, + turbopackTreeShaking: false, + turbopackSourceMaps: true + }, + + images: { + formats: ['image/avif', 'image/webp'] + }, + + turbopack: { + resolveAlias: { + underscore: 'lodash', + mocha: { browser: 'mocha/browser-entry.js' }, + } + } +}; + +module.exports = config; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/esm.input.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/esm.input.js new file mode 100644 index 000000000000..1f443eca873c --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/esm.input.js @@ -0,0 +1,19 @@ +export default { + experimental: { + turbo: { + resolveAlias: { + underscore: 'lodash', + mocha: { browser: 'mocha/browser-entry.js' }, + }, + memoryLimit: 4096, + minify: true, + treeShaking: false, + sourceMaps: true + }, + serverActions: true, + typedRoutes: false, + }, + images: { + formats: ['image/avif', 'image/webp'], + }, +} diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/esm.output.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/esm.output.js new file mode 100644 index 000000000000..bfeb635c9d5d --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/esm.output.js @@ -0,0 +1,21 @@ +export default { + experimental: { + serverActions: true, + typedRoutes: false, + turbopackMemoryLimit: 4096, + turbopackMinify: true, + turbopackTreeShaking: false, + turbopackSourceMaps: true + }, + + images: { + formats: ['image/avif', 'image/webp'], + }, + + turbopack: { + resolveAlias: { + underscore: 'lodash', + mocha: { browser: 'mocha/browser-entry.js' }, + } + } +}; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/mixed-config.input.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/mixed-config.input.js new file mode 100644 index 000000000000..a02f2e77f1c4 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/mixed-config.input.js @@ -0,0 +1,23 @@ +module.exports = { + experimental: { + turbo: { + resolveAlias: { + underscore: 'lodash', + mocha: { browser: 'mocha/browser-entry.js' }, + }, + memoryLimit: 4096, + minify: true, + treeShaking: false, + sourceMaps: true + }, + serverActions: true, + typedRoutes: false, + }, + images: { + formats: ['image/avif', 'image/webp'], + }, +} + +module.exports.turbopack.resolveAlias.chai = { + browser: 'chai/chai.js', +}; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/mixed-config.output.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/mixed-config.output.js new file mode 100644 index 000000000000..2a6fed08e5ff --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/mixed-config.output.js @@ -0,0 +1,25 @@ +module.exports = { + experimental: { + serverActions: true, + typedRoutes: false, + turbopackMemoryLimit: 4096, + turbopackMinify: true, + turbopackTreeShaking: false, + turbopackSourceMaps: true + }, + + images: { + formats: ['image/avif', 'image/webp'], + }, + + turbopack: { + resolveAlias: { + underscore: 'lodash', + mocha: { browser: 'mocha/browser-entry.js' }, + } + } +} + +module.exports.turbopack.resolveAlias.chai = { + browser: 'chai/chai.js', +}; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/modified-var.input.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/modified-var.input.js new file mode 100644 index 000000000000..7fbf84b1ff7b --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/modified-var.input.js @@ -0,0 +1,22 @@ +// CommonJS configuration with variable declaration and modification +const config = { + experimental: { + turbo: { + resolveAlias: { + underscore: 'lodash', + }, + memoryLimit: 4096, + }, + typedRoutes: true, + }, +}; + +// Add additional configuration before export +config.images = { + formats: ['image/avif', 'image/webp'] +}; + +// Add more to turbo config +config.experimental.turbo.sourceMaps = true; + +module.exports = config; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/modified-var.output.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/modified-var.output.js new file mode 100644 index 000000000000..6ebf9accb451 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/modified-var.output.js @@ -0,0 +1,23 @@ +// CommonJS configuration with variable declaration and modification +const config = { + experimental: { + typedRoutes: true, + turbopackMemoryLimit: 4096, + }, + + turbopack: { + resolveAlias: { + underscore: 'lodash', + } + } +}; + +// Add additional configuration before export +config.images = { + formats: ['image/avif', 'image/webp'] +}; + +// Add more to turbo config +config.experimental.turbopackSourceMaps = true; + +module.exports = config; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/no-change.input.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/no-change.input.js new file mode 100644 index 000000000000..fe01c7d76f0d --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/no-change.input.js @@ -0,0 +1 @@ +export const x = 3; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/no-change.output.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/no-change.output.js new file mode 100644 index 000000000000..fe01c7d76f0d --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/no-change.output.js @@ -0,0 +1 @@ +export const x = 3; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/property-assignment.input.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/property-assignment.input.js new file mode 100644 index 000000000000..9086395bbde3 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/property-assignment.input.js @@ -0,0 +1,23 @@ +// CommonJS with object property assignment +const config = { + experimental: { + turbo: { + resolveAlias: { + underscore: 'lodash', + } + }, + typedRoutes: true, + }, +}; + +// Add properties to the turbo object +config.experimental.turbo.resolveAlias.foo = 'bar'; +config.experimental.turbo.minify = true; +config.experimental.turbo.memoryLimit = 4096; + +// Add regular property +config.images = { + formats: ['image/avif', 'image/webp'], +}; + +module.exports = config; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/property-assignment.output.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/property-assignment.output.js new file mode 100644 index 000000000000..7300d481707c --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/property-assignment.output.js @@ -0,0 +1,24 @@ +// CommonJS with object property assignment +const config = { + experimental: { + typedRoutes: true + }, + + turbopack: { + resolveAlias: { + underscore: 'lodash', + } + } +}; + +// Add properties to the turbo object +config.turbopack.resolveAlias.foo = 'bar'; +config.experimental.turbopackMinify = true; +config.experimental.turbopackMemoryLimit = 4096; + +// Add regular property +config.images = { + formats: ['image/avif', 'image/webp'], +}; + +module.exports = config; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-as-const.input.ts b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-as-const.input.ts new file mode 100644 index 000000000000..e8f0de3e338d --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-as-const.input.ts @@ -0,0 +1,22 @@ +import type { NextConfig } from 'next'; + +const config = { + experimental: { + turbo: { + resolveAlias: { + underscore: 'lodash', + mocha: { browser: 'mocha/browser-entry.js' }, + }, + memoryLimit: 4096, + minify: true, + treeShaking: false, + sourceMaps: true, + }, + typedRoutes: true, + }, + images: { + formats: ['image/avif', 'image/webp'], + }, +} as const; + +export default config; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-as-const.output.ts b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-as-const.output.ts new file mode 100644 index 000000000000..26ae648d434a --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-as-const.output.ts @@ -0,0 +1,24 @@ +import type { NextConfig } from 'next'; + +const config = { + experimental: { + typedRoutes: true, + turbopackMemoryLimit: 4096, + turbopackMinify: true, + turbopackTreeShaking: false, + turbopackSourceMaps: true + }, + + images: { + formats: ['image/avif', 'image/webp'], + }, + + turbopack: { + resolveAlias: { + underscore: 'lodash', + mocha: { browser: 'mocha/browser-entry.js' }, + } + } +} as const; + +export default config; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies-wrapped.input.ts b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies-wrapped.input.ts new file mode 100644 index 000000000000..690edc74b3a5 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies-wrapped.input.ts @@ -0,0 +1,22 @@ +import type { NextConfig } from "next"; + +const nextConfig = { + experimental: { + mdxRs: true, + turbo: { + rules: { + "*.react.svg": { + loaders: ["@svgr/webpack"], + as: "*.js", + }, + }, + }, + }, + // Other config properties + webpack(config) { + return config; + }, +} satisfies NextConfig; + +const withMDX = require("@next/mdx")(); +export default withMDX(nextConfig); diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies-wrapped.output.ts b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies-wrapped.output.ts new file mode 100644 index 000000000000..9d0fc56f2dd8 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies-wrapped.output.ts @@ -0,0 +1,24 @@ +import type { NextConfig } from "next"; + +const nextConfig = { + experimental: { + mdxRs: true + }, + + // Other config properties + webpack(config) { + return config; + }, + + turbopack: { + rules: { + "*.react.svg": { + loaders: ["@svgr/webpack"], + as: "*.js", + }, + } + } +} satisfies NextConfig; + +const withMDX = require("@next/mdx")(); +export default withMDX(nextConfig); diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies.input.ts b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies.input.ts new file mode 100644 index 000000000000..4ff0c0a40d3c --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies.input.ts @@ -0,0 +1,21 @@ +import type { NextConfig } from "next"; + +const nextConfig = { + experimental: { + mdxRs: true, + turbo: { + rules: { + "*.react.svg": { + loaders: ["@svgr/webpack"], + as: "*.js", + }, + }, + }, + }, + // Other config properties + webpack(config) { + return config; + }, +} satisfies NextConfig; + +export default nextConfig; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies.output.ts b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies.output.ts new file mode 100644 index 000000000000..387333024b24 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript-satisfies.output.ts @@ -0,0 +1,23 @@ +import type { NextConfig } from "next"; + +const nextConfig = { + experimental: { + mdxRs: true + }, + + // Other config properties + webpack(config) { + return config; + }, + + turbopack: { + rules: { + "*.react.svg": { + loaders: ["@svgr/webpack"], + as: "*.js", + }, + } + } +} satisfies NextConfig; + +export default nextConfig; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript.input.ts b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript.input.ts new file mode 100644 index 000000000000..d46f592bc2e2 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript.input.ts @@ -0,0 +1,23 @@ +import type { NextConfig } from 'next'; + +const config: NextConfig = { + experimental: { + turbo: { + resolveAlias: { + underscore: 'lodash', + mocha: { browser: 'mocha/browser-entry.js' }, + }, + memoryLimit: 4096, + minify: true, + treeShaking: false, + sourceMaps: true, + }, + // Removed serverActions due to TypeScript compatibility + typedRoutes: true, + }, + images: { + formats: ['image/avif', 'image/webp'], + }, +}; + +export default config; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript.output.ts b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript.output.ts new file mode 100644 index 000000000000..60c8ef09095e --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/typescript.output.ts @@ -0,0 +1,26 @@ +import type { NextConfig } from 'next'; + +const config: NextConfig = { + experimental: { + // Removed serverActions due to TypeScript compatibility + typedRoutes: true, + + turbopackMemoryLimit: 4096, + turbopackMinify: true, + turbopackTreeShaking: false, + turbopackSourceMaps: true + }, + + images: { + formats: ['image/avif', 'image/webp'], + }, + + turbopack: { + resolveAlias: { + underscore: 'lodash', + mocha: { browser: 'mocha/browser-entry.js' }, + } + } +}; + +export default config; diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/wrapped-function.input.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/wrapped-function.input.js new file mode 100644 index 000000000000..02a8b119907c --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/wrapped-function.input.js @@ -0,0 +1,23 @@ +// Next.js config with wrapper function +const nextConfig = { + experimental: { + turbo: { + resolveAlias: { + underscore: 'lodash', + mocha: { browser: 'mocha/browser-entry.js' }, + }, + memoryLimit: 4096, + minify: true, + treeShaking: false, + sourceMaps: true + }, + typedRoutes: true, + }, + images: { + formats: ['image/avif', 'image/webp'], + }, +}; + +// Wrapper function +const withMDX = require("@next/mdx")(); +module.exports = withMDX(nextConfig); diff --git a/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/wrapped-function.output.js b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/wrapped-function.output.js new file mode 100644 index 000000000000..ee55b38d2bfa --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-experimental-turbo-to-turbopack/wrapped-function.output.js @@ -0,0 +1,25 @@ +// Next.js config with wrapper function +const nextConfig = { + experimental: { + typedRoutes: true, + turbopackMemoryLimit: 4096, + turbopackMinify: true, + turbopackTreeShaking: false, + turbopackSourceMaps: true + }, + + images: { + formats: ['image/avif', 'image/webp'], + }, + + turbopack: { + resolveAlias: { + underscore: 'lodash', + mocha: { browser: 'mocha/browser-entry.js' }, + } + } +}; + +// Wrapper function +const withMDX = require("@next/mdx")(); +module.exports = withMDX(nextConfig); diff --git a/packages/next-codemod/transforms/__tests__/next-experimental-turbo-to-turbopack.test.js b/packages/next-codemod/transforms/__tests__/next-experimental-turbo-to-turbopack.test.js new file mode 100644 index 000000000000..2ad7196db82c --- /dev/null +++ b/packages/next-codemod/transforms/__tests__/next-experimental-turbo-to-turbopack.test.js @@ -0,0 +1,17 @@ +/* global jest */ +jest.autoMockOff() +const defineTest = require('jscodeshift/dist/testUtils').defineTest + +const fixtureDir = 'next-experimental-turbo-to-turbopack' + +defineTest(__dirname, fixtureDir, null, `${fixtureDir}/commonjs-var`, { parser: 'js' }) +defineTest(__dirname, fixtureDir, null, `${fixtureDir}/esm`, { parser: 'js' }) +defineTest(__dirname, fixtureDir, null, `${fixtureDir}/mixed-config`, { parser: 'js' }) +defineTest(__dirname, fixtureDir, null, `${fixtureDir}/modified-var`, { parser: 'js' }) +defineTest(__dirname, fixtureDir, null, `${fixtureDir}/wrapped-function`, { parser: 'js' }) +defineTest(__dirname, fixtureDir, null, `${fixtureDir}/no-change`, { parser: 'js' }) +defineTest(__dirname, fixtureDir, null, `${fixtureDir}/property-assignment`, { parser: 'js' }) +defineTest(__dirname, fixtureDir, null, `${fixtureDir}/typescript-as-const`, { parser: 'ts' }) +defineTest(__dirname, fixtureDir, null, `${fixtureDir}/typescript`, { parser: 'ts' }) +defineTest(__dirname, fixtureDir, null, `${fixtureDir}/typescript-satisfies`, { parser: 'ts' }) +defineTest(__dirname, fixtureDir, null, `${fixtureDir}/typescript-satisfies-wrapped`, { parser: 'ts' }) diff --git a/packages/next-codemod/transforms/lib/utils.ts b/packages/next-codemod/transforms/lib/utils.ts new file mode 100644 index 000000000000..79d8b11c5ea8 --- /dev/null +++ b/packages/next-codemod/transforms/lib/utils.ts @@ -0,0 +1,12 @@ +import type { FileInfo } from 'jscodeshift' +import path from 'node:path' + +export function isNextConfigFile(file: FileInfo): boolean { + const parsed = path.parse(file.path || '/') + return ( + parsed.base === 'next.config.js' || + parsed.base === 'next.config.ts' || + parsed.base === 'next.config.mjs' || + parsed.base === 'next.config.cjs' + ) +} diff --git a/packages/next-codemod/transforms/next-experimental-turbo-to-turbopack.ts b/packages/next-codemod/transforms/next-experimental-turbo-to-turbopack.ts new file mode 100644 index 000000000000..8469f28dbc1d --- /dev/null +++ b/packages/next-codemod/transforms/next-experimental-turbo-to-turbopack.ts @@ -0,0 +1,314 @@ +/* + * This codemod transforms the experimental turbo configuration in Next.js config to + * the new top-level `turbopack` configuration. + * + * It moves most properties from experimental.turbo to the top-level turbopack + * property, with special handling for certain properties like memoryLimit, minify, + * treeShaking, and sourceMaps which become experimental.turbopack* properties instead. + */ + +import type { + API as JSCodeShiftAPI, + Options as JSCodeShiftOptions, + FileInfo, + ObjectExpression, + Property, + ObjectProperty, + SpreadElement, + SpreadProperty, + ObjectMethod, +} from 'jscodeshift' +import { createParserFromPath } from '../lib/parser' +import { isNextConfigFile } from './lib/utils' + +// Properties that need to be moved to experimental.turbopack* +const RENAMED_EXPERIMENTAL_PROPERTIES = { + memoryLimit: 'turbopackMemoryLimit', + minify: 'turbopackMinify', + treeShaking: 'turbopackTreeShaking', + sourceMaps: 'turbopackSourceMaps', +} + +export default function transformer( + file: FileInfo, + _api: JSCodeShiftAPI, + options: JSCodeShiftOptions +): string { + const j = createParserFromPath(file.path) + const root = j(file.source) + let hasChanges = false + + if ( + !isNextConfigFile(file) && + process.env.NODE_ENV !== 'test' // fixtures have unique basenames in test + ) { + return file.source + } + + // Process a config object once we find it + function processConfigObject(configObj: ObjectExpression): boolean { + // Check for `experimental` property in the config + const experimentalProp = configObj.properties.find( + (prop) => + isStaticProperty(prop) && + prop.key && + prop.key.type === 'Identifier' && + prop.key.name === 'experimental' + ) + + if (!experimentalProp || !isStaticProperty(experimentalProp)) { + return false + } + + const experimentalObj = experimentalProp.value + if (experimentalObj.type !== 'ObjectExpression') { + return false + } + + // Check for `experimental.turbo` property in the config + const turboProp = experimentalObj.properties.find( + (prop) => + isStaticProperty(prop) && + prop.key && + prop.key.type === 'Identifier' && + prop.key.name === 'turbo' + ) + + if (!turboProp || !isStaticProperty(turboProp)) { + return false + } + + const turboObj = turboProp.value + if (turboObj.type !== 'ObjectExpression') { + return false + } + + const regularProps = [] + const specialProps = [] + + turboObj.properties.forEach((prop) => { + if ( + isStaticProperty(prop) && + prop.key && + prop.key.type === 'Identifier' && + RENAMED_EXPERIMENTAL_PROPERTIES[prop.key.name] + ) { + // Create a new property with the renamed key + specialProps.push( + j.objectProperty( + j.identifier(RENAMED_EXPERIMENTAL_PROPERTIES[prop.key.name]), + prop.value + ) + ) + } else { + // Keep the property for turbopack + regularProps.push(prop) + } + }) + + const existingProps = experimentalObj.properties.filter( + (prop) => + !( + isStaticProperty(prop) && + prop.key && + prop.key.type === 'Identifier' && + prop.key.name === 'turbo' + ) + ) + + experimentalObj.properties = [...existingProps, ...specialProps] + + // If experimental has no properties, remove it + if (experimentalObj.properties.length === 0) { + configObj.properties = configObj.properties.filter( + (prop) => + !( + isStaticProperty(prop) && + prop.key && + prop.key.type === 'Identifier' && + prop.key.name === 'experimental' + ) + ) + } + + // Add turbopack property at top level if there are regular props + if (regularProps.length > 0) { + // Create the turbopack property + const turbopackProp = j.objectProperty( + j.identifier('turbopack'), + j.objectExpression(regularProps) + ) + + configObj.properties.push(turbopackProp) + } + + return true + } + + root.find(j.ObjectExpression).forEach((path) => { + if (processConfigObject(path.value)) { + hasChanges = true + } + }) + + // Transform config.experimental.turbo.X = value to config.turbopack.X = value + // or config.experimental.turbopackX = value for special properties + root + .find(j.AssignmentExpression, { + left: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + property: { type: 'Identifier', name: 'experimental' }, + }, + property: { type: 'Identifier', name: 'turbo' }, + }, + }, + }) + .forEach((path) => { + if (path.node.left.type !== 'MemberExpression') return + + // Get the variable name (e.g., config in config.experimental.turbo.sourceMaps) + let varName = null + let currentPath = path.node.left.object + while (currentPath?.type === 'MemberExpression') { + currentPath = currentPath.object + } + if (currentPath?.type === 'Identifier') { + varName = currentPath.name + } + + if (!varName) return + + // Get the property name being assigned (e.g., sourceMaps) + let propName: string | undefined = undefined + if ( + path.node.left.property && + path.node.left.property.type === 'Identifier' + ) { + propName = path.node.left.property.name + } else { + return + } + + // For special properties like memoryLimit, minify, etc. + if (propName && RENAMED_EXPERIMENTAL_PROPERTIES[propName]) { + const newAssignment = j.assignmentExpression( + '=', + j.memberExpression( + j.memberExpression( + j.identifier(varName), + j.identifier('experimental') + ), + j.identifier(RENAMED_EXPERIMENTAL_PROPERTIES[propName]) + ), + path.node.right + ) + + j(path).replaceWith(newAssignment) + hasChanges = true + } else if (propName) { + // Create new assignment: config.turbopack.propName = value + const newAssignment = j.assignmentExpression( + '=', + j.memberExpression( + j.memberExpression( + j.identifier(varName), + j.identifier('turbopack') + ), + j.identifier(propName) + ), + path.node.right + ) + + j(path).replaceWith(newAssignment) + hasChanges = true + } + }) + + // For nested property assignments like config.experimental.turbo.resolveAlias.foo = 'bar'; + root.find(j.AssignmentExpression).forEach((path) => { + if (path.node.left.type !== 'MemberExpression') return + + // Build a path to check if this is like `experimental.turbo.resolveAlias.foo` + let obj = path.node.left.object + let props = [] + + // Collect the property chain + while (obj && obj.type === 'MemberExpression') { + if (obj.property && obj.property.type === 'Identifier') { + props.unshift(obj.property.name) + } + obj = obj.object + } + + // Get the root variable name (e.g., 'config') + let varName = null + if (obj && obj.type === 'Identifier') { + varName = obj.name + } + + if (!varName) return + + // Check if this matches the pattern: config.experimental.turbo.resolveAlias.foo + if ( + props.length >= 3 && + props[0] === 'experimental' && + props[1] === 'turbo' + ) { + // Get the final property name, only if it's an Identifier + let finalProp: string | undefined = undefined + if ( + path.node.left.property && + path.node.left.property.type === 'Identifier' + ) { + finalProp = path.node.left.property.name + } else { + // If not an Identifier, skip this assignment + return + } + + // The properties after 'turbo' + const middleProps = props.slice(2) // e.g. ['resolveAlias'] + + // Start building the new left side: config.turbopack + let newLeft = j.memberExpression( + j.identifier(varName), + j.identifier('turbopack') + ) + + // Add the middle properties + for (const prop of middleProps) { + newLeft = j.memberExpression(newLeft, j.identifier(prop)) + } + + // Add the final property + newLeft = j.memberExpression(newLeft, j.identifier(finalProp)) + + const newAssignment = j.assignmentExpression( + '=', + newLeft, + path.node.right + ) + + j(path).replaceWith(newAssignment) + hasChanges = true + } + }) + + // Only return a string if we changed the AST, otherwise return the original source + return hasChanges ? root.toSource(options) : file.source +} + +function isStaticProperty( + prop: + | Property + | ObjectProperty + | SpreadElement + | SpreadProperty + | ObjectMethod +): prop is Property | ObjectProperty { + return prop.type === 'Property' || prop.type === 'ObjectProperty' +} diff --git a/packages/next-codemod/transforms/next-image-experimental.ts b/packages/next-codemod/transforms/next-image-experimental.ts index 0809b4638688..a0d63a63a991 100644 --- a/packages/next-codemod/transforms/next-image-experimental.ts +++ b/packages/next-codemod/transforms/next-image-experimental.ts @@ -10,6 +10,7 @@ import type { Options, } from 'jscodeshift' import { createParserFromPath } from '../lib/parser' +import { isNextConfigFile } from './lib/utils' function findAndReplaceProps( j: JSCodeshift, @@ -262,16 +263,12 @@ export default function transformer( const j = createParserFromPath(file.path) const root = j(file.source) - const parsed = parse(file.path || '/') - const isConfig = - parsed.base === 'next.config.js' || - parsed.base === 'next.config.ts' || - parsed.base === 'next.config.mjs' || - parsed.base === 'next.config.cjs' + const isConfig = isNextConfigFile(file) if (isConfig) { - const result = nextConfigTransformer(j, root, parsed.dir) - return result.toSource() + const fileDir = parse(file.path).dir + const result = nextConfigTransformer(j, root, fileDir) + return result.toSource(options) } // Before: import Image from "next/legacy/image" diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 7d50e7a82a04..79d68a78e1fe 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -1480,7 +1480,7 @@ export default async function loadConfig( if (userConfig.experimental?.turbo) { curLog.warn( - 'The config property `experimental.turbo` is deprecated. Move this setting to `config.turbopack` as Turbopack is now stable.' + 'The config property `experimental.turbo` is deprecated. Move this setting to `config.turbopack` or run `npx @next/codemod@latest next-experimental-turbo-to-turbopack .`' ) // Merge the two configs, preferring values in `config.turbopack`.