@mintlify/link-rot 3.0.1037 → 3.0.1039
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 +5 -0
- package/dist/graph.js +61 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/static-checking/getBrokenRedirects.d.ts +6 -0
- package/dist/static-checking/getBrokenRedirects.js +16 -0
- package/dist/static-checking/getNavigationHrefs.js +9 -14
- package/dist/static-checking/getOpenApiPagePaths.d.ts +7 -2
- package/dist/static-checking/getOpenApiPagePaths.js +46 -30
- package/dist/static-checking/getRedirects.js +6 -17
- package/dist/static-checking/readConfig.d.ts +7059 -0
- package/dist/static-checking/readConfig.js +47 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +5 -4
package/dist/graph.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ export declare class Graph {
|
|
|
67
67
|
private edges;
|
|
68
68
|
private fileResolutionMap;
|
|
69
69
|
private redirectMap;
|
|
70
|
+
private configRedirects;
|
|
70
71
|
private mintIgnoreGlobs;
|
|
71
72
|
private virtualNodes;
|
|
72
73
|
constructor(baseDir: string, mintIgnoreGlobs?: string[]);
|
|
@@ -88,6 +89,10 @@ export declare class Graph {
|
|
|
88
89
|
getBrokenInternalLinks(options?: {
|
|
89
90
|
checkAnchors?: boolean;
|
|
90
91
|
}): MdxPath[];
|
|
92
|
+
getBrokenRedirects(): {
|
|
93
|
+
source: string;
|
|
94
|
+
destination: string;
|
|
95
|
+
}[];
|
|
91
96
|
getExternalPathsByNode(): Map<string, MdxPath[]>;
|
|
92
97
|
getAllInternalPaths(): string[];
|
|
93
98
|
}
|
package/dist/graph.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getFileListWithDirectories } from '@mintlify/prebuild';
|
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { parse, join, resolve, relative, dirname, basename } from 'path';
|
|
5
5
|
import { sep as WINDOWS_SEPARATOR } from 'path/win32';
|
|
6
|
-
import { getLinkPaths, normalizePath } from './prebuild.js';
|
|
6
|
+
import { getLinkPaths, normalizePath, removeSelectors } from './prebuild.js';
|
|
7
7
|
var PathType;
|
|
8
8
|
(function (PathType) {
|
|
9
9
|
PathType["INTERNAL"] = "internal";
|
|
@@ -216,6 +216,7 @@ export class Graph {
|
|
|
216
216
|
this.edges = [];
|
|
217
217
|
this.fileResolutionMap = new Map();
|
|
218
218
|
this.redirectMap = new Map();
|
|
219
|
+
this.configRedirects = [];
|
|
219
220
|
this.mintIgnoreGlobs = [];
|
|
220
221
|
this.virtualNodes = new Set();
|
|
221
222
|
this.baseDir = resolve(baseDir);
|
|
@@ -239,6 +240,8 @@ export class Graph {
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
setRedirects(redirects) {
|
|
243
|
+
this.configRedirects = redirects;
|
|
244
|
+
this.redirectMap.clear();
|
|
242
245
|
for (const redirect of redirects) {
|
|
243
246
|
this.redirectMap.set(normalizePath(redirect.source), normalizePath(redirect.destination));
|
|
244
247
|
}
|
|
@@ -404,6 +407,63 @@ export class Graph {
|
|
|
404
407
|
}
|
|
405
408
|
return brokenLinks;
|
|
406
409
|
}
|
|
410
|
+
getBrokenRedirects() {
|
|
411
|
+
const nodeSet = new Set(Object.values(this.nodes).map((node) => node.toString()));
|
|
412
|
+
const hasWildcard = (pattern) => /[:*]/.test(pattern);
|
|
413
|
+
const resolvesToValidPath = (rawDestination, visited) => {
|
|
414
|
+
if (URL.canParse(rawDestination))
|
|
415
|
+
return true;
|
|
416
|
+
const pathOnly = removeSelectors(rawDestination);
|
|
417
|
+
if (!pathOnly)
|
|
418
|
+
return true;
|
|
419
|
+
// Wildcard path destinations can't be validated as concrete paths — treat as valid,
|
|
420
|
+
// matching the top-level skip semantics. Checked after stripping so `:` or `*` in
|
|
421
|
+
// the fragment/query doesn't cause a false skip.
|
|
422
|
+
if (hasWildcard(pathOnly))
|
|
423
|
+
return true;
|
|
424
|
+
const normalized = normalizePath(pathOnly);
|
|
425
|
+
if (visited.has(normalized))
|
|
426
|
+
return false;
|
|
427
|
+
visited.add(normalized);
|
|
428
|
+
// normalizePath('/') yields '.' — resolve lookups against node labels (which use
|
|
429
|
+
// path.join, so `join('.', 'index.mdx') === 'index.mdx'`) rather than naive
|
|
430
|
+
// string concatenation that would produce './index.mdx' and never match.
|
|
431
|
+
const fileCandidates = normalized === '.'
|
|
432
|
+
? ['index.mdx', 'index.md']
|
|
433
|
+
: [
|
|
434
|
+
normalized,
|
|
435
|
+
`${normalized}.mdx`,
|
|
436
|
+
`${normalized}.md`,
|
|
437
|
+
join(normalized, 'index.mdx'),
|
|
438
|
+
join(normalized, 'index.md'),
|
|
439
|
+
];
|
|
440
|
+
if (fileCandidates.some((candidate) => nodeSet.has(candidate)) ||
|
|
441
|
+
this.virtualNodes.has(normalized)) {
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
const chained = this.redirectMap.get(normalized);
|
|
445
|
+
if (chained !== undefined)
|
|
446
|
+
return resolvesToValidPath(chained, visited);
|
|
447
|
+
for (const [sourcePattern, destinationPattern] of this.redirectMap.entries()) {
|
|
448
|
+
if (hasWildcard(sourcePattern) && this.matchesWildcardPattern(normalized, sourcePattern)) {
|
|
449
|
+
const resolved = this.applyWildcardRedirect(normalized, sourcePattern, destinationPattern);
|
|
450
|
+
return resolvesToValidPath(resolved, visited);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return false;
|
|
454
|
+
};
|
|
455
|
+
const broken = [];
|
|
456
|
+
for (const { source, destination } of this.configRedirects) {
|
|
457
|
+
if (URL.canParse(destination))
|
|
458
|
+
continue;
|
|
459
|
+
if (hasWildcard(removeSelectors(destination)))
|
|
460
|
+
continue;
|
|
461
|
+
if (!resolvesToValidPath(destination, new Set())) {
|
|
462
|
+
broken.push({ source, destination });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return broken;
|
|
466
|
+
}
|
|
407
467
|
getExternalPathsByNode() {
|
|
408
468
|
const result = new Map();
|
|
409
469
|
for (const [label, node] of Object.entries(this.nodes)) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { buildGraph, getBrokenInternalLinks } from './static-checking/getBrokenInternalLinks.js';
|
|
2
2
|
export { getBrokenExternalLinks } from './static-checking/getBrokenExternalLinks.js';
|
|
3
3
|
export type { ExternalLinkResult } from './static-checking/getBrokenExternalLinks.js';
|
|
4
|
+
export { getBrokenRedirects } from './static-checking/getBrokenRedirects.js';
|
|
4
5
|
export { renameFilesAndUpdateLinksInContent } from './link-renaming/renameFileAndUpdateLinksInContent.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { buildGraph, getBrokenInternalLinks } from './static-checking/getBrokenInternalLinks.js';
|
|
2
2
|
export { getBrokenExternalLinks } from './static-checking/getBrokenExternalLinks.js';
|
|
3
|
+
export { getBrokenRedirects } from './static-checking/getBrokenRedirects.js';
|
|
3
4
|
export { renameFilesAndUpdateLinksInContent } from './link-renaming/renameFileAndUpdateLinksInContent.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { buildGraph } from './getBrokenInternalLinks.js';
|
|
11
|
+
export const getBrokenRedirects = (repoPath, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
12
|
+
const graph = yield buildGraph(repoPath, {
|
|
13
|
+
checkSnippets: options === null || options === void 0 ? void 0 : options.checkSnippets,
|
|
14
|
+
});
|
|
15
|
+
return graph.getBrokenRedirects();
|
|
16
|
+
});
|
|
@@ -7,8 +7,7 @@ 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
|
|
11
|
-
import path from 'path';
|
|
10
|
+
import { readResolvedConfigJson } from './readConfig.js';
|
|
12
11
|
const collectHrefs = (obj) => {
|
|
13
12
|
if (typeof obj !== 'object' || obj === null)
|
|
14
13
|
return [];
|
|
@@ -25,17 +24,13 @@ const collectHrefs = (obj) => {
|
|
|
25
24
|
return hrefs;
|
|
26
25
|
};
|
|
27
26
|
export const getNavigationHrefs = (baseDir) => __awaiter(void 0, void 0, void 0, function* () {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
configFile = 'docs.json';
|
|
33
|
-
}
|
|
34
|
-
else if (fs.existsSync(path.join(baseDir, 'mint.json'))) {
|
|
35
|
-
configJson = yield fs.readJSON(path.join(baseDir, 'mint.json'));
|
|
36
|
-
configFile = 'mint.json';
|
|
37
|
-
}
|
|
38
|
-
if (!(configJson === null || configJson === void 0 ? void 0 : configJson.navigation) || !configFile)
|
|
27
|
+
const resolved = yield readResolvedConfigJson(baseDir);
|
|
28
|
+
if (!resolved)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (!resolved.json.navigation)
|
|
39
31
|
return undefined;
|
|
40
|
-
return {
|
|
32
|
+
return {
|
|
33
|
+
hrefs: collectHrefs(resolved.json.navigation),
|
|
34
|
+
configFile: resolved.configFile,
|
|
35
|
+
};
|
|
41
36
|
});
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
* Extract virtual page paths that would be generated from OpenAPI specs.
|
|
3
3
|
* These pages don't exist as files but are valid link targets.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* For docs.json (v2) configs, this walks the navigation tree so that per-spec
|
|
6
|
+
* `directory` overrides and inherited directories are honored — matching the
|
|
7
|
+
* slugs production generates. For mint.json (v1) or missing configs, it falls
|
|
8
|
+
* back to generating page paths under the default `api-reference` directory.
|
|
9
|
+
*
|
|
10
|
+
* `$ref` pointers inside the config are resolved first, so navigation split
|
|
11
|
+
* across multiple JSON files is walked end-to-end.
|
|
7
12
|
*/
|
|
8
13
|
export declare const getOpenApiPagePaths: (baseDir: string) => Promise<string[]>;
|
|
@@ -7,52 +7,52 @@ 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 { categorizeFilePaths } from '@mintlify/prebuild';
|
|
10
|
+
import { categorizeFilePaths, generateOpenApiFromDocsConfig, getOpenApiFilesFromConfig, } from '@mintlify/prebuild';
|
|
11
11
|
import { generateOpenApiPagesForDocsConfig } from '@mintlify/scraping';
|
|
12
12
|
import { validateDocsConfig } from '@mintlify/validation';
|
|
13
|
-
import
|
|
14
|
-
import path from 'path';
|
|
13
|
+
import { readResolvedConfigJson } from './readConfig.js';
|
|
15
14
|
const DEFAULT_OUTPUT_DIR = 'api-reference';
|
|
16
|
-
const getOpenApiDirectory = (baseDir) => __awaiter(void 0, void 0, void 0, function* () {
|
|
17
|
-
var _a;
|
|
18
|
-
const docsJsonPath = path.join(baseDir, 'docs.json');
|
|
19
|
-
if (!fs.existsSync(docsJsonPath))
|
|
20
|
-
return DEFAULT_OUTPUT_DIR;
|
|
21
|
-
try {
|
|
22
|
-
const docsJson = yield fs.readJSON(docsJsonPath);
|
|
23
|
-
const result = validateDocsConfig(docsJson);
|
|
24
|
-
if (!result.success)
|
|
25
|
-
return DEFAULT_OUTPUT_DIR;
|
|
26
|
-
const openapi = (_a = result.data.api) === null || _a === void 0 ? void 0 : _a.openapi;
|
|
27
|
-
if (typeof openapi === 'object' && 'directory' in openapi && openapi.directory) {
|
|
28
|
-
return openapi.directory;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch (_b) {
|
|
32
|
-
throw new Error('Unable to parse OpenAPI directory from docs.json');
|
|
33
|
-
}
|
|
34
|
-
return DEFAULT_OUTPUT_DIR;
|
|
35
|
-
});
|
|
36
15
|
/**
|
|
37
16
|
* Extract virtual page paths that would be generated from OpenAPI specs.
|
|
38
17
|
* These pages don't exist as files but are valid link targets.
|
|
39
18
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
19
|
+
* For docs.json (v2) configs, this walks the navigation tree so that per-spec
|
|
20
|
+
* `directory` overrides and inherited directories are honored — matching the
|
|
21
|
+
* slugs production generates. For mint.json (v1) or missing configs, it falls
|
|
22
|
+
* back to generating page paths under the default `api-reference` directory.
|
|
23
|
+
*
|
|
24
|
+
* `$ref` pointers inside the config are resolved first, so navigation split
|
|
25
|
+
* across multiple JSON files is walked end-to-end.
|
|
42
26
|
*/
|
|
43
27
|
export const getOpenApiPagePaths = (baseDir) => __awaiter(void 0, void 0, void 0, function* () {
|
|
44
28
|
const pagePaths = [];
|
|
45
29
|
try {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
30
|
+
const { openApiFiles } = yield categorizeFilePaths(baseDir);
|
|
31
|
+
const docsConfig = yield readDocsConfig(baseDir);
|
|
32
|
+
if (docsConfig) {
|
|
33
|
+
const urlOpenApiFiles = yield safeGetOpenApiFilesFromConfig(docsConfig);
|
|
34
|
+
const allOpenApiFiles = [...openApiFiles, ...urlOpenApiFiles];
|
|
35
|
+
const pagesAcc = {};
|
|
36
|
+
try {
|
|
37
|
+
yield generateOpenApiFromDocsConfig(docsConfig.navigation, allOpenApiFiles, pagesAcc, {
|
|
38
|
+
writeFiles: false,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.warn(`Warning: Failed to extract OpenAPI pages from navigation: ${err}`);
|
|
43
|
+
}
|
|
44
|
+
pagePaths.push(...Object.keys(pagesAcc));
|
|
45
|
+
}
|
|
46
|
+
// Also enumerate default-directory pages per local spec. This preserves
|
|
47
|
+
// the previous behavior for configs that don't wire every OpenAPI file
|
|
48
|
+
// into `navigation`, and keeps us permissive (extra virtual nodes can
|
|
49
|
+
// only hide broken-link reports, never add false positives).
|
|
50
50
|
for (const openApiFile of openApiFiles) {
|
|
51
51
|
try {
|
|
52
52
|
const { pagesAcc } = yield generateOpenApiPagesForDocsConfig(openApiFile.spec, {
|
|
53
53
|
openApiFilePath: openApiFile.originalFileLocation,
|
|
54
54
|
writeFiles: false,
|
|
55
|
-
outDir,
|
|
55
|
+
outDir: DEFAULT_OUTPUT_DIR,
|
|
56
56
|
});
|
|
57
57
|
pagePaths.push(...Object.keys(pagesAcc));
|
|
58
58
|
}
|
|
@@ -66,3 +66,19 @@ export const getOpenApiPagePaths = (baseDir) => __awaiter(void 0, void 0, void 0
|
|
|
66
66
|
}
|
|
67
67
|
return [...new Set(pagePaths)];
|
|
68
68
|
});
|
|
69
|
+
const readDocsConfig = (baseDir) => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
const resolved = yield readResolvedConfigJson(baseDir);
|
|
71
|
+
if (!resolved || resolved.configFile !== 'docs.json')
|
|
72
|
+
return null;
|
|
73
|
+
const result = validateDocsConfig(resolved.json);
|
|
74
|
+
return result.success ? result.data : null;
|
|
75
|
+
});
|
|
76
|
+
const safeGetOpenApiFilesFromConfig = (docsConfig) => __awaiter(void 0, void 0, void 0, function* () {
|
|
77
|
+
try {
|
|
78
|
+
return yield getOpenApiFilesFromConfig('docs', docsConfig);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
console.warn(`Warning: Failed to fetch URL-based OpenAPI specs: ${err}`);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
});
|
|
@@ -7,25 +7,14 @@ 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 {
|
|
11
|
-
import fs from 'fs-extra';
|
|
12
|
-
import path from 'path';
|
|
10
|
+
import { readResolvedConfigJsonAndValidate } from './readConfig.js';
|
|
13
11
|
export const getRedirects = (baseDir) => __awaiter(void 0, void 0, void 0, function* () {
|
|
14
12
|
const configRedirects = [];
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
configRedirects.push(...result.data.redirects);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
else if (fs.existsSync(path.join(baseDir, 'mint.json'))) {
|
|
24
|
-
const mintJson = yield fs.readJSON(path.join(baseDir, 'mint.json'));
|
|
25
|
-
const result = validateMintConfig(mintJson);
|
|
26
|
-
if (result.success && 'redirects' in result.data && result.data.redirects) {
|
|
27
|
-
configRedirects.push(...result.data.redirects);
|
|
28
|
-
}
|
|
13
|
+
const result = yield readResolvedConfigJsonAndValidate(baseDir);
|
|
14
|
+
if (!result)
|
|
15
|
+
return configRedirects;
|
|
16
|
+
if (result.success && 'redirects' in result.data && result.data.redirects) {
|
|
17
|
+
configRedirects.push(...result.data.redirects);
|
|
29
18
|
}
|
|
30
19
|
return configRedirects;
|
|
31
20
|
});
|