@next/codemod 16.0.0-canary.7 → 16.0.0-canary.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "16.0.0-canary.7",
3
+ "version": "16.0.0-canary.9",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,6 +7,7 @@ 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");
10
11
  const handle_package_1 = require("../lib/handle-package");
11
12
  const parser_1 = require("../lib/parser");
12
13
  const picocolors_1 = require("picocolors");
@@ -19,20 +20,12 @@ exports.prefixes = {
19
20
  event: (0, picocolors_1.green)((0, picocolors_1.bold)('✓')),
20
21
  trace: (0, picocolors_1.magenta)((0, picocolors_1.bold)('»')),
21
22
  };
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
- });
23
+ const ESLINT_CONFIG_TEMPLATE_TYPESCRIPT = `import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
24
+ import nextTypescript from "eslint-config-next/typescript";
33
25
 
34
26
  const eslintConfig = [
35
- ...compat.extends("next/core-web-vitals", "next/typescript"),
27
+ ...nextCoreWebVitals,
28
+ ...nextTypescript,
36
29
  {
37
30
  ignores: [
38
31
  "node_modules/**",
@@ -46,19 +39,10 @@ const eslintConfig = [
46
39
 
47
40
  export default eslintConfig;
48
41
  `;
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
- });
42
+ const ESLINT_CONFIG_TEMPLATE_JAVASCRIPT = `import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
59
43
 
60
44
  const eslintConfig = [
61
- ...compat.extends("next/core-web-vitals"),
45
+ ...nextCoreWebVitals,
62
46
  {
63
47
  ignores: [
64
48
  "node_modules/**",
@@ -96,19 +80,19 @@ function findExistingEslintConfig(projectRoot) {
96
80
  for (const config of flatConfigs) {
97
81
  const configPath = node_path_1.default.join(projectRoot, config);
98
82
  if ((0, node_fs_1.existsSync)(configPath)) {
99
- return { exists: true, path: configPath, isFlat: true };
83
+ return { exists: true, path: configPath, isLegacy: false };
100
84
  }
101
85
  }
102
86
  // Check for legacy configs
103
87
  for (const config of legacyConfigs) {
104
88
  const configPath = node_path_1.default.join(projectRoot, config);
105
89
  if ((0, node_fs_1.existsSync)(configPath)) {
106
- return { exists: true, path: configPath, isFlat: false };
90
+ return { exists: true, path: configPath, isLegacy: true };
107
91
  }
108
92
  }
109
- return { exists: false };
93
+ return { exists: false, path: null, isLegacy: null };
110
94
  }
111
- function updateExistingFlatConfig(configPath, isTypeScript) {
95
+ function replaceFlatCompatInConfig(configPath) {
112
96
  let configContent;
113
97
  try {
114
98
  configContent = (0, node_fs_1.readFileSync)(configPath, 'utf8');
@@ -117,114 +101,277 @@ function updateExistingFlatConfig(configPath, isTypeScript) {
117
101
  console.error(` Error reading config file: ${error}`);
118
102
  return false;
119
103
  }
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
- ')');
104
+ // Check if FlatCompat is used
105
+ const hasFlatCompat = configContent.includes('FlatCompat') ||
106
+ configContent.includes('@eslint/eslintrc');
107
+ if (!hasFlatCompat) {
108
+ console.log(' No FlatCompat usage found, no changes needed');
133
109
  return false;
134
110
  }
135
111
  // Parse the file using jscodeshift
136
112
  const j = (0, parser_1.createParserFromPath)(configPath);
137
113
  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';
114
+ // Track if we need to add imports and preserve other configs
115
+ let needsNextVitals = false;
116
+ let needsNextTs = false;
117
+ let otherConfigs = [];
118
+ // Look for FlatCompat extends usage and identify which configs are being used
119
+ root.find(j.CallExpression).forEach((astPath) => {
120
+ const node = astPath.value;
121
+ if (node.callee.type === 'MemberExpression' &&
122
+ node.callee.object.type === 'Identifier' &&
123
+ node.callee.object.name === 'compat' &&
124
+ node.callee.property.type === 'Identifier' &&
125
+ node.callee.property.name === 'extends') {
126
+ // Check arguments for all configs
127
+ node.arguments.forEach((arg) => {
128
+ if (arg.type === 'Literal' || arg.type === 'StringLiteral') {
129
+ if (arg.value === 'next/core-web-vitals') {
130
+ needsNextVitals = true;
131
+ }
132
+ else if (arg.value === 'next/typescript') {
133
+ needsNextTs = true;
134
+ }
135
+ else if (typeof arg.value === 'string') {
136
+ // Preserve other configs (non-Next.js or other Next.js variants)
137
+ otherConfigs.push(arg.value);
138
+ }
139
+ }
140
+ });
141
+ }
142
+ });
143
+ if (!needsNextVitals && !needsNextTs && otherConfigs.length === 0) {
144
+ console.warn(exports.prefixes.warn, ' No ESLint configs found in FlatCompat usage');
145
+ return false;
146
+ }
147
+ if (!needsNextVitals && !needsNextTs) {
148
+ console.log(' No Next.js configs found, but preserving other configs');
149
+ }
150
+ // Only remove FlatCompat setup if no other configs need it
151
+ if (otherConfigs.length === 0) {
152
+ // Remove FlatCompat imports and setup
153
+ root.find(j.ImportDeclaration).forEach((astPath) => {
154
+ const node = astPath.value;
155
+ if (node.source.value === '@eslint/eslintrc' ||
156
+ node.source.value === '@eslint/js') {
157
+ // Only remove FlatCompat-specific imports
158
+ j(astPath).remove();
154
159
  }
155
- else {
156
- // Default to CommonJS if no package.json found
157
- isCommonJS = true;
160
+ // Leave path/url imports alone - they might be used elsewhere
161
+ });
162
+ // Remove only the compat variable - keep __dirname and __filename
163
+ root.find(j.VariableDeclaration).forEach((astPath) => {
164
+ const node = astPath.value;
165
+ if (node.declarations) {
166
+ // Filter out only the compat variable
167
+ const filteredDeclarations = node.declarations.filter((decl) => {
168
+ if (decl && decl.id && decl.id.type === 'Identifier') {
169
+ return decl.id.name !== 'compat';
170
+ }
171
+ return true;
172
+ });
173
+ if (filteredDeclarations.length === 0) {
174
+ // Remove entire declaration if no declarations left
175
+ j(astPath).remove();
176
+ }
177
+ else if (filteredDeclarations.length < node.declarations.length) {
178
+ // Update declaration with filtered declarations
179
+ node.declarations = filteredDeclarations;
180
+ }
158
181
  }
182
+ });
183
+ }
184
+ else {
185
+ console.log(' Preserving FlatCompat setup for other ESLint configs');
186
+ }
187
+ // Add new imports after the eslint/config import
188
+ const imports = [];
189
+ // Add imports in correct order: core-web-vitals first, then typescript
190
+ if (needsNextVitals) {
191
+ imports.push(j.importDeclaration([j.importDefaultSpecifier(j.identifier('nextCoreWebVitals'))], j.literal('eslint-config-next/core-web-vitals')));
192
+ }
193
+ if (needsNextTs) {
194
+ imports.push(j.importDeclaration([j.importDefaultSpecifier(j.identifier('nextTypescript'))], j.literal('eslint-config-next/typescript')));
195
+ }
196
+ // Find the eslint/config import and insert our imports after it
197
+ let eslintConfigImportPath = null;
198
+ root.find(j.ImportDeclaration).forEach((astPath) => {
199
+ if (astPath.value.source.value === 'eslint/config') {
200
+ eslintConfigImportPath = astPath;
201
+ }
202
+ });
203
+ // Insert imports after eslint/config import (or at beginning if not found)
204
+ if (eslintConfigImportPath) {
205
+ // Insert after the eslint/config import in correct order
206
+ for (let i = imports.length - 1; i >= 0; i--) {
207
+ eslintConfigImportPath.insertAfter(imports[i]);
159
208
  }
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
209
  }
187
210
  else {
188
- // For other extensions (.ts, .mts, .cts), assume based on extension
189
- isCommonJS = configPath.endsWith('.cts');
211
+ // Fallback: insert at the beginning in correct order
212
+ const program = root.find(j.Program);
213
+ for (let i = imports.length - 1; i >= 0; i--) {
214
+ program.get('body', 0).insertBefore(imports[i]);
215
+ }
190
216
  }
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' },
217
+ // Replace FlatCompat extends with spread imports
218
+ root.find(j.SpreadElement).forEach((astPath) => {
219
+ const node = astPath.value;
220
+ if (node.argument.type === 'CallExpression' &&
221
+ node.argument.callee.type === 'MemberExpression' &&
222
+ node.argument.callee.object.type === 'Identifier' &&
223
+ node.argument.callee.object.name === 'compat' &&
224
+ node.argument.callee.property.type === 'Identifier' &&
225
+ node.argument.callee.property.name === 'extends') {
226
+ // Replace with spread of direct imports and preserve other configs
227
+ const replacements = [];
228
+ node.argument.arguments.forEach((arg) => {
229
+ if (arg.type === 'Literal' || arg.type === 'StringLiteral') {
230
+ if (arg.value === 'next/core-web-vitals') {
231
+ replacements.push(j.spreadElement(j.identifier('nextCoreWebVitals')));
232
+ }
233
+ else if (arg.value === 'next/typescript') {
234
+ replacements.push(j.spreadElement(j.identifier('nextTypescript')));
235
+ }
236
+ else if (typeof arg.value === 'string') {
237
+ // Preserve other configs as compat.extends() calls
238
+ replacements.push(j.spreadElement(j.callExpression(j.memberExpression(j.identifier('compat'), j.identifier('extends')), [j.literal(arg.value)])));
239
+ }
240
+ }
241
+ });
242
+ if (replacements.length > 0) {
243
+ // Replace the current spread element with multiple spread elements
244
+ const parent = astPath.parent;
245
+ if (parent.value.type === 'ArrayExpression') {
246
+ const index = parent.value.elements.indexOf(node);
247
+ if (index !== -1) {
248
+ parent.value.elements.splice(index, 1, ...replacements);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ });
254
+ // Also handle the case where extends is used as a property value (not spread)
255
+ root.find(j.ObjectExpression).forEach((astPath) => {
256
+ const objectNode = astPath.value;
257
+ objectNode.properties?.forEach((prop) => {
258
+ if (prop.type === 'ObjectProperty' &&
259
+ prop.key.type === 'Identifier' &&
260
+ prop.key.name === 'extends' &&
261
+ prop.value.type === 'CallExpression' &&
262
+ prop.value.callee.type === 'MemberExpression' &&
263
+ prop.value.callee.object.type === 'Identifier' &&
264
+ prop.value.callee.object.name === 'compat' &&
265
+ prop.value.callee.property.type === 'Identifier' &&
266
+ prop.value.callee.property.name === 'extends') {
267
+ // Replace with array of spread imports and preserve other configs
268
+ const replacements = [];
269
+ prop.value.arguments.forEach((arg) => {
270
+ if (arg.type === 'Literal' || arg.type === 'StringLiteral') {
271
+ if (arg.value === 'next/core-web-vitals') {
272
+ replacements.push(j.spreadElement(j.identifier('nextCoreWebVitals')));
273
+ }
274
+ else if (arg.value === 'next/typescript') {
275
+ replacements.push(j.spreadElement(j.identifier('nextTypescript')));
276
+ }
277
+ else if (typeof arg.value === 'string') {
278
+ // Preserve other configs as compat.extends() calls
279
+ replacements.push(j.spreadElement(j.callExpression(j.memberExpression(j.identifier('compat'), j.identifier('extends')), [j.literal(arg.value)])));
280
+ }
281
+ }
282
+ });
283
+ if (replacements.length > 0) {
284
+ // Replace the property value with an array of spreads
285
+ prop.value = j.arrayExpression(replacements);
286
+ }
287
+ }
203
288
  });
204
- if (moduleExports.size() > 0) {
205
- exportNode = moduleExports.at(0);
206
- exportedArray = exportNode.get('right');
289
+ });
290
+ // Generate the updated code
291
+ const updatedContent = root.toSource();
292
+ if (updatedContent !== configContent) {
293
+ // Validate the generated code by parsing it
294
+ try {
295
+ const validateJ = (0, parser_1.createParserFromPath)(configPath);
296
+ validateJ(updatedContent); // This will throw if the syntax is invalid
297
+ }
298
+ catch (parseError) {
299
+ console.error(` Generated code has invalid syntax: ${parseError instanceof Error ? parseError.message : parseError}`);
300
+ console.error(' Skipping update to prevent breaking the config file');
301
+ return false;
302
+ }
303
+ // Create backup of original file
304
+ const backupPath = `${configPath}.backup-${Date.now()}`;
305
+ try {
306
+ (0, node_fs_1.writeFileSync)(backupPath, configContent);
307
+ }
308
+ catch (backupError) {
309
+ console.warn(` Warning: Could not create backup file: ${backupError}`);
310
+ }
311
+ try {
312
+ (0, node_fs_1.writeFileSync)(configPath, updatedContent);
313
+ console.log(` Updated ${node_path_1.default.basename(configPath)} to use direct eslint-config-next imports`);
314
+ // Remove backup on success
315
+ try {
316
+ if ((0, node_fs_1.existsSync)(backupPath)) {
317
+ (0, node_fs_1.unlinkSync)(backupPath);
318
+ }
319
+ }
320
+ catch (cleanupError) {
321
+ console.warn(` Warning: Could not remove backup file ${backupPath}: ${cleanupError}`);
322
+ }
323
+ return true;
324
+ }
325
+ catch (error) {
326
+ console.error(` Error writing config file: ${error}`);
327
+ // Restore from backup on failure
328
+ try {
329
+ if ((0, node_fs_1.existsSync)(backupPath)) {
330
+ (0, node_fs_1.writeFileSync)(configPath, (0, node_fs_1.readFileSync)(backupPath, 'utf8'));
331
+ console.log(' Restored original config from backup');
332
+ }
333
+ }
334
+ catch (restoreError) {
335
+ console.error(` Error restoring backup: ${restoreError}`);
336
+ }
337
+ return false;
207
338
  }
208
339
  }
340
+ return true;
341
+ }
342
+ function updateExistingFlatConfig(configPath, isTypeScript = false) {
343
+ let configContent;
344
+ try {
345
+ configContent = (0, node_fs_1.readFileSync)(configPath, 'utf8');
346
+ }
347
+ catch (error) {
348
+ console.error(` Error reading config file: ${error}`);
349
+ return false;
350
+ }
351
+ // Check if Next.js configs are already imported directly
352
+ const hasNextVitals = configContent.includes('eslint-config-next/core-web-vitals');
353
+ const hasNextTs = configContent.includes('eslint-config-next/typescript');
354
+ const hasNextConfigs = hasNextVitals || hasNextTs;
355
+ // Parse the file using jscodeshift
356
+ const j = (0, parser_1.createParserFromPath)(configPath);
357
+ const root = j(configContent);
358
+ // Find the exported array - support different export patterns
359
+ let exportedArray = null;
360
+ // Pattern 1: export default [...]
361
+ const directArrayExports = root.find(j.ExportDefaultDeclaration, {
362
+ declaration: { type: 'ArrayExpression' },
363
+ });
364
+ if (directArrayExports.size() > 0) {
365
+ exportedArray = directArrayExports.at(0).get('declaration');
366
+ }
209
367
  else {
210
- // Look for export default [...]
211
- const defaultExports = root.find(j.ExportDefaultDeclaration, {
212
- declaration: { type: 'ArrayExpression' },
368
+ // Pattern 2: const config = [...]; export default config
369
+ const defaultExportIdentifier = root.find(j.ExportDefaultDeclaration, {
370
+ declaration: { type: 'Identifier' },
213
371
  });
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
- }
372
+ if (defaultExportIdentifier.size() > 0) {
373
+ const declarationNode = defaultExportIdentifier.at(0).get('declaration');
374
+ if (declarationNode.value) {
228
375
  const varName = declarationNode.value.name;
229
376
  const varDeclaration = root.find(j.VariableDeclarator, {
230
377
  id: { name: varName },
@@ -233,201 +380,135 @@ function updateExistingFlatConfig(configPath, isTypeScript) {
233
380
  if (varDeclaration.size() > 0) {
234
381
  exportedArray = varDeclaration.at(0).get('init');
235
382
  }
383
+ else {
384
+ // Pattern 3: defineConfig([...]) or similar wrapper function
385
+ const callDeclaration = root.find(j.VariableDeclarator, {
386
+ id: { name: varName },
387
+ init: { type: 'CallExpression' },
388
+ });
389
+ if (callDeclaration.size() > 0) {
390
+ const callExpression = callDeclaration.at(0).get('init');
391
+ if (callExpression.value.arguments.length > 0 &&
392
+ callExpression.value.arguments[0].type === 'ArrayExpression') {
393
+ exportedArray = callExpression.get('arguments', 0);
394
+ }
395
+ else {
396
+ console.warn(exports.prefixes.warn, ' Wrapper function does not have an array parameter. Manual migration required.');
397
+ return false;
398
+ }
399
+ }
400
+ }
236
401
  }
237
402
  }
238
403
  }
239
404
  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.');
405
+ console.warn(exports.prefixes.warn, ' Config does not export an array or supported pattern. Manual migration required.');
242
406
  return false;
243
407
  }
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
- }
408
+ // Add Next.js imports if not present
409
+ const program = root.find(j.Program);
410
+ const imports = [];
411
+ if (!hasNextVitals) {
412
+ imports.push(j.importDeclaration([j.importDefaultSpecifier(j.identifier('nextCoreWebVitals'))], j.literal('eslint-config-next/core-web-vitals')));
333
413
  }
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
- }
414
+ if (!hasNextTs && isTypeScript) {
415
+ imports.push(j.importDeclaration([j.importDefaultSpecifier(j.identifier('nextTypescript'))], j.literal('eslint-config-next/typescript')));
373
416
  }
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);
417
+ // Insert imports at the beginning in correct order
418
+ for (let i = imports.length - 1; i >= 0; i--) {
419
+ program.get('body', 0).insertBefore(imports[i]);
378
420
  }
379
- else {
380
- // Merge with existing ignores
381
- const existingIgnoresArr = exportedArray.value.elements[existingIgnoresIndex]?.properties ?? [];
382
- const ignoresProp = existingIgnoresArr.find((prop) => prop.type === 'Property' &&
421
+ // Add spread elements to config array if not already present
422
+ if (!exportedArray.value.elements) {
423
+ exportedArray.value.elements = [];
424
+ }
425
+ const spreadsToAdd = [];
426
+ if (!hasNextVitals) {
427
+ spreadsToAdd.push(j.spreadElement(j.identifier('nextCoreWebVitals')));
428
+ }
429
+ if (!hasNextTs && isTypeScript) {
430
+ spreadsToAdd.push(j.spreadElement(j.identifier('nextTypescript')));
431
+ }
432
+ // Insert at the beginning of array in correct order
433
+ for (let i = spreadsToAdd.length - 1; i >= 0; i--) {
434
+ exportedArray.value.elements.unshift(spreadsToAdd[i]);
435
+ }
436
+ // Add ignores config if not already present
437
+ const hasIgnores = exportedArray.value.elements.some((element) => element &&
438
+ element.type === 'ObjectExpression' &&
439
+ element.properties &&
440
+ element.properties.some((prop) => prop.type === 'ObjectProperty' &&
383
441
  prop.key &&
384
442
  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
- }
443
+ prop.key.name === 'ignores'));
444
+ if (!hasIgnores) {
445
+ const ignoresConfig = j.objectExpression([
446
+ j.property('init', j.identifier('ignores'), j.arrayExpression([
447
+ j.literal('node_modules/**'),
448
+ j.literal('.next/**'),
449
+ j.literal('out/**'),
450
+ j.literal('build/**'),
451
+ j.literal('next-env.d.ts'),
452
+ ])),
453
+ ]);
454
+ exportedArray.value.elements.push(ignoresConfig);
406
455
  }
407
456
  // Generate the updated code
408
457
  const updatedContent = root.toSource();
409
458
  if (updatedContent !== configContent) {
459
+ // Validate the generated code by parsing it
460
+ try {
461
+ const validateJ = (0, parser_1.createParserFromPath)(configPath);
462
+ validateJ(updatedContent); // This will throw if the syntax is invalid
463
+ }
464
+ catch (parseError) {
465
+ console.error(` Generated code has invalid syntax: ${parseError instanceof Error ? parseError.message : parseError}`);
466
+ console.error(' Skipping update to prevent breaking the config file');
467
+ return false;
468
+ }
469
+ // Create backup of original file
470
+ const backupPath = `${configPath}.backup-${Date.now()}`;
471
+ try {
472
+ (0, node_fs_1.writeFileSync)(backupPath, configContent);
473
+ }
474
+ catch (backupError) {
475
+ console.warn(` Warning: Could not create backup file: ${backupError}`);
476
+ }
410
477
  try {
411
478
  (0, node_fs_1.writeFileSync)(configPath, updatedContent);
479
+ console.log(` Updated ${node_path_1.default.basename(configPath)} with Next.js configurations`);
480
+ // Remove backup on success
481
+ try {
482
+ if ((0, node_fs_1.existsSync)(backupPath)) {
483
+ (0, node_fs_1.unlinkSync)(backupPath);
484
+ }
485
+ }
486
+ catch (cleanupError) {
487
+ console.warn(` Warning: Could not remove backup file ${backupPath}: ${cleanupError}`);
488
+ }
489
+ return true;
412
490
  }
413
491
  catch (error) {
414
492
  console.error(` Error writing config file: ${error}`);
493
+ // Restore from backup on failure
494
+ try {
495
+ if ((0, node_fs_1.existsSync)(backupPath)) {
496
+ (0, node_fs_1.writeFileSync)(configPath, (0, node_fs_1.readFileSync)(backupPath, 'utf8'));
497
+ console.log(' Restored original config from backup');
498
+ }
499
+ }
500
+ catch (restoreError) {
501
+ console.error(` Error restoring backup: ${restoreError}`);
502
+ }
415
503
  return false;
416
504
  }
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
505
  }
425
- // If nothing changed but Next.js configs were already present, that's still success
506
+ // If nothing changed but configs are present, that's still success
426
507
  if (hasNextConfigs) {
427
508
  console.log(' Next.js ESLint configs already present in flat config');
428
509
  return true;
429
510
  }
430
- return false;
511
+ return true;
431
512
  }
432
513
  function updatePackageJsonScripts(packageJsonContent) {
433
514
  try {
@@ -577,10 +658,13 @@ function updatePackageJsonScripts(packageJsonContent) {
577
658
  nextVersion || 'latest';
578
659
  needsUpdate = true;
579
660
  }
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';
661
+ // Remove @eslint/eslintrc if it exists since we no longer use FlatCompat
662
+ if (packageJson.devDependencies?.['@eslint/eslintrc']) {
663
+ delete packageJson.devDependencies['@eslint/eslintrc'];
664
+ needsUpdate = true;
665
+ }
666
+ if (packageJson.dependencies?.['@eslint/eslintrc']) {
667
+ delete packageJson.dependencies['@eslint/eslintrc'];
584
668
  needsUpdate = true;
585
669
  }
586
670
  const updatedContent = `${JSON.stringify(packageJson, null, 2)}\n`;
@@ -612,36 +696,8 @@ function transformer(files, options = {}) {
612
696
  console.log('Migrating from next lint to the ESLint CLI...');
613
697
  // Check for existing ESLint config
614
698
  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 {
699
+ // If no existing ESLint config found, create a new one.
700
+ if (existingConfig.exists === false) {
645
701
  // Create new ESLint flat config
646
702
  const eslintConfigPath = node_path_1.default.join(projectRoot, 'eslint.config.mjs');
647
703
  const template = isTypeScript
@@ -655,7 +711,48 @@ function transformer(files, options = {}) {
655
711
  console.error(' Error creating ESLint config:', error);
656
712
  }
657
713
  }
658
- // Update package.json
714
+ else {
715
+ let eslintConfigFilename = node_path_1.default.basename(existingConfig.path);
716
+ let eslintConfigPath = existingConfig.path;
717
+ // If legacy config found, run ESLint migration tool first. It will
718
+ // use FlatCompat, so will continue to migrate using Flat config format.
719
+ if (existingConfig.isLegacy && existingConfig.path) {
720
+ console.log(` Found legacy ESLint config: ${eslintConfigFilename}`);
721
+ // Run npx @eslint/migrate-config
722
+ const command = `npx @eslint/migrate-config ${existingConfig.path}`;
723
+ console.log(` Running "${command}" to convert legacy config...`);
724
+ try {
725
+ (0, node_child_process_1.execSync)(command, {
726
+ cwd: projectRoot,
727
+ stdio: 'pipe',
728
+ });
729
+ // The migration tool creates eslint.config.mjs by default
730
+ const outputPath = node_path_1.default.join(projectRoot, 'eslint.config.mjs');
731
+ if (!(0, node_fs_1.existsSync)(outputPath)) {
732
+ throw new Error(`Failed to find the expected output file "${outputPath}" generated by the migration tool.`);
733
+ }
734
+ // Use generated config will have FlatCompat, so continue to apply
735
+ // the next steps to it.
736
+ eslintConfigPath = outputPath;
737
+ eslintConfigFilename = node_path_1.default.basename(eslintConfigPath);
738
+ }
739
+ catch (cause) {
740
+ throw new Error(`Failed to run "${command}" to migrate the legacy ESLint config "${eslintConfigFilename}".\n` +
741
+ `Please try the migration to Flat config manually.\n` +
742
+ `Learn more: https://eslint.org/docs/latest/use/configure/migration-guide`, { cause });
743
+ }
744
+ }
745
+ console.log(` Found existing ESLint Flat config: ${eslintConfigFilename}`);
746
+ // First try to replace FlatCompat usage if present
747
+ replaceFlatCompatInConfig(eslintConfigPath);
748
+ // Always try to update flat config with Next.js configurations
749
+ // regardless of whether FlatCompat was found
750
+ const updated = updateExistingFlatConfig(eslintConfigPath, isTypeScript);
751
+ if (!updated) {
752
+ console.log(' Could not automatically update the existing flat config.');
753
+ console.log(' Please manually ensure your ESLint config includes the Next.js configurations');
754
+ }
755
+ }
659
756
  const packageJsonContent = (0, node_fs_1.readFileSync)(packageJsonPath, 'utf8');
660
757
  const result = updatePackageJsonScripts(packageJsonContent);
661
758
  if (result.updated) {