@next/codemod 15.4.7 → 15.5.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.
- package/README.md +1 -1
- package/bin/transform.js +5 -1
- package/lib/handle-package.js +2 -0
- package/lib/utils.js +10 -0
- package/package.json +1 -1
- package/transforms/lib/utils.js +15 -0
- package/transforms/next-experimental-turbo-to-turbopack.js +204 -0
- package/transforms/next-image-experimental.js +5 -7
- package/transforms/next-lint-to-eslint-cli.js +710 -0
package/README.md
CHANGED
|
@@ -6,4 +6,4 @@ Codemods are transformations that run on your codebase programmatically. This al
|
|
|
6
6
|
|
|
7
7
|
## Documentation
|
|
8
8
|
|
|
9
|
-
Visit [nextjs.org/docs/advanced-features/codemods](https://nextjs.org/docs/
|
|
9
|
+
Visit [nextjs.org/docs/advanced-features/codemods](https://nextjs.org/docs/app/guides/upgrading/codemods) to view the documentation for this package.
|
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) {
|
|
@@ -92,7 +96,7 @@ async function runTransform(transform, path, options) {
|
|
|
92
96
|
if (verbose) {
|
|
93
97
|
args.push('--verbose=2');
|
|
94
98
|
}
|
|
95
|
-
args.push('--
|
|
99
|
+
args.push('--parser=tsx');
|
|
96
100
|
args.push('--ignore-pattern=**/node_modules/**');
|
|
97
101
|
args.push('--ignore-pattern=**/.next/**');
|
|
98
102
|
args.push('--extensions=tsx,ts,jsx,js');
|
package/lib/handle-package.js
CHANGED
package/lib/utils.js
CHANGED
|
@@ -106,5 +106,15 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
|
|
|
106
106
|
value: 'app-dir-runtime-config-experimental-edge',
|
|
107
107
|
version: '15.0.0-canary.179',
|
|
108
108
|
},
|
|
109
|
+
{
|
|
110
|
+
title: 'Updates `next.config.js` to use the new `turbopack` configuration',
|
|
111
|
+
value: 'next-experimental-turbo-to-turbopack',
|
|
112
|
+
version: '10.0.0',
|
|
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
|
+
},
|
|
109
119
|
];
|
|
110
120
|
//# sourceMappingURL=utils.js.map
|
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
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.isNextConfigFile = isNextConfigFile;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
function isNextConfigFile(file) {
|
|
9
|
+
const parsed = node_path_1.default.parse(file.path || '/');
|
|
10
|
+
return (parsed.base === 'next.config.js' ||
|
|
11
|
+
parsed.base === 'next.config.ts' ||
|
|
12
|
+
parsed.base === 'next.config.mjs' ||
|
|
13
|
+
parsed.base === 'next.config.cjs');
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* This codemod transforms the experimental turbo configuration in Next.js config to
|
|
4
|
+
* the new top-level `turbopack` configuration.
|
|
5
|
+
*
|
|
6
|
+
* It moves most properties from experimental.turbo to the top-level turbopack
|
|
7
|
+
* property, with special handling for certain properties like memoryLimit, minify,
|
|
8
|
+
* treeShaking, and sourceMaps which become experimental.turbopack* properties instead.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.default = transformer;
|
|
12
|
+
const parser_1 = require("../lib/parser");
|
|
13
|
+
const utils_1 = require("./lib/utils");
|
|
14
|
+
// Properties that need to be moved to experimental.turbopack*
|
|
15
|
+
const RENAMED_EXPERIMENTAL_PROPERTIES = {
|
|
16
|
+
memoryLimit: 'turbopackMemoryLimit',
|
|
17
|
+
minify: 'turbopackMinify',
|
|
18
|
+
treeShaking: 'turbopackTreeShaking',
|
|
19
|
+
sourceMaps: 'turbopackSourceMaps',
|
|
20
|
+
};
|
|
21
|
+
function transformer(file, _api, options) {
|
|
22
|
+
const j = (0, parser_1.createParserFromPath)(file.path);
|
|
23
|
+
const root = j(file.source);
|
|
24
|
+
let hasChanges = false;
|
|
25
|
+
if (!(0, utils_1.isNextConfigFile)(file) &&
|
|
26
|
+
process.env.NODE_ENV !== 'test' // fixtures have unique basenames in test
|
|
27
|
+
) {
|
|
28
|
+
return file.source;
|
|
29
|
+
}
|
|
30
|
+
// Process a config object once we find it
|
|
31
|
+
function processConfigObject(configObj) {
|
|
32
|
+
// Check for `experimental` property in the config
|
|
33
|
+
const experimentalProp = configObj.properties.find((prop) => isStaticProperty(prop) &&
|
|
34
|
+
prop.key &&
|
|
35
|
+
prop.key.type === 'Identifier' &&
|
|
36
|
+
prop.key.name === 'experimental');
|
|
37
|
+
if (!experimentalProp || !isStaticProperty(experimentalProp)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const experimentalObj = experimentalProp.value;
|
|
41
|
+
if (experimentalObj.type !== 'ObjectExpression') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// Check for `experimental.turbo` property in the config
|
|
45
|
+
const turboProp = experimentalObj.properties.find((prop) => isStaticProperty(prop) &&
|
|
46
|
+
prop.key &&
|
|
47
|
+
prop.key.type === 'Identifier' &&
|
|
48
|
+
prop.key.name === 'turbo');
|
|
49
|
+
if (!turboProp || !isStaticProperty(turboProp)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const turboObj = turboProp.value;
|
|
53
|
+
if (turboObj.type !== 'ObjectExpression') {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const regularProps = [];
|
|
57
|
+
const specialProps = [];
|
|
58
|
+
turboObj.properties.forEach((prop) => {
|
|
59
|
+
if (isStaticProperty(prop) &&
|
|
60
|
+
prop.key &&
|
|
61
|
+
prop.key.type === 'Identifier' &&
|
|
62
|
+
RENAMED_EXPERIMENTAL_PROPERTIES[prop.key.name]) {
|
|
63
|
+
// Create a new property with the renamed key
|
|
64
|
+
specialProps.push(j.objectProperty(j.identifier(RENAMED_EXPERIMENTAL_PROPERTIES[prop.key.name]), prop.value));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Keep the property for turbopack
|
|
68
|
+
regularProps.push(prop);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const existingProps = experimentalObj.properties.filter((prop) => !(isStaticProperty(prop) &&
|
|
72
|
+
prop.key &&
|
|
73
|
+
prop.key.type === 'Identifier' &&
|
|
74
|
+
prop.key.name === 'turbo'));
|
|
75
|
+
experimentalObj.properties = [...existingProps, ...specialProps];
|
|
76
|
+
// If experimental has no properties, remove it
|
|
77
|
+
if (experimentalObj.properties.length === 0) {
|
|
78
|
+
configObj.properties = configObj.properties.filter((prop) => !(isStaticProperty(prop) &&
|
|
79
|
+
prop.key &&
|
|
80
|
+
prop.key.type === 'Identifier' &&
|
|
81
|
+
prop.key.name === 'experimental'));
|
|
82
|
+
}
|
|
83
|
+
// Add turbopack property at top level if there are regular props
|
|
84
|
+
if (regularProps.length > 0) {
|
|
85
|
+
// Create the turbopack property
|
|
86
|
+
const turbopackProp = j.objectProperty(j.identifier('turbopack'), j.objectExpression(regularProps));
|
|
87
|
+
configObj.properties.push(turbopackProp);
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
root.find(j.ObjectExpression).forEach((path) => {
|
|
92
|
+
if (processConfigObject(path.value)) {
|
|
93
|
+
hasChanges = true;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// Transform config.experimental.turbo.X = value to config.turbopack.X = value
|
|
97
|
+
// or config.experimental.turbopackX = value for special properties
|
|
98
|
+
root
|
|
99
|
+
.find(j.AssignmentExpression, {
|
|
100
|
+
left: {
|
|
101
|
+
type: 'MemberExpression',
|
|
102
|
+
object: {
|
|
103
|
+
type: 'MemberExpression',
|
|
104
|
+
object: {
|
|
105
|
+
type: 'MemberExpression',
|
|
106
|
+
property: { type: 'Identifier', name: 'experimental' },
|
|
107
|
+
},
|
|
108
|
+
property: { type: 'Identifier', name: 'turbo' },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
.forEach((path) => {
|
|
113
|
+
if (path.node.left.type !== 'MemberExpression')
|
|
114
|
+
return;
|
|
115
|
+
// Get the variable name (e.g., config in config.experimental.turbo.sourceMaps)
|
|
116
|
+
let varName = null;
|
|
117
|
+
let currentPath = path.node.left.object;
|
|
118
|
+
while (currentPath?.type === 'MemberExpression') {
|
|
119
|
+
currentPath = currentPath.object;
|
|
120
|
+
}
|
|
121
|
+
if (currentPath?.type === 'Identifier') {
|
|
122
|
+
varName = currentPath.name;
|
|
123
|
+
}
|
|
124
|
+
if (!varName)
|
|
125
|
+
return;
|
|
126
|
+
// Get the property name being assigned (e.g., sourceMaps)
|
|
127
|
+
let propName = undefined;
|
|
128
|
+
if (path.node.left.property &&
|
|
129
|
+
path.node.left.property.type === 'Identifier') {
|
|
130
|
+
propName = path.node.left.property.name;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// For special properties like memoryLimit, minify, etc.
|
|
136
|
+
if (propName && RENAMED_EXPERIMENTAL_PROPERTIES[propName]) {
|
|
137
|
+
const newAssignment = j.assignmentExpression('=', j.memberExpression(j.memberExpression(j.identifier(varName), j.identifier('experimental')), j.identifier(RENAMED_EXPERIMENTAL_PROPERTIES[propName])), path.node.right);
|
|
138
|
+
j(path).replaceWith(newAssignment);
|
|
139
|
+
hasChanges = true;
|
|
140
|
+
}
|
|
141
|
+
else if (propName) {
|
|
142
|
+
// Create new assignment: config.turbopack.propName = value
|
|
143
|
+
const newAssignment = j.assignmentExpression('=', j.memberExpression(j.memberExpression(j.identifier(varName), j.identifier('turbopack')), j.identifier(propName)), path.node.right);
|
|
144
|
+
j(path).replaceWith(newAssignment);
|
|
145
|
+
hasChanges = true;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
// For nested property assignments like config.experimental.turbo.resolveAlias.foo = 'bar';
|
|
149
|
+
root.find(j.AssignmentExpression).forEach((path) => {
|
|
150
|
+
if (path.node.left.type !== 'MemberExpression')
|
|
151
|
+
return;
|
|
152
|
+
// Build a path to check if this is like `experimental.turbo.resolveAlias.foo`
|
|
153
|
+
let obj = path.node.left.object;
|
|
154
|
+
let props = [];
|
|
155
|
+
// Collect the property chain
|
|
156
|
+
while (obj && obj.type === 'MemberExpression') {
|
|
157
|
+
if (obj.property && obj.property.type === 'Identifier') {
|
|
158
|
+
props.unshift(obj.property.name);
|
|
159
|
+
}
|
|
160
|
+
obj = obj.object;
|
|
161
|
+
}
|
|
162
|
+
// Get the root variable name (e.g., 'config')
|
|
163
|
+
let varName = null;
|
|
164
|
+
if (obj && obj.type === 'Identifier') {
|
|
165
|
+
varName = obj.name;
|
|
166
|
+
}
|
|
167
|
+
if (!varName)
|
|
168
|
+
return;
|
|
169
|
+
// Check if this matches the pattern: config.experimental.turbo.resolveAlias.foo
|
|
170
|
+
if (props.length >= 3 &&
|
|
171
|
+
props[0] === 'experimental' &&
|
|
172
|
+
props[1] === 'turbo') {
|
|
173
|
+
// Get the final property name, only if it's an Identifier
|
|
174
|
+
let finalProp = undefined;
|
|
175
|
+
if (path.node.left.property &&
|
|
176
|
+
path.node.left.property.type === 'Identifier') {
|
|
177
|
+
finalProp = path.node.left.property.name;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// If not an Identifier, skip this assignment
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// The properties after 'turbo'
|
|
184
|
+
const middleProps = props.slice(2); // e.g. ['resolveAlias']
|
|
185
|
+
// Start building the new left side: config.turbopack
|
|
186
|
+
let newLeft = j.memberExpression(j.identifier(varName), j.identifier('turbopack'));
|
|
187
|
+
// Add the middle properties
|
|
188
|
+
for (const prop of middleProps) {
|
|
189
|
+
newLeft = j.memberExpression(newLeft, j.identifier(prop));
|
|
190
|
+
}
|
|
191
|
+
// Add the final property
|
|
192
|
+
newLeft = j.memberExpression(newLeft, j.identifier(finalProp));
|
|
193
|
+
const newAssignment = j.assignmentExpression('=', newLeft, path.node.right);
|
|
194
|
+
j(path).replaceWith(newAssignment);
|
|
195
|
+
hasChanges = true;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
// Only return a string if we changed the AST, otherwise return the original source
|
|
199
|
+
return hasChanges ? root.toSource(options) : file.source;
|
|
200
|
+
}
|
|
201
|
+
function isStaticProperty(prop) {
|
|
202
|
+
return prop.type === 'Property' || prop.type === 'ObjectProperty';
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=next-experimental-turbo-to-turbopack.js.map
|
|
@@ -4,6 +4,7 @@ exports.default = transformer;
|
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const parser_1 = require("../lib/parser");
|
|
7
|
+
const utils_1 = require("./lib/utils");
|
|
7
8
|
function findAndReplaceProps(j, root, tagName) {
|
|
8
9
|
const layoutToStyle = {
|
|
9
10
|
intrinsic: { maxWidth: '100%', height: 'auto' },
|
|
@@ -192,14 +193,11 @@ function nextConfigTransformer(j, root, appDir) {
|
|
|
192
193
|
function transformer(file, _api, options) {
|
|
193
194
|
const j = (0, parser_1.createParserFromPath)(file.path);
|
|
194
195
|
const root = j(file.source);
|
|
195
|
-
const
|
|
196
|
-
const isConfig = parsed.base === 'next.config.js' ||
|
|
197
|
-
parsed.base === 'next.config.ts' ||
|
|
198
|
-
parsed.base === 'next.config.mjs' ||
|
|
199
|
-
parsed.base === 'next.config.cjs';
|
|
196
|
+
const isConfig = (0, utils_1.isNextConfigFile)(file);
|
|
200
197
|
if (isConfig) {
|
|
201
|
-
const
|
|
202
|
-
|
|
198
|
+
const fileDir = (0, path_1.parse)(file.path).dir;
|
|
199
|
+
const result = nextConfigTransformer(j, root, fileDir);
|
|
200
|
+
return result.toSource(options);
|
|
203
201
|
}
|
|
204
202
|
// Before: import Image from "next/legacy/image"
|
|
205
203
|
// After: import Image from "next/image"
|
|
@@ -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
|