@mintlify/link-rot 3.0.1516 → 3.0.1518

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/dist/graph.d.ts CHANGED
@@ -76,6 +76,7 @@ export declare class Graph {
76
76
  private edges;
77
77
  private fileResolutionMap;
78
78
  private redirectMap;
79
+ private wildcardRedirects;
79
80
  private configRedirects;
80
81
  private mintIgnoreGlobs;
81
82
  private virtualNodes;
@@ -107,6 +108,12 @@ export declare class Graph {
107
108
  getBrokenInternalLinks(options?: {
108
109
  checkAnchors?: boolean;
109
110
  }): MdxPath[];
111
+ private getPageCandidates;
112
+ private isPageRoute;
113
+ private resolvesToValidPath;
114
+ private reachesPage;
115
+ private getExactRedirectTarget;
116
+ private getRedirectTarget;
110
117
  getBrokenRedirects(): {
111
118
  source: string;
112
119
  destination: string;
package/dist/graph.js CHANGED
@@ -20,6 +20,7 @@ const decodeUrlComponent = (value) => {
20
20
  return value;
21
21
  }
22
22
  };
23
+ const hasWildcard = (pattern) => /[:*]/.test(pattern);
23
24
  export var Wrapper;
24
25
  (function (Wrapper) {
25
26
  Wrapper["MD"] = "md";
@@ -244,10 +245,11 @@ export class Edge {
244
245
  export class Graph {
245
246
  constructor(baseDir, mintIgnoreGlobs = []) {
246
247
  this.baseDir = baseDir;
247
- this.nodes = {};
248
+ this.nodes = Object.create(null);
248
249
  this.edges = [];
249
250
  this.fileResolutionMap = new Map();
250
251
  this.redirectMap = new Map();
252
+ this.wildcardRedirects = [];
251
253
  this.configRedirects = [];
252
254
  this.mintIgnoreGlobs = [];
253
255
  this.virtualNodes = new Set();
@@ -277,8 +279,12 @@ export class Graph {
277
279
  this.configRedirects = redirects;
278
280
  this.redirectMap.clear();
279
281
  for (const redirect of redirects) {
280
- this.redirectMap.set(normalizePath(redirect.source), normalizePath(redirect.destination));
282
+ const source = normalizePath(redirect.source);
283
+ if (!this.redirectMap.has(source)) {
284
+ this.redirectMap.set(source, normalizePath(redirect.destination));
285
+ }
281
286
  }
287
+ this.wildcardRedirects = [...this.redirectMap].filter(([source]) => hasWildcard(source));
282
288
  }
283
289
  matchesWildcardPattern(path, pattern) {
284
290
  // Escape special regex characters except : and *
@@ -435,7 +441,6 @@ export class Graph {
435
441
  }
436
442
  const brokenLinks = new Set();
437
443
  const nodeSet = new Set(Object.values(this.nodes).map((node) => node.toString()));
438
- const wildcardRedirects = [...this.redirectMap].filter(([source]) => /[:*]/.test(source));
439
444
  for (const node of Object.values(this.nodes)) {
440
445
  for (const path of node.paths.flatMap((path) => [path, ...path.pagePaths])) {
441
446
  if (path.pathType === PathType.INTERNAL) {
@@ -443,21 +448,7 @@ export class Graph {
443
448
  const resolvedFiles = this.fileResolutionMap.get(pathString);
444
449
  const hasExistingFile = (resolvedFiles && [...resolvedFiles].some((file) => nodeSet.has(file))) ||
445
450
  this.virtualNodes.has(pathString);
446
- let redirectDestination = hasExistingFile ? undefined : this.redirectMap.get(pathString);
447
- let hasValidRedirect = redirectDestination &&
448
- (nodeSet.has(`${redirectDestination}.mdx`) || nodeSet.has(redirectDestination));
449
- if (!hasExistingFile && !hasValidRedirect) {
450
- for (const [sourcePattern, destinationPattern] of wildcardRedirects) {
451
- if (this.matchesWildcardPattern(pathString, sourcePattern)) {
452
- redirectDestination = this.applyWildcardRedirect(pathString, sourcePattern, destinationPattern);
453
- hasValidRedirect =
454
- nodeSet.has(`${redirectDestination}.mdx`) || nodeSet.has(redirectDestination);
455
- if (hasValidRedirect)
456
- break;
457
- }
458
- }
459
- }
460
- if (!hasExistingFile && !hasValidRedirect) {
451
+ if (!hasExistingFile && !this.reachesPage(pathString)) {
461
452
  brokenLinks.add(path);
462
453
  }
463
454
  else if ((options === null || options === void 0 ? void 0 : options.checkAnchors) && path.anchorLink) {
@@ -476,58 +467,75 @@ export class Graph {
476
467
  (path.pagePaths.length === 0 ||
477
468
  path.pagePaths.some((pagePath) => brokenLinks.has(pagePath))));
478
469
  }
479
- getBrokenRedirects() {
480
- const nodeSet = new Set(Object.values(this.nodes).map((node) => node.toString()));
481
- const hasWildcard = (pattern) => /[:*]/.test(pattern);
482
- const resolvesToValidPath = (rawDestination, visited) => {
483
- if (URL.canParse(rawDestination))
484
- return true;
485
- const pathOnly = removeSelectors(rawDestination);
486
- if (!pathOnly)
487
- return true;
488
- // Wildcard path destinations can't be validated as concrete paths — treat as valid,
489
- // matching the top-level skip semantics. Checked after stripping so `:` or `*` in
490
- // the fragment/query doesn't cause a false skip.
491
- if (hasWildcard(pathOnly))
492
- return true;
493
- const normalized = normalizePath(pathOnly);
494
- if (visited.has(normalized))
495
- return false;
496
- visited.add(normalized);
497
- // normalizePath('/') yields '.' — resolve lookups against node labels (which use
498
- // path.join, so `join('.', 'index.mdx') === 'index.mdx'`) rather than naive
499
- // string concatenation that would produce './index.mdx' and never match.
500
- const fileCandidates = normalized === '.'
501
- ? ['index.mdx', 'index.md']
502
- : [
503
- normalized,
504
- `${normalized}.mdx`,
505
- `${normalized}.md`,
506
- join(normalized, 'index.mdx'),
507
- join(normalized, 'index.md'),
508
- ];
509
- if (fileCandidates.some((candidate) => nodeSet.has(candidate)) ||
510
- this.virtualNodes.has(normalized)) {
511
- return true;
512
- }
513
- const chained = this.redirectMap.get(normalized);
514
- if (chained !== undefined)
515
- return resolvesToValidPath(chained, visited);
516
- for (const [sourcePattern, destinationPattern] of this.redirectMap.entries()) {
517
- if (hasWildcard(sourcePattern) && this.matchesWildcardPattern(normalized, sourcePattern)) {
518
- const resolved = this.applyWildcardRedirect(normalized, sourcePattern, destinationPattern);
519
- return resolvesToValidPath(resolved, visited);
520
- }
521
- }
470
+ getPageCandidates(normalized) {
471
+ // normalizePath('/') yields '.' — resolve lookups against node labels (which use
472
+ // path.join, so `join('.', 'index.mdx') === 'index.mdx'`) rather than naive
473
+ // string concatenation that would produce './index.mdx' and never match.
474
+ return normalized === '.'
475
+ ? ['index.mdx', 'index.md']
476
+ : [
477
+ `${normalized}.mdx`,
478
+ `${normalized}.md`,
479
+ join(normalized, 'index.mdx'),
480
+ join(normalized, 'index.md'),
481
+ ];
482
+ }
483
+ isPageRoute(normalized) {
484
+ return (this.getPageCandidates(normalized).some((candidate) => this.nodes[candidate]) ||
485
+ this.virtualNodes.has(normalized));
486
+ }
487
+ resolvesToValidPath(rawDestination, visited = new Set()) {
488
+ if (URL.canParse(rawDestination))
489
+ return true;
490
+ const pathOnly = removeSelectors(rawDestination);
491
+ if (!pathOnly)
492
+ return true;
493
+ // Wildcard path destinations can't be validated as concrete paths — treat as valid,
494
+ // matching the top-level skip semantics. Checked after stripping so `:` or `*` in
495
+ // the fragment/query doesn't cause a false skip.
496
+ if (hasWildcard(pathOnly))
497
+ return true;
498
+ return this.reachesPage(normalizePath(pathOnly), visited);
499
+ }
500
+ reachesPage(normalized, visited = new Set()) {
501
+ if (visited.has(normalized))
522
502
  return false;
523
- };
503
+ visited.add(normalized);
504
+ const exact = this.getExactRedirectTarget(normalized);
505
+ if (exact !== undefined)
506
+ return this.resolvesToValidPath(exact, visited);
507
+ if ((normalized !== '.' && this.nodes[normalized]) || this.isPageRoute(normalized))
508
+ return true;
509
+ const redirected = this.getRedirectTarget(normalized);
510
+ return redirected !== undefined && this.resolvesToValidPath(redirected, visited);
511
+ }
512
+ getExactRedirectTarget(normalized) {
513
+ return this.redirectMap.get(normalized);
514
+ }
515
+ getRedirectTarget(normalized) {
516
+ const exact = this.getExactRedirectTarget(normalized);
517
+ if (exact !== undefined)
518
+ return exact;
519
+ for (const [sourcePattern, destinationPattern] of this.wildcardRedirects) {
520
+ if (this.matchesWildcardPattern(normalized, sourcePattern)) {
521
+ return this.applyWildcardRedirect(normalized, sourcePattern, destinationPattern);
522
+ }
523
+ }
524
+ return undefined;
525
+ }
526
+ getBrokenRedirects() {
524
527
  const broken = [];
528
+ const seenSources = new Set();
525
529
  for (const { source, destination } of this.configRedirects) {
530
+ const sourcePath = normalizePath(source);
531
+ if (seenSources.has(sourcePath))
532
+ continue;
533
+ seenSources.add(sourcePath);
526
534
  if (URL.canParse(destination))
527
535
  continue;
528
536
  if (hasWildcard(removeSelectors(destination)))
529
537
  continue;
530
- if (!resolvesToValidPath(destination, new Set())) {
538
+ if (!this.resolvesToValidPath(destination)) {
531
539
  broken.push({ source, destination });
532
540
  }
533
541
  }
@@ -7,7 +7,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ import { parseGraphqlSelector, potentiallyParseOpenApiString } from '@mintlify/common';
10
11
  import { readResolvedConfigJson } from './readConfig.js';
12
+ const isPagePath = (entry) => typeof entry === 'string' &&
13
+ entry !== '' &&
14
+ potentiallyParseOpenApiString(entry) === undefined &&
15
+ parseGraphqlSelector(entry) === undefined;
11
16
  const collectHrefs = (obj) => {
12
17
  if (typeof obj !== 'object' || obj === null)
13
18
  return [];
@@ -18,6 +23,15 @@ const collectHrefs = (obj) => {
18
23
  if (typeof record.href === 'string') {
19
24
  hrefs.push(record.href);
20
25
  }
26
+ if (isPagePath(record.root)) {
27
+ hrefs.push(record.root.startsWith('/') ? record.root : `/${record.root}`);
28
+ }
29
+ if (Array.isArray(record.pages)) {
30
+ for (const entry of record.pages) {
31
+ if (isPagePath(entry))
32
+ hrefs.push(entry.startsWith('/') ? entry : `/${entry}`);
33
+ }
34
+ }
21
35
  for (const value of Object.values(record)) {
22
36
  hrefs.push(...collectHrefs(value));
23
37
  }