@chappibunny/repolens 1.15.5 → 1.16.1

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.
@@ -366,15 +366,20 @@ export function renderDependencyGraph(depResult) {
366
366
  lines.push("");
367
367
  }
368
368
 
369
- // Circular dependencies
369
+ // Circular dependencies. Priority comes from the cycle heuristic in cycle-priority.js.
370
370
  if (depResult.cycles.length > 0) {
371
+ const reports = Array.isArray(depResult.cycleReports) ? depResult.cycleReports : [];
371
372
  lines.push("## Circular Dependencies");
372
373
  lines.push("");
373
- lines.push(`**${depResult.cycles.length}** circular dependency chain${depResult.cycles.length === 1 ? " was" : "s were"} detected. Circular imports can cause initialization errors, increase bundle sizes, and make modules harder to test in isolation. Consider refactoring shared logic into a separate module.`);
374
+ lines.push(`**${depResult.cycles.length}** circular dependency chain${depResult.cycles.length === 1 ? " was" : "s were"} detected. A large cycle across three or more folders is a high-priority refactor. A two-folder recursive-descent pair (many imports and calls both ways) is mutual recursion, probably intentional; verify. A cycle that exists only through type-only imports is not a runtime cycle.`);
374
375
  lines.push("");
375
- for (let i = 0; i < Math.min(depResult.cycles.length, 20); i++) {
376
+ const limit = Math.min(depResult.cycles.length, 20);
377
+ for (let i = 0; i < limit; i++) {
376
378
  const cycle = depResult.cycles[i];
377
- lines.push(`${i + 1}. \`${cycle.join("` → `")}\``);
379
+ const report = reports[i];
380
+ const pathText = Array.isArray(cycle) ? cycle.join("` → `") : String(cycle);
381
+ lines.push(`${i + 1}. \`${pathText}\``);
382
+ if (report?.priorityLine) lines.push(` ${report.priorityLine}`);
378
383
  }
379
384
  if (depResult.cycles.length > 20) {
380
385
  lines.push(`\n*...and ${depResult.cycles.length - 20} more cycles*`);
@@ -382,6 +387,13 @@ export function renderDependencyGraph(depResult) {
382
387
  lines.push("");
383
388
  }
384
389
 
390
+ if (Array.isArray(depResult.typeOnlyReports) && depResult.typeOnlyReports.length > 0) {
391
+ lines.push("## Type-only cycles");
392
+ lines.push("");
393
+ lines.push(`**${depResult.typeOnlyReports.length}** cycle${depResult.typeOnlyReports.length === 1 ? " exists" : "s exist"} only through \`import type\` / \`export type\`. They are not runtime dependencies. Priority: Low — type-only imports; verify.`);
394
+ lines.push("");
395
+ }
396
+
385
397
  // External dependencies
386
398
  if (depResult.externalDeps.length > 0) {
387
399
  lines.push("## External Dependencies");
@@ -1,3 +1,5 @@
1
+ import { replaceBracketParams } from "../utils/route-params.js";
2
+
1
3
  function isRouteFile(file) {
2
4
  return (
3
5
  file.includes("/pages/api/") ||
@@ -15,10 +17,10 @@ function routePathFromFile(file) {
15
17
  const appIndex = file.indexOf("/app/");
16
18
  const relative = file.slice(appIndex + 5);
17
19
 
18
- return "/" + relative
20
+ const cleaned = relative
19
21
  .replace(/\/page\.(ts|tsx|js|jsx)$/, "")
20
- .replace(/\/route\.(ts|tsx|js|jsx)$/, "")
21
- .replace(/\[(.*?)\]/g, ":$1");
22
+ .replace(/\/route\.(ts|tsx|js|jsx)$/, "");
23
+ return `/${replaceBracketParams(cleaned)}`;
22
24
  }
23
25
 
24
26
  if (file.includes("/pages/api/")) {
@@ -1,13 +1,12 @@
1
+ import { replaceLeadingAppGroup, replaceSlashGroups } from "../utils/route-params.js";
2
+
1
3
  function sanitizeNodeId(value) {
2
4
  return value.replace(/[^a-zA-Z0-9_]/g, "_");
3
5
  }
4
6
 
5
7
  function normalizeLabel(value) {
6
- return value
7
- .replace(/^src\//, "")
8
- .replace(/^app\/\((.*?)\)/, "app/$1")
9
- .replace(/\/\((.*?)\)/g, "/$1")
10
- .replace(/\/+/g, "/");
8
+ const withoutSrc = value.replace(/^src\//, "");
9
+ return replaceSlashGroups(replaceLeadingAppGroup(withoutSrc)).replace(/\/+/g, "/");
11
10
  }
12
11
 
13
12
  function buildModuleGraph(modules, depGraph) {
@@ -96,9 +95,11 @@ function buildModuleGraph(modules, depGraph) {
96
95
  */
97
96
  function findModuleForFile(fileKey, modules) {
98
97
  const normalized = fileKey.replace(/\\/g, "/");
98
+ const flat = modules.find((mod) => mod.flat) || modules.find((mod) => mod.key === "root");
99
99
  let bestMatch = null;
100
100
  for (const mod of modules) {
101
101
  const modKey = mod.key;
102
+ if (flat && modKey === flat.key) continue;
102
103
  // Direct match (with or without extension)
103
104
  if (normalized === modKey || normalized === modKey.replace(/\.[^/.]+$/, "")) {
104
105
  return modKey;
@@ -110,6 +111,7 @@ function findModuleForFile(fileKey, modules) {
110
111
  }
111
112
  }
112
113
  }
114
+ if (!bestMatch && flat && !normalized.includes("/")) return flat.key;
113
115
  return bestMatch;
114
116
  }
115
117
 
@@ -3,7 +3,8 @@
3
3
  // so tests and examples never drive hub lists, coupling advice, or severity.
4
4
 
5
5
  const TEST_DIRECTORIES = new Set([
6
- "test", "tests", "spec", "specs", "__tests__", "__mocks__", "fixtures", "testdata", "e2e", "cypress",
6
+ "test", "tests", "spec", "specs", "__tests__", "__mocks__", "fixtures", "fixture", "__fixtures__",
7
+ "testdata", "e2e", "e2e-fixture", "e2e-fixtures", "cypress", "__snapshots__",
7
8
  ]);
8
9
 
9
10
  // Directories holding tooling, samples, and docs. Code in here describes a build script
@@ -31,12 +32,39 @@ export function isTestPath(filePath) {
31
32
  const parts = segments(filePath);
32
33
  if (directorySegments(filePath).some((segment) => TEST_DIRECTORIES.has(segment))) return true;
33
34
  const base = parts[parts.length - 1] || "";
35
+ if (base.endsWith(".snap") || base.includes(".snap.")) return true;
34
36
  if (/^(test_|spec_)/.test(base)) return true;
35
37
  if (/(_test|_spec)\.[a-z0-9]+$/.test(base)) return true;
36
38
  if (/\.(test|spec)\.[a-z0-9]+$/.test(base)) return true;
37
39
  return false;
38
40
  }
39
41
 
42
+ /**
43
+ * Snapshot, sample, fixture, example, and generated trees.
44
+ * These are dev tooling, like tests: they describe the suite or a demo, not the product.
45
+ * Scan ignores them unless `scan.include_dev_artifacts` is true.
46
+ */
47
+ export const DEV_ARTIFACT_DIRECTORIES = new Set([
48
+ "__snapshots__",
49
+ "sample", "samples",
50
+ "example", "examples",
51
+ "fixture", "fixtures", "__fixtures__",
52
+ "e2e-fixture", "e2e-fixtures",
53
+ "generated",
54
+ ]);
55
+
56
+ export function isSnapshotPath(filePath) {
57
+ const parts = segments(filePath);
58
+ if (parts.some((segment) => segment === "__snapshots__")) return true;
59
+ const base = parts[parts.length - 1] || "";
60
+ return base.endsWith(".snap") || base.includes(".snap.");
61
+ }
62
+
63
+ export function isDevArtifactPath(filePath) {
64
+ if (isSnapshotPath(filePath)) return true;
65
+ return directorySegments(filePath).some((segment) => DEV_ARTIFACT_DIRECTORIES.has(segment));
66
+ }
67
+
40
68
  /** True when a directory above the file is a tooling, example, or docs directory. */
41
69
  export function isToolingPath(filePath) {
42
70
  return directorySegments(filePath).some((segment) => TOOLING_DIRECTORIES.has(segment));
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Linear replacements for route and label text.
3
+ *
4
+ * A lazy `.*?` between two markers is polynomial: on a long string with no
5
+ * closer, each start position scans to the end (CodeQL js/polynomial-redos).
6
+ * These walks stop at the first closer and never re-scan, which matches the
7
+ * old `String.prototype.replace` result.
8
+ */
9
+
10
+ function replaceMarked(input, open, close, mapInner) {
11
+ const value = String(input ?? "");
12
+ let out = "";
13
+ let cursor = 0;
14
+ while (cursor < value.length) {
15
+ const start = value.indexOf(open, cursor);
16
+ if (start === -1) {
17
+ out += value.slice(cursor);
18
+ break;
19
+ }
20
+ const end = value.indexOf(close, start + open.length);
21
+ if (end === -1) {
22
+ out += value.slice(cursor);
23
+ break;
24
+ }
25
+ out += value.slice(cursor, start);
26
+ out += mapInner(value.slice(start + open.length, end));
27
+ cursor = end + close.length;
28
+ }
29
+ return out;
30
+ }
31
+
32
+ /** `[param]` → `:param`, including the first `]` only. Global, non-overlapping. */
33
+ export function replaceBracketParams(value) {
34
+ return replaceMarked(value, "[", "]", (inner) => `:${inner}`);
35
+ }
36
+
37
+ /** `/(group)` → `/group`. Used for Next.js route groups in diagram labels. */
38
+ export function replaceSlashGroups(value) {
39
+ return replaceMarked(value, "/(", ")", (inner) => `/${inner}`);
40
+ }
41
+
42
+ /** A leading `app/(group)` → `app/group`. Later groups use {@link replaceSlashGroups}. */
43
+ export function replaceLeadingAppGroup(value) {
44
+ const input = String(value ?? "");
45
+ const prefix = "app/(";
46
+ if (!input.startsWith(prefix)) return input;
47
+ const end = input.indexOf(")", prefix.length);
48
+ if (end === -1) return input;
49
+ return `app/${input.slice(prefix.length, end)}${input.slice(end + 1)}`;
50
+ }
package/src/utils/yaml.js CHANGED
@@ -1,14 +1,15 @@
1
- import yaml from "js-yaml";
1
+ import { CORE_SCHEMA, load, mergeTag } from "js-yaml";
2
2
 
3
3
  const MAX_YAML_CHARS = 1_000_000;
4
4
  const MAX_YAML_ALIASES = 2000;
5
5
 
6
6
  /**
7
7
  * Parse YAML without executing custom tags.
8
- * js-yaml 4 `load` is already safe (no JS types). This adds a size cap and
9
- * an alias cap so a config cannot expand into a billion-laughs document.
10
- * The js-yaml 4 default schema is unchanged (`true`/`false` are booleans;
11
- * the YAML 1.1 spellings `yes` and `no` stay strings).
8
+ * js-yaml 5 `load` uses the YAML 1.2 Core schema (`true`/`false` are booleans;
9
+ * the YAML 1.1 spellings `yes` and `no` stay strings). The merge key (`<<`)
10
+ * is added back so configs that relied on js-yaml 4's merge still parse.
11
+ * A size cap and `maxAliases` stop a billion-laughs document.
12
+ * An empty file returns `undefined`, matching the previous loader.
12
13
  */
13
14
  export function loadYaml(raw) {
14
15
  if (typeof raw !== "string") {
@@ -17,11 +18,15 @@ export function loadYaml(raw) {
17
18
  if (raw.length > MAX_YAML_CHARS) {
18
19
  throw new Error(`YAML document exceeds ${MAX_YAML_CHARS} characters`);
19
20
  }
21
+ if (raw.trim() === "") return undefined;
20
22
 
21
23
  const aliasUses = raw.match(/(^|[\s\[\{,])\*[A-Za-z_][\w-]*/g);
22
24
  if (aliasUses && aliasUses.length > MAX_YAML_ALIASES) {
23
25
  throw new Error(`YAML document has too many aliases (${aliasUses.length})`);
24
26
  }
25
27
 
26
- return yaml.load(raw);
28
+ return load(raw, {
29
+ schema: CORE_SCHEMA.withTags(mergeTag),
30
+ maxAliases: MAX_YAML_ALIASES,
31
+ });
27
32
  }