@santi020k/eslint-config-basic 2.1.1 → 3.1.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.
@@ -0,0 +1,782 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ const BASIC_PACKAGE = "@santi020k/eslint-config-basic";
4
+ const FULL_PACKAGE = "@santi020k/eslint-config-full";
5
+ const INTEGRATIONS_PACKAGE = "@santi020k/eslint-config-integrations";
6
+ const LITE_PACKAGE = "@santi020k/eslint-config-lite";
7
+ const REMIX_PACKAGE = "@santi020k/eslint-config-remix";
8
+ const FEATURE_PACKAGES = {
9
+ extensions: "@santi020k/eslint-config-extensions",
10
+ formats: "@santi020k/eslint-config-formats",
11
+ libraries: "@santi020k/eslint-config-libraries",
12
+ testing: "@santi020k/eslint-config-testing",
13
+ tools: "@santi020k/eslint-config-tools"
14
+ };
15
+ const FRAMEWORK_PACKAGES = {
16
+ angular: "@santi020k/eslint-config-angular",
17
+ astro: "@santi020k/eslint-config-astro",
18
+ expo: "@santi020k/eslint-config-expo",
19
+ hono: "@santi020k/eslint-config-hono",
20
+ lit: "@santi020k/eslint-config-lit",
21
+ nest: "@santi020k/eslint-config-nest",
22
+ next: "@santi020k/eslint-config-next",
23
+ nuxt: "@santi020k/eslint-config-nuxt",
24
+ preact: "@santi020k/eslint-config-preact",
25
+ qwik: "@santi020k/eslint-config-qwik",
26
+ react: "@santi020k/eslint-config-react",
27
+ "react-router": "@santi020k/eslint-config-react-router",
28
+ slidev: "@santi020k/eslint-config-slidev",
29
+ solid: "@santi020k/eslint-config-solid",
30
+ svelte: "@santi020k/eslint-config-svelte",
31
+ "tanstack-start": "@santi020k/eslint-config-tanstack-start",
32
+ vite: "@santi020k/eslint-config-vite",
33
+ vue: "@santi020k/eslint-config-vue"
34
+ };
35
+ const FEATURE_EXPORTS = {
36
+ extensions: [
37
+ "a11y",
38
+ "astroDoctor",
39
+ "bestPractices",
40
+ "biome",
41
+ "boundaries",
42
+ "compat",
43
+ "deMorgan",
44
+ "depend",
45
+ "noOnlyTests",
46
+ "node",
47
+ "oxlint",
48
+ "perfectionist",
49
+ "regexp",
50
+ "security",
51
+ "sonarjs",
52
+ "unicorn"
53
+ ],
54
+ formats: [
55
+ "css",
56
+ "graphql",
57
+ "html",
58
+ "jsonc",
59
+ "markdown",
60
+ "mdx",
61
+ "packageJson",
62
+ "toml",
63
+ "yaml"
64
+ ],
65
+ libraries: [
66
+ "aiSdk",
67
+ "autogen",
68
+ "drizzle",
69
+ "googleGenAi",
70
+ "i18next",
71
+ "langchain",
72
+ "llamaIndex",
73
+ "mastra",
74
+ "mcp",
75
+ "mikroOrm",
76
+ "openAiAgents",
77
+ "prisma",
78
+ "sequelize",
79
+ "stencil",
80
+ "storybook",
81
+ "tailwind",
82
+ "tanstackQuery",
83
+ "tanstackRouter",
84
+ "typeorm",
85
+ "turbo",
86
+ "zod"
87
+ ],
88
+ testing: [
89
+ "cypress",
90
+ "jest",
91
+ "jestDom",
92
+ "playwright",
93
+ "testingLibrary",
94
+ "vitest"
95
+ ],
96
+ tools: [
97
+ "command",
98
+ "cspell",
99
+ "docker",
100
+ "githubActions",
101
+ "jsdoc",
102
+ "nx",
103
+ "pnpm",
104
+ "prettier",
105
+ "swagger"
106
+ ]
107
+ };
108
+ const EXPORT_TO_FEATURE_PACKAGE = new Map(
109
+ Object.entries(FEATURE_EXPORTS).flatMap(([category, exports]) => exports.map((exportName) => [
110
+ exportName,
111
+ FEATURE_PACKAGES[category]
112
+ ]))
113
+ );
114
+ const toKebabCase = (value) => value.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
115
+ const FEATURE_NAME_TO_PACKAGE = new Map(
116
+ Object.entries(FEATURE_EXPORTS).flatMap(([category, exports]) => exports.flatMap((exportName) => {
117
+ const packageName = FEATURE_PACKAGES[category];
118
+ return [
119
+ [exportName.toLowerCase(), packageName],
120
+ [toKebabCase(exportName), packageName]
121
+ ];
122
+ }))
123
+ );
124
+ const PRESET_FEATURE_PACKAGES = {
125
+ all: Object.values(FEATURE_PACKAGES),
126
+ app: [
127
+ FEATURE_PACKAGES.testing,
128
+ FEATURE_PACKAGES.tools
129
+ ],
130
+ ci: [
131
+ FEATURE_PACKAGES.extensions,
132
+ FEATURE_PACKAGES.tools
133
+ ],
134
+ library: [
135
+ FEATURE_PACKAGES.extensions,
136
+ FEATURE_PACKAGES.tools
137
+ ],
138
+ monorepo: [
139
+ FEATURE_PACKAGES.extensions,
140
+ FEATURE_PACKAGES.tools
141
+ ]
142
+ };
143
+ const REMOVED_ALIAS_REPLACEMENTS = {
144
+ angularConfig: "angular",
145
+ astroConfig: "createAstroConfig",
146
+ eslintConfig: "defineConfig",
147
+ expoConfig: "expo",
148
+ gitignore: "createGitignoreConfig",
149
+ jsConfig: "coreConfig",
150
+ nestConfig: "nest",
151
+ nextConfig: "next",
152
+ preactConfig: "preact",
153
+ reactConfig: "react",
154
+ solidConfig: "solid",
155
+ svelteConfig: "svelte",
156
+ tsConfig: "typescriptConfig",
157
+ vueConfig: "vue"
158
+ };
159
+ const REMOVED_ALIAS_OWNERS = {
160
+ angularConfig: "@santi020k/eslint-config-angular",
161
+ astroConfig: "@santi020k/eslint-config-astro",
162
+ expoConfig: "@santi020k/eslint-config-expo",
163
+ gitignore: "@santi020k/eslint-config-core",
164
+ jsConfig: "@santi020k/eslint-config-core",
165
+ nestConfig: "@santi020k/eslint-config-nest",
166
+ nextConfig: "@santi020k/eslint-config-next",
167
+ preactConfig: "@santi020k/eslint-config-preact",
168
+ reactConfig: "@santi020k/eslint-config-react",
169
+ solidConfig: "@santi020k/eslint-config-solid",
170
+ svelteConfig: "@santi020k/eslint-config-svelte",
171
+ tsConfig: "@santi020k/eslint-config-typescript",
172
+ vueConfig: "@santi020k/eslint-config-vue"
173
+ };
174
+ const FACTORY_ALIAS_REPLACEMENTS = /* @__PURE__ */ new Set([
175
+ "astroConfig",
176
+ "gitignore"
177
+ ]);
178
+ const dependencyFields = [
179
+ "dependencies",
180
+ "devDependencies",
181
+ "optionalDependencies",
182
+ "peerDependencies"
183
+ ];
184
+ const readPackageJson = (cwd) => {
185
+ const packagePath = join(cwd, "package.json");
186
+ if (!existsSync(packagePath)) throw new Error("package.json is required for a v3 migration.");
187
+ try {
188
+ return JSON.parse(readFileSync(packagePath, "utf8"));
189
+ } catch {
190
+ throw new Error("package.json is invalid JSON.");
191
+ }
192
+ };
193
+ const getConfigPath = (cwd) => {
194
+ const filenames = [
195
+ "eslint.config.js",
196
+ "eslint.config.mjs",
197
+ "eslint.config.cjs",
198
+ "eslint.config.ts",
199
+ "eslint.config.mts",
200
+ "eslint.config.cts"
201
+ ];
202
+ return filenames.map((filename) => join(cwd, filename)).find((path) => existsSync(path)) ?? null;
203
+ };
204
+ const createBackupPath = (filePath, suffix) => {
205
+ const preferred = `${filePath}.${suffix}.bak`;
206
+ if (!existsSync(preferred)) return preferred;
207
+ let index = 2;
208
+ while (existsSync(`${preferred}.${index}`)) index++;
209
+ return `${preferred}.${index}`;
210
+ };
211
+ const getDependencyRecord = (packageJson, field) => {
212
+ const value = packageJson[field];
213
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
214
+ return value;
215
+ };
216
+ const getPackageDependencyFields = (packageJson, packageName) => dependencyFields.filter((field) => packageName in getDependencyRecord(packageJson, field));
217
+ const getPreferredConfigDependencyFields = (packageJson) => {
218
+ const fields = /* @__PURE__ */ new Set();
219
+ for (const packageName of [
220
+ BASIC_PACKAGE,
221
+ FULL_PACKAGE,
222
+ INTEGRATIONS_PACKAGE,
223
+ LITE_PACKAGE
224
+ ]) {
225
+ for (const field of getPackageDependencyFields(packageJson, packageName)) fields.add(field);
226
+ }
227
+ return fields.size > 0 ? [...fields] : ["devDependencies"];
228
+ };
229
+ const getDeclaredConfigPackages = (packageJson) => {
230
+ const names = /* @__PURE__ */ new Set();
231
+ for (const field of dependencyFields) {
232
+ for (const name of Object.keys(getDependencyRecord(packageJson, field))) {
233
+ if (name.startsWith("@santi020k/eslint-config-")) names.add(name);
234
+ }
235
+ }
236
+ return names;
237
+ };
238
+ const getImportRanges = (content) => [...content.matchAll(/import\s*\{[\s\S]*?\}\s*from\s*(['"])[^'"]+\1/g)].map((match) => ({
239
+ end: match.index + match[0].length,
240
+ start: match.index
241
+ }));
242
+ const maskNonCode = (content, preserveStrings = false) => {
243
+ const characters = Array(content.length);
244
+ let quote = null;
245
+ let blockComment = false;
246
+ let lineComment = false;
247
+ for (let index = 0; index < content.length; index++) {
248
+ characters[index] = content[index];
249
+ }
250
+ for (let index = 0; index < characters.length; index++) {
251
+ const character = characters[index];
252
+ const next = characters[index + 1];
253
+ if (lineComment) {
254
+ if (character === "\n") {
255
+ lineComment = false;
256
+ } else {
257
+ characters[index] = " ";
258
+ }
259
+ continue;
260
+ }
261
+ if (blockComment) {
262
+ characters[index] = character === "\n" ? "\n" : " ";
263
+ if (character === "*" && next === "/") {
264
+ characters[index + 1] = " ";
265
+ blockComment = false;
266
+ index++;
267
+ }
268
+ continue;
269
+ }
270
+ if (quote) {
271
+ if (!preserveStrings) characters[index] = character === "\n" ? "\n" : " ";
272
+ if (character === "\\") {
273
+ if (!preserveStrings && index + 1 < characters.length) {
274
+ characters[index + 1] = next === "\n" ? "\n" : " ";
275
+ }
276
+ index++;
277
+ } else if (character === quote) {
278
+ quote = null;
279
+ }
280
+ continue;
281
+ }
282
+ if (character === "/" && next === "/") {
283
+ characters[index] = " ";
284
+ characters[index + 1] = " ";
285
+ lineComment = true;
286
+ index++;
287
+ continue;
288
+ }
289
+ if (character === "/" && next === "*") {
290
+ characters[index] = " ";
291
+ characters[index + 1] = " ";
292
+ blockComment = true;
293
+ index++;
294
+ continue;
295
+ }
296
+ if (character === "'" || character === '"' || character === "`") {
297
+ if (!preserveStrings) characters[index] = " ";
298
+ quote = character;
299
+ }
300
+ }
301
+ return characters.join("");
302
+ };
303
+ const getEnclosingDelimiter = (content, offset) => {
304
+ const expectedOpeners = [];
305
+ const closingDelimiters = {
306
+ ")": "(",
307
+ "]": "[",
308
+ "}": "{"
309
+ };
310
+ for (let index = offset - 1; index >= 0; index--) {
311
+ const character = content[index];
312
+ const expectedOpener = closingDelimiters[character];
313
+ if (expectedOpener) {
314
+ expectedOpeners.push(expectedOpener);
315
+ continue;
316
+ }
317
+ if (!["(", "[", "{"].includes(character)) continue;
318
+ if (expectedOpeners.at(-1) === character) {
319
+ expectedOpeners.pop();
320
+ } else if (expectedOpeners.length === 0) {
321
+ return character;
322
+ }
323
+ }
324
+ return null;
325
+ };
326
+ const replaceBindingReferences = (content, from, to, invokeReferences = false) => {
327
+ const importRanges = getImportRanges(content);
328
+ const masked = maskNonCode(content);
329
+ const identifierPattern = new RegExp(`\\b${from}\\b`, "g");
330
+ const matches = [...masked.matchAll(identifierPattern)];
331
+ let updated = content;
332
+ for (const match of matches.reverse()) {
333
+ const offset = match.index;
334
+ const isImportSpecifier = importRanges.some((range) => offset >= range.start && offset < range.end);
335
+ if (isImportSpecifier) continue;
336
+ const precedingContent = masked.slice(0, offset).trimEnd();
337
+ const preceding = precedingContent.at(-1);
338
+ const following = masked.slice(offset + from.length);
339
+ const isMemberAccess = preceding === "." && !precedingContent.endsWith("...");
340
+ const isProperty = isMemberAccess || /^\s*:/.test(following);
341
+ const isAlreadyInvoked = /^\s*\(/.test(following);
342
+ const isObjectShorthand = getEnclosingDelimiter(masked, offset) === "{" && (preceding === "{" || preceding === ",") && /^\s*[,}]/.test(following);
343
+ if (isProperty) continue;
344
+ let replacement = to;
345
+ if (invokeReferences && isObjectShorthand) {
346
+ replacement = `${from}: ${to}()`;
347
+ } else if (invokeReferences && !isAlreadyInvoked) {
348
+ replacement = `${to}()`;
349
+ }
350
+ updated = `${updated.slice(0, offset)}${replacement}${updated.slice(offset + from.length)}`;
351
+ }
352
+ return updated;
353
+ };
354
+ const replaceImportedAlias = (content, from, to, invokeReferences = false) => {
355
+ const bindings = [];
356
+ const owner = REMOVED_ALIAS_OWNERS[from];
357
+ const acceptedPackages = /* @__PURE__ */ new Set([BASIC_PACKAGE, FULL_PACKAGE, ...owner ? [owner] : []]);
358
+ const importPattern = /import\s*\{([^}]*)\}\s*from\s*(['"])(@santi020k\/eslint-config-[^'"]+)\2/g;
359
+ let updated = content.replace(importPattern, (statement, specifierText, _quote, packageName) => {
360
+ if (!acceptedPackages.has(packageName)) return statement;
361
+ const specifiers = specifierText.split(",");
362
+ const rewritten = specifiers.map((specifier) => {
363
+ const aliasMatch = /^(\s*)([a-zA-Z_$][\w$]*)\s+as\s+([a-zA-Z_$][\w$]*)(\s*)$/.exec(specifier);
364
+ if (aliasMatch?.[2] === from) {
365
+ const localName = aliasMatch[3];
366
+ bindings.push({ from: localName, to: localName });
367
+ return `${aliasMatch[1]}${to} as ${localName}${aliasMatch[4]}`;
368
+ }
369
+ const directMatch = /^(\s*)([a-zA-Z_$][\w$]*)(\s*)$/.exec(specifier);
370
+ if (directMatch?.[2] !== from) return specifier;
371
+ bindings.push({ from, to });
372
+ return `${directMatch[1]}${to}${directMatch[3]}`;
373
+ });
374
+ const rewrittenText = rewritten.join(",");
375
+ return rewrittenText === specifierText ? statement : statement.replace(specifierText, rewrittenText);
376
+ });
377
+ for (const binding of bindings) {
378
+ updated = replaceBindingReferences(updated, binding.from, binding.to, invokeReferences);
379
+ }
380
+ return updated;
381
+ };
382
+ const splitBasicIntegrationImports = (content) => {
383
+ const moved = /* @__PURE__ */ new Set();
384
+ const featurePackages = /* @__PURE__ */ new Set();
385
+ const importPattern = /import\s*\{([^}]+)\}\s*from\s*(['"])@santi020k\/eslint-config-(?:basic|lite)\2/g;
386
+ const transformed = content.replace(importPattern, (statement, specifierText, quote) => {
387
+ const specifiers = specifierText.split(",").map((value) => value.trim()).filter(Boolean);
388
+ const grouped = /* @__PURE__ */ new Map();
389
+ const integrations = specifiers.filter((specifier) => {
390
+ const imported = specifier.split(/\s+as\s+/)[0]?.trim() ?? "";
391
+ const packageName = EXPORT_TO_FEATURE_PACKAGE.get(imported);
392
+ if (!packageName) return false;
393
+ const values = grouped.get(packageName) ?? [];
394
+ values.push(specifier);
395
+ grouped.set(packageName, values);
396
+ return true;
397
+ });
398
+ if (integrations.length === 0) return statement;
399
+ for (const specifier of integrations) moved.add(specifier);
400
+ const basic = specifiers.filter((specifier) => !integrations.includes(specifier));
401
+ const lines = [];
402
+ if (basic.length > 0) {
403
+ lines.push(`import { ${basic.join(", ")} } from ${quote}${BASIC_PACKAGE}${quote}`);
404
+ }
405
+ for (const [packageName, values] of grouped) {
406
+ featurePackages.add(packageName);
407
+ lines.push(`import { ${values.join(", ")} } from ${quote}${packageName}${quote}`);
408
+ }
409
+ return lines.join("\n");
410
+ });
411
+ return {
412
+ changed: transformed !== content,
413
+ content: transformed,
414
+ featurePackages: [...featurePackages],
415
+ moved: [...moved]
416
+ };
417
+ };
418
+ const hasCodePropertySeparator = (codeContent, match) => {
419
+ const separatorOffset = match[0].indexOf(":");
420
+ const matchIndex = match.index ?? -1;
421
+ return matchIndex >= 0 && separatorOffset >= 0 && codeContent[matchIndex + separatorOffset] === ":";
422
+ };
423
+ const getExplicitConfigFeaturePackages = (content) => {
424
+ const packages = /* @__PURE__ */ new Set();
425
+ const codeContent = maskNonCode(content);
426
+ const searchableContent = maskNonCode(content, true);
427
+ for (const category of Object.keys(FEATURE_PACKAGES)) {
428
+ const categorySelections = searchableContent.matchAll(
429
+ // eslint-disable-next-line security/detect-non-literal-regexp -- category names come from the internal package registry
430
+ new RegExp(`(?:\\b${category}\\b|['"]${category}['"])\\s*:\\s*\\[([\\s\\S]*?)\\]`, "g")
431
+ );
432
+ for (const selection of categorySelections) {
433
+ if (hasCodePropertySeparator(codeContent, selection) && selection[1].trim()) {
434
+ packages.add(FEATURE_PACKAGES[category]);
435
+ break;
436
+ }
437
+ }
438
+ }
439
+ const featureSelections = /(?:\b(?:features|integrations)\b|['"](?:features|integrations)['"])\s*:\s*\{([\s\S]*?)\}/g;
440
+ for (const selection of searchableContent.matchAll(featureSelections)) {
441
+ if (!hasCodePropertySeparator(codeContent, selection)) continue;
442
+ const enabledFeaturePattern = /(?:['"]([^'"]+)['"]|([a-zA-Z_$][\w$-]*))\s*:\s*true\b/g;
443
+ for (const match of selection[1].matchAll(enabledFeaturePattern)) {
444
+ const featureName = (match[1] || match[2]).toLowerCase();
445
+ const packageName = FEATURE_NAME_TO_PACKAGE.get(featureName);
446
+ if (packageName) packages.add(packageName);
447
+ }
448
+ }
449
+ const enumPresetPattern = /(?:\bpreset\b|['"]preset['"])\s*:\s*Preset\.(All|App|CI|Library|Monorepo)/g;
450
+ for (const match of searchableContent.matchAll(enumPresetPattern)) {
451
+ if (!hasCodePropertySeparator(codeContent, match)) continue;
452
+ const presetName = match[1].toLowerCase();
453
+ for (const packageName of PRESET_FEATURE_PACKAGES[presetName] ?? []) {
454
+ packages.add(packageName);
455
+ }
456
+ }
457
+ const stringPresetPattern = /(?:\bpreset\b|['"]preset['"])\s*:\s*(['"`])(all|app|ci|library|monorepo)\1/g;
458
+ for (const match of searchableContent.matchAll(stringPresetPattern)) {
459
+ if (!hasCodePropertySeparator(codeContent, match)) continue;
460
+ for (const packageName of PRESET_FEATURE_PACKAGES[match[2]] ?? []) {
461
+ packages.add(packageName);
462
+ }
463
+ }
464
+ const strictPattern = /(?:\bstrict\b|['"]strict['"])\s*:\s*(['"`])pedantic\1/g;
465
+ for (const match of searchableContent.matchAll(strictPattern)) {
466
+ if (hasCodePropertySeparator(codeContent, match)) {
467
+ packages.add(FEATURE_PACKAGES.extensions);
468
+ break;
469
+ }
470
+ }
471
+ return [...packages];
472
+ };
473
+ const migrateConfigToV3 = (content, mode) => {
474
+ const changes = [];
475
+ const featurePackages = new Set(getExplicitConfigFeaturePackages(content));
476
+ let migrated = content;
477
+ if (migrated.includes(LITE_PACKAGE)) {
478
+ migrated = migrated.replaceAll(LITE_PACKAGE, BASIC_PACKAGE);
479
+ changes.push("Replaced the Lite compatibility import with Basic.");
480
+ }
481
+ if (mode === "full" && migrated.includes(BASIC_PACKAGE)) {
482
+ migrated = migrated.replaceAll(BASIC_PACKAGE, FULL_PACKAGE);
483
+ changes.push("Switched the root config import to the full bundle.");
484
+ }
485
+ if (mode === "lean") {
486
+ const split = splitBasicIntegrationImports(migrated);
487
+ migrated = split.content;
488
+ if (split.changed) {
489
+ changes.push(`Moved feature factories to their granular packages: ${split.moved.join(", ")}.`);
490
+ }
491
+ for (const packageName of split.featurePackages) featurePackages.add(packageName);
492
+ }
493
+ for (const [removed, replacement] of Object.entries(REMOVED_ALIAS_REPLACEMENTS)) {
494
+ const invokeReferences = FACTORY_ALIAS_REPLACEMENTS.has(removed);
495
+ const updated = replaceImportedAlias(migrated, removed, replacement, invokeReferences);
496
+ if (updated !== migrated) {
497
+ migrated = updated;
498
+ changes.push(`Replaced ${removed} with ${replacement}${invokeReferences ? "()" : ""}.`);
499
+ }
500
+ }
501
+ const remixMigrated = migrated.replace(
502
+ /(\bframeworks\s*:\s*\{)([\s\S]*?)(\})/,
503
+ (_match, opening, body, closing) => `${opening}${body.replaceAll(/\bremix(\s*:)/g, "'react-router'$1")}${closing}`
504
+ );
505
+ if (remixMigrated !== migrated) {
506
+ migrated = remixMigrated;
507
+ changes.push('Moved frameworks.remix to frameworks["react-router"].');
508
+ }
509
+ if (migrated.includes(REMIX_PACKAGE)) {
510
+ migrated = migrated.replaceAll(REMIX_PACKAGE, FRAMEWORK_PACKAGES["react-router"]);
511
+ changes.push("Replaced the Remix package import with React Router.");
512
+ }
513
+ if (/\bloadModule\b/.test(migrated)) {
514
+ changes.push("Manual action required: replace loadModule usage with createModuleLoader(resolver).");
515
+ }
516
+ return {
517
+ changes,
518
+ content: migrated,
519
+ featurePackages: [...featurePackages]
520
+ };
521
+ };
522
+ const shouldUseFullBundle = (configContent, packageJson, forceFull) => {
523
+ if (forceFull) return true;
524
+ if (configContent?.includes(FULL_PACKAGE)) return true;
525
+ if (configContent) {
526
+ const codeContent = maskNonCode(configContent);
527
+ const searchableContent = maskNonCode(configContent, true);
528
+ const enumPresetPattern = /(?:\bpreset\b|['"]preset['"])\s*:\s*Preset\.All/g;
529
+ for (const match of searchableContent.matchAll(enumPresetPattern)) {
530
+ if (hasCodePropertySeparator(codeContent, match)) return true;
531
+ }
532
+ const stringPresetPattern = /(?:\bpreset\b|['"]preset['"])\s*:\s*(['"`])all\1/g;
533
+ for (const match of searchableContent.matchAll(stringPresetPattern)) {
534
+ if (hasCodePropertySeparator(codeContent, match)) return true;
535
+ }
536
+ }
537
+ return getDeclaredConfigPackages(packageJson).has(FULL_PACKAGE);
538
+ };
539
+ const createInstallCommand = (packageManager, packages, workspaceRoot = false) => {
540
+ const packageList = packages.join(" ");
541
+ switch (packageManager) {
542
+ case "bun":
543
+ return `bun add -d ${packageList}`;
544
+ case "pnpm":
545
+ return `pnpm add -D${workspaceRoot ? " --workspace-root" : ""} ${packageList}`;
546
+ case "yarn":
547
+ return `yarn add -D ${packageList}`;
548
+ default:
549
+ return `npm install -D ${packageList}`;
550
+ }
551
+ };
552
+ const createManifestInstallCommand = (packageManager) => {
553
+ switch (packageManager) {
554
+ case "bun":
555
+ return "bun install";
556
+ case "pnpm":
557
+ return "pnpm install";
558
+ case "yarn":
559
+ return "yarn install";
560
+ default:
561
+ return "npm install";
562
+ }
563
+ };
564
+ const migratePackageJson = (packageJson, context, mode, configFeaturePackages) => {
565
+ const next = structuredClone(packageJson);
566
+ const preferredFields = getPreferredConfigDependencyFields(packageJson);
567
+ const originalFields = /* @__PURE__ */ new Map();
568
+ const migratedPackages = /* @__PURE__ */ new Map();
569
+ const previouslyDeclared = /* @__PURE__ */ new Set([
570
+ ...getDeclaredConfigPackages(next),
571
+ ...context.declaredConfigPackages ?? []
572
+ ]);
573
+ const migratableConfigPackages = [
574
+ BASIC_PACKAGE,
575
+ FULL_PACKAGE,
576
+ INTEGRATIONS_PACKAGE,
577
+ LITE_PACKAGE,
578
+ REMIX_PACKAGE,
579
+ ...Object.values(FEATURE_PACKAGES),
580
+ ...Object.values(FRAMEWORK_PACKAGES)
581
+ ];
582
+ for (const packageName of ["eslint", "typescript", ...migratableConfigPackages]) {
583
+ originalFields.set(packageName, getPackageDependencyFields(packageJson, packageName));
584
+ }
585
+ for (const field of dependencyFields) {
586
+ const dependencies = { ...getDependencyRecord(next, field) };
587
+ for (const packageName of [
588
+ BASIC_PACKAGE,
589
+ FULL_PACKAGE,
590
+ INTEGRATIONS_PACKAGE,
591
+ LITE_PACKAGE,
592
+ REMIX_PACKAGE,
593
+ ...Object.values(FEATURE_PACKAGES),
594
+ ...Object.values(FRAMEWORK_PACKAGES)
595
+ ]) {
596
+ delete dependencies[packageName];
597
+ }
598
+ if (Object.keys(dependencies).length > 0) next[field] = dependencies;
599
+ else delete next[field];
600
+ }
601
+ const addPackage = (packageName, version, fields = originalFields.get(packageName) ?? []) => {
602
+ const targetFields = fields.length > 0 ? fields : preferredFields;
603
+ for (const field of targetFields) {
604
+ const dependencies = {
605
+ ...getDependencyRecord(next, field),
606
+ [packageName]: version
607
+ };
608
+ next[field] = dependencies;
609
+ const packageFields = migratedPackages.get(packageName) ?? /* @__PURE__ */ new Set();
610
+ packageFields.add(field);
611
+ migratedPackages.set(packageName, packageFields);
612
+ }
613
+ };
614
+ addPackage(
615
+ "eslint",
616
+ "^10.0.0",
617
+ originalFields.get("eslint")?.length ? originalFields.get("eslint") : ["devDependencies"]
618
+ );
619
+ const mainPackageFields = mode === "full" ? [
620
+ ...new Set(
621
+ migratableConfigPackages.flatMap((packageName) => originalFields.get(packageName) ?? [])
622
+ )
623
+ ] : [
624
+ .../* @__PURE__ */ new Set([
625
+ ...originalFields.get(BASIC_PACKAGE) ?? [],
626
+ ...originalFields.get(FULL_PACKAGE) ?? [],
627
+ ...originalFields.get(LITE_PACKAGE) ?? []
628
+ ])
629
+ ];
630
+ addPackage(
631
+ mode === "full" ? FULL_PACKAGE : BASIC_PACKAGE,
632
+ "^3.0.0",
633
+ mainPackageFields
634
+ );
635
+ if (mode === "lean") {
636
+ const frameworkNames = new Set(context.frameworks);
637
+ if (previouslyDeclared.has(REMIX_PACKAGE)) frameworkNames.add("react-router");
638
+ for (const [framework, packageName] of Object.entries(FRAMEWORK_PACKAGES)) {
639
+ if (frameworkNames.has(framework) || previouslyDeclared.has(packageName)) {
640
+ const fields = [
641
+ ...originalFields.get(packageName) ?? [],
642
+ ...framework === "react-router" ? originalFields.get(REMIX_PACKAGE) ?? [] : []
643
+ ];
644
+ addPackage(packageName, "^3.0.0", [...new Set(fields)]);
645
+ }
646
+ }
647
+ const selectedFeaturePackages = new Set(configFeaturePackages);
648
+ for (const category of Object.keys(FEATURE_PACKAGES)) {
649
+ const packageName = FEATURE_PACKAGES[category];
650
+ if (context[category].length > 0 || previouslyDeclared.has(packageName)) {
651
+ selectedFeaturePackages.add(packageName);
652
+ }
653
+ }
654
+ for (const packageName of selectedFeaturePackages) {
655
+ addPackage(packageName, "^3.0.0");
656
+ }
657
+ if (previouslyDeclared.has(INTEGRATIONS_PACKAGE)) {
658
+ addPackage(INTEGRATIONS_PACKAGE, "^3.0.0");
659
+ }
660
+ }
661
+ if (context.typescript) {
662
+ const typescriptFields = originalFields.get("typescript") ?? [];
663
+ if (typescriptFields.length === 0) addPackage("typescript", "^5.0.0");
664
+ }
665
+ const peerDependenciesMeta = packageJson.peerDependenciesMeta;
666
+ if (peerDependenciesMeta && typeof peerDependenciesMeta === "object" && !Array.isArray(peerDependenciesMeta)) {
667
+ const nextPeerNames = new Set(Object.keys(getDependencyRecord(next, "peerDependencies")));
668
+ const nextPeerMeta = {};
669
+ for (const [packageName, metadata] of Object.entries(peerDependenciesMeta)) {
670
+ let targetPackage = packageName;
671
+ if (!nextPeerNames.has(targetPackage)) {
672
+ if (packageName === REMIX_PACKAGE) {
673
+ targetPackage = FRAMEWORK_PACKAGES["react-router"];
674
+ } else if ([BASIC_PACKAGE, FULL_PACKAGE, LITE_PACKAGE].includes(packageName)) {
675
+ targetPackage = mode === "full" ? FULL_PACKAGE : BASIC_PACKAGE;
676
+ } else if (mode === "full" && migratableConfigPackages.includes(packageName)) {
677
+ targetPackage = FULL_PACKAGE;
678
+ }
679
+ }
680
+ if (nextPeerNames.has(targetPackage)) nextPeerMeta[targetPackage] = metadata;
681
+ }
682
+ if (Object.keys(nextPeerMeta).length > 0) next.peerDependenciesMeta = nextPeerMeta;
683
+ else delete next.peerDependenciesMeta;
684
+ }
685
+ for (const field of dependencyFields) {
686
+ const dependencies = getDependencyRecord(next, field);
687
+ if (Object.keys(dependencies).length > 0) {
688
+ next[field] = Object.fromEntries(
689
+ Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))
690
+ );
691
+ }
692
+ }
693
+ const packages = [...migratedPackages.keys()].sort((a, b) => a.localeCompare(b)).map((name) => {
694
+ for (const field of dependencyFields) {
695
+ const version = getDependencyRecord(next, field)[name];
696
+ if (version) return `${name}@${version}`;
697
+ }
698
+ return name;
699
+ });
700
+ return {
701
+ changed: JSON.stringify(next) !== JSON.stringify(packageJson),
702
+ packageJson: next,
703
+ packages,
704
+ preservesRuntimeFields: [...migratedPackages.values()].some((fields) => [...fields].some((field) => field !== "devDependencies"))
705
+ };
706
+ };
707
+ const outputMigration = (result, json) => {
708
+ if (json) {
709
+ console.log(JSON.stringify(result, null, 2));
710
+ return;
711
+ }
712
+ console.log(`v2 to v3 migration plan (${result.mode}):`);
713
+ if (result.changes.length === 0) {
714
+ console.log("- No v3 migration changes are required.");
715
+ } else {
716
+ for (const change of result.changes) console.log(`- ${change.target}: ${change.detail}`);
717
+ }
718
+ console.log(`- Install command: ${result.installCommand}`);
719
+ if (result.written.length > 0) {
720
+ console.log(`- Updated: ${result.written.join(", ")}`);
721
+ } else {
722
+ console.log("- Dry run only; pass --write to apply safe changes.");
723
+ }
724
+ };
725
+ const handleMigrateV3 = (cwd, context, options = {}) => {
726
+ const packagePath = join(cwd, "package.json");
727
+ const packageJson = readPackageJson(cwd);
728
+ const configPath = getConfigPath(cwd);
729
+ const configContent = configPath ? readFileSync(configPath, "utf8") : null;
730
+ const mode = shouldUseFullBundle(configContent, packageJson, options.full ?? false) ? "full" : "lean";
731
+ const configMigration = configContent ? migrateConfigToV3(configContent, mode) : null;
732
+ const packageMigration = migratePackageJson(
733
+ packageJson,
734
+ context,
735
+ mode,
736
+ configMigration?.featurePackages ?? []
737
+ );
738
+ const changes = [];
739
+ const written = [];
740
+ for (const detail of configMigration?.changes ?? []) {
741
+ changes.push({ detail, target: configPath ? basename(configPath) : "eslint.config.*" });
742
+ }
743
+ if (packageMigration.changed) {
744
+ changes.push({ detail: "Updated the ESLint v3 dependency set.", target: "package.json" });
745
+ }
746
+ const configChanged = Boolean(configPath && configMigration && configMigration.content !== configContent);
747
+ if (options.write) {
748
+ if (configPath && configMigration && configChanged) {
749
+ const backupPath = createBackupPath(configPath, "v2");
750
+ writeFileSync(backupPath, configContent ?? "");
751
+ writeFileSync(configPath, configMigration.content);
752
+ written.push(basename(configPath), basename(backupPath));
753
+ }
754
+ if (packageMigration.changed) {
755
+ const backupPath = createBackupPath(packagePath, "v2");
756
+ writeFileSync(backupPath, `${JSON.stringify(packageJson, null, 2)}
757
+ `);
758
+ writeFileSync(packagePath, `${JSON.stringify(packageMigration.packageJson, null, 2)}
759
+ `);
760
+ written.push("package.json", basename(backupPath));
761
+ }
762
+ }
763
+ const result = {
764
+ changes,
765
+ configFile: configPath ? basename(configPath) : null,
766
+ installCommand: packageMigration.preservesRuntimeFields ? createManifestInstallCommand(context.packageManager) : createInstallCommand(
767
+ context.packageManager,
768
+ packageMigration.packages,
769
+ context.packageManager === "pnpm" && existsSync(join(cwd, "pnpm-workspace.yaml"))
770
+ ),
771
+ mode,
772
+ packages: packageMigration.packages,
773
+ written
774
+ };
775
+ outputMigration(result, options.json ?? false);
776
+ if (options.check && changes.length > 0) process.exitCode = 1;
777
+ };
778
+ export {
779
+ getExplicitConfigFeaturePackages,
780
+ handleMigrateV3,
781
+ migrateConfigToV3
782
+ };