@next/codemod 15.4.7 → 15.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,4 +6,4 @@ Codemods are transformations that run on your codebase programmatically. This al
6
6
 
7
7
  ## Documentation
8
8
 
9
- Visit [nextjs.org/docs/advanced-features/codemods](https://nextjs.org/docs/advanced-features/codemods) to view the documentation for this package.
9
+ Visit [nextjs.org/docs/advanced-features/codemods](https://nextjs.org/docs/app/guides/upgrading/codemods) to view the documentation for this package.
package/bin/transform.js CHANGED
@@ -78,6 +78,10 @@ async function runTransform(transform, path, options) {
78
78
  // cra-to-next transform doesn't use jscodeshift directly
79
79
  return require(transformerPath).default(filesExpanded, options);
80
80
  }
81
+ if (transformer === 'next-lint-to-eslint-cli') {
82
+ // next-lint-to-eslint-cli transform doesn't use jscodeshift directly
83
+ return require(transformerPath).default(filesExpanded, options);
84
+ }
81
85
  let args = [];
82
86
  const { dry, print, runInBand, jscodeshift, verbose } = options;
83
87
  if (dry) {
@@ -92,7 +96,7 @@ async function runTransform(transform, path, options) {
92
96
  if (verbose) {
93
97
  args.push('--verbose=2');
94
98
  }
95
- args.push('--no-babel');
99
+ args.push('--parser=tsx');
96
100
  args.push('--ignore-pattern=**/node_modules/**');
97
101
  args.push('--ignore-pattern=**/.next/**');
98
102
  args.push('--extensions=tsx,ts,jsx,js');
@@ -35,6 +35,8 @@ function getPkgManager(baseDir) {
35
35
  return 'npm';
36
36
  }
37
37
  }
38
+ // No lock file found, default to npm
39
+ return 'npm';
38
40
  }
39
41
  catch {
40
42
  return 'npm';
package/lib/utils.js CHANGED
@@ -106,5 +106,15 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
106
106
  value: 'app-dir-runtime-config-experimental-edge',
107
107
  version: '15.0.0-canary.179',
108
108
  },
109
+ {
110
+ title: 'Updates `next.config.js` to use the new `turbopack` configuration',
111
+ value: 'next-experimental-turbo-to-turbopack',
112
+ version: '10.0.0',
113
+ },
114
+ {
115
+ title: 'Migrate from `next lint` to the ESLint CLI',
116
+ value: 'next-lint-to-eslint-cli',
117
+ version: '16.0.0',
118
+ },
109
119
  ];
110
120
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "15.4.7",
3
+ "version": "15.5.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isNextConfigFile = isNextConfigFile;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ function isNextConfigFile(file) {
9
+ const parsed = node_path_1.default.parse(file.path || '/');
10
+ return (parsed.base === 'next.config.js' ||
11
+ parsed.base === 'next.config.ts' ||
12
+ parsed.base === 'next.config.mjs' ||
13
+ parsed.base === 'next.config.cjs');
14
+ }
15
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ /*
3
+ * This codemod transforms the experimental turbo configuration in Next.js config to
4
+ * the new top-level `turbopack` configuration.
5
+ *
6
+ * It moves most properties from experimental.turbo to the top-level turbopack
7
+ * property, with special handling for certain properties like memoryLimit, minify,
8
+ * treeShaking, and sourceMaps which become experimental.turbopack* properties instead.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.default = transformer;
12
+ const parser_1 = require("../lib/parser");
13
+ const utils_1 = require("./lib/utils");
14
+ // Properties that need to be moved to experimental.turbopack*
15
+ const RENAMED_EXPERIMENTAL_PROPERTIES = {
16
+ memoryLimit: 'turbopackMemoryLimit',
17
+ minify: 'turbopackMinify',
18
+ treeShaking: 'turbopackTreeShaking',
19
+ sourceMaps: 'turbopackSourceMaps',
20
+ };
21
+ function transformer(file, _api, options) {
22
+ const j = (0, parser_1.createParserFromPath)(file.path);
23
+ const root = j(file.source);
24
+ let hasChanges = false;
25
+ if (!(0, utils_1.isNextConfigFile)(file) &&
26
+ process.env.NODE_ENV !== 'test' // fixtures have unique basenames in test
27
+ ) {
28
+ return file.source;
29
+ }
30
+ // Process a config object once we find it
31
+ function processConfigObject(configObj) {
32
+ // Check for `experimental` property in the config
33
+ const experimentalProp = configObj.properties.find((prop) => isStaticProperty(prop) &&
34
+ prop.key &&
35
+ prop.key.type === 'Identifier' &&
36
+ prop.key.name === 'experimental');
37
+ if (!experimentalProp || !isStaticProperty(experimentalProp)) {
38
+ return false;
39
+ }
40
+ const experimentalObj = experimentalProp.value;
41
+ if (experimentalObj.type !== 'ObjectExpression') {
42
+ return false;
43
+ }
44
+ // Check for `experimental.turbo` property in the config
45
+ const turboProp = experimentalObj.properties.find((prop) => isStaticProperty(prop) &&
46
+ prop.key &&
47
+ prop.key.type === 'Identifier' &&
48
+ prop.key.name === 'turbo');
49
+ if (!turboProp || !isStaticProperty(turboProp)) {
50
+ return false;
51
+ }
52
+ const turboObj = turboProp.value;
53
+ if (turboObj.type !== 'ObjectExpression') {
54
+ return false;
55
+ }
56
+ const regularProps = [];
57
+ const specialProps = [];
58
+ turboObj.properties.forEach((prop) => {
59
+ if (isStaticProperty(prop) &&
60
+ prop.key &&
61
+ prop.key.type === 'Identifier' &&
62
+ RENAMED_EXPERIMENTAL_PROPERTIES[prop.key.name]) {
63
+ // Create a new property with the renamed key
64
+ specialProps.push(j.objectProperty(j.identifier(RENAMED_EXPERIMENTAL_PROPERTIES[prop.key.name]), prop.value));
65
+ }
66
+ else {
67
+ // Keep the property for turbopack
68
+ regularProps.push(prop);
69
+ }
70
+ });
71
+ const existingProps = experimentalObj.properties.filter((prop) => !(isStaticProperty(prop) &&
72
+ prop.key &&
73
+ prop.key.type === 'Identifier' &&
74
+ prop.key.name === 'turbo'));
75
+ experimentalObj.properties = [...existingProps, ...specialProps];
76
+ // If experimental has no properties, remove it
77
+ if (experimentalObj.properties.length === 0) {
78
+ configObj.properties = configObj.properties.filter((prop) => !(isStaticProperty(prop) &&
79
+ prop.key &&
80
+ prop.key.type === 'Identifier' &&
81
+ prop.key.name === 'experimental'));
82
+ }
83
+ // Add turbopack property at top level if there are regular props
84
+ if (regularProps.length > 0) {
85
+ // Create the turbopack property
86
+ const turbopackProp = j.objectProperty(j.identifier('turbopack'), j.objectExpression(regularProps));
87
+ configObj.properties.push(turbopackProp);
88
+ }
89
+ return true;
90
+ }
91
+ root.find(j.ObjectExpression).forEach((path) => {
92
+ if (processConfigObject(path.value)) {
93
+ hasChanges = true;
94
+ }
95
+ });
96
+ // Transform config.experimental.turbo.X = value to config.turbopack.X = value
97
+ // or config.experimental.turbopackX = value for special properties
98
+ root
99
+ .find(j.AssignmentExpression, {
100
+ left: {
101
+ type: 'MemberExpression',
102
+ object: {
103
+ type: 'MemberExpression',
104
+ object: {
105
+ type: 'MemberExpression',
106
+ property: { type: 'Identifier', name: 'experimental' },
107
+ },
108
+ property: { type: 'Identifier', name: 'turbo' },
109
+ },
110
+ },
111
+ })
112
+ .forEach((path) => {
113
+ if (path.node.left.type !== 'MemberExpression')
114
+ return;
115
+ // Get the variable name (e.g., config in config.experimental.turbo.sourceMaps)
116
+ let varName = null;
117
+ let currentPath = path.node.left.object;
118
+ while (currentPath?.type === 'MemberExpression') {
119
+ currentPath = currentPath.object;
120
+ }
121
+ if (currentPath?.type === 'Identifier') {
122
+ varName = currentPath.name;
123
+ }
124
+ if (!varName)
125
+ return;
126
+ // Get the property name being assigned (e.g., sourceMaps)
127
+ let propName = undefined;
128
+ if (path.node.left.property &&
129
+ path.node.left.property.type === 'Identifier') {
130
+ propName = path.node.left.property.name;
131
+ }
132
+ else {
133
+ return;
134
+ }
135
+ // For special properties like memoryLimit, minify, etc.
136
+ if (propName && RENAMED_EXPERIMENTAL_PROPERTIES[propName]) {
137
+ const newAssignment = j.assignmentExpression('=', j.memberExpression(j.memberExpression(j.identifier(varName), j.identifier('experimental')), j.identifier(RENAMED_EXPERIMENTAL_PROPERTIES[propName])), path.node.right);
138
+ j(path).replaceWith(newAssignment);
139
+ hasChanges = true;
140
+ }
141
+ else if (propName) {
142
+ // Create new assignment: config.turbopack.propName = value
143
+ const newAssignment = j.assignmentExpression('=', j.memberExpression(j.memberExpression(j.identifier(varName), j.identifier('turbopack')), j.identifier(propName)), path.node.right);
144
+ j(path).replaceWith(newAssignment);
145
+ hasChanges = true;
146
+ }
147
+ });
148
+ // For nested property assignments like config.experimental.turbo.resolveAlias.foo = 'bar';
149
+ root.find(j.AssignmentExpression).forEach((path) => {
150
+ if (path.node.left.type !== 'MemberExpression')
151
+ return;
152
+ // Build a path to check if this is like `experimental.turbo.resolveAlias.foo`
153
+ let obj = path.node.left.object;
154
+ let props = [];
155
+ // Collect the property chain
156
+ while (obj && obj.type === 'MemberExpression') {
157
+ if (obj.property && obj.property.type === 'Identifier') {
158
+ props.unshift(obj.property.name);
159
+ }
160
+ obj = obj.object;
161
+ }
162
+ // Get the root variable name (e.g., 'config')
163
+ let varName = null;
164
+ if (obj && obj.type === 'Identifier') {
165
+ varName = obj.name;
166
+ }
167
+ if (!varName)
168
+ return;
169
+ // Check if this matches the pattern: config.experimental.turbo.resolveAlias.foo
170
+ if (props.length >= 3 &&
171
+ props[0] === 'experimental' &&
172
+ props[1] === 'turbo') {
173
+ // Get the final property name, only if it's an Identifier
174
+ let finalProp = undefined;
175
+ if (path.node.left.property &&
176
+ path.node.left.property.type === 'Identifier') {
177
+ finalProp = path.node.left.property.name;
178
+ }
179
+ else {
180
+ // If not an Identifier, skip this assignment
181
+ return;
182
+ }
183
+ // The properties after 'turbo'
184
+ const middleProps = props.slice(2); // e.g. ['resolveAlias']
185
+ // Start building the new left side: config.turbopack
186
+ let newLeft = j.memberExpression(j.identifier(varName), j.identifier('turbopack'));
187
+ // Add the middle properties
188
+ for (const prop of middleProps) {
189
+ newLeft = j.memberExpression(newLeft, j.identifier(prop));
190
+ }
191
+ // Add the final property
192
+ newLeft = j.memberExpression(newLeft, j.identifier(finalProp));
193
+ const newAssignment = j.assignmentExpression('=', newLeft, path.node.right);
194
+ j(path).replaceWith(newAssignment);
195
+ hasChanges = true;
196
+ }
197
+ });
198
+ // Only return a string if we changed the AST, otherwise return the original source
199
+ return hasChanges ? root.toSource(options) : file.source;
200
+ }
201
+ function isStaticProperty(prop) {
202
+ return prop.type === 'Property' || prop.type === 'ObjectProperty';
203
+ }
204
+ //# sourceMappingURL=next-experimental-turbo-to-turbopack.js.map
@@ -4,6 +4,7 @@ exports.default = transformer;
4
4
  const path_1 = require("path");
5
5
  const fs_1 = require("fs");
6
6
  const parser_1 = require("../lib/parser");
7
+ const utils_1 = require("./lib/utils");
7
8
  function findAndReplaceProps(j, root, tagName) {
8
9
  const layoutToStyle = {
9
10
  intrinsic: { maxWidth: '100%', height: 'auto' },
@@ -192,14 +193,11 @@ function nextConfigTransformer(j, root, appDir) {
192
193
  function transformer(file, _api, options) {
193
194
  const j = (0, parser_1.createParserFromPath)(file.path);
194
195
  const root = j(file.source);
195
- const parsed = (0, path_1.parse)(file.path || '/');
196
- const isConfig = parsed.base === 'next.config.js' ||
197
- parsed.base === 'next.config.ts' ||
198
- parsed.base === 'next.config.mjs' ||
199
- parsed.base === 'next.config.cjs';
196
+ const isConfig = (0, utils_1.isNextConfigFile)(file);
200
197
  if (isConfig) {
201
- const result = nextConfigTransformer(j, root, parsed.dir);
202
- return result.toSource();
198
+ const fileDir = (0, path_1.parse)(file.path).dir;
199
+ const result = nextConfigTransformer(j, root, fileDir);
200
+ return result.toSource(options);
203
201
  }
204
202
  // Before: import Image from "next/legacy/image"
205
203
  // After: import Image from "next/image"
@@ -0,0 +1,710 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.prefixes = void 0;
7
+ exports.default = transformer;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const handle_package_1 = require("../lib/handle-package");
11
+ const parser_1 = require("../lib/parser");
12
+ const picocolors_1 = require("picocolors");
13
+ exports.prefixes = {
14
+ wait: (0, picocolors_1.white)((0, picocolors_1.bold)('○')),
15
+ error: (0, picocolors_1.red)((0, picocolors_1.bold)('⨯')),
16
+ warn: (0, picocolors_1.yellow)((0, picocolors_1.bold)('⚠')),
17
+ ready: '▲', // no color
18
+ info: (0, picocolors_1.white)((0, picocolors_1.bold)(' ')),
19
+ event: (0, picocolors_1.green)((0, picocolors_1.bold)('✓')),
20
+ trace: (0, picocolors_1.magenta)((0, picocolors_1.bold)('»')),
21
+ };
22
+ const ESLINT_CONFIG_TEMPLATE_TYPESCRIPT = `\
23
+ import { dirname } from "path";
24
+ import { fileURLToPath } from "url";
25
+ import { FlatCompat } from "@eslint/eslintrc";
26
+
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = dirname(__filename);
29
+
30
+ const compat = new FlatCompat({
31
+ baseDirectory: __dirname,
32
+ });
33
+
34
+ const eslintConfig = [
35
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
36
+ {
37
+ ignores: [
38
+ "node_modules/**",
39
+ ".next/**",
40
+ "out/**",
41
+ "build/**",
42
+ "next-env.d.ts",
43
+ ],
44
+ },
45
+ ];
46
+
47
+ export default eslintConfig;
48
+ `;
49
+ const ESLINT_CONFIG_TEMPLATE_JAVASCRIPT = `import { dirname } from "path";
50
+ import { fileURLToPath } from "url";
51
+ import { FlatCompat } from "@eslint/eslintrc";
52
+
53
+ const __filename = fileURLToPath(import.meta.url);
54
+ const __dirname = dirname(__filename);
55
+
56
+ const compat = new FlatCompat({
57
+ baseDirectory: __dirname,
58
+ });
59
+
60
+ const eslintConfig = [
61
+ ...compat.extends("next/core-web-vitals"),
62
+ {
63
+ ignores: [
64
+ "node_modules/**",
65
+ ".next/**",
66
+ "out/**",
67
+ "build/**",
68
+ "next-env.d.ts",
69
+ ],
70
+ },
71
+ ];
72
+
73
+ export default eslintConfig;
74
+ `;
75
+ function detectTypeScript(projectRoot) {
76
+ return (0, node_fs_1.existsSync)(node_path_1.default.join(projectRoot, 'tsconfig.json'));
77
+ }
78
+ function findExistingEslintConfig(projectRoot) {
79
+ const flatConfigs = [
80
+ 'eslint.config.js',
81
+ 'eslint.config.mjs',
82
+ 'eslint.config.cjs',
83
+ 'eslint.config.ts',
84
+ 'eslint.config.mts',
85
+ 'eslint.config.cts',
86
+ ];
87
+ const legacyConfigs = [
88
+ '.eslintrc.js',
89
+ '.eslintrc.cjs',
90
+ '.eslintrc.yaml',
91
+ '.eslintrc.yml',
92
+ '.eslintrc.json',
93
+ '.eslintrc',
94
+ ];
95
+ // Check for flat configs first (preferred for v9+)
96
+ for (const config of flatConfigs) {
97
+ const configPath = node_path_1.default.join(projectRoot, config);
98
+ if ((0, node_fs_1.existsSync)(configPath)) {
99
+ return { exists: true, path: configPath, isFlat: true };
100
+ }
101
+ }
102
+ // Check for legacy configs
103
+ for (const config of legacyConfigs) {
104
+ const configPath = node_path_1.default.join(projectRoot, config);
105
+ if ((0, node_fs_1.existsSync)(configPath)) {
106
+ return { exists: true, path: configPath, isFlat: false };
107
+ }
108
+ }
109
+ return { exists: false };
110
+ }
111
+ function updateExistingFlatConfig(configPath, isTypeScript) {
112
+ let configContent;
113
+ try {
114
+ configContent = (0, node_fs_1.readFileSync)(configPath, 'utf8');
115
+ }
116
+ catch (error) {
117
+ console.error(` Error reading config file: ${error}`);
118
+ return false;
119
+ }
120
+ // Check if Next.js configs are already imported
121
+ const hasNextConfigs = configContent.includes('next/core-web-vitals') ||
122
+ configContent.includes('next/typescript');
123
+ // TypeScript config files need special handling
124
+ if (configPath.endsWith('.ts') ||
125
+ configPath.endsWith('.mts') ||
126
+ configPath.endsWith('.cts')) {
127
+ console.warn(exports.prefixes.warn, ' TypeScript config files require manual migration');
128
+ console.log(' Please add the following to your config:');
129
+ console.log(' - Import: import { FlatCompat } from "@eslint/eslintrc"');
130
+ console.log(' - Extend: ...compat.extends("next/core-web-vitals"' +
131
+ (isTypeScript ? ', "next/typescript"' : '') +
132
+ ')');
133
+ return false;
134
+ }
135
+ // Parse the file using jscodeshift
136
+ const j = (0, parser_1.createParserFromPath)(configPath);
137
+ const root = j(configContent);
138
+ // Determine if it's CommonJS or ES modules
139
+ let isCommonJS = false;
140
+ if (configPath.endsWith('.cjs')) {
141
+ isCommonJS = true;
142
+ }
143
+ else if (configPath.endsWith('.mjs')) {
144
+ isCommonJS = false;
145
+ }
146
+ else if (configPath.endsWith('.js')) {
147
+ // For .js files, check package.json type field
148
+ const projectRoot = node_path_1.default.dirname(configPath);
149
+ const packageJsonPath = node_path_1.default.join(projectRoot, 'package.json');
150
+ try {
151
+ if ((0, node_fs_1.existsSync)(packageJsonPath)) {
152
+ const packageJson = JSON.parse((0, node_fs_1.readFileSync)(packageJsonPath, 'utf8'));
153
+ isCommonJS = packageJson.type !== 'module';
154
+ }
155
+ else {
156
+ // Default to CommonJS if no package.json found
157
+ isCommonJS = true;
158
+ }
159
+ }
160
+ catch {
161
+ // Default to CommonJS if package.json can't be read
162
+ isCommonJS = true;
163
+ }
164
+ // Always check file syntax to override package.json detection if needed
165
+ // This handles cases where package.json doesn't specify type but file uses ES modules
166
+ const hasESModuleSyntax = root.find(j.ExportDefaultDeclaration).size() > 0 ||
167
+ root.find(j.ExportNamedDeclaration).size() > 0 ||
168
+ root.find(j.ImportDeclaration).size() > 0;
169
+ const hasCommonJSSyntax = root
170
+ .find(j.AssignmentExpression, {
171
+ left: {
172
+ type: 'MemberExpression',
173
+ object: { name: 'module' },
174
+ property: { name: 'exports' },
175
+ },
176
+ })
177
+ .size() > 0;
178
+ // Override package.json detection based on actual syntax
179
+ if (hasESModuleSyntax && !hasCommonJSSyntax) {
180
+ isCommonJS = false;
181
+ }
182
+ else if (hasCommonJSSyntax && !hasESModuleSyntax) {
183
+ isCommonJS = true;
184
+ }
185
+ // If both or neither are found, keep the package.json-based detection
186
+ }
187
+ else {
188
+ // For other extensions (.ts, .mts, .cts), assume based on extension
189
+ isCommonJS = configPath.endsWith('.cts');
190
+ }
191
+ // Find the exported array
192
+ let exportedArray = null;
193
+ let exportNode = null;
194
+ if (isCommonJS) {
195
+ // Look for module.exports = [...]
196
+ const moduleExports = root.find(j.AssignmentExpression, {
197
+ left: {
198
+ type: 'MemberExpression',
199
+ object: { name: 'module' },
200
+ property: { name: 'exports' },
201
+ },
202
+ right: { type: 'ArrayExpression' },
203
+ });
204
+ if (moduleExports.size() > 0) {
205
+ exportNode = moduleExports.at(0);
206
+ exportedArray = exportNode.get('right');
207
+ }
208
+ }
209
+ else {
210
+ // Look for export default [...]
211
+ const defaultExports = root.find(j.ExportDefaultDeclaration, {
212
+ declaration: { type: 'ArrayExpression' },
213
+ });
214
+ if (defaultExports.size() > 0) {
215
+ exportNode = defaultExports.at(0);
216
+ exportedArray = exportNode.get('declaration');
217
+ }
218
+ else {
219
+ // Look for const variable = [...]; export default variable
220
+ const defaultExportIdentifier = root.find(j.ExportDefaultDeclaration, {
221
+ declaration: { type: 'Identifier' },
222
+ });
223
+ if (defaultExportIdentifier.size() > 0) {
224
+ const declarationNode = defaultExportIdentifier.at(0).get('declaration');
225
+ if (!declarationNode.value) {
226
+ return false;
227
+ }
228
+ const varName = declarationNode.value.name;
229
+ const varDeclaration = root.find(j.VariableDeclarator, {
230
+ id: { name: varName },
231
+ init: { type: 'ArrayExpression' },
232
+ });
233
+ if (varDeclaration.size() > 0) {
234
+ exportedArray = varDeclaration.at(0).get('init');
235
+ }
236
+ }
237
+ }
238
+ }
239
+ if (!exportedArray) {
240
+ console.warn(exports.prefixes.warn, ' Config does not export an array. Manual migration required.');
241
+ console.warn(exports.prefixes.warn, ' ESLint flat configs must export an array of configuration objects.');
242
+ return false;
243
+ }
244
+ // Check if FlatCompat is already imported
245
+ const hasFlatCompat = isCommonJS
246
+ ? root
247
+ .find(j.CallExpression, {
248
+ callee: { name: 'require' },
249
+ arguments: [{ value: '@eslint/eslintrc' }],
250
+ })
251
+ .size() > 0
252
+ : root
253
+ .find(j.ImportDeclaration, {
254
+ source: { value: '@eslint/eslintrc' },
255
+ })
256
+ .size() > 0;
257
+ // Add necessary imports if not present and if we're adding Next.js extends
258
+ if (!hasFlatCompat && !hasNextConfigs) {
259
+ if (isCommonJS) {
260
+ // Add CommonJS requires at the top
261
+ const firstNode = root.find(j.Program).get('body', 0);
262
+ const compatRequire = j.variableDeclaration('const', [
263
+ j.variableDeclarator(j.objectPattern([
264
+ j.property('init', j.identifier('FlatCompat'), j.identifier('FlatCompat')),
265
+ ]), j.callExpression(j.identifier('require'), [
266
+ j.literal('@eslint/eslintrc'),
267
+ ])),
268
+ ]);
269
+ const pathRequire = j.variableDeclaration('const', [
270
+ j.variableDeclarator(j.identifier('path'), j.callExpression(j.identifier('require'), [j.literal('path')])),
271
+ ]);
272
+ const compatNew = j.variableDeclaration('const', [
273
+ j.variableDeclarator(j.identifier('compat'), j.newExpression(j.identifier('FlatCompat'), [
274
+ j.objectExpression([
275
+ j.property('init', j.identifier('baseDirectory'), j.identifier('__dirname')),
276
+ ]),
277
+ ])),
278
+ ]);
279
+ j(firstNode).insertBefore(compatRequire);
280
+ j(firstNode).insertBefore(pathRequire);
281
+ j(firstNode).insertBefore(compatNew);
282
+ }
283
+ else {
284
+ // Add ES module imports
285
+ const firstImport = root.find(j.ImportDeclaration).at(0);
286
+ const insertPoint = firstImport.size() > 0
287
+ ? firstImport
288
+ : root.find(j.Program).get('body', 0);
289
+ const imports = [
290
+ j.importDeclaration([j.importSpecifier(j.identifier('dirname'))], j.literal('path')),
291
+ j.importDeclaration([j.importSpecifier(j.identifier('fileURLToPath'))], j.literal('url')),
292
+ j.importDeclaration([j.importSpecifier(j.identifier('FlatCompat'))], j.literal('@eslint/eslintrc')),
293
+ ];
294
+ const setupVars = [
295
+ j.variableDeclaration('const', [
296
+ j.variableDeclarator(j.identifier('__filename'), j.callExpression(j.identifier('fileURLToPath'), [
297
+ j.memberExpression(j.memberExpression(j.identifier('import'), j.identifier('meta')), j.identifier('url')),
298
+ ])),
299
+ ]),
300
+ j.variableDeclaration('const', [
301
+ j.variableDeclarator(j.identifier('__dirname'), j.callExpression(j.identifier('dirname'), [
302
+ j.identifier('__filename'),
303
+ ])),
304
+ ]),
305
+ j.variableDeclaration('const', [
306
+ j.variableDeclarator(j.identifier('compat'), j.newExpression(j.identifier('FlatCompat'), [
307
+ j.objectExpression([
308
+ j.property('init', j.identifier('baseDirectory'), j.identifier('__dirname')),
309
+ ]),
310
+ ])),
311
+ ]),
312
+ ];
313
+ if (firstImport.size() > 0) {
314
+ // Insert after the last import
315
+ const lastImportPath = root.find(j.ImportDeclaration).at(-1).get();
316
+ if (!lastImportPath) {
317
+ // Fallback to inserting at the beginning
318
+ const fallbackInsertPoint = root.find(j.Program).get('body', 0);
319
+ imports.forEach((imp) => j(fallbackInsertPoint).insertBefore(imp));
320
+ setupVars.forEach((v) => j(fallbackInsertPoint).insertBefore(v));
321
+ }
322
+ else {
323
+ imports.forEach((imp) => j(lastImportPath).insertAfter(imp));
324
+ setupVars.forEach((v) => j(lastImportPath).insertAfter(v));
325
+ }
326
+ }
327
+ else {
328
+ // Insert at the beginning
329
+ imports.forEach((imp) => j(insertPoint).insertBefore(imp));
330
+ setupVars.forEach((v) => j(insertPoint).insertBefore(v));
331
+ }
332
+ }
333
+ }
334
+ // Create ignores configuration object
335
+ const ignoresConfig = j.objectExpression([
336
+ j.property('init', j.identifier('ignores'), j.arrayExpression([
337
+ j.literal('node_modules/**'),
338
+ j.literal('.next/**'),
339
+ j.literal('out/**'),
340
+ j.literal('build/**'),
341
+ j.literal('next-env.d.ts'),
342
+ ])),
343
+ ]);
344
+ // Only add Next.js extends if they're not already present
345
+ if (!hasNextConfigs) {
346
+ // Add Next.js configs to the array
347
+ const nextExtends = isTypeScript
348
+ ? ['next/core-web-vitals', 'next/typescript']
349
+ : ['next/core-web-vitals'];
350
+ const spreadElement = j.spreadElement(j.callExpression(j.memberExpression(j.identifier('compat'), j.identifier('extends')), nextExtends.map((ext) => j.literal(ext))));
351
+ // Insert Next.js extends at the beginning of the array
352
+ if (!exportedArray.value.elements) {
353
+ exportedArray.value.elements = [];
354
+ }
355
+ exportedArray.value.elements.unshift(spreadElement);
356
+ }
357
+ // Check if ignores already exist in the config and merge if needed
358
+ let existingIgnoresIndex = -1;
359
+ if (exportedArray.value.elements) {
360
+ for (let i = 0; i < exportedArray.value.elements.length; i++) {
361
+ const element = exportedArray.value.elements[i];
362
+ if (element &&
363
+ element.type === 'ObjectExpression' &&
364
+ element.properties &&
365
+ element.properties.some((prop) => prop.type === 'Property' &&
366
+ prop.key &&
367
+ prop.key.type === 'Identifier' &&
368
+ prop.key.name === 'ignores')) {
369
+ existingIgnoresIndex = i;
370
+ break;
371
+ }
372
+ }
373
+ }
374
+ if (existingIgnoresIndex === -1) {
375
+ // No existing ignores, add our own at appropriate position
376
+ const insertIndex = hasNextConfigs ? 0 : 1;
377
+ exportedArray.value.elements.splice(insertIndex, 0, ignoresConfig);
378
+ }
379
+ else {
380
+ // Merge with existing ignores
381
+ const existingIgnoresArr = exportedArray.value.elements[existingIgnoresIndex]?.properties ?? [];
382
+ const ignoresProp = existingIgnoresArr.find((prop) => prop.type === 'Property' &&
383
+ prop.key &&
384
+ prop.key.type === 'Identifier' &&
385
+ prop.key.name === 'ignores');
386
+ if (ignoresProp &&
387
+ ignoresProp.value &&
388
+ ignoresProp.value.type === 'ArrayExpression') {
389
+ // Add our ignores to the existing array if they're not already there
390
+ const nextIgnores = [
391
+ 'node_modules/**',
392
+ '.next/**',
393
+ 'out/**',
394
+ 'build/**',
395
+ 'next-env.d.ts',
396
+ ];
397
+ const existingIgnores = ignoresProp.value.elements
398
+ .map((el) => (el.type === 'Literal' ? el.value : null))
399
+ .filter(Boolean);
400
+ for (const ignore of nextIgnores) {
401
+ if (!existingIgnores.includes(ignore)) {
402
+ ignoresProp.value.elements.push(j.literal(ignore));
403
+ }
404
+ }
405
+ }
406
+ }
407
+ // Generate the updated code
408
+ const updatedContent = root.toSource();
409
+ if (updatedContent !== configContent) {
410
+ try {
411
+ (0, node_fs_1.writeFileSync)(configPath, updatedContent);
412
+ }
413
+ catch (error) {
414
+ console.error(` Error writing config file: ${error}`);
415
+ return false;
416
+ }
417
+ if (hasNextConfigs) {
418
+ console.log(` Updated ${node_path_1.default.basename(configPath)} with Next.js ignores configuration`);
419
+ }
420
+ else {
421
+ console.log(` Updated ${node_path_1.default.basename(configPath)} with Next.js ESLint configs`);
422
+ }
423
+ return true;
424
+ }
425
+ // If nothing changed but Next.js configs were already present, that's still success
426
+ if (hasNextConfigs) {
427
+ console.log(' Next.js ESLint configs already present in flat config');
428
+ return true;
429
+ }
430
+ return false;
431
+ }
432
+ function updatePackageJsonScripts(packageJsonContent) {
433
+ try {
434
+ const packageJson = JSON.parse(packageJsonContent);
435
+ let needsUpdate = false;
436
+ if (!packageJson.scripts) {
437
+ packageJson.scripts = {};
438
+ }
439
+ // Process all scripts that contain "next lint"
440
+ for (const scriptName in packageJson.scripts) {
441
+ const scriptValue = packageJson.scripts[scriptName];
442
+ if (typeof scriptValue === 'string' &&
443
+ scriptValue.includes('next lint')) {
444
+ // Replace "next lint" with "eslint" and handle special arguments
445
+ const updatedScript = scriptValue.replace(/\bnext\s+lint\b([^&|;]*)/gi, (_match, args = '') => {
446
+ // Track whether we need a trailing space before operators
447
+ let trailingSpace = '';
448
+ if (args.endsWith(' ')) {
449
+ trailingSpace = ' ';
450
+ args = args.trimEnd();
451
+ }
452
+ // Check for redirects (2>, 1>, etc.) and preserve them
453
+ let redirect = '';
454
+ const redirectMatch = args.match(/\s+(\d*>[>&]?.*)$/);
455
+ if (redirectMatch) {
456
+ redirect = ` ${redirectMatch[1]}`;
457
+ args = args.substring(0, redirectMatch.index);
458
+ }
459
+ // Parse arguments - handle quoted strings properly
460
+ const argTokens = [];
461
+ let current = '';
462
+ let inQuotes = false;
463
+ let quoteChar = '';
464
+ for (let j = 0; j < args.length; j++) {
465
+ const char = args[j];
466
+ if ((char === '"' || char === "'") &&
467
+ (j === 0 || args[j - 1] !== '\\')) {
468
+ if (!inQuotes) {
469
+ inQuotes = true;
470
+ quoteChar = char;
471
+ current += char;
472
+ }
473
+ else if (char === quoteChar) {
474
+ inQuotes = false;
475
+ quoteChar = '';
476
+ current += char;
477
+ }
478
+ else {
479
+ current += char;
480
+ }
481
+ }
482
+ else if (char === ' ' && !inQuotes) {
483
+ if (current) {
484
+ argTokens.push(current);
485
+ current = '';
486
+ }
487
+ }
488
+ else {
489
+ current += char;
490
+ }
491
+ }
492
+ if (current) {
493
+ argTokens.push(current);
494
+ }
495
+ const eslintArgs = [];
496
+ const paths = [];
497
+ for (let i = 0; i < argTokens.length; i++) {
498
+ const token = argTokens[i];
499
+ if (token === '--strict') {
500
+ eslintArgs.push('--max-warnings', '0');
501
+ }
502
+ else if (token === '--dir' && i + 1 < argTokens.length) {
503
+ paths.push(argTokens[++i]);
504
+ }
505
+ else if (token === '--file' && i + 1 < argTokens.length) {
506
+ paths.push(argTokens[++i]);
507
+ }
508
+ else if (token === '--rulesdir' && i + 1 < argTokens.length) {
509
+ // Skip rulesdir and its value
510
+ i++;
511
+ }
512
+ else if (token === '--ext' && i + 1 < argTokens.length) {
513
+ // Skip ext and its value
514
+ i++;
515
+ }
516
+ else if (token.startsWith('--')) {
517
+ // Keep other flags and their values
518
+ eslintArgs.push(token);
519
+ if (i + 1 < argTokens.length &&
520
+ !argTokens[i + 1].startsWith('--')) {
521
+ eslintArgs.push(argTokens[++i]);
522
+ }
523
+ }
524
+ else {
525
+ // Positional arguments (paths)
526
+ paths.push(token);
527
+ }
528
+ }
529
+ // Build the result
530
+ let result = 'eslint';
531
+ if (eslintArgs.length > 0) {
532
+ result += ` ${eslintArgs.join(' ')}`;
533
+ }
534
+ // Add paths or default to .
535
+ if (paths.length > 0) {
536
+ result += ` ${paths.join(' ')}`;
537
+ }
538
+ else {
539
+ result += ' .';
540
+ }
541
+ // Add redirect if present
542
+ result += redirect;
543
+ // Add back trailing space if we had one
544
+ result += trailingSpace;
545
+ return result;
546
+ });
547
+ if (updatedScript !== scriptValue) {
548
+ packageJson.scripts[scriptName] = updatedScript;
549
+ needsUpdate = true;
550
+ console.log(` Updated script "${scriptName}": "${scriptValue}" → "${updatedScript}"`);
551
+ // Note about unsupported flags
552
+ if (scriptValue.includes('--rulesdir')) {
553
+ console.log(` Note: --rulesdir is not supported in ESLint v9`);
554
+ }
555
+ if (scriptValue.includes('--ext')) {
556
+ console.log(` Note: --ext is not needed in ESLint v9 flat config`);
557
+ }
558
+ }
559
+ }
560
+ }
561
+ // Ensure required devDependencies exist
562
+ if (!packageJson.devDependencies) {
563
+ packageJson.devDependencies = {};
564
+ }
565
+ // Check if eslint exists in either dependencies or devDependencies
566
+ if (!packageJson.devDependencies.eslint &&
567
+ !packageJson.dependencies?.eslint) {
568
+ packageJson.devDependencies.eslint = '^9';
569
+ needsUpdate = true;
570
+ }
571
+ // Check if eslint-config-next exists in either dependencies or devDependencies
572
+ if (!packageJson.devDependencies['eslint-config-next'] &&
573
+ !packageJson.dependencies?.['eslint-config-next']) {
574
+ // Use the same version as next if available
575
+ const nextVersion = packageJson.dependencies?.next || packageJson.devDependencies?.next;
576
+ packageJson.devDependencies['eslint-config-next'] =
577
+ nextVersion || 'latest';
578
+ needsUpdate = true;
579
+ }
580
+ // Check if @eslint/eslintrc exists in either dependencies or devDependencies
581
+ if (!packageJson.devDependencies['@eslint/eslintrc'] &&
582
+ !packageJson.dependencies?.['@eslint/eslintrc']) {
583
+ packageJson.devDependencies['@eslint/eslintrc'] = '^3';
584
+ needsUpdate = true;
585
+ }
586
+ const updatedContent = `${JSON.stringify(packageJson, null, 2)}\n`;
587
+ return { updated: needsUpdate, content: updatedContent };
588
+ }
589
+ catch (error) {
590
+ console.error('Error updating package.json:', error);
591
+ return { updated: false, content: packageJsonContent };
592
+ }
593
+ }
594
+ function transformer(files, options = {}) {
595
+ // The codemod CLI passes arguments as an array for consistency with file-based transforms,
596
+ // but project-level transforms like this one only process a single directory.
597
+ // Usage: npx @next/codemod next-lint-to-eslint-cli <project-directory>
598
+ const dir = files[0];
599
+ if (!dir) {
600
+ console.error('Error: Please specify a directory path');
601
+ return;
602
+ }
603
+ // Allow skipping installation via option
604
+ const skipInstall = options.skipInstall === true;
605
+ const projectRoot = node_path_1.default.resolve(dir);
606
+ const packageJsonPath = node_path_1.default.join(projectRoot, 'package.json');
607
+ if (!(0, node_fs_1.existsSync)(packageJsonPath)) {
608
+ console.error('Error: package.json not found in the specified directory');
609
+ return;
610
+ }
611
+ const isTypeScript = detectTypeScript(projectRoot);
612
+ console.log('Migrating from next lint to the ESLint CLI...');
613
+ // Check for existing ESLint config
614
+ const existingConfig = findExistingEslintConfig(projectRoot);
615
+ if (existingConfig.exists) {
616
+ if (existingConfig.isFlat) {
617
+ // Try to update existing flat config
618
+ if (existingConfig.path) {
619
+ console.log(` Found existing flat config: ${node_path_1.default.basename(existingConfig.path)}`);
620
+ const updated = updateExistingFlatConfig(existingConfig.path, isTypeScript);
621
+ if (!updated) {
622
+ console.log(' Could not automatically update the existing flat config.');
623
+ console.log(' Please manually ensure your ESLint config extends "next/core-web-vitals"');
624
+ if (isTypeScript) {
625
+ console.log(' and "next/typescript" for TypeScript projects.');
626
+ }
627
+ }
628
+ }
629
+ }
630
+ else {
631
+ // Legacy config exists
632
+ if (existingConfig.path) {
633
+ console.log(` Found legacy ESLint config: ${node_path_1.default.basename(existingConfig.path)}`);
634
+ console.log(' Legacy .eslintrc configs are not automatically migrated.');
635
+ console.log(' Please migrate to flat config format (eslint.config.js) and ensure it extends:');
636
+ console.log(' - "next/core-web-vitals"');
637
+ if (isTypeScript) {
638
+ console.log(' - "next/typescript"');
639
+ }
640
+ console.log(' Learn more: https://eslint.org/docs/latest/use/configure/migration-guide');
641
+ }
642
+ }
643
+ }
644
+ else {
645
+ // Create new ESLint flat config
646
+ const eslintConfigPath = node_path_1.default.join(projectRoot, 'eslint.config.mjs');
647
+ const template = isTypeScript
648
+ ? ESLINT_CONFIG_TEMPLATE_TYPESCRIPT
649
+ : ESLINT_CONFIG_TEMPLATE_JAVASCRIPT;
650
+ try {
651
+ (0, node_fs_1.writeFileSync)(eslintConfigPath, template);
652
+ console.log(` Created ${node_path_1.default.basename(eslintConfigPath)}`);
653
+ }
654
+ catch (error) {
655
+ console.error(' Error creating ESLint config:', error);
656
+ }
657
+ }
658
+ // Update package.json
659
+ const packageJsonContent = (0, node_fs_1.readFileSync)(packageJsonPath, 'utf8');
660
+ const result = updatePackageJsonScripts(packageJsonContent);
661
+ if (result.updated) {
662
+ try {
663
+ (0, node_fs_1.writeFileSync)(packageJsonPath, result.content);
664
+ console.log('Updated package.json scripts and dependencies');
665
+ // Parse the updated package.json to find new dependencies
666
+ const updatedPackageJson = JSON.parse(result.content);
667
+ const originalPackageJson = JSON.parse(packageJsonContent);
668
+ const newDependencies = [];
669
+ // Check for new devDependencies
670
+ if (updatedPackageJson.devDependencies) {
671
+ for (const [pkg, version] of Object.entries(updatedPackageJson.devDependencies)) {
672
+ if (!originalPackageJson.devDependencies?.[pkg] &&
673
+ !originalPackageJson.dependencies?.[pkg]) {
674
+ newDependencies.push(`${pkg}@${version}`);
675
+ }
676
+ }
677
+ }
678
+ // Install new dependencies if any were added
679
+ if (newDependencies.length > 0) {
680
+ if (skipInstall) {
681
+ console.log('\nNew dependencies added to package.json:');
682
+ newDependencies.forEach((dep) => console.log(` - ${dep}`));
683
+ console.log(`Please run: ${(0, handle_package_1.getPkgManager)(projectRoot)} install`);
684
+ }
685
+ else {
686
+ console.log('\nInstalling new dependencies...');
687
+ try {
688
+ const packageManager = (0, handle_package_1.getPkgManager)(projectRoot);
689
+ console.log(` Using ${packageManager}...`);
690
+ (0, handle_package_1.installPackages)(newDependencies, {
691
+ packageManager,
692
+ dev: true,
693
+ silent: false,
694
+ });
695
+ console.log(' Dependencies installed successfully!');
696
+ }
697
+ catch (_error) {
698
+ console.error(' Failed to install dependencies automatically.');
699
+ console.error(` Please run: ${(0, handle_package_1.getPkgManager)(projectRoot)} install`);
700
+ }
701
+ }
702
+ }
703
+ }
704
+ catch (error) {
705
+ console.error('Error writing package.json:', error);
706
+ }
707
+ }
708
+ console.log('\nMigration complete! Your project now uses the ESLint CLI.');
709
+ }
710
+ //# sourceMappingURL=next-lint-to-eslint-cli.js.map