pruny 1.40.0 → 1.41.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.
package/README.md CHANGED
@@ -17,7 +17,7 @@ npx pruny
17
17
  | Scanner | What it finds |
18
18
  | :------ | :------------ |
19
19
  | **API Routes** | Unused Next.js `route.ts` handlers and NestJS controller methods |
20
- | **Broken Links** | `<Link>`, `router.push()`, `redirect()` pointing to pages that don't exist |
20
+ | **Broken Links** | `<Link>`, `router.push()`, `redirect()`, `href: "/path"` in arrays/objects pointing to pages that don't exist |
21
21
  | **Unused Exports** | Named exports and class methods not imported anywhere |
22
22
  | **Unused Files** | Source files not reachable from any entry point |
23
23
  | **Unused Services** | NestJS service methods never called by controllers or other services |
@@ -100,7 +100,7 @@ For example, if your file structure is:
100
100
  app/(code)/tenant_sites/[domain]/view_seat/page.tsx
101
101
  ```
102
102
 
103
- And your components reference `/view_seat` (resolved at runtime via subdomain), Pruny recognizes this as a valid route and will **not** report it as a broken link.
103
+ And your components reference `/view_seat` (resolved at runtime via subdomain), Pruny recognizes this as a valid route and will **not** report it as a broken link. The matched tail must contain at least one literal segment (e.g., `view_seat`) — fully-dynamic tails like `[token]` alone won't match arbitrary paths.
104
104
 
105
105
  If auto-detection doesn't cover your case, use `ignore.links` in config:
106
106
 
@@ -140,7 +140,7 @@ This scans all monorepo apps and exits with code 1 if any issues are found. Comb
140
140
  ## How It Works
141
141
 
142
142
  1. **Route Detection**: Finds all `app/api/**/route.ts` (Next.js) and `*.controller.ts` (NestJS) files
143
- 2. **Link Detection**: Finds `<Link>`, `router.push()`, `redirect()`, `href:` patterns and validates against known page routes
143
+ 2. **Link Detection**: Finds `<Link>`, `router.push()`, `redirect()`, `href: "/path"` in arrays/config objects, `<a>` tags, `revalidatePath()`, and `pathname ===` comparisons — validates all against known page routes. The summary table always shows an "Internal Links" row when links are scanned, so you can see the feature is active
144
144
  3. **Reference Scanning**: Searches the entire codebase for string references to routes, exports, and assets
145
145
  4. **Dynamic Route Matching**: Understands `[id]`, `[...slug]`, `[[...slug]]` dynamic segments
146
146
  5. **Fix Mode**: Removes unused methods, exports, and files with a cascading second pass to catch newly dead code
package/dist/index.js CHANGED
@@ -15905,6 +15905,9 @@ function matchesDynamicSuffix(refSegments, routeSegments) {
15905
15905
  if (!prefix.some((s) => /^\[.+\]$/.test(s)))
15906
15906
  return false;
15907
15907
  const tail = routeSegments.slice(prefixLen);
15908
+ const hasLiteralInTail = tail.some((s) => !/^\[/.test(s));
15909
+ if (!hasLiteralInTail)
15910
+ return false;
15908
15911
  return matchSegments(refSegments, tail);
15909
15912
  }
15910
15913
  function matchSegments(refSegments, routeSegments) {
@@ -15939,7 +15942,7 @@ async function scanBrokenLinks(config) {
15939
15942
  ignore: [...config.ignore.folders, "**/node_modules/**", "**/_*/**"]
15940
15943
  });
15941
15944
  if (pageFiles.length === 0) {
15942
- return { total: 0, links: [] };
15945
+ return { total: 0, scanned: 0, links: [] };
15943
15946
  }
15944
15947
  const knownRoutes = new Set;
15945
15948
  const routeSegmentsList = [];
@@ -15965,6 +15968,7 @@ async function scanBrokenLinks(config) {
15965
15968
  absolute: true
15966
15969
  });
15967
15970
  const brokenMap = new Map;
15971
+ const allLinkPaths = new Set;
15968
15972
  for (const file of sourceFiles) {
15969
15973
  try {
15970
15974
  const content = readFileSync9(file, "utf-8");
@@ -15980,6 +15984,7 @@ async function scanBrokenLinks(config) {
15980
15984
  const cleaned = cleanPath(rawPath);
15981
15985
  if (!cleaned || cleaned === "/")
15982
15986
  continue;
15987
+ allLinkPaths.add(cleaned);
15983
15988
  if (!matchesRoute(cleaned, knownRoutes, routeSegmentsList)) {
15984
15989
  const ignorePatterns = [
15985
15990
  ...config.ignore.links || [],
@@ -16012,6 +16017,7 @@ async function scanBrokenLinks(config) {
16012
16017
  links.sort((a, b) => b.references.length - a.references.length);
16013
16018
  return {
16014
16019
  total: links.length,
16020
+ scanned: allLinkPaths.size,
16015
16021
  links
16016
16022
  };
16017
16023
  }
@@ -17589,12 +17595,14 @@ function printSummaryTable(result, context) {
17589
17595
  Unused: result.missingAssets.total
17590
17596
  });
17591
17597
  }
17592
- if (result.brokenLinks && result.brokenLinks.total > 0) {
17598
+ if (result.brokenLinks && result.brokenLinks.scanned > 0) {
17599
+ const broken = result.brokenLinks.total;
17600
+ const valid = result.brokenLinks.scanned - broken;
17593
17601
  summary.push({
17594
- Category: source_default.red.bold("\uD83D\uDD17 Broken Links"),
17595
- Total: result.brokenLinks.total,
17596
- Used: "-",
17597
- Unused: result.brokenLinks.total
17602
+ Category: broken > 0 ? source_default.red.bold("Internal Links") : "Internal Links",
17603
+ Total: result.brokenLinks.scanned,
17604
+ Used: valid,
17605
+ Unused: broken
17598
17606
  });
17599
17607
  }
17600
17608
  if (result.unusedFiles)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "Find and remove unused Next.js API routes & Nest.js Controllers",
5
5
  "type": "module",
6
6
  "files": [