@next/codemod 16.0.0-canary.8 → 16.0.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.
@@ -7,6 +7,8 @@ exports.prefixes = void 0;
7
7
  exports.default = transformer;
8
8
  const node_fs_1 = require("node:fs");
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
+ const node_child_process_1 = require("node:child_process");
11
+ const semver_1 = __importDefault(require("semver"));
10
12
  const handle_package_1 = require("../lib/handle-package");
11
13
  const parser_1 = require("../lib/parser");
12
14
  const picocolors_1 = require("picocolors");
@@ -19,20 +21,12 @@ exports.prefixes = {
19
21
  event: (0, picocolors_1.green)((0, picocolors_1.bold)('✓')),
20
22
  trace: (0, picocolors_1.magenta)((0, picocolors_1.bold)('»')),
21
23
  };
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
- });
24
+ const ESLINT_CONFIG_TEMPLATE_TYPESCRIPT = `import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
25
+ import nextTypescript from "eslint-config-next/typescript";
33
26
 
34
27
  const eslintConfig = [
35
- ...compat.extends("next/core-web-vitals", "next/typescript"),
28
+ ...nextCoreWebVitals,
29
+ ...nextTypescript,
36
30
  {
37
31
  ignores: [
38
32
  "node_modules/**",
@@ -46,19 +40,10 @@ const eslintConfig = [
46
40
 
47
41
  export default eslintConfig;
48
42
  `;
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
- });
43
+ const ESLINT_CONFIG_TEMPLATE_JAVASCRIPT = `import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
59
44
 
60
45
  const eslintConfig = [
61
- ...compat.extends("next/core-web-vitals"),
46
+ ...nextCoreWebVitals,
62
47
  {
63
48
  ignores: [
64
49
  "node_modules/**",
@@ -96,19 +81,19 @@ function findExistingEslintConfig(projectRoot) {
96
81
  for (const config of flatConfigs) {
97
82
  const configPath = node_path_1.default.join(projectRoot, config);
98
83
  if ((0, node_fs_1.existsSync)(configPath)) {
99
- return { exists: true, path: configPath, isFlat: true };
84
+ return { exists: true, path: configPath, isLegacy: false };
100
85
  }
101
86
  }
102
87
  // Check for legacy configs
103
88
  for (const config of legacyConfigs) {
104
89
  const configPath = node_path_1.default.join(projectRoot, config);
105
90
  if ((0, node_fs_1.existsSync)(configPath)) {
106
- return { exists: true, path: configPath, isFlat: false };
91
+ return { exists: true, path: configPath, isLegacy: true };
107
92
  }
108
93
  }
109
- return { exists: false };
94
+ return { exists: false, path: null, isLegacy: null };
110
95
  }
111
- function updateExistingFlatConfig(configPath, isTypeScript) {
96
+ function replaceFlatCompatInConfig(configPath) {
112
97
  let configContent;
113
98
  try {
114
99
  configContent = (0, node_fs_1.readFileSync)(configPath, 'utf8');
@@ -117,114 +102,378 @@ function updateExistingFlatConfig(configPath, isTypeScript) {
117
102
  console.error(` Error reading config file: ${error}`);
118
103
  return false;
119
104
  }
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
- ')');
105
+ // Check if FlatCompat is used
106
+ const hasFlatCompat = configContent.includes('FlatCompat') ||
107
+ configContent.includes('@eslint/eslintrc');
108
+ if (!hasFlatCompat) {
109
+ console.log(' No FlatCompat usage found, no changes needed');
133
110
  return false;
134
111
  }
135
112
  // Parse the file using jscodeshift
136
113
  const j = (0, parser_1.createParserFromPath)(configPath);
137
114
  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';
115
+ // Track if we need to add imports and preserve other configs
116
+ let needsNextVitals = false;
117
+ let needsNextTs = false;
118
+ let otherConfigs = [];
119
+ // Look for FlatCompat extends usage and identify which configs are being used
120
+ root.find(j.CallExpression).forEach((astPath) => {
121
+ const node = astPath.value;
122
+ // Detect compat.extends() calls and identify which configs are being used
123
+ if (node.callee.type === 'MemberExpression' &&
124
+ node.callee.object.type === 'Identifier' &&
125
+ node.callee.object.name === 'compat' &&
126
+ node.callee.property.type === 'Identifier' &&
127
+ node.callee.property.name === 'extends') {
128
+ // Check arguments for all configs
129
+ node.arguments.forEach((arg) => {
130
+ if (arg.type === 'Literal' || arg.type === 'StringLiteral') {
131
+ if (arg.value === 'next/core-web-vitals') {
132
+ needsNextVitals = true;
133
+ }
134
+ else if (arg.value === 'next/typescript') {
135
+ needsNextTs = true;
136
+ }
137
+ else if (typeof arg.value === 'string') {
138
+ // Preserve other configs (non-Next.js or other Next.js variants)
139
+ otherConfigs.push(arg.value);
140
+ }
141
+ }
142
+ });
143
+ }
144
+ // Detect compat.config({ extends: [...] }) calls and identify which configs are being used
145
+ if (node.callee.type === 'MemberExpression' &&
146
+ node.callee.object.type === 'Identifier' &&
147
+ node.callee.object.name === 'compat' &&
148
+ node.callee.property.type === 'Identifier' &&
149
+ node.callee.property.name === 'config') {
150
+ // Look for extends property in the object argument
151
+ node.arguments.forEach((arg) => {
152
+ if (arg.type === 'ObjectExpression') {
153
+ arg.properties?.forEach((prop) => {
154
+ if (prop.type === 'ObjectProperty' &&
155
+ prop.key.type === 'Identifier' &&
156
+ prop.key.name === 'extends' &&
157
+ prop.value.type === 'ArrayExpression') {
158
+ // Process the extends array
159
+ prop.value.elements?.forEach((element) => {
160
+ if (element.type === 'Literal' ||
161
+ element.type === 'StringLiteral') {
162
+ if (element.value === 'next/core-web-vitals') {
163
+ needsNextVitals = true;
164
+ }
165
+ else if (element.value === 'next/typescript') {
166
+ needsNextTs = true;
167
+ }
168
+ else if (typeof element.value === 'string') {
169
+ // Preserve other configs (non-Next.js or other Next.js variants)
170
+ otherConfigs.push(element.value);
171
+ }
172
+ }
173
+ });
174
+ }
175
+ });
176
+ }
177
+ });
178
+ }
179
+ });
180
+ if (!needsNextVitals && !needsNextTs && otherConfigs.length === 0) {
181
+ console.warn(exports.prefixes.warn, ' No ESLint configs found in FlatCompat usage');
182
+ return false;
183
+ }
184
+ if (!needsNextVitals && !needsNextTs) {
185
+ console.log(' No Next.js configs found, but preserving other configs');
186
+ }
187
+ // Only remove FlatCompat setup if no other configs need it
188
+ if (otherConfigs.length === 0) {
189
+ // Remove FlatCompat imports and setup
190
+ root.find(j.ImportDeclaration).forEach((astPath) => {
191
+ const node = astPath.value;
192
+ if (node.source.value === '@eslint/eslintrc' ||
193
+ node.source.value === '@eslint/js') {
194
+ // Only remove FlatCompat-specific imports
195
+ j(astPath).remove();
154
196
  }
155
- else {
156
- // Default to CommonJS if no package.json found
157
- isCommonJS = true;
197
+ // Leave path/url imports alone - they might be used elsewhere
198
+ });
199
+ // Remove only the compat variable - keep __dirname and __filename
200
+ root.find(j.VariableDeclaration).forEach((astPath) => {
201
+ const node = astPath.value;
202
+ if (node.declarations) {
203
+ // Filter out only the compat variable
204
+ const filteredDeclarations = node.declarations.filter((decl) => {
205
+ if (decl && decl.id && decl.id.type === 'Identifier') {
206
+ return decl.id.name !== 'compat';
207
+ }
208
+ return true;
209
+ });
210
+ if (filteredDeclarations.length === 0) {
211
+ // Remove entire declaration if no declarations left
212
+ j(astPath).remove();
213
+ }
214
+ else if (filteredDeclarations.length < node.declarations.length) {
215
+ // Update declaration with filtered declarations
216
+ node.declarations = filteredDeclarations;
217
+ }
158
218
  }
219
+ });
220
+ }
221
+ else {
222
+ console.log(' Preserving FlatCompat setup for other ESLint configs');
223
+ }
224
+ // Add new imports after the eslint/config import
225
+ const imports = [];
226
+ // Add imports in correct order: core-web-vitals first, then typescript
227
+ if (needsNextVitals) {
228
+ imports.push(j.importDeclaration([j.importDefaultSpecifier(j.identifier('nextCoreWebVitals'))], j.literal('eslint-config-next/core-web-vitals')));
229
+ }
230
+ if (needsNextTs) {
231
+ imports.push(j.importDeclaration([j.importDefaultSpecifier(j.identifier('nextTypescript'))], j.literal('eslint-config-next/typescript')));
232
+ }
233
+ // Find the eslint/config import and insert our imports after it
234
+ let eslintConfigImportPath = null;
235
+ root.find(j.ImportDeclaration).forEach((astPath) => {
236
+ if (astPath.value.source.value === 'eslint/config') {
237
+ eslintConfigImportPath = astPath;
238
+ }
239
+ });
240
+ // Insert imports after eslint/config import (or at beginning if not found)
241
+ if (eslintConfigImportPath) {
242
+ // Insert after the eslint/config import in correct order
243
+ for (let i = imports.length - 1; i >= 0; i--) {
244
+ eslintConfigImportPath.insertAfter(imports[i]);
159
245
  }
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
246
  }
187
247
  else {
188
- // For other extensions (.ts, .mts, .cts), assume based on extension
189
- isCommonJS = configPath.endsWith('.cts');
248
+ // Fallback: insert at the beginning in correct order
249
+ const program = root.find(j.Program);
250
+ for (let i = imports.length - 1; i >= 0; i--) {
251
+ program.get('body', 0).insertBefore(imports[i]);
252
+ }
190
253
  }
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' },
254
+ // Replace FlatCompat extends with spread imports
255
+ root.find(j.SpreadElement).forEach((astPath) => {
256
+ const node = astPath.value;
257
+ // Replace spread of compat.extends(...) calls with direct imports
258
+ if (node.argument.type === 'CallExpression' &&
259
+ node.argument.callee.type === 'MemberExpression' &&
260
+ node.argument.callee.object.type === 'Identifier' &&
261
+ node.argument.callee.object.name === 'compat' &&
262
+ node.argument.callee.property.type === 'Identifier' &&
263
+ node.argument.callee.property.name === 'extends') {
264
+ // Replace with spread of direct imports and preserve other configs
265
+ const replacements = [];
266
+ node.argument.arguments.forEach((arg) => {
267
+ if (arg.type === 'Literal' || arg.type === 'StringLiteral') {
268
+ if (arg.value === 'next/core-web-vitals') {
269
+ replacements.push(j.spreadElement(j.identifier('nextCoreWebVitals')));
270
+ }
271
+ else if (arg.value === 'next/typescript') {
272
+ replacements.push(j.spreadElement(j.identifier('nextTypescript')));
273
+ }
274
+ else if (typeof arg.value === 'string') {
275
+ // Preserve other configs as compat.extends() calls
276
+ replacements.push(j.spreadElement(j.callExpression(j.memberExpression(j.identifier('compat'), j.identifier('extends')), [j.literal(arg.value)])));
277
+ }
278
+ }
279
+ });
280
+ if (replacements.length > 0) {
281
+ // Replace the current spread element with multiple spread elements
282
+ const parent = astPath.parent;
283
+ if (parent.value.type === 'ArrayExpression') {
284
+ const index = parent.value.elements.indexOf(node);
285
+ if (index !== -1) {
286
+ parent.value.elements.splice(index, 1, ...replacements);
287
+ }
288
+ }
289
+ }
290
+ }
291
+ // Replace spread of compat.config({ extends: [...] }) calls with direct imports
292
+ if (node.argument.type === 'CallExpression' &&
293
+ node.argument.callee.type === 'MemberExpression' &&
294
+ node.argument.callee.object.type === 'Identifier' &&
295
+ node.argument.callee.object.name === 'compat' &&
296
+ node.argument.callee.property.type === 'Identifier' &&
297
+ node.argument.callee.property.name === 'config') {
298
+ const replacements = [];
299
+ const preservedConfigs = [];
300
+ // Process each argument to compat.config
301
+ node.argument.arguments.forEach((arg) => {
302
+ if (arg.type === 'ObjectExpression') {
303
+ const updatedProperties = [];
304
+ arg.properties?.forEach((prop) => {
305
+ if (prop.type === 'ObjectProperty' &&
306
+ prop.key.type === 'Identifier' &&
307
+ prop.key.name === 'extends' &&
308
+ prop.value.type === 'ArrayExpression') {
309
+ const nonNextConfigs = [];
310
+ // Process extends array
311
+ prop.value.elements?.forEach((element) => {
312
+ if (element.type === 'Literal' ||
313
+ element.type === 'StringLiteral') {
314
+ if (element.value === 'next/core-web-vitals') {
315
+ replacements.push(j.spreadElement(j.identifier('nextCoreWebVitals')));
316
+ }
317
+ else if (element.value === 'next/typescript') {
318
+ replacements.push(j.spreadElement(j.identifier('nextTypescript')));
319
+ }
320
+ else if (typeof element.value === 'string') {
321
+ // Keep non-Next.js configs
322
+ nonNextConfigs.push(element);
323
+ }
324
+ }
325
+ });
326
+ // If there are non-Next.js configs, preserve the extends property with them
327
+ if (nonNextConfigs.length > 0) {
328
+ updatedProperties.push(j.property('init', j.identifier('extends'), j.arrayExpression(nonNextConfigs)));
329
+ }
330
+ }
331
+ else {
332
+ // Preserve other properties (not extends)
333
+ updatedProperties.push(prop);
334
+ }
335
+ });
336
+ // If we still have properties to preserve, keep the compat.config call
337
+ if (updatedProperties.length > 0) {
338
+ preservedConfigs.push(j.spreadElement(j.callExpression(j.memberExpression(j.identifier('compat'), j.identifier('config')), [j.objectExpression(updatedProperties)])));
339
+ }
340
+ }
341
+ });
342
+ // Add all replacements
343
+ const allReplacements = [...replacements, ...preservedConfigs];
344
+ if (allReplacements.length > 0) {
345
+ // Replace the current spread element with multiple spread elements
346
+ const parent = astPath.parent;
347
+ if (parent.value.type === 'ArrayExpression') {
348
+ const index = parent.value.elements.indexOf(node);
349
+ if (index !== -1) {
350
+ parent.value.elements.splice(index, 1, ...allReplacements);
351
+ }
352
+ }
353
+ }
354
+ }
355
+ });
356
+ // Also handle the case where extends is used as a property value (not spread)
357
+ root.find(j.ObjectExpression).forEach((astPath) => {
358
+ const objectNode = astPath.value;
359
+ objectNode.properties?.forEach((prop) => {
360
+ if (prop.type === 'ObjectProperty' &&
361
+ prop.key.type === 'Identifier' &&
362
+ prop.key.name === 'extends' &&
363
+ prop.value.type === 'CallExpression' &&
364
+ prop.value.callee.type === 'MemberExpression' &&
365
+ prop.value.callee.object.type === 'Identifier' &&
366
+ prop.value.callee.object.name === 'compat' &&
367
+ prop.value.callee.property.type === 'Identifier' &&
368
+ prop.value.callee.property.name === 'extends') {
369
+ // Replace with array of spread imports and preserve other configs
370
+ const replacements = [];
371
+ prop.value.arguments.forEach((arg) => {
372
+ if (arg.type === 'Literal' || arg.type === 'StringLiteral') {
373
+ if (arg.value === 'next/core-web-vitals') {
374
+ replacements.push(j.spreadElement(j.identifier('nextCoreWebVitals')));
375
+ }
376
+ else if (arg.value === 'next/typescript') {
377
+ replacements.push(j.spreadElement(j.identifier('nextTypescript')));
378
+ }
379
+ else if (typeof arg.value === 'string') {
380
+ // Preserve other configs as compat.extends() calls
381
+ replacements.push(j.spreadElement(j.callExpression(j.memberExpression(j.identifier('compat'), j.identifier('extends')), [j.literal(arg.value)])));
382
+ }
383
+ }
384
+ });
385
+ if (replacements.length > 0) {
386
+ // Replace the property value with an array of spreads
387
+ prop.value = j.arrayExpression(replacements);
388
+ }
389
+ }
203
390
  });
204
- if (moduleExports.size() > 0) {
205
- exportNode = moduleExports.at(0);
206
- exportedArray = exportNode.get('right');
391
+ });
392
+ // Generate the updated code
393
+ const updatedContent = root.toSource();
394
+ if (updatedContent !== configContent) {
395
+ // Validate the generated code by parsing it
396
+ try {
397
+ const validateJ = (0, parser_1.createParserFromPath)(configPath);
398
+ validateJ(updatedContent); // This will throw if the syntax is invalid
399
+ }
400
+ catch (parseError) {
401
+ console.error(` Generated code has invalid syntax: ${parseError instanceof Error ? parseError.message : parseError}`);
402
+ console.error(' Skipping update to prevent breaking the config file');
403
+ return false;
404
+ }
405
+ // Create backup of original file
406
+ const backupPath = `${configPath}.backup-${Date.now()}`;
407
+ try {
408
+ (0, node_fs_1.writeFileSync)(backupPath, configContent);
409
+ }
410
+ catch (backupError) {
411
+ console.warn(` Warning: Could not create backup file: ${backupError}`);
412
+ }
413
+ try {
414
+ (0, node_fs_1.writeFileSync)(configPath, updatedContent);
415
+ console.log(` Updated ${node_path_1.default.basename(configPath)} to use direct eslint-config-next imports`);
416
+ // Remove backup on success
417
+ try {
418
+ if ((0, node_fs_1.existsSync)(backupPath)) {
419
+ (0, node_fs_1.unlinkSync)(backupPath);
420
+ }
421
+ }
422
+ catch (cleanupError) {
423
+ console.warn(` Warning: Could not remove backup file ${backupPath}: ${cleanupError}`);
424
+ }
425
+ return true;
426
+ }
427
+ catch (error) {
428
+ console.error(` Error writing config file: ${error}`);
429
+ // Restore from backup on failure
430
+ try {
431
+ if ((0, node_fs_1.existsSync)(backupPath)) {
432
+ (0, node_fs_1.writeFileSync)(configPath, (0, node_fs_1.readFileSync)(backupPath, 'utf8'));
433
+ console.log(' Restored original config from backup');
434
+ }
435
+ }
436
+ catch (restoreError) {
437
+ console.error(` Error restoring backup: ${restoreError}`);
438
+ }
439
+ return false;
207
440
  }
208
441
  }
442
+ return true;
443
+ }
444
+ function updateExistingFlatConfig(configPath, isTypeScript = false) {
445
+ let configContent;
446
+ try {
447
+ configContent = (0, node_fs_1.readFileSync)(configPath, 'utf8');
448
+ }
449
+ catch (error) {
450
+ console.error(` Error reading config file: ${error}`);
451
+ return false;
452
+ }
453
+ // Check if Next.js configs are already imported directly
454
+ const hasNextVitals = configContent.includes('eslint-config-next/core-web-vitals');
455
+ const hasNextTs = configContent.includes('eslint-config-next/typescript');
456
+ const hasNextConfigs = hasNextVitals || hasNextTs;
457
+ // Parse the file using jscodeshift
458
+ const j = (0, parser_1.createParserFromPath)(configPath);
459
+ const root = j(configContent);
460
+ // Find the exported array - support different export patterns
461
+ let exportedArray = null;
462
+ // Pattern 1: export default [...]
463
+ const directArrayExports = root.find(j.ExportDefaultDeclaration, {
464
+ declaration: { type: 'ArrayExpression' },
465
+ });
466
+ if (directArrayExports.size() > 0) {
467
+ exportedArray = directArrayExports.at(0).get('declaration');
468
+ }
209
469
  else {
210
- // Look for export default [...]
211
- const defaultExports = root.find(j.ExportDefaultDeclaration, {
212
- declaration: { type: 'ArrayExpression' },
470
+ // Pattern 2: const config = [...]; export default config
471
+ const defaultExportIdentifier = root.find(j.ExportDefaultDeclaration, {
472
+ declaration: { type: 'Identifier' },
213
473
  });
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
- }
474
+ if (defaultExportIdentifier.size() > 0) {
475
+ const declarationNode = defaultExportIdentifier.at(0).get('declaration');
476
+ if (declarationNode.value) {
228
477
  const varName = declarationNode.value.name;
229
478
  const varDeclaration = root.find(j.VariableDeclarator, {
230
479
  id: { name: varName },
@@ -233,201 +482,135 @@ function updateExistingFlatConfig(configPath, isTypeScript) {
233
482
  if (varDeclaration.size() > 0) {
234
483
  exportedArray = varDeclaration.at(0).get('init');
235
484
  }
485
+ else {
486
+ // Pattern 3: defineConfig([...]) or similar wrapper function
487
+ const callDeclaration = root.find(j.VariableDeclarator, {
488
+ id: { name: varName },
489
+ init: { type: 'CallExpression' },
490
+ });
491
+ if (callDeclaration.size() > 0) {
492
+ const callExpression = callDeclaration.at(0).get('init');
493
+ if (callExpression.value.arguments.length > 0 &&
494
+ callExpression.value.arguments[0].type === 'ArrayExpression') {
495
+ exportedArray = callExpression.get('arguments', 0);
496
+ }
497
+ else {
498
+ console.warn(exports.prefixes.warn, ' Wrapper function does not have an array parameter. Manual migration required.');
499
+ return false;
500
+ }
501
+ }
502
+ }
236
503
  }
237
504
  }
238
505
  }
239
506
  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.');
507
+ console.warn(exports.prefixes.warn, ' Config does not export an array or supported pattern. Manual migration required.');
242
508
  return false;
243
509
  }
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
- }
510
+ // Add Next.js imports if not present
511
+ const program = root.find(j.Program);
512
+ const imports = [];
513
+ if (!hasNextVitals) {
514
+ imports.push(j.importDeclaration([j.importDefaultSpecifier(j.identifier('nextCoreWebVitals'))], j.literal('eslint-config-next/core-web-vitals')));
333
515
  }
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
- }
516
+ if (!hasNextTs && isTypeScript) {
517
+ imports.push(j.importDeclaration([j.importDefaultSpecifier(j.identifier('nextTypescript'))], j.literal('eslint-config-next/typescript')));
373
518
  }
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);
519
+ // Insert imports at the beginning in correct order
520
+ for (let i = imports.length - 1; i >= 0; i--) {
521
+ program.get('body', 0).insertBefore(imports[i]);
378
522
  }
379
- else {
380
- // Merge with existing ignores
381
- const existingIgnoresArr = exportedArray.value.elements[existingIgnoresIndex]?.properties ?? [];
382
- const ignoresProp = existingIgnoresArr.find((prop) => prop.type === 'Property' &&
523
+ // Add spread elements to config array if not already present
524
+ if (!exportedArray.value.elements) {
525
+ exportedArray.value.elements = [];
526
+ }
527
+ const spreadsToAdd = [];
528
+ if (!hasNextVitals) {
529
+ spreadsToAdd.push(j.spreadElement(j.identifier('nextCoreWebVitals')));
530
+ }
531
+ if (!hasNextTs && isTypeScript) {
532
+ spreadsToAdd.push(j.spreadElement(j.identifier('nextTypescript')));
533
+ }
534
+ // Insert at the beginning of array in correct order
535
+ for (let i = spreadsToAdd.length - 1; i >= 0; i--) {
536
+ exportedArray.value.elements.unshift(spreadsToAdd[i]);
537
+ }
538
+ // Add ignores config if not already present
539
+ const hasIgnores = exportedArray.value.elements.some((element) => element &&
540
+ element.type === 'ObjectExpression' &&
541
+ element.properties &&
542
+ element.properties.some((prop) => prop.type === 'ObjectProperty' &&
383
543
  prop.key &&
384
544
  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
- }
545
+ prop.key.name === 'ignores'));
546
+ if (!hasIgnores) {
547
+ const ignoresConfig = j.objectExpression([
548
+ j.property('init', j.identifier('ignores'), j.arrayExpression([
549
+ j.literal('node_modules/**'),
550
+ j.literal('.next/**'),
551
+ j.literal('out/**'),
552
+ j.literal('build/**'),
553
+ j.literal('next-env.d.ts'),
554
+ ])),
555
+ ]);
556
+ exportedArray.value.elements.push(ignoresConfig);
406
557
  }
407
558
  // Generate the updated code
408
559
  const updatedContent = root.toSource();
409
560
  if (updatedContent !== configContent) {
561
+ // Validate the generated code by parsing it
562
+ try {
563
+ const validateJ = (0, parser_1.createParserFromPath)(configPath);
564
+ validateJ(updatedContent); // This will throw if the syntax is invalid
565
+ }
566
+ catch (parseError) {
567
+ console.error(` Generated code has invalid syntax: ${parseError instanceof Error ? parseError.message : parseError}`);
568
+ console.error(' Skipping update to prevent breaking the config file');
569
+ return false;
570
+ }
571
+ // Create backup of original file
572
+ const backupPath = `${configPath}.backup-${Date.now()}`;
573
+ try {
574
+ (0, node_fs_1.writeFileSync)(backupPath, configContent);
575
+ }
576
+ catch (backupError) {
577
+ console.warn(` Warning: Could not create backup file: ${backupError}`);
578
+ }
410
579
  try {
411
580
  (0, node_fs_1.writeFileSync)(configPath, updatedContent);
581
+ console.log(` Updated ${node_path_1.default.basename(configPath)} with Next.js configurations`);
582
+ // Remove backup on success
583
+ try {
584
+ if ((0, node_fs_1.existsSync)(backupPath)) {
585
+ (0, node_fs_1.unlinkSync)(backupPath);
586
+ }
587
+ }
588
+ catch (cleanupError) {
589
+ console.warn(` Warning: Could not remove backup file ${backupPath}: ${cleanupError}`);
590
+ }
591
+ return true;
412
592
  }
413
593
  catch (error) {
414
594
  console.error(` Error writing config file: ${error}`);
595
+ // Restore from backup on failure
596
+ try {
597
+ if ((0, node_fs_1.existsSync)(backupPath)) {
598
+ (0, node_fs_1.writeFileSync)(configPath, (0, node_fs_1.readFileSync)(backupPath, 'utf8'));
599
+ console.log(' Restored original config from backup');
600
+ }
601
+ }
602
+ catch (restoreError) {
603
+ console.error(` Error restoring backup: ${restoreError}`);
604
+ }
415
605
  return false;
416
606
  }
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
607
  }
425
- // If nothing changed but Next.js configs were already present, that's still success
608
+ // If nothing changed but configs are present, that's still success
426
609
  if (hasNextConfigs) {
427
610
  console.log(' Next.js ESLint configs already present in flat config');
428
611
  return true;
429
612
  }
430
- return false;
613
+ return true;
431
614
  }
432
615
  function updatePackageJsonScripts(packageJsonContent) {
433
616
  try {
@@ -577,10 +760,26 @@ function updatePackageJsonScripts(packageJsonContent) {
577
760
  nextVersion || 'latest';
578
761
  needsUpdate = true;
579
762
  }
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';
763
+ // Bump eslint to v9 for full Flat config support
764
+ if (packageJson.dependencies?.['eslint'] &&
765
+ semver_1.default.lt(semver_1.default.minVersion(packageJson.dependencies['eslint'])?.version ??
766
+ '0.0.0', '9.0.0')) {
767
+ packageJson.dependencies['eslint'] = '^9';
768
+ needsUpdate = true;
769
+ }
770
+ if (packageJson.devDependencies?.['eslint'] &&
771
+ semver_1.default.lt(semver_1.default.minVersion(packageJson.devDependencies['eslint'])?.version ??
772
+ '0.0.0', '9.0.0')) {
773
+ packageJson.devDependencies['eslint'] = '^9';
774
+ needsUpdate = true;
775
+ }
776
+ // Remove @eslint/eslintrc if it exists since we no longer use FlatCompat
777
+ if (packageJson.devDependencies?.['@eslint/eslintrc']) {
778
+ delete packageJson.devDependencies['@eslint/eslintrc'];
779
+ needsUpdate = true;
780
+ }
781
+ if (packageJson.dependencies?.['@eslint/eslintrc']) {
782
+ delete packageJson.dependencies['@eslint/eslintrc'];
584
783
  needsUpdate = true;
585
784
  }
586
785
  const updatedContent = `${JSON.stringify(packageJson, null, 2)}\n`;
@@ -612,36 +811,8 @@ function transformer(files, options = {}) {
612
811
  console.log('Migrating from next lint to the ESLint CLI...');
613
812
  // Check for existing ESLint config
614
813
  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 {
814
+ // If no existing ESLint config found, create a new one.
815
+ if (existingConfig.exists === false) {
645
816
  // Create new ESLint flat config
646
817
  const eslintConfigPath = node_path_1.default.join(projectRoot, 'eslint.config.mjs');
647
818
  const template = isTypeScript
@@ -655,7 +826,48 @@ function transformer(files, options = {}) {
655
826
  console.error(' Error creating ESLint config:', error);
656
827
  }
657
828
  }
658
- // Update package.json
829
+ else {
830
+ let eslintConfigFilename = node_path_1.default.basename(existingConfig.path);
831
+ let eslintConfigPath = existingConfig.path;
832
+ // If legacy config found, run ESLint migration tool first. It will
833
+ // use FlatCompat, so will continue to migrate using Flat config format.
834
+ if (existingConfig.isLegacy && existingConfig.path) {
835
+ console.log(` Found legacy ESLint config: ${eslintConfigFilename}`);
836
+ // Run npx @eslint/migrate-config
837
+ const command = `npx @eslint/migrate-config ${existingConfig.path}`;
838
+ console.log(` Running "${command}" to convert legacy config...`);
839
+ try {
840
+ (0, node_child_process_1.execSync)(command, {
841
+ cwd: projectRoot,
842
+ stdio: 'pipe',
843
+ });
844
+ // The migration tool creates eslint.config.mjs by default
845
+ const outputPath = node_path_1.default.join(projectRoot, 'eslint.config.mjs');
846
+ if (!(0, node_fs_1.existsSync)(outputPath)) {
847
+ throw new Error(`Failed to find the expected output file "${outputPath}" generated by the migration tool.`);
848
+ }
849
+ // Use generated config will have FlatCompat, so continue to apply
850
+ // the next steps to it.
851
+ eslintConfigPath = outputPath;
852
+ eslintConfigFilename = node_path_1.default.basename(eslintConfigPath);
853
+ }
854
+ catch (cause) {
855
+ throw new Error(`Failed to run "${command}" to migrate the legacy ESLint config "${eslintConfigFilename}".\n` +
856
+ `Please try the migration to Flat config manually.\n` +
857
+ `Learn more: https://eslint.org/docs/latest/use/configure/migration-guide`, { cause });
858
+ }
859
+ }
860
+ console.log(` Found existing ESLint Flat config: ${eslintConfigFilename}`);
861
+ // First try to replace FlatCompat usage if present
862
+ replaceFlatCompatInConfig(eslintConfigPath);
863
+ // Always try to update flat config with Next.js configurations
864
+ // regardless of whether FlatCompat was found
865
+ const updated = updateExistingFlatConfig(eslintConfigPath, isTypeScript);
866
+ if (!updated) {
867
+ console.log(' Could not automatically update the existing flat config.');
868
+ console.log(' Please manually ensure your ESLint config includes the Next.js configurations');
869
+ }
870
+ }
659
871
  const packageJsonContent = (0, node_fs_1.readFileSync)(packageJsonPath, 'utf8');
660
872
  const result = updatePackageJsonScripts(packageJsonContent);
661
873
  if (result.updated) {