@next/codemod 15.4.2-canary.54 → 15.4.2-canary.56

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/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) {
@@ -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
@@ -111,5 +111,10 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
111
111
  value: 'next-experimental-turbo-to-turbopack',
112
112
  version: '10.0.0',
113
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
+ },
114
119
  ];
115
120
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "15.4.2-canary.54",
3
+ "version": "15.4.2-canary.56",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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