uilint 0.2.23 → 0.2.26

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.
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ detectPackageManager,
4
+ installDependencies,
5
+ runTestsWithCoverage
6
+ } from "./chunk-P4I4RKBY.js";
2
7
 
3
8
  // src/utils/prompts.ts
4
9
  import * as p from "@clack/prompts";
@@ -94,38 +99,125 @@ async function multiselect2(options) {
94
99
  return handleCancel(result);
95
100
  }
96
101
 
97
- // src/utils/next-detect.ts
98
- import { existsSync, readdirSync } from "fs";
102
+ // src/utils/coverage-detect.ts
103
+ import { existsSync, readFileSync as readFileSync2, statSync } from "fs";
99
104
  import { join as join2 } from "path";
105
+ var VITEST_CONFIG_FILES = [
106
+ "vitest.config.ts",
107
+ "vitest.config.js",
108
+ "vitest.config.mts",
109
+ "vitest.config.mjs"
110
+ ];
111
+ function checkPackageDeps(projectPath) {
112
+ try {
113
+ const pkgPath = join2(projectPath, "package.json");
114
+ if (!existsSync(pkgPath)) return { hasVitest: false, hasCoveragePackage: false };
115
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
116
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
117
+ const hasVitest = "vitest" in deps;
118
+ const hasCoveragePackage = "@vitest/coverage-v8" in deps || "@vitest/coverage-istanbul" in deps;
119
+ return { hasVitest, hasCoveragePackage };
120
+ } catch {
121
+ return { hasVitest: false, hasCoveragePackage: false };
122
+ }
123
+ }
124
+ function findVitestConfig(projectPath) {
125
+ for (const configFile of VITEST_CONFIG_FILES) {
126
+ const configPath = join2(projectPath, configFile);
127
+ if (existsSync(configPath)) {
128
+ return configPath;
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ function parseCoverageConfig(configPath) {
134
+ try {
135
+ const content = readFileSync2(configPath, "utf-8");
136
+ const hasCoverageConfig = /coverage\s*:\s*\{/.test(content);
137
+ if (!hasCoverageConfig) {
138
+ return { hasCoverageConfig: false, coverageProvider: null };
139
+ }
140
+ const providerMatch = content.match(/provider\s*:\s*["']?(v8|istanbul)["']?/);
141
+ const coverageProvider = providerMatch ? providerMatch[1] : null;
142
+ return { hasCoverageConfig, coverageProvider };
143
+ } catch {
144
+ return { hasCoverageConfig: false, coverageProvider: null };
145
+ }
146
+ }
147
+ function findCoverageData(projectPath) {
148
+ const coverageDataPath = join2(projectPath, "coverage", "coverage-final.json");
149
+ if (existsSync(coverageDataPath)) {
150
+ try {
151
+ const stats = statSync(coverageDataPath);
152
+ const age = Date.now() - stats.mtimeMs;
153
+ return { path: coverageDataPath, age };
154
+ } catch {
155
+ return { path: coverageDataPath, age: null };
156
+ }
157
+ }
158
+ return { path: null, age: null };
159
+ }
160
+ function detectCoverageSetup(projectPath) {
161
+ const { hasVitest, hasCoveragePackage } = checkPackageDeps(projectPath);
162
+ const vitestConfigPath = findVitestConfig(projectPath);
163
+ const hasVitestConfig = vitestConfigPath !== null;
164
+ let hasCoverageConfig = false;
165
+ let coverageProvider = null;
166
+ if (vitestConfigPath) {
167
+ const coverageInfo = parseCoverageConfig(vitestConfigPath);
168
+ hasCoverageConfig = coverageInfo.hasCoverageConfig;
169
+ coverageProvider = coverageInfo.coverageProvider;
170
+ }
171
+ const coverageData = findCoverageData(projectPath);
172
+ const hasCoverageData = coverageData.path !== null;
173
+ const needsCoveragePackage = hasVitest && !hasCoveragePackage;
174
+ const needsCoverageConfig = hasVitest && hasVitestConfig && !hasCoverageConfig;
175
+ return {
176
+ hasVitest,
177
+ hasVitestConfig,
178
+ vitestConfigPath,
179
+ hasCoverageConfig,
180
+ coverageProvider,
181
+ hasCoverageData,
182
+ coverageDataPath: coverageData.path,
183
+ needsCoveragePackage,
184
+ needsCoverageConfig,
185
+ coverageDataAge: coverageData.age
186
+ };
187
+ }
188
+
189
+ // src/utils/next-detect.ts
190
+ import { existsSync as existsSync2, readdirSync } from "fs";
191
+ import { join as join3 } from "path";
100
192
  function fileExists(projectPath, relPath) {
101
- return existsSync(join2(projectPath, relPath));
193
+ return existsSync2(join3(projectPath, relPath));
102
194
  }
103
195
  function detectNextAppRouter(projectPath) {
104
- const roots = ["app", join2("src", "app")];
196
+ const roots = ["app", join3("src", "app")];
105
197
  const candidates = [];
106
198
  let chosenRoot = null;
107
199
  for (const root of roots) {
108
- if (existsSync(join2(projectPath, root))) {
200
+ if (existsSync2(join3(projectPath, root))) {
109
201
  chosenRoot = root;
110
202
  break;
111
203
  }
112
204
  }
113
205
  if (!chosenRoot) return null;
114
206
  const entryCandidates = [
115
- join2(chosenRoot, "layout.tsx"),
116
- join2(chosenRoot, "layout.jsx"),
117
- join2(chosenRoot, "layout.ts"),
118
- join2(chosenRoot, "layout.js"),
207
+ join3(chosenRoot, "layout.tsx"),
208
+ join3(chosenRoot, "layout.jsx"),
209
+ join3(chosenRoot, "layout.ts"),
210
+ join3(chosenRoot, "layout.js"),
119
211
  // Fallbacks (less ideal, but can work):
120
- join2(chosenRoot, "page.tsx"),
121
- join2(chosenRoot, "page.jsx")
212
+ join3(chosenRoot, "page.tsx"),
213
+ join3(chosenRoot, "page.jsx")
122
214
  ];
123
215
  for (const rel of entryCandidates) {
124
216
  if (fileExists(projectPath, rel)) candidates.push(rel);
125
217
  }
126
218
  return {
127
219
  appRoot: chosenRoot,
128
- appRootAbs: join2(projectPath, chosenRoot),
220
+ appRootAbs: join3(projectPath, chosenRoot),
129
221
  candidates
130
222
  };
131
223
  }
@@ -169,7 +261,7 @@ function findNextAppRouterProjects(rootDir, options) {
169
261
  if (!ent.isDirectory) continue;
170
262
  if (ignoreDirs.has(ent.name)) continue;
171
263
  if (ent.name.startsWith(".") && ent.name !== ".") continue;
172
- walk(join2(dir, ent.name), depth + 1);
264
+ walk(join3(dir, ent.name), depth + 1);
173
265
  }
174
266
  }
175
267
  walk(rootDir, 0);
@@ -177,15 +269,15 @@ function findNextAppRouterProjects(rootDir, options) {
177
269
  }
178
270
 
179
271
  // 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";
272
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
273
+ import { join as join4, relative, dirname as dirname2 } from "path";
182
274
  import { parseExpression, parseModule, generateCode } from "magicast";
183
275
  import { findWorkspaceRoot } from "uilint-core/node";
184
276
  var CONFIG_EXTENSIONS = [".ts", ".mjs", ".js", ".cjs"];
185
277
  function findEslintConfigFile(projectPath) {
186
278
  for (const ext of CONFIG_EXTENSIONS) {
187
- const configPath = join3(projectPath, `eslint.config${ext}`);
188
- if (existsSync2(configPath)) {
279
+ const configPath = join4(projectPath, `eslint.config${ext}`);
280
+ if (existsSync3(configPath)) {
189
281
  return configPath;
190
282
  }
191
283
  }
@@ -384,6 +476,15 @@ function collectTopLevelBindings(program) {
384
476
  const names = /* @__PURE__ */ new Set();
385
477
  if (!program || program.type !== "Program") return names;
386
478
  for (const stmt of program.body ?? []) {
479
+ if (stmt?.type === "ImportDeclaration") {
480
+ for (const spec of stmt.specifiers ?? []) {
481
+ const local = spec?.local;
482
+ if (local?.type === "Identifier" && typeof local.name === "string") {
483
+ names.add(local.name);
484
+ }
485
+ }
486
+ continue;
487
+ }
387
488
  if (stmt?.type === "VariableDeclaration") {
388
489
  for (const decl of stmt.declarations ?? []) {
389
490
  const id = decl?.id;
@@ -405,22 +506,45 @@ function chooseUniqueIdentifier(base, used) {
405
506
  while (used.has(`${base}${i}`)) i++;
406
507
  return `${base}${i}`;
407
508
  }
509
+ function findExistingDefaultImportLocalName(program, from) {
510
+ if (!program || program.type !== "Program") return null;
511
+ for (const stmt of program.body ?? []) {
512
+ if (stmt?.type !== "ImportDeclaration") continue;
513
+ const src = stmt.source?.value;
514
+ if (typeof src !== "string" || src !== from) continue;
515
+ for (const spec of stmt.specifiers ?? []) {
516
+ if (spec?.type === "ImportDefaultSpecifier") {
517
+ const local = spec.local;
518
+ if (local?.type === "Identifier" && typeof local.name === "string") {
519
+ return local.name;
520
+ }
521
+ }
522
+ }
523
+ }
524
+ return null;
525
+ }
408
526
  function addLocalRuleImportsAst(mod, selectedRules, configPath, rulesRoot, fileExtension = ".js") {
409
527
  const importNames = /* @__PURE__ */ new Map();
410
528
  let changed = false;
411
529
  const configDir = dirname2(configPath);
412
- const rulesDir = join3(rulesRoot, ".uilint", "rules");
530
+ const rulesDir = join4(rulesRoot, ".uilint", "rules");
413
531
  const relativeRulesPath = relative(configDir, rulesDir).replace(/\\/g, "/");
414
532
  const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
415
533
  const used = collectTopLevelBindings(mod.$ast);
416
534
  for (const rule of selectedRules) {
535
+ const rulePath = `${normalizedRulesPath}/${rule.id}${fileExtension}`;
536
+ const existingLocal = findExistingDefaultImportLocalName(mod.$ast, rulePath);
537
+ if (existingLocal) {
538
+ importNames.set(rule.id, existingLocal);
539
+ used.add(existingLocal);
540
+ continue;
541
+ }
417
542
  const importName = chooseUniqueIdentifier(
418
543
  `${rule.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase())}Rule`,
419
544
  used
420
545
  );
421
546
  importNames.set(rule.id, importName);
422
547
  used.add(importName);
423
- const rulePath = `${normalizedRulesPath}/${rule.id}${fileExtension}`;
424
548
  mod.imports.$add({
425
549
  imported: "default",
426
550
  local: importName,
@@ -437,7 +561,7 @@ function addLocalRuleRequiresAst(program, selectedRules, configPath, rulesRoot,
437
561
  return { importNames, changed };
438
562
  }
439
563
  const configDir = dirname2(configPath);
440
- const rulesDir = join3(rulesRoot, ".uilint", "rules");
564
+ const rulesDir = join4(rulesRoot, ".uilint", "rules");
441
565
  const relativeRulesPath = relative(configDir, rulesDir).replace(/\\/g, "/");
442
566
  const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
443
567
  const used = collectTopLevelBindings(program);
@@ -596,7 +720,7 @@ async function installEslintPlugin(opts) {
596
720
  };
597
721
  }
598
722
  const configFilename = getEslintConfigFilename(configPath);
599
- const original = readFileSync2(configPath, "utf-8");
723
+ const original = readFileSync3(configPath, "utf-8");
600
724
  const isCommonJS = configPath.endsWith(".cjs");
601
725
  const ast = getUilintEslintConfigInfoFromSourceAst(original);
602
726
  if ("error" in ast) {
@@ -648,10 +772,10 @@ async function installEslintPlugin(opts) {
648
772
  };
649
773
  }
650
774
  let modifiedAst = false;
651
- const localRulesDir = join3(opts.projectPath, ".uilint", "rules");
775
+ const localRulesDir = join4(opts.projectPath, ".uilint", "rules");
652
776
  const workspaceRoot = findWorkspaceRoot(opts.projectPath);
653
- const workspaceRulesDir = join3(workspaceRoot, ".uilint", "rules");
654
- const rulesRoot = existsSync2(localRulesDir) ? opts.projectPath : workspaceRoot;
777
+ const workspaceRulesDir = join4(workspaceRoot, ".uilint", "rules");
778
+ const rulesRoot = existsSync3(localRulesDir) ? opts.projectPath : workspaceRoot;
655
779
  const isTypeScriptConfig = configPath.endsWith(".ts");
656
780
  let fileExtension = isTypeScriptConfig ? "" : ".js";
657
781
  let ruleImportNames;
@@ -732,7 +856,7 @@ async function uninstallEslintPlugin(options) {
732
856
  };
733
857
  }
734
858
  try {
735
- const original = readFileSync2(configPath, "utf-8");
859
+ const original = readFileSync3(configPath, "utf-8");
736
860
  let updated = original.replace(
737
861
  /^import\s+\{[^}]*\}\s+from\s+["'][^"']*\.uilint\/rules[^"']*["'];?\s*$/gm,
738
862
  ""
@@ -846,7 +970,7 @@ function extractOptionsFromValueNode(valueNode) {
846
970
  function readRuleConfigsFromConfig(configPath) {
847
971
  const configs = /* @__PURE__ */ new Map();
848
972
  try {
849
- const source = readFileSync2(configPath, "utf-8");
973
+ const source = readFileSync3(configPath, "utf-8");
850
974
  const mod = parseModule(source);
851
975
  const found = findExportedConfigArrayExpression(mod);
852
976
  if (!found) {
@@ -887,7 +1011,7 @@ function findRulePropertyInConfigArray(arrayExpr, ruleId) {
887
1011
  }
888
1012
  function updateRuleSeverityInConfig(configPath, ruleId, severity) {
889
1013
  try {
890
- const source = readFileSync2(configPath, "utf-8");
1014
+ const source = readFileSync3(configPath, "utf-8");
891
1015
  const mod = parseModule(source);
892
1016
  const found = findExportedConfigArrayExpression(mod);
893
1017
  if (!found) {
@@ -938,7 +1062,7 @@ function updateRuleSeverityInConfig(configPath, ruleId, severity) {
938
1062
  }
939
1063
  function updateRuleConfigInConfig(configPath, ruleId, severity, options) {
940
1064
  try {
941
- const source = readFileSync2(configPath, "utf-8");
1065
+ const source = readFileSync3(configPath, "utf-8");
942
1066
  const mod = parseModule(source);
943
1067
  const found = findExportedConfigArrayExpression(mod);
944
1068
  if (!found) {
@@ -971,6 +1095,104 @@ function updateRuleConfigInConfig(configPath, ruleId, severity, options) {
971
1095
  }
972
1096
  }
973
1097
 
1098
+ // src/utils/coverage-prepare.ts
1099
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1100
+ function injectCoverageConfig(vitestConfigPath) {
1101
+ try {
1102
+ const content = readFileSync4(vitestConfigPath, "utf-8");
1103
+ if (/coverage\s*:\s*\{/.test(content)) {
1104
+ return false;
1105
+ }
1106
+ const testBlockRegex = /(test\s*:\s*\{)/;
1107
+ const match = content.match(testBlockRegex);
1108
+ if (!match) {
1109
+ return false;
1110
+ }
1111
+ const coverageConfig = `
1112
+ coverage: {
1113
+ provider: "v8",
1114
+ reporter: ["text", "json"],
1115
+ reportsDirectory: "./coverage",
1116
+ },`;
1117
+ const newContent = content.replace(
1118
+ testBlockRegex,
1119
+ `$1${coverageConfig}`
1120
+ );
1121
+ writeFileSync2(vitestConfigPath, newContent, "utf-8");
1122
+ return true;
1123
+ } catch {
1124
+ return false;
1125
+ }
1126
+ }
1127
+ async function prepareCoverage(options) {
1128
+ const { appRoot, onProgress, skipPackageInstall, skipTests } = options;
1129
+ const start = Date.now();
1130
+ const result = {
1131
+ packageAdded: false,
1132
+ configModified: false,
1133
+ testsRan: false,
1134
+ coverageGenerated: false,
1135
+ duration: 0
1136
+ };
1137
+ try {
1138
+ onProgress?.("Detecting coverage setup...", "detect");
1139
+ const setup = detectCoverageSetup(appRoot);
1140
+ if (!setup.hasVitest) {
1141
+ result.error = "Vitest not found in dependencies";
1142
+ result.duration = Date.now() - start;
1143
+ return result;
1144
+ }
1145
+ if (setup.needsCoveragePackage && !skipPackageInstall) {
1146
+ onProgress?.("Installing @vitest/coverage-v8...", "install");
1147
+ const pm = detectPackageManager(appRoot);
1148
+ try {
1149
+ await installDependencies(pm, appRoot, ["@vitest/coverage-v8"]);
1150
+ result.packageAdded = true;
1151
+ } catch (err) {
1152
+ const msg = err instanceof Error ? err.message : String(err);
1153
+ result.error = `Failed to install coverage package: ${msg}`;
1154
+ result.duration = Date.now() - start;
1155
+ return result;
1156
+ }
1157
+ }
1158
+ if (setup.needsCoverageConfig && setup.vitestConfigPath) {
1159
+ onProgress?.("Adding coverage configuration...", "config");
1160
+ result.configModified = injectCoverageConfig(setup.vitestConfigPath);
1161
+ }
1162
+ if (!skipTests) {
1163
+ const updatedSetup = detectCoverageSetup(appRoot);
1164
+ if (!updatedSetup.hasCoverageData || result.configModified) {
1165
+ onProgress?.("Running tests with coverage...", "test");
1166
+ const pm = detectPackageManager(appRoot);
1167
+ try {
1168
+ await runTestsWithCoverage(pm, appRoot);
1169
+ result.testsRan = true;
1170
+ } catch (err) {
1171
+ const msg = err instanceof Error ? err.message : String(err);
1172
+ result.error = `Tests failed: ${msg}`;
1173
+ }
1174
+ const finalSetup = detectCoverageSetup(appRoot);
1175
+ result.coverageGenerated = finalSetup.hasCoverageData;
1176
+ }
1177
+ } else {
1178
+ onProgress?.("Skipping tests (skipTests=true)", "skip");
1179
+ }
1180
+ result.duration = Date.now() - start;
1181
+ return result;
1182
+ } catch (err) {
1183
+ const msg = err instanceof Error ? err.message : String(err);
1184
+ result.error = `Coverage preparation failed: ${msg}`;
1185
+ result.duration = Date.now() - start;
1186
+ return result;
1187
+ }
1188
+ }
1189
+ function needsCoveragePreparation(setup) {
1190
+ if (!setup.hasVitest) {
1191
+ return false;
1192
+ }
1193
+ return setup.needsCoveragePackage || setup.needsCoverageConfig || !setup.hasCoverageData;
1194
+ }
1195
+
974
1196
  export {
975
1197
  pc,
976
1198
  intro2 as intro,
@@ -995,6 +1217,10 @@ export {
995
1217
  uninstallEslintPlugin,
996
1218
  readRuleConfigsFromConfig,
997
1219
  updateRuleSeverityInConfig,
998
- updateRuleConfigInConfig
1220
+ updateRuleConfigInConfig,
1221
+ detectCoverageSetup,
1222
+ injectCoverageConfig,
1223
+ prepareCoverage,
1224
+ needsCoveragePreparation
999
1225
  };
1000
- //# sourceMappingURL=chunk-PB5DLLVC.js.map
1226
+ //# sourceMappingURL=chunk-VNANPKR2.js.map