@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
|
|
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
|
-
...
|
|
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
|
|
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
|
-
...
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
121
|
-
const
|
|
122
|
-
configContent.includes('
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
//
|
|
139
|
-
let
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
//
|
|
189
|
-
|
|
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
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
//
|
|
211
|
-
const
|
|
212
|
-
declaration: { type: '
|
|
470
|
+
// Pattern 2: const config = [...]; export default config
|
|
471
|
+
const defaultExportIdentifier = root.find(j.ExportDefaultDeclaration, {
|
|
472
|
+
declaration: { type: 'Identifier' },
|
|
213
473
|
});
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
//
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
'
|
|
392
|
-
'
|
|
393
|
-
'
|
|
394
|
-
|
|
395
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
581
|
-
if (
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
616
|
-
|
|
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
|
-
|
|
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) {
|