uilint 0.2.22 → 0.2.23

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,1000 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/prompts.ts
4
+ import * as p from "@clack/prompts";
5
+ import pc from "picocolors";
6
+ import { readFileSync } from "fs";
7
+ import { dirname, join } from "path";
8
+ import { fileURLToPath } from "url";
9
+ function getCLIVersion() {
10
+ try {
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const pkgPath = join(__dirname, "..", "..", "package.json");
13
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
14
+ return pkg.version || "0.0.0";
15
+ } catch {
16
+ return "0.0.0";
17
+ }
18
+ }
19
+ function intro2(title) {
20
+ const version = getCLIVersion();
21
+ const header = pc.bold(pc.cyan("\u25C6 UILint")) + pc.dim(` v${version}`);
22
+ console.log();
23
+ p.intro(title ? `${header} ${pc.dim("\xB7")} ${title}` : header);
24
+ }
25
+ function outro2(message) {
26
+ p.outro(pc.green(message));
27
+ }
28
+ function cancel2(message = "Operation cancelled.") {
29
+ p.cancel(pc.yellow(message));
30
+ process.exit(0);
31
+ }
32
+ function handleCancel(value) {
33
+ if (p.isCancel(value)) {
34
+ cancel2();
35
+ process.exit(0);
36
+ }
37
+ return value;
38
+ }
39
+ async function withSpinner(message, fn) {
40
+ const s = p.spinner();
41
+ s.start(message);
42
+ try {
43
+ const result = fn.length >= 1 ? await fn(s) : await fn();
44
+ s.stop(pc.green("\u2713 ") + message);
45
+ return result;
46
+ } catch (error) {
47
+ s.stop(pc.red("\u2717 ") + message);
48
+ throw error;
49
+ }
50
+ }
51
+ function createSpinner() {
52
+ return p.spinner();
53
+ }
54
+ function note2(message, title) {
55
+ p.note(message, title);
56
+ }
57
+ function log2(message) {
58
+ p.log.message(message);
59
+ }
60
+ function logInfo(message) {
61
+ p.log.info(message);
62
+ }
63
+ function logSuccess(message) {
64
+ p.log.success(message);
65
+ }
66
+ function logWarning(message) {
67
+ p.log.warn(message);
68
+ }
69
+ function logError(message) {
70
+ p.log.error(message);
71
+ }
72
+ async function select2(options) {
73
+ const result = await p.select({
74
+ message: options.message,
75
+ options: options.options,
76
+ initialValue: options.initialValue
77
+ });
78
+ return handleCancel(result);
79
+ }
80
+ async function confirm2(options) {
81
+ const result = await p.confirm({
82
+ message: options.message,
83
+ initialValue: options.initialValue ?? true
84
+ });
85
+ return handleCancel(result);
86
+ }
87
+ async function multiselect2(options) {
88
+ const result = await p.multiselect({
89
+ message: options.message,
90
+ options: options.options,
91
+ required: options.required,
92
+ initialValues: options.initialValues
93
+ });
94
+ return handleCancel(result);
95
+ }
96
+
97
+ // src/utils/next-detect.ts
98
+ import { existsSync, readdirSync } from "fs";
99
+ import { join as join2 } from "path";
100
+ function fileExists(projectPath, relPath) {
101
+ return existsSync(join2(projectPath, relPath));
102
+ }
103
+ function detectNextAppRouter(projectPath) {
104
+ const roots = ["app", join2("src", "app")];
105
+ const candidates = [];
106
+ let chosenRoot = null;
107
+ for (const root of roots) {
108
+ if (existsSync(join2(projectPath, root))) {
109
+ chosenRoot = root;
110
+ break;
111
+ }
112
+ }
113
+ if (!chosenRoot) return null;
114
+ const entryCandidates = [
115
+ join2(chosenRoot, "layout.tsx"),
116
+ join2(chosenRoot, "layout.jsx"),
117
+ join2(chosenRoot, "layout.ts"),
118
+ join2(chosenRoot, "layout.js"),
119
+ // Fallbacks (less ideal, but can work):
120
+ join2(chosenRoot, "page.tsx"),
121
+ join2(chosenRoot, "page.jsx")
122
+ ];
123
+ for (const rel of entryCandidates) {
124
+ if (fileExists(projectPath, rel)) candidates.push(rel);
125
+ }
126
+ return {
127
+ appRoot: chosenRoot,
128
+ appRootAbs: join2(projectPath, chosenRoot),
129
+ candidates
130
+ };
131
+ }
132
+ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
133
+ "node_modules",
134
+ ".git",
135
+ ".next",
136
+ "dist",
137
+ "build",
138
+ "out",
139
+ ".turbo",
140
+ ".vercel",
141
+ ".cursor",
142
+ "coverage",
143
+ ".uilint"
144
+ ]);
145
+ function findNextAppRouterProjects(rootDir, options) {
146
+ const maxDepth = options?.maxDepth ?? 4;
147
+ const ignoreDirs = options?.ignoreDirs ?? DEFAULT_IGNORE_DIRS;
148
+ const results = [];
149
+ const visited = /* @__PURE__ */ new Set();
150
+ function walk(dir, depth) {
151
+ if (depth > maxDepth) return;
152
+ if (visited.has(dir)) return;
153
+ visited.add(dir);
154
+ const detection = detectNextAppRouter(dir);
155
+ if (detection) {
156
+ results.push({ projectPath: dir, detection });
157
+ return;
158
+ }
159
+ let entries = [];
160
+ try {
161
+ entries = readdirSync(dir, { withFileTypes: true }).map((d) => ({
162
+ name: d.name,
163
+ isDirectory: d.isDirectory()
164
+ }));
165
+ } catch {
166
+ return;
167
+ }
168
+ for (const ent of entries) {
169
+ if (!ent.isDirectory) continue;
170
+ if (ignoreDirs.has(ent.name)) continue;
171
+ if (ent.name.startsWith(".") && ent.name !== ".") continue;
172
+ walk(join2(dir, ent.name), depth + 1);
173
+ }
174
+ }
175
+ walk(rootDir, 0);
176
+ return results;
177
+ }
178
+
179
+ // src/utils/eslint-config-inject.ts
180
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
181
+ import { join as join3, relative, dirname as dirname2 } from "path";
182
+ import { parseExpression, parseModule, generateCode } from "magicast";
183
+ import { findWorkspaceRoot } from "uilint-core/node";
184
+ var CONFIG_EXTENSIONS = [".ts", ".mjs", ".js", ".cjs"];
185
+ function findEslintConfigFile(projectPath) {
186
+ for (const ext of CONFIG_EXTENSIONS) {
187
+ const configPath = join3(projectPath, `eslint.config${ext}`);
188
+ if (existsSync2(configPath)) {
189
+ return configPath;
190
+ }
191
+ }
192
+ return null;
193
+ }
194
+ function getEslintConfigFilename(configPath) {
195
+ const parts = configPath.split("/");
196
+ return parts[parts.length - 1] || "eslint.config.mjs";
197
+ }
198
+ function isIdentifier(node, name) {
199
+ return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
200
+ }
201
+ function isStringLiteral(node) {
202
+ return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
203
+ }
204
+ function getObjectPropertyValue(obj, keyName) {
205
+ if (!obj || obj.type !== "ObjectExpression") return null;
206
+ for (const prop of obj.properties ?? []) {
207
+ if (!prop) continue;
208
+ if (prop.type === "ObjectProperty" || prop.type === "Property") {
209
+ const key = prop.key;
210
+ const keyMatch = key?.type === "Identifier" && key.name === keyName || isStringLiteral(key) && key.value === keyName;
211
+ if (keyMatch) return prop.value;
212
+ }
213
+ }
214
+ return null;
215
+ }
216
+ function hasSpreadProperties(obj) {
217
+ if (!obj || obj.type !== "ObjectExpression") return false;
218
+ return (obj.properties ?? []).some(
219
+ (p2) => p2 && (p2.type === "SpreadElement" || p2.type === "SpreadProperty")
220
+ );
221
+ }
222
+ var IGNORED_AST_KEYS = /* @__PURE__ */ new Set([
223
+ "loc",
224
+ "start",
225
+ "end",
226
+ "extra",
227
+ "leadingComments",
228
+ "trailingComments",
229
+ "innerComments"
230
+ ]);
231
+ function normalizeAstForCompare(node) {
232
+ if (node === null) return null;
233
+ if (node === void 0) return void 0;
234
+ if (typeof node !== "object") return node;
235
+ if (Array.isArray(node)) return node.map(normalizeAstForCompare);
236
+ const out = {};
237
+ const keys = Object.keys(node).filter((k) => !IGNORED_AST_KEYS.has(k)).sort();
238
+ for (const k of keys) {
239
+ if (k.startsWith("$")) continue;
240
+ out[k] = normalizeAstForCompare(node[k]);
241
+ }
242
+ return out;
243
+ }
244
+ function astEquivalent(a, b) {
245
+ try {
246
+ return JSON.stringify(normalizeAstForCompare(a)) === JSON.stringify(normalizeAstForCompare(b));
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+ function collectUilintRuleIdsFromRulesObject(rulesObj) {
252
+ const ids = /* @__PURE__ */ new Set();
253
+ if (!rulesObj || rulesObj.type !== "ObjectExpression") return ids;
254
+ for (const prop of rulesObj.properties ?? []) {
255
+ if (!prop) continue;
256
+ if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
257
+ const key = prop.key;
258
+ if (!isStringLiteral(key)) continue;
259
+ const val = key.value;
260
+ if (typeof val !== "string") continue;
261
+ if (val.startsWith("uilint/")) {
262
+ ids.add(val.slice("uilint/".length));
263
+ }
264
+ }
265
+ return ids;
266
+ }
267
+ function findExportedConfigArrayExpression(mod) {
268
+ function unwrapExpression(expr) {
269
+ let e = expr;
270
+ while (e) {
271
+ if (e.type === "TSAsExpression" || e.type === "TSNonNullExpression") {
272
+ e = e.expression;
273
+ continue;
274
+ }
275
+ if (e.type === "TSSatisfiesExpression") {
276
+ e = e.expression;
277
+ continue;
278
+ }
279
+ if (e.type === "ParenthesizedExpression") {
280
+ e = e.expression;
281
+ continue;
282
+ }
283
+ break;
284
+ }
285
+ return e;
286
+ }
287
+ function resolveTopLevelIdentifierToArrayExpr(program2, name) {
288
+ if (!program2 || program2.type !== "Program") return null;
289
+ for (const stmt of program2.body ?? []) {
290
+ if (stmt?.type !== "VariableDeclaration") continue;
291
+ for (const decl of stmt.declarations ?? []) {
292
+ const id = decl?.id;
293
+ if (!isIdentifier(id, name)) continue;
294
+ const init = unwrapExpression(decl?.init);
295
+ if (!init) return null;
296
+ if (init.type === "ArrayExpression") return init;
297
+ if (init.type === "CallExpression" && isIdentifier(init.callee, "defineConfig") && unwrapExpression(init.arguments?.[0])?.type === "ArrayExpression") {
298
+ return unwrapExpression(init.arguments?.[0]);
299
+ }
300
+ return null;
301
+ }
302
+ }
303
+ return null;
304
+ }
305
+ const program = mod?.$ast;
306
+ if (program && program.type === "Program") {
307
+ for (const stmt of program.body ?? []) {
308
+ if (!stmt || stmt.type !== "ExportDefaultDeclaration") continue;
309
+ const decl = unwrapExpression(stmt.declaration);
310
+ if (!decl) break;
311
+ if (decl.type === "ArrayExpression") {
312
+ return { kind: "esm", arrayExpr: decl, program };
313
+ }
314
+ if (decl.type === "CallExpression" && isIdentifier(decl.callee, "defineConfig") && unwrapExpression(decl.arguments?.[0])?.type === "ArrayExpression") {
315
+ return {
316
+ kind: "esm",
317
+ arrayExpr: unwrapExpression(decl.arguments?.[0]),
318
+ program
319
+ };
320
+ }
321
+ if (decl.type === "Identifier" && typeof decl.name === "string") {
322
+ const resolved = resolveTopLevelIdentifierToArrayExpr(
323
+ program,
324
+ decl.name
325
+ );
326
+ if (resolved) return { kind: "esm", arrayExpr: resolved, program };
327
+ }
328
+ break;
329
+ }
330
+ }
331
+ if (!program || program.type !== "Program") return null;
332
+ for (const stmt of program.body ?? []) {
333
+ if (!stmt || stmt.type !== "ExpressionStatement") continue;
334
+ const expr = stmt.expression;
335
+ if (!expr || expr.type !== "AssignmentExpression") continue;
336
+ const left = expr.left;
337
+ const right = expr.right;
338
+ const isModuleExports = left?.type === "MemberExpression" && isIdentifier(left.object, "module") && isIdentifier(left.property, "exports");
339
+ if (!isModuleExports) continue;
340
+ if (right?.type === "ArrayExpression") {
341
+ return { kind: "cjs", arrayExpr: right, program };
342
+ }
343
+ if (right?.type === "CallExpression" && isIdentifier(right.callee, "defineConfig") && right.arguments?.[0]?.type === "ArrayExpression") {
344
+ return { kind: "cjs", arrayExpr: right.arguments[0], program };
345
+ }
346
+ if (right?.type === "Identifier" && typeof right.name === "string") {
347
+ const resolved = resolveTopLevelIdentifierToArrayExpr(
348
+ program,
349
+ right.name
350
+ );
351
+ if (resolved) return { kind: "cjs", arrayExpr: resolved, program };
352
+ }
353
+ }
354
+ return null;
355
+ }
356
+ function collectConfiguredUilintRuleIdsFromConfigArray(arrayExpr) {
357
+ const ids = /* @__PURE__ */ new Set();
358
+ if (!arrayExpr || arrayExpr.type !== "ArrayExpression") return ids;
359
+ for (const el of arrayExpr.elements ?? []) {
360
+ if (!el || el.type !== "ObjectExpression") continue;
361
+ const rules = getObjectPropertyValue(el, "rules");
362
+ for (const id of collectUilintRuleIdsFromRulesObject(rules)) ids.add(id);
363
+ }
364
+ return ids;
365
+ }
366
+ function findExistingUilintRulesObject(arrayExpr) {
367
+ if (!arrayExpr || arrayExpr.type !== "ArrayExpression") {
368
+ return { configObj: null, rulesObj: null, safeToMutate: false };
369
+ }
370
+ for (const el of arrayExpr.elements ?? []) {
371
+ if (!el || el.type !== "ObjectExpression") continue;
372
+ const plugins = getObjectPropertyValue(el, "plugins");
373
+ const rules = getObjectPropertyValue(el, "rules");
374
+ const hasUilintPlugin = plugins?.type === "ObjectExpression" && getObjectPropertyValue(plugins, "uilint") !== null;
375
+ const uilintIds = collectUilintRuleIdsFromRulesObject(rules);
376
+ const hasUilintRules = uilintIds.size > 0;
377
+ if (!hasUilintPlugin && !hasUilintRules) continue;
378
+ const safe = rules?.type === "ObjectExpression" && !hasSpreadProperties(rules);
379
+ return { configObj: el, rulesObj: rules, safeToMutate: safe };
380
+ }
381
+ return { configObj: null, rulesObj: null, safeToMutate: false };
382
+ }
383
+ function collectTopLevelBindings(program) {
384
+ const names = /* @__PURE__ */ new Set();
385
+ if (!program || program.type !== "Program") return names;
386
+ for (const stmt of program.body ?? []) {
387
+ if (stmt?.type === "VariableDeclaration") {
388
+ for (const decl of stmt.declarations ?? []) {
389
+ const id = decl?.id;
390
+ if (id?.type === "Identifier" && typeof id.name === "string") {
391
+ names.add(id.name);
392
+ }
393
+ }
394
+ } else if (stmt?.type === "FunctionDeclaration") {
395
+ if (stmt.id?.type === "Identifier" && typeof stmt.id.name === "string") {
396
+ names.add(stmt.id.name);
397
+ }
398
+ }
399
+ }
400
+ return names;
401
+ }
402
+ function chooseUniqueIdentifier(base, used) {
403
+ if (!used.has(base)) return base;
404
+ let i = 2;
405
+ while (used.has(`${base}${i}`)) i++;
406
+ return `${base}${i}`;
407
+ }
408
+ function addLocalRuleImportsAst(mod, selectedRules, configPath, rulesRoot, fileExtension = ".js") {
409
+ const importNames = /* @__PURE__ */ new Map();
410
+ let changed = false;
411
+ const configDir = dirname2(configPath);
412
+ const rulesDir = join3(rulesRoot, ".uilint", "rules");
413
+ const relativeRulesPath = relative(configDir, rulesDir).replace(/\\/g, "/");
414
+ const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
415
+ const used = collectTopLevelBindings(mod.$ast);
416
+ for (const rule of selectedRules) {
417
+ const importName = chooseUniqueIdentifier(
418
+ `${rule.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase())}Rule`,
419
+ used
420
+ );
421
+ importNames.set(rule.id, importName);
422
+ used.add(importName);
423
+ const rulePath = `${normalizedRulesPath}/${rule.id}${fileExtension}`;
424
+ mod.imports.$add({
425
+ imported: "default",
426
+ local: importName,
427
+ from: rulePath
428
+ });
429
+ changed = true;
430
+ }
431
+ return { importNames, changed };
432
+ }
433
+ function addLocalRuleRequiresAst(program, selectedRules, configPath, rulesRoot, fileExtension = ".js") {
434
+ const importNames = /* @__PURE__ */ new Map();
435
+ let changed = false;
436
+ if (!program || program.type !== "Program") {
437
+ return { importNames, changed };
438
+ }
439
+ const configDir = dirname2(configPath);
440
+ const rulesDir = join3(rulesRoot, ".uilint", "rules");
441
+ const relativeRulesPath = relative(configDir, rulesDir).replace(/\\/g, "/");
442
+ const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
443
+ const used = collectTopLevelBindings(program);
444
+ for (const rule of selectedRules) {
445
+ const importName = chooseUniqueIdentifier(
446
+ `${rule.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase())}Rule`,
447
+ used
448
+ );
449
+ importNames.set(rule.id, importName);
450
+ used.add(importName);
451
+ const rulePath = `${normalizedRulesPath}/${rule.id}${fileExtension}`;
452
+ const stmtMod = parseModule(
453
+ `const ${importName} = require("${rulePath}");`
454
+ );
455
+ const stmt = stmtMod.$ast.body?.[0];
456
+ if (stmt) {
457
+ let insertAt = 0;
458
+ const first = program.body?.[0];
459
+ if (first?.type === "ExpressionStatement" && first.expression?.type === "StringLiteral" && first.expression.value === "use strict") {
460
+ insertAt = 1;
461
+ }
462
+ program.body.splice(insertAt, 0, stmt);
463
+ changed = true;
464
+ }
465
+ }
466
+ return { importNames, changed };
467
+ }
468
+ function appendUilintConfigBlockToArray(arrayExpr, selectedRules, ruleImportNames) {
469
+ const pluginRulesCode = Array.from(ruleImportNames.entries()).map(([ruleId, importName]) => ` "${ruleId}": ${importName},`).join("\n");
470
+ const rulesPropsCode = selectedRules.map((r) => {
471
+ const ruleKey = `uilint/${r.id}`;
472
+ const valueCode = r.defaultOptions && r.defaultOptions.length > 0 ? `["${r.defaultSeverity}", ...${JSON.stringify(
473
+ r.defaultOptions,
474
+ null,
475
+ 2
476
+ )}]` : `"${r.defaultSeverity}"`;
477
+ return ` "${ruleKey}": ${valueCode},`;
478
+ }).join("\n");
479
+ const blockCode = `{
480
+ files: [
481
+ "src/**/*.{js,jsx,ts,tsx}",
482
+ "app/**/*.{js,jsx,ts,tsx}",
483
+ "pages/**/*.{js,jsx,ts,tsx}",
484
+ ],
485
+ plugins: {
486
+ uilint: {
487
+ rules: {
488
+ ${pluginRulesCode}
489
+ },
490
+ },
491
+ },
492
+ rules: {
493
+ ${rulesPropsCode}
494
+ },
495
+ }`;
496
+ const objExpr = parseExpression(blockCode).$ast;
497
+ arrayExpr.elements.push(objExpr);
498
+ }
499
+ function getUilintEslintConfigInfoFromSourceAst(source) {
500
+ try {
501
+ const mod = parseModule(source);
502
+ const found = findExportedConfigArrayExpression(mod);
503
+ if (!found) {
504
+ return {
505
+ error: "Could not locate an exported ESLint flat config array (expected `export default [...]`, `export default defineConfig([...])`, `module.exports = [...]`, or `module.exports = defineConfig([...])`)."
506
+ };
507
+ }
508
+ const configuredRuleIds = collectConfiguredUilintRuleIdsFromConfigArray(
509
+ found.arrayExpr
510
+ );
511
+ const existingUilint = findExistingUilintRulesObject(found.arrayExpr);
512
+ const configured = configuredRuleIds.size > 0 || existingUilint.configObj !== null;
513
+ return {
514
+ info: { configuredRuleIds, configured },
515
+ mod,
516
+ arrayExpr: found.arrayExpr,
517
+ kind: found.kind
518
+ };
519
+ } catch {
520
+ return {
521
+ error: "Unable to parse ESLint config as JavaScript. Please update it manually or simplify the config so it can be safely auto-modified."
522
+ };
523
+ }
524
+ }
525
+ function getUilintEslintConfigInfoFromSource(source) {
526
+ const ast = getUilintEslintConfigInfoFromSourceAst(source);
527
+ if ("error" in ast) {
528
+ const configuredRuleIds = extractConfiguredUilintRuleIds(source);
529
+ return {
530
+ configuredRuleIds,
531
+ configured: configuredRuleIds.size > 0
532
+ };
533
+ }
534
+ return ast.info;
535
+ }
536
+ function extractConfiguredUilintRuleIds(source) {
537
+ const ids = /* @__PURE__ */ new Set();
538
+ const re = /["']uilint\/([^"']+)["']\s*:/g;
539
+ for (const m of source.matchAll(re)) {
540
+ if (m[1]) ids.add(m[1]);
541
+ }
542
+ return ids;
543
+ }
544
+ function getMissingSelectedRules(selectedRules, configuredIds) {
545
+ return selectedRules.filter((r) => !configuredIds.has(r.id));
546
+ }
547
+ function buildDesiredRuleValueExpression(rule) {
548
+ if (rule.defaultOptions && rule.defaultOptions.length > 0) {
549
+ return `["${rule.defaultSeverity}", ...${JSON.stringify(
550
+ rule.defaultOptions,
551
+ null,
552
+ 2
553
+ )}]`;
554
+ }
555
+ return `"${rule.defaultSeverity}"`;
556
+ }
557
+ function collectUilintRuleValueNodesFromConfigArray(arrayExpr) {
558
+ const out = /* @__PURE__ */ new Map();
559
+ if (!arrayExpr || arrayExpr.type !== "ArrayExpression") return out;
560
+ for (const el of arrayExpr.elements ?? []) {
561
+ if (!el || el.type !== "ObjectExpression") continue;
562
+ const rules = getObjectPropertyValue(el, "rules");
563
+ if (!rules || rules.type !== "ObjectExpression") continue;
564
+ for (const prop of rules.properties ?? []) {
565
+ if (!prop) continue;
566
+ if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
567
+ const key = prop.key;
568
+ if (!isStringLiteral(key)) continue;
569
+ const k = key.value;
570
+ if (typeof k !== "string" || !k.startsWith("uilint/")) continue;
571
+ const id = k.slice("uilint/".length);
572
+ if (!out.has(id)) out.set(id, prop.value);
573
+ }
574
+ }
575
+ return out;
576
+ }
577
+ function getRulesNeedingUpdate(selectedRules, configuredIds, arrayExpr) {
578
+ const existingVals = collectUilintRuleValueNodesFromConfigArray(arrayExpr);
579
+ return selectedRules.filter((r) => {
580
+ if (!configuredIds.has(r.id)) return false;
581
+ const existing = existingVals.get(r.id);
582
+ if (!existing) return true;
583
+ const desiredExpr = buildDesiredRuleValueExpression(r);
584
+ const desiredAst = parseExpression(desiredExpr).$ast;
585
+ return !astEquivalent(existing, desiredAst);
586
+ });
587
+ }
588
+ async function installEslintPlugin(opts) {
589
+ const configPath = findEslintConfigFile(opts.projectPath);
590
+ if (!configPath) {
591
+ return {
592
+ configFile: null,
593
+ modified: false,
594
+ missingRuleIds: [],
595
+ configured: false
596
+ };
597
+ }
598
+ const configFilename = getEslintConfigFilename(configPath);
599
+ const original = readFileSync2(configPath, "utf-8");
600
+ const isCommonJS = configPath.endsWith(".cjs");
601
+ const ast = getUilintEslintConfigInfoFromSourceAst(original);
602
+ if ("error" in ast) {
603
+ return {
604
+ configFile: configFilename,
605
+ modified: false,
606
+ missingRuleIds: [],
607
+ configured: false,
608
+ error: ast.error
609
+ };
610
+ }
611
+ const { info, mod, arrayExpr, kind } = ast;
612
+ const configuredIds = info.configuredRuleIds;
613
+ const missingRules = getMissingSelectedRules(
614
+ opts.selectedRules,
615
+ configuredIds
616
+ );
617
+ const rulesToUpdate = getRulesNeedingUpdate(
618
+ opts.selectedRules,
619
+ configuredIds,
620
+ arrayExpr
621
+ );
622
+ let rulesToApply = [];
623
+ if (!info.configured) {
624
+ rulesToApply = opts.selectedRules;
625
+ } else {
626
+ rulesToApply = [...missingRules, ...rulesToUpdate];
627
+ if (missingRules.length > 0 && !opts.force) {
628
+ const ok = await opts.confirmAddMissingRules?.(
629
+ configFilename,
630
+ missingRules
631
+ );
632
+ if (!ok) {
633
+ return {
634
+ configFile: configFilename,
635
+ modified: false,
636
+ missingRuleIds: missingRules.map((r) => r.id),
637
+ configured: true
638
+ };
639
+ }
640
+ }
641
+ }
642
+ if (rulesToApply.length === 0) {
643
+ return {
644
+ configFile: configFilename,
645
+ modified: false,
646
+ missingRuleIds: missingRules.map((r) => r.id),
647
+ configured: info.configured
648
+ };
649
+ }
650
+ let modifiedAst = false;
651
+ const localRulesDir = join3(opts.projectPath, ".uilint", "rules");
652
+ const workspaceRoot = findWorkspaceRoot(opts.projectPath);
653
+ const workspaceRulesDir = join3(workspaceRoot, ".uilint", "rules");
654
+ const rulesRoot = existsSync2(localRulesDir) ? opts.projectPath : workspaceRoot;
655
+ const isTypeScriptConfig = configPath.endsWith(".ts");
656
+ let fileExtension = isTypeScriptConfig ? "" : ".js";
657
+ let ruleImportNames;
658
+ if (kind === "esm") {
659
+ const result = addLocalRuleImportsAst(
660
+ mod,
661
+ rulesToApply,
662
+ configPath,
663
+ rulesRoot,
664
+ fileExtension
665
+ );
666
+ ruleImportNames = result.importNames;
667
+ if (result.changed) modifiedAst = true;
668
+ } else {
669
+ const result = addLocalRuleRequiresAst(
670
+ mod.$ast,
671
+ rulesToApply,
672
+ configPath,
673
+ rulesRoot,
674
+ fileExtension
675
+ );
676
+ ruleImportNames = result.importNames;
677
+ if (result.changed) modifiedAst = true;
678
+ }
679
+ if (ruleImportNames && ruleImportNames.size > 0) {
680
+ appendUilintConfigBlockToArray(arrayExpr, rulesToApply, ruleImportNames);
681
+ modifiedAst = true;
682
+ }
683
+ if (!info.configured) {
684
+ if (kind === "esm") {
685
+ mod.imports.$add({
686
+ imported: "createRule",
687
+ local: "createRule",
688
+ from: "uilint-eslint"
689
+ });
690
+ modifiedAst = true;
691
+ } else {
692
+ const stmtMod = parseModule(
693
+ `const { createRule } = require("uilint-eslint");`
694
+ );
695
+ const stmt = stmtMod.$ast.body?.[0];
696
+ if (stmt) {
697
+ let insertAt = 0;
698
+ const first = mod.$ast.body?.[0];
699
+ if (first?.type === "ExpressionStatement" && first.expression?.type === "StringLiteral" && first.expression.value === "use strict") {
700
+ insertAt = 1;
701
+ }
702
+ mod.$ast.body.splice(insertAt, 0, stmt);
703
+ modifiedAst = true;
704
+ }
705
+ }
706
+ }
707
+ const updated = modifiedAst ? generateCode(mod).code : original;
708
+ if (updated !== original) {
709
+ writeFileSync(configPath, updated, "utf-8");
710
+ return {
711
+ configFile: configFilename,
712
+ modified: true,
713
+ missingRuleIds: missingRules.map((r) => r.id),
714
+ configured: getUilintEslintConfigInfoFromSource(updated).configured
715
+ };
716
+ }
717
+ return {
718
+ configFile: configFilename,
719
+ modified: false,
720
+ missingRuleIds: missingRules.map((r) => r.id),
721
+ configured: getUilintEslintConfigInfoFromSource(updated).configured
722
+ };
723
+ }
724
+ async function uninstallEslintPlugin(options) {
725
+ const { projectPath } = options;
726
+ const configPath = findEslintConfigFile(projectPath);
727
+ if (!configPath) {
728
+ return {
729
+ success: true,
730
+ // Nothing to uninstall
731
+ modifiedFiles: []
732
+ };
733
+ }
734
+ try {
735
+ const original = readFileSync2(configPath, "utf-8");
736
+ let updated = original.replace(
737
+ /^import\s+\{[^}]*\}\s+from\s+["'][^"']*\.uilint\/rules[^"']*["'];?\s*$/gm,
738
+ ""
739
+ );
740
+ updated = updated.replace(
741
+ /^import\s+\w+\s+from\s+["'][^"']*\.uilint\/rules[^"']*["'];?\s*$/gm,
742
+ ""
743
+ );
744
+ updated = updated.replace(
745
+ /^import\s+\{[^}]*\}\s+from\s+["']uilint-eslint["'];?\s*$/gm,
746
+ ""
747
+ );
748
+ updated = updated.replace(
749
+ /^const\s+\{[^}]*createRule[^}]*\}\s*=\s*require\s*\(\s*["']uilint-eslint["']\s*\)\s*;?\s*$/gm,
750
+ ""
751
+ );
752
+ updated = updated.replace(
753
+ /["']uilint\/[^"']+["']\s*:\s*["'][^"']+["']\s*,?\s*/g,
754
+ ""
755
+ );
756
+ updated = updated.replace(
757
+ /["']uilint\/[^"']+["']\s*:\s*\[[^\]]*\]\s*,?\s*/g,
758
+ ""
759
+ );
760
+ updated = updated.replace(
761
+ /\{\s*plugins:\s*\{\s*uilint:\s*\{[^}]*\}[^}]*\}[^}]*rules:\s*\{[^}]*\}[^}]*\}\s*,?\s*/gs,
762
+ ""
763
+ );
764
+ updated = updated.replace(/\n{3,}/g, "\n\n");
765
+ if (updated !== original) {
766
+ writeFileSync(configPath, updated, "utf-8");
767
+ return {
768
+ success: true,
769
+ modifiedFiles: [configPath]
770
+ };
771
+ }
772
+ return {
773
+ success: true,
774
+ modifiedFiles: []
775
+ };
776
+ } catch (error) {
777
+ return {
778
+ success: false,
779
+ error: error instanceof Error ? error.message : String(error)
780
+ };
781
+ }
782
+ }
783
+ function extractSeverityFromValueNode(valueNode) {
784
+ if (!valueNode) return null;
785
+ if (isStringLiteral(valueNode)) {
786
+ const val = valueNode.value;
787
+ if (val === "error" || val === "warn" || val === "off") {
788
+ return val;
789
+ }
790
+ return null;
791
+ }
792
+ if (valueNode.type === "ArrayExpression") {
793
+ const firstEl = valueNode.elements?.[0];
794
+ if (firstEl && isStringLiteral(firstEl)) {
795
+ const val = firstEl.value;
796
+ if (val === "error" || val === "warn" || val === "off") {
797
+ return val;
798
+ }
799
+ }
800
+ }
801
+ return null;
802
+ }
803
+ function extractOptionsFromValueNode(valueNode) {
804
+ if (!valueNode || valueNode.type !== "ArrayExpression") return void 0;
805
+ const elements = valueNode.elements ?? [];
806
+ if (elements.length < 2) return void 0;
807
+ const optionsNode = elements[1];
808
+ if (!optionsNode || optionsNode.type !== "ObjectExpression") return void 0;
809
+ try {
810
+ const obj = {};
811
+ for (const prop of optionsNode.properties ?? []) {
812
+ if (!prop || prop.type !== "ObjectProperty" && prop.type !== "Property")
813
+ continue;
814
+ const key = prop.key;
815
+ let keyName = null;
816
+ if (key?.type === "Identifier") {
817
+ keyName = key.name;
818
+ } else if (isStringLiteral(key)) {
819
+ keyName = key.value;
820
+ }
821
+ if (!keyName) continue;
822
+ const val = prop.value;
823
+ if (isStringLiteral(val)) {
824
+ obj[keyName] = val.value;
825
+ } else if (val?.type === "NumericLiteral" || val?.type === "Literal" && typeof val.value === "number") {
826
+ obj[keyName] = val.value;
827
+ } else if (val?.type === "BooleanLiteral" || val?.type === "Literal" && typeof val.value === "boolean") {
828
+ obj[keyName] = val.value;
829
+ } else if (val?.type === "ArrayExpression") {
830
+ const arr = [];
831
+ for (const el of val.elements ?? []) {
832
+ if (isStringLiteral(el)) arr.push(el.value);
833
+ else if (el?.type === "NumericLiteral" || el?.type === "Literal" && typeof el.value === "number")
834
+ arr.push(el.value);
835
+ else if (el?.type === "BooleanLiteral" || el?.type === "Literal" && typeof el.value === "boolean")
836
+ arr.push(el.value);
837
+ }
838
+ obj[keyName] = arr;
839
+ }
840
+ }
841
+ return Object.keys(obj).length > 0 ? obj : void 0;
842
+ } catch {
843
+ return void 0;
844
+ }
845
+ }
846
+ function readRuleConfigsFromConfig(configPath) {
847
+ const configs = /* @__PURE__ */ new Map();
848
+ try {
849
+ const source = readFileSync2(configPath, "utf-8");
850
+ const mod = parseModule(source);
851
+ const found = findExportedConfigArrayExpression(mod);
852
+ if (!found) {
853
+ return configs;
854
+ }
855
+ const { arrayExpr } = found;
856
+ const valueNodes = collectUilintRuleValueNodesFromConfigArray(arrayExpr);
857
+ for (const [ruleId, valueNode] of valueNodes) {
858
+ const severity = extractSeverityFromValueNode(valueNode);
859
+ if (severity) {
860
+ const options = extractOptionsFromValueNode(valueNode);
861
+ configs.set(ruleId, { severity, options });
862
+ }
863
+ }
864
+ } catch (error) {
865
+ console.error("[eslint-config-inject] Failed to read rule configs:", error);
866
+ }
867
+ return configs;
868
+ }
869
+ function findRulePropertyInConfigArray(arrayExpr, ruleId) {
870
+ const fullRuleKey = `uilint/${ruleId}`;
871
+ if (!arrayExpr || arrayExpr.type !== "ArrayExpression") return null;
872
+ for (const el of arrayExpr.elements ?? []) {
873
+ if (!el || el.type !== "ObjectExpression") continue;
874
+ const rules = getObjectPropertyValue(el, "rules");
875
+ if (!rules || rules.type !== "ObjectExpression") continue;
876
+ for (const prop of rules.properties ?? []) {
877
+ if (!prop) continue;
878
+ if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
879
+ const key = prop.key;
880
+ if (!isStringLiteral(key)) continue;
881
+ if (key.value === fullRuleKey) {
882
+ return { prop, rulesObj: rules };
883
+ }
884
+ }
885
+ }
886
+ return null;
887
+ }
888
+ function updateRuleSeverityInConfig(configPath, ruleId, severity) {
889
+ try {
890
+ const source = readFileSync2(configPath, "utf-8");
891
+ const mod = parseModule(source);
892
+ const found = findExportedConfigArrayExpression(mod);
893
+ if (!found) {
894
+ return {
895
+ success: false,
896
+ error: "Could not parse ESLint config array"
897
+ };
898
+ }
899
+ const { arrayExpr } = found;
900
+ const ruleInfo = findRulePropertyInConfigArray(arrayExpr, ruleId);
901
+ if (!ruleInfo) {
902
+ return {
903
+ success: false,
904
+ error: `Rule "uilint/${ruleId}" not found in config. Use 'uilint install' to add new rules.`
905
+ };
906
+ }
907
+ const { prop } = ruleInfo;
908
+ const valueNode = prop.value;
909
+ if (isStringLiteral(valueNode)) {
910
+ valueNode.value = severity;
911
+ } else if (valueNode?.type === "ArrayExpression") {
912
+ const firstEl = valueNode.elements?.[0];
913
+ if (firstEl && isStringLiteral(firstEl)) {
914
+ firstEl.value = severity;
915
+ } else {
916
+ const severityNode = parseExpression(`"${severity}"`).$ast;
917
+ if (valueNode.elements && valueNode.elements.length > 0) {
918
+ valueNode.elements[0] = severityNode;
919
+ } else {
920
+ valueNode.elements = [severityNode];
921
+ }
922
+ }
923
+ } else {
924
+ return {
925
+ success: false,
926
+ error: `Rule "uilint/${ruleId}" has unexpected format`
927
+ };
928
+ }
929
+ const updated = generateCode(mod).code;
930
+ writeFileSync(configPath, updated, "utf-8");
931
+ return { success: true };
932
+ } catch (error) {
933
+ return {
934
+ success: false,
935
+ error: error instanceof Error ? error.message : String(error)
936
+ };
937
+ }
938
+ }
939
+ function updateRuleConfigInConfig(configPath, ruleId, severity, options) {
940
+ try {
941
+ const source = readFileSync2(configPath, "utf-8");
942
+ const mod = parseModule(source);
943
+ const found = findExportedConfigArrayExpression(mod);
944
+ if (!found) {
945
+ return {
946
+ success: false,
947
+ error: "Could not parse ESLint config array"
948
+ };
949
+ }
950
+ const { arrayExpr } = found;
951
+ const ruleInfo = findRulePropertyInConfigArray(arrayExpr, ruleId);
952
+ if (!ruleInfo) {
953
+ return {
954
+ success: false,
955
+ error: `Rule "uilint/${ruleId}" not found in config. Use 'uilint install' to add new rules.`
956
+ };
957
+ }
958
+ const { prop } = ruleInfo;
959
+ const optionsJson = JSON.stringify(options);
960
+ const newValueExpr = `["${severity}", ${optionsJson}]`;
961
+ const newValueNode = parseExpression(newValueExpr).$ast;
962
+ prop.value = newValueNode;
963
+ const updated = generateCode(mod).code;
964
+ writeFileSync(configPath, updated, "utf-8");
965
+ return { success: true };
966
+ } catch (error) {
967
+ return {
968
+ success: false,
969
+ error: error instanceof Error ? error.message : String(error)
970
+ };
971
+ }
972
+ }
973
+
974
+ export {
975
+ pc,
976
+ intro2 as intro,
977
+ outro2 as outro,
978
+ withSpinner,
979
+ createSpinner,
980
+ note2 as note,
981
+ log2 as log,
982
+ logInfo,
983
+ logSuccess,
984
+ logWarning,
985
+ logError,
986
+ select2 as select,
987
+ confirm2 as confirm,
988
+ multiselect2 as multiselect,
989
+ detectNextAppRouter,
990
+ findNextAppRouterProjects,
991
+ findEslintConfigFile,
992
+ getEslintConfigFilename,
993
+ getUilintEslintConfigInfoFromSource,
994
+ installEslintPlugin,
995
+ uninstallEslintPlugin,
996
+ readRuleConfigsFromConfig,
997
+ updateRuleSeverityInConfig,
998
+ updateRuleConfigInConfig
999
+ };
1000
+ //# sourceMappingURL=chunk-PB5DLLVC.js.map