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