@mintlify/cli 4.0.1705 → 4.0.1707
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/bin/brokenLinks.js +122 -0
- package/bin/cli.js +4 -110
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/brokenLinks.tsx +152 -0
- package/src/cli.tsx +2 -134
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mintlify/cli",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1707",
|
|
4
4
|
"description": "The Mintlify CLI",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@inquirer/prompts": "7.9.0",
|
|
48
48
|
"@mintlify/common": "1.0.1293",
|
|
49
|
-
"@mintlify/link-rot": "3.0.
|
|
49
|
+
"@mintlify/link-rot": "3.0.1518",
|
|
50
50
|
"@mintlify/models": "0.0.378",
|
|
51
|
-
"@mintlify/og": "0.0.
|
|
51
|
+
"@mintlify/og": "0.0.92",
|
|
52
52
|
"@mintlify/prebuild": "1.0.1458",
|
|
53
53
|
"@mintlify/previewing": "4.0.1536",
|
|
54
54
|
"@mintlify/validation": "0.1.940",
|
|
@@ -101,5 +101,5 @@
|
|
|
101
101
|
"vitest": "2.1.9",
|
|
102
102
|
"vitest-mock-process": "1.0.4"
|
|
103
103
|
},
|
|
104
|
-
"gitHead": "
|
|
104
|
+
"gitHead": "0ad30201edb281c1a5e6db5b54ef4c0f992d0883"
|
|
105
105
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { buildGraph, getBrokenExternalLinks, getFilesLinksDependOn } from '@mintlify/link-rot';
|
|
2
|
+
import {
|
|
3
|
+
addLog,
|
|
4
|
+
clearLogs,
|
|
5
|
+
BrokenLinksLog,
|
|
6
|
+
ErrorLog,
|
|
7
|
+
SpinnerLog,
|
|
8
|
+
SuccessLog,
|
|
9
|
+
WarningLog,
|
|
10
|
+
} from '@mintlify/previewing';
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
|
|
14
|
+
import { autoUpgradeIfNeeded, CMD_EXEC_PATH, findDocsRoot, terminate } from './helpers.js';
|
|
15
|
+
import { resolveExplicitFiles } from './resolveFiles.js';
|
|
16
|
+
|
|
17
|
+
type BrokenLinksArgs = {
|
|
18
|
+
files?: (string | number)[];
|
|
19
|
+
'check-anchors': boolean;
|
|
20
|
+
'check-external': boolean;
|
|
21
|
+
'check-snippets': boolean;
|
|
22
|
+
'check-redirects': boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const brokenLinksHandler = async (argv: BrokenLinksArgs): Promise<void> => {
|
|
26
|
+
await autoUpgradeIfNeeded();
|
|
27
|
+
addLog(<SpinnerLog message="checking for broken links..." />);
|
|
28
|
+
try {
|
|
29
|
+
const docsRoot = await findDocsRoot(CMD_EXEC_PATH);
|
|
30
|
+
const fileArgs = (argv.files ?? []).map(String).filter(Boolean);
|
|
31
|
+
const sourceFiles =
|
|
32
|
+
fileArgs.length > 0
|
|
33
|
+
? new Set(
|
|
34
|
+
(await resolveExplicitFiles(CMD_EXEC_PATH, fileArgs)).map((file) =>
|
|
35
|
+
path.relative(docsRoot, path.resolve(CMD_EXEC_PATH, file))
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
: undefined;
|
|
39
|
+
// One unparseable page shouldn't hide every other result: report it and keep going
|
|
40
|
+
const parseErrorsByFile = new Map<string, string>();
|
|
41
|
+
const graph = await buildGraph(docsRoot, {
|
|
42
|
+
checkSnippets: argv['check-snippets'],
|
|
43
|
+
onParseError: (filePath, error) => parseErrorsByFile.set(filePath, error.message),
|
|
44
|
+
});
|
|
45
|
+
graph.precomputeFileResolutions();
|
|
46
|
+
// With --files, a failure elsewhere only matters when a selected link needs that
|
|
47
|
+
// file: an anchor pointing at it, or a snippet the selected page renders
|
|
48
|
+
const dependencies = sourceFiles
|
|
49
|
+
? getFilesLinksDependOn(graph, sourceFiles, {
|
|
50
|
+
checkAnchors: argv['check-anchors'],
|
|
51
|
+
checkSnippets: argv['check-snippets'],
|
|
52
|
+
})
|
|
53
|
+
: undefined;
|
|
54
|
+
const parseErrors = [...parseErrorsByFile]
|
|
55
|
+
.filter(
|
|
56
|
+
([filePath]) =>
|
|
57
|
+
!sourceFiles ||
|
|
58
|
+
sourceFiles.has(filePath) ||
|
|
59
|
+
dependencies?.has(filePath.split(path.sep).join('/'))
|
|
60
|
+
)
|
|
61
|
+
.map(([, message]) => message);
|
|
62
|
+
|
|
63
|
+
const brokenInternalLinks = graph
|
|
64
|
+
.getBrokenInternalLinks({
|
|
65
|
+
checkAnchors: argv['check-anchors'],
|
|
66
|
+
})
|
|
67
|
+
.filter(
|
|
68
|
+
({ relativeDir, filename }) =>
|
|
69
|
+
!sourceFiles || sourceFiles.has(path.join(relativeDir, filename))
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const brokenLinksByFile: Record<string, string[]> = {};
|
|
73
|
+
|
|
74
|
+
brokenInternalLinks.forEach((mdxPath) => {
|
|
75
|
+
const filename = path.join(mdxPath.relativeDir, mdxPath.filename);
|
|
76
|
+
const existing = brokenLinksByFile[filename];
|
|
77
|
+
if (existing) {
|
|
78
|
+
existing.push(mdxPath.originalPath);
|
|
79
|
+
} else {
|
|
80
|
+
brokenLinksByFile[filename] = [mdxPath.originalPath];
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (argv['check-external']) {
|
|
85
|
+
const brokenExternalLinks = await getBrokenExternalLinks(
|
|
86
|
+
graph,
|
|
87
|
+
sourceFiles ? { sourceFiles } : undefined
|
|
88
|
+
);
|
|
89
|
+
for (const result of brokenExternalLinks) {
|
|
90
|
+
for (const source of result.sources) {
|
|
91
|
+
const label = result.status
|
|
92
|
+
? `${result.url} (${result.status})`
|
|
93
|
+
: `${result.url} (${result.error})`;
|
|
94
|
+
const existing = brokenLinksByFile[source.file];
|
|
95
|
+
if (existing) {
|
|
96
|
+
existing.push(label);
|
|
97
|
+
} else {
|
|
98
|
+
brokenLinksByFile[source.file] = [label];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (argv['check-redirects']) {
|
|
105
|
+
const brokenRedirects = graph.getBrokenRedirects();
|
|
106
|
+
if (brokenRedirects.length > 0) {
|
|
107
|
+
const configFilename = await fs
|
|
108
|
+
.access(path.join(docsRoot, 'docs.json'))
|
|
109
|
+
.then(() => 'docs.json')
|
|
110
|
+
.catch(() => 'mint.json');
|
|
111
|
+
const labels = brokenRedirects.map(
|
|
112
|
+
({ source, destination }) => `${source} → ${destination}`
|
|
113
|
+
);
|
|
114
|
+
const existing = brokenLinksByFile[configFilename];
|
|
115
|
+
if (existing) {
|
|
116
|
+
existing.push(...labels);
|
|
117
|
+
} else {
|
|
118
|
+
brokenLinksByFile[configFilename] = labels;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const hasBrokenLinks = Object.keys(brokenLinksByFile).length > 0;
|
|
124
|
+
if (!hasBrokenLinks && parseErrors.length === 0) {
|
|
125
|
+
clearLogs();
|
|
126
|
+
addLog(<SuccessLog message="no broken links found" />);
|
|
127
|
+
await terminate(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
clearLogs();
|
|
131
|
+
if (hasBrokenLinks) {
|
|
132
|
+
addLog(<BrokenLinksLog brokenLinksByFile={brokenLinksByFile} />);
|
|
133
|
+
} else {
|
|
134
|
+
addLog(<SuccessLog message="no broken links found in the files that parsed" />);
|
|
135
|
+
}
|
|
136
|
+
for (const message of parseErrors) {
|
|
137
|
+
addLog(<ErrorLog message={message} />);
|
|
138
|
+
}
|
|
139
|
+
if (parseErrors.length > 0) {
|
|
140
|
+
addLog(
|
|
141
|
+
<WarningLog
|
|
142
|
+
message={`skipped ${parseErrors.length} file(s) that could not be parsed; links in them were not checked`}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
addLog(<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />);
|
|
148
|
+
await terminate(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await terminate(1);
|
|
152
|
+
};
|
package/src/cli.tsx
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import { buildGraph, getBrokenExternalLinks, getFilesLinksDependOn } from '@mintlify/link-rot';
|
|
2
1
|
import {
|
|
3
2
|
addLog,
|
|
4
3
|
dev,
|
|
5
4
|
exportSite,
|
|
6
5
|
validateBuild,
|
|
7
6
|
ErrorLog,
|
|
8
|
-
SpinnerLog,
|
|
9
7
|
SuccessLog,
|
|
10
8
|
Logs,
|
|
11
|
-
clearLogs,
|
|
12
|
-
BrokenLinksLog,
|
|
13
9
|
WarningLog,
|
|
14
10
|
} from '@mintlify/previewing';
|
|
15
11
|
import { thumbnailOverrideSchema } from '@mintlify/validation';
|
|
@@ -23,6 +19,7 @@ import { accessibilityCheck } from './accessibilityCheck.js';
|
|
|
23
19
|
import { addDomain } from './addDomain.js';
|
|
24
20
|
import { createAgentClaimLink } from './agent-claim.js';
|
|
25
21
|
import { analyticsBuilder } from './analytics/index.js';
|
|
22
|
+
import { brokenLinksHandler } from './brokenLinks.js';
|
|
26
23
|
import { comingSoon } from './comingSoon.js';
|
|
27
24
|
import { setTelemetryEnabled } from './config.js';
|
|
28
25
|
import { getConfigValue, setConfigValue, clearConfigValue } from './config.js';
|
|
@@ -35,7 +32,6 @@ import {
|
|
|
35
32
|
checkPort,
|
|
36
33
|
checkNodeVersion,
|
|
37
34
|
autoUpgradeIfNeeded,
|
|
38
|
-
findDocsRoot,
|
|
39
35
|
getVersions,
|
|
40
36
|
isAI,
|
|
41
37
|
suppressConsoleWarnings,
|
|
@@ -53,7 +49,6 @@ import {
|
|
|
53
49
|
checkOpenApiFile,
|
|
54
50
|
getOpenApiFilenamesFromDocsConfig,
|
|
55
51
|
} from './openApiCheck.js';
|
|
56
|
-
import { resolveExplicitFiles } from './resolveFiles.js';
|
|
57
52
|
import { scoreHandler } from './score/index.js';
|
|
58
53
|
import { sendPublishPrompt, signup } from './signup.js';
|
|
59
54
|
import { status, getCliSubdomains } from './status.js';
|
|
@@ -451,134 +446,7 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
|
|
|
451
446
|
})
|
|
452
447
|
.example('mint broken-links --files introduction.mdx', 'Check a specific page')
|
|
453
448
|
.example('mint broken-links --files "guides/**/*.mdx"', 'Check pages matching a glob'),
|
|
454
|
-
|
|
455
|
-
await autoUpgradeIfNeeded();
|
|
456
|
-
addLog(<SpinnerLog message="checking for broken links..." />);
|
|
457
|
-
try {
|
|
458
|
-
const docsRoot = await findDocsRoot(CMD_EXEC_PATH);
|
|
459
|
-
const fileArgs = (argv.files ?? []).map(String).filter(Boolean);
|
|
460
|
-
const sourceFiles =
|
|
461
|
-
fileArgs.length > 0
|
|
462
|
-
? new Set(
|
|
463
|
-
(await resolveExplicitFiles(CMD_EXEC_PATH, fileArgs)).map((file) =>
|
|
464
|
-
path.relative(docsRoot, path.resolve(CMD_EXEC_PATH, file))
|
|
465
|
-
)
|
|
466
|
-
)
|
|
467
|
-
: undefined;
|
|
468
|
-
// One unparseable page shouldn't hide every other result: report it and keep going
|
|
469
|
-
const parseErrorsByFile = new Map<string, string>();
|
|
470
|
-
const graph = await buildGraph(docsRoot, {
|
|
471
|
-
checkSnippets: argv['check-snippets'],
|
|
472
|
-
onParseError: (filePath, error) => parseErrorsByFile.set(filePath, error.message),
|
|
473
|
-
});
|
|
474
|
-
graph.precomputeFileResolutions();
|
|
475
|
-
// With --files, a failure elsewhere only matters when a selected link needs that
|
|
476
|
-
// file: an anchor pointing at it, or a snippet the selected page renders
|
|
477
|
-
const dependencies = sourceFiles
|
|
478
|
-
? getFilesLinksDependOn(graph, sourceFiles, {
|
|
479
|
-
checkAnchors: argv['check-anchors'],
|
|
480
|
-
checkSnippets: argv['check-snippets'],
|
|
481
|
-
})
|
|
482
|
-
: undefined;
|
|
483
|
-
const parseErrors = [...parseErrorsByFile]
|
|
484
|
-
.filter(
|
|
485
|
-
([filePath]) =>
|
|
486
|
-
!sourceFiles ||
|
|
487
|
-
sourceFiles.has(filePath) ||
|
|
488
|
-
dependencies?.has(filePath.split(path.sep).join('/'))
|
|
489
|
-
)
|
|
490
|
-
.map(([, message]) => message);
|
|
491
|
-
|
|
492
|
-
const brokenInternalLinks = graph
|
|
493
|
-
.getBrokenInternalLinks({
|
|
494
|
-
checkAnchors: argv['check-anchors'],
|
|
495
|
-
})
|
|
496
|
-
.filter(
|
|
497
|
-
({ relativeDir, filename }) =>
|
|
498
|
-
!sourceFiles || sourceFiles.has(path.join(relativeDir, filename))
|
|
499
|
-
);
|
|
500
|
-
|
|
501
|
-
const brokenLinksByFile: Record<string, string[]> = {};
|
|
502
|
-
|
|
503
|
-
brokenInternalLinks.forEach((mdxPath) => {
|
|
504
|
-
const filename = path.join(mdxPath.relativeDir, mdxPath.filename);
|
|
505
|
-
const existing = brokenLinksByFile[filename];
|
|
506
|
-
if (existing) {
|
|
507
|
-
existing.push(mdxPath.originalPath);
|
|
508
|
-
} else {
|
|
509
|
-
brokenLinksByFile[filename] = [mdxPath.originalPath];
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
if (argv['check-external']) {
|
|
514
|
-
const brokenExternalLinks = await getBrokenExternalLinks(
|
|
515
|
-
graph,
|
|
516
|
-
sourceFiles ? { sourceFiles } : undefined
|
|
517
|
-
);
|
|
518
|
-
for (const result of brokenExternalLinks) {
|
|
519
|
-
for (const source of result.sources) {
|
|
520
|
-
const label = result.status
|
|
521
|
-
? `${result.url} (${result.status})`
|
|
522
|
-
: `${result.url} (${result.error})`;
|
|
523
|
-
const existing = brokenLinksByFile[source.file];
|
|
524
|
-
if (existing) {
|
|
525
|
-
existing.push(label);
|
|
526
|
-
} else {
|
|
527
|
-
brokenLinksByFile[source.file] = [label];
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (argv['check-redirects']) {
|
|
534
|
-
const brokenRedirects = graph.getBrokenRedirects();
|
|
535
|
-
if (brokenRedirects.length > 0) {
|
|
536
|
-
const configFilename = await fs
|
|
537
|
-
.access(path.join(docsRoot, 'docs.json'))
|
|
538
|
-
.then(() => 'docs.json')
|
|
539
|
-
.catch(() => 'mint.json');
|
|
540
|
-
const labels = brokenRedirects.map(
|
|
541
|
-
({ source, destination }) => `${source} → ${destination}`
|
|
542
|
-
);
|
|
543
|
-
const existing = brokenLinksByFile[configFilename];
|
|
544
|
-
if (existing) {
|
|
545
|
-
existing.push(...labels);
|
|
546
|
-
} else {
|
|
547
|
-
brokenLinksByFile[configFilename] = labels;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const hasBrokenLinks = Object.keys(brokenLinksByFile).length > 0;
|
|
553
|
-
if (!hasBrokenLinks && parseErrors.length === 0) {
|
|
554
|
-
clearLogs();
|
|
555
|
-
addLog(<SuccessLog message="no broken links found" />);
|
|
556
|
-
await terminate(0);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
clearLogs();
|
|
560
|
-
if (hasBrokenLinks) {
|
|
561
|
-
addLog(<BrokenLinksLog brokenLinksByFile={brokenLinksByFile} />);
|
|
562
|
-
} else {
|
|
563
|
-
addLog(<SuccessLog message="no broken links found in the files that parsed" />);
|
|
564
|
-
}
|
|
565
|
-
for (const message of parseErrors) {
|
|
566
|
-
addLog(<ErrorLog message={message} />);
|
|
567
|
-
}
|
|
568
|
-
if (parseErrors.length > 0) {
|
|
569
|
-
addLog(
|
|
570
|
-
<WarningLog
|
|
571
|
-
message={`skipped ${parseErrors.length} file(s) that could not be parsed; links in them were not checked`}
|
|
572
|
-
/>
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
} catch (err) {
|
|
576
|
-
addLog(<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />);
|
|
577
|
-
await terminate(1);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
await terminate(1);
|
|
581
|
-
}
|
|
449
|
+
(argv) => brokenLinksHandler(argv)
|
|
582
450
|
)
|
|
583
451
|
.command(
|
|
584
452
|
'status',
|