@santi020k/eslint-config-basic 3.0.0 → 3.2.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,1134 @@
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 PARAMETER_PROPERTY_MODIFIERS = /* @__PURE__ */ new Set([
179
+ "override",
180
+ "private",
181
+ "protected",
182
+ "public",
183
+ "readonly"
184
+ ]);
185
+ const dependencyFields = [
186
+ "dependencies",
187
+ "devDependencies",
188
+ "optionalDependencies",
189
+ "peerDependencies"
190
+ ];
191
+ const readPackageJson = (cwd) => {
192
+ const packagePath = join(cwd, "package.json");
193
+ if (!existsSync(packagePath)) throw new Error("package.json is required for a v3 migration.");
194
+ try {
195
+ return JSON.parse(readFileSync(packagePath, "utf8"));
196
+ } catch {
197
+ throw new Error("package.json is invalid JSON.");
198
+ }
199
+ };
200
+ const getConfigPath = (cwd) => {
201
+ const filenames = [
202
+ "eslint.config.js",
203
+ "eslint.config.mjs",
204
+ "eslint.config.cjs",
205
+ "eslint.config.ts",
206
+ "eslint.config.mts",
207
+ "eslint.config.cts"
208
+ ];
209
+ return filenames.map((filename) => join(cwd, filename)).find((path) => existsSync(path)) ?? null;
210
+ };
211
+ const createBackupPath = (filePath, suffix) => {
212
+ const preferred = `${filePath}.${suffix}.bak`;
213
+ if (!existsSync(preferred)) return preferred;
214
+ let index = 2;
215
+ while (existsSync(`${preferred}.${index}`)) index++;
216
+ return `${preferred}.${index}`;
217
+ };
218
+ const getDependencyRecord = (packageJson, field) => {
219
+ const value = packageJson[field];
220
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
221
+ return value;
222
+ };
223
+ const getPackageDependencyFields = (packageJson, packageName) => dependencyFields.filter((field) => packageName in getDependencyRecord(packageJson, field));
224
+ const getPreferredConfigDependencyFields = (packageJson) => {
225
+ const fields = /* @__PURE__ */ new Set();
226
+ for (const packageName of [
227
+ BASIC_PACKAGE,
228
+ FULL_PACKAGE,
229
+ INTEGRATIONS_PACKAGE,
230
+ LITE_PACKAGE
231
+ ]) {
232
+ for (const field of getPackageDependencyFields(packageJson, packageName)) fields.add(field);
233
+ }
234
+ return fields.size > 0 ? [...fields] : ["devDependencies"];
235
+ };
236
+ const getDeclaredConfigPackages = (packageJson) => {
237
+ const names = /* @__PURE__ */ new Set();
238
+ for (const field of dependencyFields) {
239
+ for (const name of Object.keys(getDependencyRecord(packageJson, field))) {
240
+ if (name.startsWith("@santi020k/eslint-config-")) names.add(name);
241
+ }
242
+ }
243
+ return names;
244
+ };
245
+ const getImportRanges = (content) => [...content.matchAll(/import\s*\{[\s\S]*?\}\s*from\s*(['"])[^'"]+\1/g)].map((match) => ({
246
+ end: match.index + match[0].length,
247
+ start: match.index
248
+ }));
249
+ const maskNonCode = (content, preserveStrings = false) => {
250
+ const characters = Array(content.length);
251
+ let quote = null;
252
+ let blockComment = false;
253
+ let lineComment = false;
254
+ for (let index = 0; index < content.length; index++) {
255
+ characters[index] = content[index];
256
+ }
257
+ for (let index = 0; index < characters.length; index++) {
258
+ const character = characters[index];
259
+ const next = characters[index + 1];
260
+ if (lineComment) {
261
+ if (character === "\n") {
262
+ lineComment = false;
263
+ } else {
264
+ characters[index] = " ";
265
+ }
266
+ continue;
267
+ }
268
+ if (blockComment) {
269
+ characters[index] = character === "\n" ? "\n" : " ";
270
+ if (character === "*" && next === "/") {
271
+ characters[index + 1] = " ";
272
+ blockComment = false;
273
+ index++;
274
+ }
275
+ continue;
276
+ }
277
+ if (quote) {
278
+ if (!preserveStrings) characters[index] = character === "\n" ? "\n" : " ";
279
+ if (character === "\\") {
280
+ if (!preserveStrings && index + 1 < characters.length) {
281
+ characters[index + 1] = next === "\n" ? "\n" : " ";
282
+ }
283
+ index++;
284
+ } else if (character === quote) {
285
+ quote = null;
286
+ }
287
+ continue;
288
+ }
289
+ if (character === "/" && next === "/") {
290
+ characters[index] = " ";
291
+ characters[index + 1] = " ";
292
+ lineComment = true;
293
+ index++;
294
+ continue;
295
+ }
296
+ if (character === "/" && next === "*") {
297
+ characters[index] = " ";
298
+ characters[index + 1] = " ";
299
+ blockComment = true;
300
+ index++;
301
+ continue;
302
+ }
303
+ if (character === "'" || character === '"' || character === "`") {
304
+ if (!preserveStrings) characters[index] = " ";
305
+ quote = character;
306
+ }
307
+ }
308
+ return characters.join("");
309
+ };
310
+ const getEnclosingDelimiter = (content, offset) => {
311
+ const expectedOpeners = [];
312
+ const closingDelimiters = {
313
+ ")": "(",
314
+ "]": "[",
315
+ "}": "{"
316
+ };
317
+ for (let index = offset - 1; index >= 0; index--) {
318
+ const character = content[index];
319
+ const expectedOpener = closingDelimiters[character];
320
+ if (expectedOpener) {
321
+ expectedOpeners.push(expectedOpener);
322
+ continue;
323
+ }
324
+ if (!["(", "[", "{"].includes(character)) continue;
325
+ if (expectedOpeners.at(-1) === character) {
326
+ expectedOpeners.pop();
327
+ } else if (expectedOpeners.length === 0) {
328
+ return character;
329
+ }
330
+ }
331
+ return null;
332
+ };
333
+ const findClosingDelimiter = (content, start, opener, closer) => {
334
+ let depth = 0;
335
+ for (let index = start; index < content.length; index++) {
336
+ if (content[index] === opener) depth++;
337
+ else if (content[index] === closer && --depth === 0) return index + 1;
338
+ }
339
+ return content.length;
340
+ };
341
+ const getShadowedRanges = (content, name) => {
342
+ const masked = maskNonCode(content);
343
+ const importRanges = getImportRanges(content);
344
+ let searchable = masked;
345
+ for (const range of importRanges.toReversed()) {
346
+ searchable = `${searchable.slice(0, range.start)}${" ".repeat(range.end - range.start)}${searchable.slice(range.end)}`;
347
+ }
348
+ const getArrowRange = (start, arrowIndex) => {
349
+ const relativeBodyStart = searchable.slice(arrowIndex + 2).search(/\S/);
350
+ const bodyStart = relativeBodyStart === -1 ? searchable.length : arrowIndex + 2 + relativeBodyStart;
351
+ const bodyDelimiter = searchable[bodyStart];
352
+ let bodyCloser;
353
+ if (bodyDelimiter === "(") bodyCloser = ")";
354
+ else if (bodyDelimiter === "[") bodyCloser = "]";
355
+ if (bodyDelimiter === "{") {
356
+ return { end: findClosingDelimiter(searchable, bodyStart, "{", "}"), start };
357
+ }
358
+ if (bodyCloser) {
359
+ return { end: findClosingDelimiter(searchable, bodyStart, bodyDelimiter, bodyCloser), start };
360
+ }
361
+ const lineEnd = searchable.indexOf("\n", arrowIndex + 2);
362
+ return { end: lineEnd === -1 ? searchable.length : lineEnd, start };
363
+ };
364
+ const unparenthesizedArrowPattern = new RegExp(`\\b${name}\\b\\s*=>`, "gm");
365
+ const ranges = [...searchable.matchAll(unparenthesizedArrowPattern)].map((match) => getArrowRange(match.index, searchable.indexOf("=>", match.index)));
366
+ const splitAtTopLevel = (value, separator) => {
367
+ const parts = [];
368
+ const closers = { "(": ")", "[": "]", "{": "}" };
369
+ const expectedClosers = [];
370
+ let partStart = 0;
371
+ for (let index = 0; index < value.length; index++) {
372
+ const character = value[index];
373
+ const closer = closers[character];
374
+ if (closer) expectedClosers.push(closer);
375
+ else if (character === expectedClosers.at(-1)) expectedClosers.pop();
376
+ else if (character === separator && expectedClosers.length === 0) {
377
+ parts.push(value.slice(partStart, index));
378
+ partStart = index + 1;
379
+ }
380
+ }
381
+ parts.push(value.slice(partStart));
382
+ return parts;
383
+ };
384
+ const hasBindingName = (value) => {
385
+ let binding = splitAtTopLevel(value, "=")[0].trim();
386
+ if (binding.startsWith("...")) binding = binding.slice(3).trim();
387
+ binding = splitAtTopLevel(binding, ":")[0].trim();
388
+ const bindingParts = binding.split(/\s+/);
389
+ while (PARAMETER_PROPERTY_MODIFIERS.has(bindingParts[0])) bindingParts.shift();
390
+ binding = bindingParts.join(" ");
391
+ if (binding.startsWith("{") && binding.endsWith("}")) {
392
+ return splitAtTopLevel(binding.slice(1, -1), ",").some((property) => {
393
+ const propertyParts = splitAtTopLevel(property, ":");
394
+ return hasBindingName(propertyParts.length > 1 ? propertyParts.slice(1).join(":") : property);
395
+ });
396
+ }
397
+ if (binding.startsWith("[") && binding.endsWith("]")) {
398
+ return splitAtTopLevel(binding.slice(1, -1), ",").some(hasBindingName);
399
+ }
400
+ return binding.replace(/\?$/, "") === name;
401
+ };
402
+ const hasParameterBinding = (parameters) => splitAtTopLevel(parameters, ",").some(hasBindingName);
403
+ const parameterOwnerPattern = /(?:\bfunction\b[^()]*|\bcatch|\b(?:constructor|[a-zA-Z_$][\w$]*))\s*$/;
404
+ for (let parameterStart = 0; parameterStart < searchable.length; parameterStart++) {
405
+ if (searchable[parameterStart] !== "(") continue;
406
+ const parameterEnd = findClosingDelimiter(searchable, parameterStart, "(", ")");
407
+ if (searchable[parameterEnd - 1] !== ")") continue;
408
+ const parameters = searchable.slice(parameterStart + 1, parameterEnd - 1);
409
+ if (!hasParameterBinding(parameters)) continue;
410
+ const relativeBodyStart = searchable.slice(parameterEnd).search(/\S/);
411
+ const bodyStart = relativeBodyStart === -1 ? searchable.length : parameterEnd + relativeBodyStart;
412
+ if (searchable.startsWith("=>", bodyStart)) {
413
+ ranges.push(getArrowRange(parameterStart, bodyStart));
414
+ continue;
415
+ }
416
+ if (searchable[bodyStart] !== "{") continue;
417
+ const ownerMatch = parameterOwnerPattern.exec(searchable.slice(0, parameterStart));
418
+ if (!ownerMatch) continue;
419
+ ranges.push({
420
+ end: findClosingDelimiter(searchable, bodyStart, "{", "}"),
421
+ start: ownerMatch.index
422
+ });
423
+ }
424
+ const blockBindingPattern = new RegExp(`\\b(?:const|let|var|function|class)\\s+${name}\\b`, "g");
425
+ for (const match of searchable.matchAll(blockBindingPattern)) {
426
+ const blockStart = searchable.lastIndexOf("{", match.index);
427
+ if (blockStart !== -1) {
428
+ ranges.push({
429
+ end: findClosingDelimiter(searchable, blockStart, "{", "}"),
430
+ start: blockStart
431
+ });
432
+ }
433
+ }
434
+ return ranges;
435
+ };
436
+ const replaceBindingReferences = (content, from, to, invokeReferences = false) => {
437
+ const importRanges = getImportRanges(content);
438
+ const masked = maskNonCode(content);
439
+ const identifierPattern = new RegExp(`\\b${from}\\b`, "g");
440
+ const matches = [...masked.matchAll(identifierPattern)];
441
+ const shadowedRanges = getShadowedRanges(content, from);
442
+ let updated = content;
443
+ for (const match of matches.reverse()) {
444
+ const offset = match.index;
445
+ const isImportSpecifier = importRanges.some((range) => offset >= range.start && offset < range.end);
446
+ const isShadowed = shadowedRanges.some((range) => offset >= range.start && offset < range.end);
447
+ if (isImportSpecifier || isShadowed) continue;
448
+ const precedingContent = masked.slice(0, offset).trimEnd();
449
+ const preceding = precedingContent.at(-1);
450
+ const following = masked.slice(offset + from.length);
451
+ const isMemberAccess = preceding === "." && !precedingContent.endsWith("...");
452
+ const isProperty = isMemberAccess || /^\s*:/.test(following);
453
+ const isAlreadyInvoked = /^\s*\(/.test(following);
454
+ const isObjectShorthand = getEnclosingDelimiter(masked, offset) === "{" && (preceding === "{" || preceding === ",") && /^\s*[,}]/.test(following);
455
+ if (isProperty) continue;
456
+ let replacement = to;
457
+ if (invokeReferences && isObjectShorthand) {
458
+ replacement = `${from}: ${to}()`;
459
+ } else if (invokeReferences && !isAlreadyInvoked) {
460
+ replacement = `${to}()`;
461
+ }
462
+ updated = `${updated.slice(0, offset)}${replacement}${updated.slice(offset + from.length)}`;
463
+ }
464
+ return updated;
465
+ };
466
+ const replaceImportedAlias = (content, from, to, invokeReferences = false) => {
467
+ const bindings = [];
468
+ const owner = REMOVED_ALIAS_OWNERS[from];
469
+ const acceptedPackages = /* @__PURE__ */ new Set([BASIC_PACKAGE, FULL_PACKAGE, ...owner ? [owner] : []]);
470
+ const importPattern = /import\s*\{([^}]*)\}\s*from\s*(['"])(@santi020k\/eslint-config-[^'"]+)\2/g;
471
+ let updated = content.replace(importPattern, (statement, specifierText, _quote, packageName) => {
472
+ if (!acceptedPackages.has(packageName)) return statement;
473
+ const specifiers = specifierText.split(",");
474
+ const rewritten = specifiers.map((specifier) => {
475
+ const aliasMatch = /^(\s*)([a-zA-Z_$][\w$]*)\s+as\s+([a-zA-Z_$][\w$]*)(\s*)$/.exec(specifier);
476
+ if (aliasMatch?.[2] === from) {
477
+ const localName = aliasMatch[3];
478
+ bindings.push({ from: localName, to: localName });
479
+ return `${aliasMatch[1]}${to} as ${localName}${aliasMatch[4]}`;
480
+ }
481
+ const directMatch = /^(\s*)([a-zA-Z_$][\w$]*)(\s*)$/.exec(specifier);
482
+ if (directMatch?.[2] !== from) return specifier;
483
+ bindings.push({ from, to });
484
+ return `${directMatch[1]}${to}${directMatch[3]}`;
485
+ });
486
+ const rewrittenText = rewritten.join(",");
487
+ return rewrittenText === specifierText ? statement : statement.replace(specifierText, rewrittenText);
488
+ });
489
+ for (const binding of bindings) {
490
+ updated = replaceBindingReferences(updated, binding.from, binding.to, invokeReferences);
491
+ }
492
+ return updated;
493
+ };
494
+ const splitBasicIntegrationImports = (content) => {
495
+ const moved = /* @__PURE__ */ new Set();
496
+ const featurePackages = /* @__PURE__ */ new Set();
497
+ const importPattern = /import\s*\{([^}]+)\}\s*from\s*(['"])@santi020k\/eslint-config-(?:basic|lite)\2/g;
498
+ const transformed = content.replace(importPattern, (statement, specifierText, quote) => {
499
+ const specifiers = specifierText.split(",").map((value) => value.trim()).filter(Boolean);
500
+ const grouped = /* @__PURE__ */ new Map();
501
+ const integrations = specifiers.filter((specifier) => {
502
+ const imported = specifier.split(/\s+as\s+/)[0]?.trim() ?? "";
503
+ const packageName = EXPORT_TO_FEATURE_PACKAGE.get(imported);
504
+ if (!packageName) return false;
505
+ const values = grouped.get(packageName) ?? [];
506
+ values.push(specifier);
507
+ grouped.set(packageName, values);
508
+ return true;
509
+ });
510
+ if (integrations.length === 0) return statement;
511
+ for (const specifier of integrations) moved.add(specifier);
512
+ const basic = specifiers.filter((specifier) => !integrations.includes(specifier));
513
+ const lines = [];
514
+ if (basic.length > 0) {
515
+ lines.push(`import { ${basic.join(", ")} } from ${quote}${BASIC_PACKAGE}${quote}`);
516
+ }
517
+ for (const [packageName, values] of grouped) {
518
+ featurePackages.add(packageName);
519
+ lines.push(`import { ${values.join(", ")} } from ${quote}${packageName}${quote}`);
520
+ }
521
+ return lines.join("\n");
522
+ });
523
+ return {
524
+ changed: transformed !== content,
525
+ content: transformed,
526
+ featurePackages: [...featurePackages],
527
+ moved: [...moved]
528
+ };
529
+ };
530
+ const hasCodePropertySeparator = (codeContent, match) => {
531
+ const separatorOffset = match[0].indexOf(":");
532
+ const matchIndex = match.index ?? -1;
533
+ return matchIndex >= 0 && separatorOffset >= 0 && codeContent[matchIndex + separatorOffset] === ":";
534
+ };
535
+ const replaceCodeProperty = (content, property, replacement, matches) => {
536
+ const masked = maskNonCode(content, true);
537
+ const pattern = new RegExp(`\\b${property}\\b(?=\\s*:)`, "g");
538
+ const propertyMatches = matches ?? [...masked.matchAll(pattern)];
539
+ let updated = content;
540
+ for (const match of propertyMatches.toReversed()) {
541
+ const { index } = match;
542
+ if (index === void 0) continue;
543
+ updated = `${updated.slice(0, index)}${replacement}${updated.slice(index + property.length)}`;
544
+ }
545
+ return updated;
546
+ };
547
+ const toFeatureName = (value) => {
548
+ const trimmed = value.trim();
549
+ const enumMatch = /^(?:Extension|Format|Library|Testing|Tool)\.([a-zA-Z0-9_$]+)$/.exec(trimmed);
550
+ const stringMatch = /^(['"])([^'"]+)\1$/.exec(trimmed);
551
+ const raw = enumMatch?.[1] ?? stringMatch?.[2];
552
+ return raw ? toKebabCase(raw) : null;
553
+ };
554
+ const getOwningObjectStart = (structure, position) => {
555
+ const objectStarts = [];
556
+ for (let index = 0; index < position; index++) {
557
+ if (structure[index] === "{") objectStarts.push(index);
558
+ if (structure[index] === "}") objectStarts.pop();
559
+ }
560
+ return objectStarts.at(-1);
561
+ };
562
+ const getDefineConfigOptionOwners = (searchable) => {
563
+ const owners = new Set(
564
+ [...searchable.matchAll(/\bdefineConfig\s*\(\s*\{/g)].map((match) => match.index + match[0].lastIndexOf("{"))
565
+ );
566
+ const optionNames = new Set(
567
+ [...searchable.matchAll(/\bdefineConfig\s*\(\s*([a-zA-Z_$][\w$]*)\b/g)].map((match) => match[1])
568
+ );
569
+ for (const optionName of optionNames) {
570
+ const bindingPattern = new RegExp(`\\b(?:const|let|var)\\s+${optionName}\\s*=\\s*\\{`, "g");
571
+ for (const match of searchable.matchAll(bindingPattern)) {
572
+ owners.add(match.index + match[0].lastIndexOf("{"));
573
+ }
574
+ }
575
+ return owners;
576
+ };
577
+ const getDirectChildObjectStarts = (structure, containerStart) => {
578
+ const children = [];
579
+ const objectStarts = [containerStart];
580
+ for (let index = containerStart + 1; index < structure.length && objectStarts.length > 0; index++) {
581
+ if (structure[index] === "{") {
582
+ if (objectStarts.at(-1) === containerStart) children.push(index);
583
+ objectStarts.push(index);
584
+ } else if (structure[index] === "}") {
585
+ objectStarts.pop();
586
+ }
587
+ }
588
+ return children;
589
+ };
590
+ const getOwnedObjectPropertyStarts = (searchable, structure, property, owners) => {
591
+ const pattern = new RegExp(`\\b${property}\\b\\s*:\\s*\\{`, "g");
592
+ return [...searchable.matchAll(pattern)].flatMap((match) => {
593
+ const owner = getOwningObjectStart(structure, match.index);
594
+ const objectStart = match.index + match[0].lastIndexOf("{");
595
+ return owner !== void 0 && owners.has(owner) && hasCodePropertySeparator(structure, match) && structure[objectStart] === "{" ? [objectStart] : [];
596
+ });
597
+ };
598
+ const getBasicOptionOwners = (searchable, structure) => {
599
+ const owners = getDefineConfigOptionOwners(searchable);
600
+ for (const projectDefaultsOwner of getOwnedObjectPropertyStarts(
601
+ searchable,
602
+ structure,
603
+ "projectDefaults",
604
+ owners
605
+ )) {
606
+ owners.add(projectDefaultsOwner);
607
+ }
608
+ const visitedProjectContainers = /* @__PURE__ */ new Set();
609
+ let projectContainers = getOwnedObjectPropertyStarts(searchable, structure, "projects", owners);
610
+ while (projectContainers.length > 0) {
611
+ for (const container of projectContainers) {
612
+ visitedProjectContainers.add(container);
613
+ for (const projectOwner of getDirectChildObjectStarts(structure, container)) {
614
+ owners.add(projectOwner);
615
+ }
616
+ }
617
+ projectContainers = getOwnedObjectPropertyStarts(searchable, structure, "projects", owners).filter((container) => !visitedProjectContainers.has(container));
618
+ }
619
+ return owners;
620
+ };
621
+ const getOwnedPropertyMatches = (searchable, structure, property, owners) => {
622
+ const pattern = new RegExp(`\\b${property}\\b\\s*:`, "g");
623
+ return [...searchable.matchAll(pattern)].flatMap((match) => {
624
+ const owner = getOwningObjectStart(structure, match.index);
625
+ return owner !== void 0 && owners.has(owner) && hasCodePropertySeparator(structure, match) ? [match] : [];
626
+ });
627
+ };
628
+ const modernizeLiteralFeatureArrays = (content) => {
629
+ const searchable = maskNonCode(content, true);
630
+ const structure = maskNonCode(content);
631
+ const optionOwners = getBasicOptionOwners(searchable, structure);
632
+ const selections = [...searchable.matchAll(
633
+ /\b(?:extensions|formats|libraries|testing|tools)\b\s*:\s*\[([\s\S]*?)\]\s*,?/g
634
+ )].flatMap((match) => {
635
+ if (/\/[/*]/.test(content.slice(match.index, match.index + match[0].length))) return [];
636
+ const features2 = match[1].split(",").filter(Boolean).map(toFeatureName);
637
+ const owner = getOwningObjectStart(structure, match.index);
638
+ if (owner === void 0 || !optionOwners.has(owner) || !hasCodePropertySeparator(structure, match) || features2.length === 0 || features2.some((feature) => feature === null)) return [];
639
+ return [{
640
+ end: match.index + match[0].length,
641
+ features: features2,
642
+ owner,
643
+ start: match.index
644
+ }];
645
+ });
646
+ if (selections.length === 0) return { changed: false, content, features: [] };
647
+ const features = [...new Set(selections.flatMap((selection) => selection.features))];
648
+ const selectionsByOwner = /* @__PURE__ */ new Map();
649
+ for (const selection of selections) {
650
+ const ownedSelections = selectionsByOwner.get(selection.owner);
651
+ if (ownedSelections) ownedSelections.push(selection);
652
+ else selectionsByOwner.set(selection.owner, [selection]);
653
+ }
654
+ const existingFeaturesByOwner = /* @__PURE__ */ new Map();
655
+ for (const match of searchable.matchAll(/\bfeatures\b\s*:\s*\{/g)) {
656
+ const owner = getOwningObjectStart(structure, match.index);
657
+ if (owner !== void 0 && !existingFeaturesByOwner.has(owner)) {
658
+ existingFeaturesByOwner.set(owner, match);
659
+ }
660
+ }
661
+ const edits = [];
662
+ for (const [owner, ownedSelections] of selectionsByOwner) {
663
+ const ownedFeatures = [...new Set(ownedSelections.flatMap((selection) => selection.features))];
664
+ const first = ownedSelections[0];
665
+ const lineStart = content.lastIndexOf("\n", first.start) + 1;
666
+ const indent = /^\s*/.exec(content.slice(lineStart, first.start))?.[0] ?? "";
667
+ const existingFeatures = existingFeaturesByOwner.get(owner);
668
+ const featureBlock = existingFeatures ? `
669
+ ${ownedFeatures.map((feature) => `${indent} ${JSON.stringify(feature)}: true,`).join("\n")}` : [
670
+ "features: {",
671
+ ...ownedFeatures.map((feature) => `${indent} ${JSON.stringify(feature)}: true,`),
672
+ `${indent}},`
673
+ ].join("\n");
674
+ for (const selection of ownedSelections) {
675
+ edits.push({
676
+ end: selection.end,
677
+ replacement: !existingFeatures && selection === first ? featureBlock : "",
678
+ start: selection.start
679
+ });
680
+ }
681
+ if (existingFeatures) {
682
+ const insertionIndex = (existingFeatures.index ?? 0) + existingFeatures[0].length;
683
+ edits.push({
684
+ end: insertionIndex,
685
+ replacement: featureBlock,
686
+ start: insertionIndex
687
+ });
688
+ }
689
+ }
690
+ let updated = content;
691
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
692
+ updated = `${updated.slice(0, edit.start)}${edit.replacement}${updated.slice(edit.end)}`;
693
+ }
694
+ return { changed: updated !== content, content: updated, features };
695
+ };
696
+ const modernizeConfigOptions = (content) => {
697
+ const changes = [];
698
+ let updated = content;
699
+ const searchable = maskNonCode(updated, true);
700
+ const structure = maskNonCode(updated);
701
+ const basicOptionOwners = getBasicOptionOwners(searchable, structure);
702
+ const featureOwners = new Set(
703
+ getOwnedPropertyMatches(searchable, structure, "features", basicOptionOwners).flatMap((match) => {
704
+ if (match.index === void 0) return [];
705
+ const owner = getOwningObjectStart(structure, match.index);
706
+ return owner === void 0 ? [] : [owner];
707
+ })
708
+ );
709
+ const integrationMatches = getOwnedPropertyMatches(searchable, structure, "integrations", basicOptionOwners).filter((match) => {
710
+ if (match.index === void 0) return false;
711
+ const owner = getOwningObjectStart(structure, match.index);
712
+ return owner !== void 0 && !featureOwners.has(owner);
713
+ });
714
+ if (integrationMatches.length > 0) {
715
+ updated = replaceCodeProperty(updated, "integrations", "features", integrationMatches);
716
+ changes.push("Replaced the deprecated integrations map with features.");
717
+ }
718
+ const updatedSearchable = maskNonCode(updated, true);
719
+ const updatedStructure = maskNonCode(updated);
720
+ const optionOwners = getDefineConfigOptionOwners(updatedSearchable);
721
+ const rootMatches = getOwnedPropertyMatches(updatedSearchable, updatedStructure, "root", optionOwners);
722
+ const rootAliasMatches = /* @__PURE__ */ new Map([
723
+ ["detectRootDir", getOwnedPropertyMatches(updatedSearchable, updatedStructure, "detectRootDir", optionOwners)],
724
+ ["tsconfigRootDir", getOwnedPropertyMatches(updatedSearchable, updatedStructure, "tsconfigRootDir", optionOwners)]
725
+ ]);
726
+ const rootAliases = [...rootAliasMatches].filter(([, matches]) => matches.length > 0).map(([alias]) => alias);
727
+ if (rootMatches.length === 0 && rootAliases.length === 1) {
728
+ const alias = rootAliases[0];
729
+ updated = replaceCodeProperty(updated, alias, "root", rootAliasMatches.get(alias));
730
+ changes.push(`Replaced ${alias} with the v3 root option.`);
731
+ } else if (rootAliases.length > 0) {
732
+ changes.push(
733
+ `Manual action required: consolidate ${rootAliases.join(" and ")} into root after confirming they resolve to the same directory.`
734
+ );
735
+ }
736
+ const featureMigration = modernizeLiteralFeatureArrays(updated);
737
+ if (featureMigration.changed) {
738
+ updated = featureMigration.content;
739
+ changes.push(`Replaced literal category arrays with the v3 features map (${featureMigration.features.join(", ")}).`);
740
+ }
741
+ if (maskNonCode(updated, true).includes("better-tailwindcss/no-unknown-classes")) {
742
+ changes.push(
743
+ 'Manual action required: replace the raw better-tailwindcss/no-unknown-classes override with projects["<scope>"].tailwind.noUnknownClasses.'
744
+ );
745
+ }
746
+ return { changes, content: updated };
747
+ };
748
+ const getExplicitConfigFeaturePackages = (content) => {
749
+ const packages = /* @__PURE__ */ new Set();
750
+ const codeContent = maskNonCode(content);
751
+ const searchableContent = maskNonCode(content, true);
752
+ const optionOwners = getBasicOptionOwners(searchableContent, codeContent);
753
+ const isBasicOption = (match) => {
754
+ const owner = getOwningObjectStart(codeContent, match.index ?? -1);
755
+ return owner !== void 0 && optionOwners.has(owner) && hasCodePropertySeparator(codeContent, match);
756
+ };
757
+ for (const category of Object.keys(FEATURE_PACKAGES)) {
758
+ const categorySelections = searchableContent.matchAll(
759
+ // eslint-disable-next-line security/detect-non-literal-regexp -- category names come from the internal package registry
760
+ new RegExp(`(?:\\b${category}\\b|['"]${category}['"])\\s*:\\s*\\[([\\s\\S]*?)\\]`, "g")
761
+ );
762
+ for (const selection of categorySelections) {
763
+ if (isBasicOption(selection) && selection[1].trim()) {
764
+ packages.add(FEATURE_PACKAGES[category]);
765
+ break;
766
+ }
767
+ }
768
+ }
769
+ const featureSelections = /(?:\b(?:features|integrations)\b|['"](?:features|integrations)['"])\s*:\s*\{([\s\S]*?)\}/g;
770
+ for (const selection of searchableContent.matchAll(featureSelections)) {
771
+ if (!isBasicOption(selection)) continue;
772
+ const enabledFeaturePattern = /(?:['"]([^'"]+)['"]|([a-zA-Z_$][\w$-]*))\s*:\s*true\b/g;
773
+ for (const match of selection[1].matchAll(enabledFeaturePattern)) {
774
+ const featureName = (match[1] || match[2]).toLowerCase();
775
+ const packageName = FEATURE_NAME_TO_PACKAGE.get(featureName);
776
+ if (packageName) packages.add(packageName);
777
+ }
778
+ }
779
+ const enumPresetPattern = /(?:\bpreset\b|['"]preset['"])\s*:\s*Preset\.(All|App|CI|Library|Monorepo)/g;
780
+ for (const match of searchableContent.matchAll(enumPresetPattern)) {
781
+ if (!isBasicOption(match)) continue;
782
+ const presetName = match[1].toLowerCase();
783
+ for (const packageName of PRESET_FEATURE_PACKAGES[presetName] ?? []) {
784
+ packages.add(packageName);
785
+ }
786
+ }
787
+ const stringPresetPattern = /(?:\bpreset\b|['"]preset['"])\s*:\s*(['"`])(all|app|ci|library|monorepo)\1/g;
788
+ for (const match of searchableContent.matchAll(stringPresetPattern)) {
789
+ if (!isBasicOption(match)) continue;
790
+ for (const packageName of PRESET_FEATURE_PACKAGES[match[2]] ?? []) {
791
+ packages.add(packageName);
792
+ }
793
+ }
794
+ const strictPattern = /(?:\bstrict\b|['"]strict['"])\s*:\s*(['"`])pedantic\1/g;
795
+ for (const match of searchableContent.matchAll(strictPattern)) {
796
+ if (isBasicOption(match)) {
797
+ packages.add(FEATURE_PACKAGES.extensions);
798
+ break;
799
+ }
800
+ }
801
+ return [...packages];
802
+ };
803
+ const replacePackageSpecifiers = (content, from, to) => content.replace(
804
+ // eslint-disable-next-line security/detect-unsafe-regex -- bounded module specifiers cannot cross quote characters
805
+ /(\b(?:from|import|export)\s*(?:\(\s*)?|\brequire\s*\(\s*)(['"])([^'"]+)\2/g,
806
+ (statement, prefix, quote, specifier) => specifier === from ? `${prefix}${quote}${to}${quote}` : statement
807
+ );
808
+ const migrateConfigToV3 = (content, mode) => {
809
+ const changes = [];
810
+ const featurePackages = new Set(getExplicitConfigFeaturePackages(content));
811
+ let migrated = content;
812
+ const liteMigrated = replacePackageSpecifiers(migrated, LITE_PACKAGE, BASIC_PACKAGE);
813
+ if (liteMigrated !== migrated) {
814
+ migrated = liteMigrated;
815
+ changes.push("Replaced the Lite compatibility import with Basic.");
816
+ }
817
+ if (mode === "full") {
818
+ const fullMigrated = replacePackageSpecifiers(migrated, BASIC_PACKAGE, FULL_PACKAGE);
819
+ if (fullMigrated !== migrated) {
820
+ migrated = fullMigrated;
821
+ changes.push("Switched the root config import to the full bundle.");
822
+ }
823
+ }
824
+ if (mode === "lean") {
825
+ const split = splitBasicIntegrationImports(migrated);
826
+ migrated = split.content;
827
+ if (split.changed) {
828
+ changes.push(`Moved feature factories to their granular packages: ${split.moved.join(", ")}.`);
829
+ }
830
+ for (const packageName of split.featurePackages) featurePackages.add(packageName);
831
+ }
832
+ for (const [removed, replacement] of Object.entries(REMOVED_ALIAS_REPLACEMENTS)) {
833
+ const invokeReferences = FACTORY_ALIAS_REPLACEMENTS.has(removed);
834
+ const updated = replaceImportedAlias(migrated, removed, replacement, invokeReferences);
835
+ if (updated !== migrated) {
836
+ migrated = updated;
837
+ changes.push(`Replaced ${removed} with ${replacement}${invokeReferences ? "()" : ""}.`);
838
+ }
839
+ }
840
+ const remixMigrated = migrated.replace(
841
+ /(\bframeworks\s*:\s*\{)([\s\S]*?)(\})/,
842
+ (_match, opening, body, closing) => `${opening}${body.replaceAll(/\bremix(\s*:)/g, "'react-router'$1")}${closing}`
843
+ );
844
+ if (remixMigrated !== migrated) {
845
+ migrated = remixMigrated;
846
+ changes.push('Moved frameworks.remix to frameworks["react-router"].');
847
+ }
848
+ const reactRouterMigrated = replacePackageSpecifiers(
849
+ migrated,
850
+ REMIX_PACKAGE,
851
+ FRAMEWORK_PACKAGES["react-router"]
852
+ );
853
+ if (reactRouterMigrated !== migrated) {
854
+ migrated = reactRouterMigrated;
855
+ changes.push("Replaced the Remix package import with React Router.");
856
+ }
857
+ if (/\bloadModule\b/.test(migrated)) {
858
+ changes.push("Manual action required: replace loadModule usage with createModuleLoader(resolver).");
859
+ }
860
+ const modernized = modernizeConfigOptions(migrated);
861
+ migrated = modernized.content;
862
+ changes.push(...modernized.changes);
863
+ return {
864
+ changes,
865
+ content: migrated,
866
+ featurePackages: [...featurePackages]
867
+ };
868
+ };
869
+ const shouldUseFullBundle = (configContent, packageJson, forceFull) => {
870
+ if (forceFull) return true;
871
+ if (configContent?.includes(FULL_PACKAGE)) return true;
872
+ if (configContent) {
873
+ const codeContent = maskNonCode(configContent);
874
+ const searchableContent = maskNonCode(configContent, true);
875
+ const optionOwners = getDefineConfigOptionOwners(searchableContent);
876
+ const isRootOption = (match) => {
877
+ const owner = getOwningObjectStart(codeContent, match.index ?? -1);
878
+ return owner !== void 0 && optionOwners.has(owner) && hasCodePropertySeparator(codeContent, match);
879
+ };
880
+ const enumPresetPattern = /(?:\bpreset\b|['"]preset['"])\s*:\s*Preset\.All/g;
881
+ for (const match of searchableContent.matchAll(enumPresetPattern)) {
882
+ if (isRootOption(match)) return true;
883
+ }
884
+ const stringPresetPattern = /(?:\bpreset\b|['"]preset['"])\s*:\s*(['"`])all\1/g;
885
+ for (const match of searchableContent.matchAll(stringPresetPattern)) {
886
+ if (isRootOption(match)) return true;
887
+ }
888
+ }
889
+ return getDeclaredConfigPackages(packageJson).has(FULL_PACKAGE);
890
+ };
891
+ const createInstallCommand = (packageManager, packages, workspaceRoot = false) => {
892
+ const packageList = packages.join(" ");
893
+ switch (packageManager) {
894
+ case "bun":
895
+ return `bun add -d ${packageList}`;
896
+ case "pnpm":
897
+ return `pnpm add -D${workspaceRoot ? " --workspace-root" : ""} ${packageList}`;
898
+ case "yarn":
899
+ return `yarn add -D ${packageList}`;
900
+ default:
901
+ return `npm install -D ${packageList}`;
902
+ }
903
+ };
904
+ const createManifestInstallCommand = (packageManager) => {
905
+ switch (packageManager) {
906
+ case "bun":
907
+ return "bun install";
908
+ case "pnpm":
909
+ return "pnpm install";
910
+ case "yarn":
911
+ return "yarn install";
912
+ default:
913
+ return "npm install";
914
+ }
915
+ };
916
+ const migratePackageJson = (packageJson, context, mode, configFeaturePackages) => {
917
+ const next = structuredClone(packageJson);
918
+ const preferredFields = getPreferredConfigDependencyFields(packageJson);
919
+ const originalFields = /* @__PURE__ */ new Map();
920
+ const migratedPackages = /* @__PURE__ */ new Map();
921
+ const previouslyDeclared = /* @__PURE__ */ new Set([
922
+ ...getDeclaredConfigPackages(next),
923
+ ...context.declaredConfigPackages ?? []
924
+ ]);
925
+ const migratableConfigPackages = [
926
+ BASIC_PACKAGE,
927
+ FULL_PACKAGE,
928
+ INTEGRATIONS_PACKAGE,
929
+ LITE_PACKAGE,
930
+ REMIX_PACKAGE,
931
+ ...Object.values(FEATURE_PACKAGES),
932
+ ...Object.values(FRAMEWORK_PACKAGES)
933
+ ];
934
+ for (const packageName of ["eslint", "typescript", ...migratableConfigPackages]) {
935
+ originalFields.set(packageName, getPackageDependencyFields(packageJson, packageName));
936
+ }
937
+ for (const field of dependencyFields) {
938
+ const dependencies = { ...getDependencyRecord(next, field) };
939
+ for (const packageName of [
940
+ BASIC_PACKAGE,
941
+ FULL_PACKAGE,
942
+ INTEGRATIONS_PACKAGE,
943
+ LITE_PACKAGE,
944
+ REMIX_PACKAGE,
945
+ ...Object.values(FEATURE_PACKAGES),
946
+ ...Object.values(FRAMEWORK_PACKAGES)
947
+ ]) {
948
+ delete dependencies[packageName];
949
+ }
950
+ if (Object.keys(dependencies).length > 0) next[field] = dependencies;
951
+ else delete next[field];
952
+ }
953
+ const addPackage = (packageName, version, fields = originalFields.get(packageName) ?? []) => {
954
+ const targetFields = fields.length > 0 ? fields : preferredFields;
955
+ for (const field of targetFields) {
956
+ const dependencies = {
957
+ ...getDependencyRecord(next, field),
958
+ [packageName]: version
959
+ };
960
+ next[field] = dependencies;
961
+ const packageFields = migratedPackages.get(packageName) ?? /* @__PURE__ */ new Set();
962
+ packageFields.add(field);
963
+ migratedPackages.set(packageName, packageFields);
964
+ }
965
+ };
966
+ addPackage(
967
+ "eslint",
968
+ "^10.0.0",
969
+ originalFields.get("eslint")?.length ? originalFields.get("eslint") : ["devDependencies"]
970
+ );
971
+ const mainPackageFields = mode === "full" ? [
972
+ ...new Set(
973
+ migratableConfigPackages.flatMap((packageName) => originalFields.get(packageName) ?? [])
974
+ )
975
+ ] : [
976
+ .../* @__PURE__ */ new Set([
977
+ ...originalFields.get(BASIC_PACKAGE) ?? [],
978
+ ...originalFields.get(FULL_PACKAGE) ?? [],
979
+ ...originalFields.get(LITE_PACKAGE) ?? []
980
+ ])
981
+ ];
982
+ addPackage(
983
+ mode === "full" ? FULL_PACKAGE : BASIC_PACKAGE,
984
+ "^3.0.0",
985
+ mainPackageFields
986
+ );
987
+ if (mode === "lean") {
988
+ const frameworkNames = new Set(context.frameworks);
989
+ if (previouslyDeclared.has(REMIX_PACKAGE)) frameworkNames.add("react-router");
990
+ for (const [framework, packageName] of Object.entries(FRAMEWORK_PACKAGES)) {
991
+ if (frameworkNames.has(framework) || previouslyDeclared.has(packageName)) {
992
+ const fields = [
993
+ ...originalFields.get(packageName) ?? [],
994
+ ...framework === "react-router" ? originalFields.get(REMIX_PACKAGE) ?? [] : []
995
+ ];
996
+ addPackage(packageName, "^3.0.0", [...new Set(fields)]);
997
+ }
998
+ }
999
+ const selectedFeaturePackages = new Set(configFeaturePackages);
1000
+ for (const category of Object.keys(FEATURE_PACKAGES)) {
1001
+ const packageName = FEATURE_PACKAGES[category];
1002
+ if (context[category].length > 0 || previouslyDeclared.has(packageName)) {
1003
+ selectedFeaturePackages.add(packageName);
1004
+ }
1005
+ }
1006
+ for (const packageName of selectedFeaturePackages) {
1007
+ addPackage(packageName, "^3.0.0");
1008
+ }
1009
+ if (previouslyDeclared.has(INTEGRATIONS_PACKAGE)) {
1010
+ addPackage(INTEGRATIONS_PACKAGE, "^3.0.0");
1011
+ }
1012
+ }
1013
+ if (context.typescript) {
1014
+ const typescriptFields = originalFields.get("typescript") ?? [];
1015
+ if (typescriptFields.length === 0) addPackage("typescript", "^5.0.0");
1016
+ }
1017
+ const peerDependenciesMeta = packageJson.peerDependenciesMeta;
1018
+ if (peerDependenciesMeta && typeof peerDependenciesMeta === "object" && !Array.isArray(peerDependenciesMeta)) {
1019
+ const nextPeerNames = new Set(Object.keys(getDependencyRecord(next, "peerDependencies")));
1020
+ const nextPeerMeta = {};
1021
+ for (const [packageName, metadata] of Object.entries(peerDependenciesMeta)) {
1022
+ let targetPackage = packageName;
1023
+ if (!nextPeerNames.has(targetPackage)) {
1024
+ if (packageName === REMIX_PACKAGE) {
1025
+ targetPackage = FRAMEWORK_PACKAGES["react-router"];
1026
+ } else if ([BASIC_PACKAGE, FULL_PACKAGE, LITE_PACKAGE].includes(packageName)) {
1027
+ targetPackage = mode === "full" ? FULL_PACKAGE : BASIC_PACKAGE;
1028
+ } else if (mode === "full" && migratableConfigPackages.includes(packageName)) {
1029
+ targetPackage = FULL_PACKAGE;
1030
+ }
1031
+ }
1032
+ if (nextPeerNames.has(targetPackage)) nextPeerMeta[targetPackage] = metadata;
1033
+ }
1034
+ if (Object.keys(nextPeerMeta).length > 0) next.peerDependenciesMeta = nextPeerMeta;
1035
+ else delete next.peerDependenciesMeta;
1036
+ }
1037
+ for (const field of dependencyFields) {
1038
+ const dependencies = getDependencyRecord(next, field);
1039
+ if (Object.keys(dependencies).length > 0) {
1040
+ next[field] = Object.fromEntries(
1041
+ Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))
1042
+ );
1043
+ }
1044
+ }
1045
+ const packages = [...migratedPackages.keys()].sort((a, b) => a.localeCompare(b)).map((name) => {
1046
+ for (const field of dependencyFields) {
1047
+ const version = getDependencyRecord(next, field)[name];
1048
+ if (version) return `${name}@${version}`;
1049
+ }
1050
+ return name;
1051
+ });
1052
+ return {
1053
+ changed: JSON.stringify(next) !== JSON.stringify(packageJson),
1054
+ packageJson: next,
1055
+ packages,
1056
+ preservesRuntimeFields: [...migratedPackages.values()].some((fields) => [...fields].some((field) => field !== "devDependencies"))
1057
+ };
1058
+ };
1059
+ const outputMigration = (result, json) => {
1060
+ if (json) {
1061
+ console.log(JSON.stringify(result, null, 2));
1062
+ return;
1063
+ }
1064
+ console.log(`v2 to v3 migration plan (${result.mode}):`);
1065
+ if (result.changes.length === 0) {
1066
+ console.log("- No v3 migration changes are required.");
1067
+ } else {
1068
+ for (const change of result.changes) console.log(`- ${change.target}: ${change.detail}`);
1069
+ }
1070
+ console.log(`- Install command: ${result.installCommand}`);
1071
+ if (result.written.length > 0) {
1072
+ console.log(`- Updated: ${result.written.join(", ")}`);
1073
+ } else {
1074
+ console.log("- Dry run only; pass --write to apply safe changes.");
1075
+ }
1076
+ };
1077
+ const handleMigrateV3 = (cwd, context, options = {}) => {
1078
+ const packagePath = join(cwd, "package.json");
1079
+ const packageJson = readPackageJson(cwd);
1080
+ const configPath = getConfigPath(cwd);
1081
+ const configContent = configPath ? readFileSync(configPath, "utf8") : null;
1082
+ const mode = shouldUseFullBundle(configContent, packageJson, options.full ?? false) ? "full" : "lean";
1083
+ const configMigration = configContent ? migrateConfigToV3(configContent, mode) : null;
1084
+ const packageMigration = migratePackageJson(
1085
+ packageJson,
1086
+ context,
1087
+ mode,
1088
+ configMigration?.featurePackages ?? []
1089
+ );
1090
+ const changes = [];
1091
+ const written = [];
1092
+ for (const detail of configMigration?.changes ?? []) {
1093
+ changes.push({ detail, target: configPath ? basename(configPath) : "eslint.config.*" });
1094
+ }
1095
+ if (packageMigration.changed) {
1096
+ changes.push({ detail: "Updated the ESLint v3 dependency set.", target: "package.json" });
1097
+ }
1098
+ const configChanged = Boolean(configPath && configMigration && configMigration.content !== configContent);
1099
+ if (options.write) {
1100
+ if (configPath && configMigration && configChanged) {
1101
+ const backupPath = createBackupPath(configPath, "v2");
1102
+ writeFileSync(backupPath, configContent ?? "");
1103
+ writeFileSync(configPath, configMigration.content);
1104
+ written.push(basename(configPath), basename(backupPath));
1105
+ }
1106
+ if (packageMigration.changed) {
1107
+ const backupPath = createBackupPath(packagePath, "v2");
1108
+ writeFileSync(backupPath, `${JSON.stringify(packageJson, null, 2)}
1109
+ `);
1110
+ writeFileSync(packagePath, `${JSON.stringify(packageMigration.packageJson, null, 2)}
1111
+ `);
1112
+ written.push("package.json", basename(backupPath));
1113
+ }
1114
+ }
1115
+ const result = {
1116
+ changes,
1117
+ configFile: configPath ? basename(configPath) : null,
1118
+ installCommand: packageMigration.preservesRuntimeFields ? createManifestInstallCommand(context.packageManager) : createInstallCommand(
1119
+ context.packageManager,
1120
+ packageMigration.packages,
1121
+ context.packageManager === "pnpm" && existsSync(join(cwd, "pnpm-workspace.yaml"))
1122
+ ),
1123
+ mode,
1124
+ packages: packageMigration.packages,
1125
+ written
1126
+ };
1127
+ outputMigration(result, options.json ?? false);
1128
+ if (options.check && changes.length > 0) process.exitCode = 1;
1129
+ };
1130
+ export {
1131
+ getExplicitConfigFeaturePackages,
1132
+ handleMigrateV3,
1133
+ migrateConfigToV3
1134
+ };