@mintlify/cli 4.0.1704 → 4.0.1706

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mintlify/cli",
3
- "version": "4.0.1704",
3
+ "version": "4.0.1706",
4
4
  "description": "The Mintlify CLI",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -45,12 +45,12 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@inquirer/prompts": "7.9.0",
48
- "@mintlify/common": "1.0.1292",
49
- "@mintlify/link-rot": "3.0.1516",
48
+ "@mintlify/common": "1.0.1293",
49
+ "@mintlify/link-rot": "3.0.1518",
50
50
  "@mintlify/models": "0.0.378",
51
- "@mintlify/og": "0.0.90",
52
- "@mintlify/prebuild": "1.0.1457",
53
- "@mintlify/previewing": "4.0.1535",
51
+ "@mintlify/og": "0.0.91",
52
+ "@mintlify/prebuild": "1.0.1458",
53
+ "@mintlify/previewing": "4.0.1536",
54
54
  "@mintlify/validation": "0.1.940",
55
55
  "adm-zip": "0.6.0",
56
56
  "chalk": "5.2.0",
@@ -82,7 +82,7 @@
82
82
  "keytar": "7.9.0"
83
83
  },
84
84
  "devDependencies": {
85
- "@mintlify/editor": "0.0.555",
85
+ "@mintlify/editor": "0.0.556",
86
86
  "@mintlify/ts-config": "2.0.2",
87
87
  "@tsconfig/recommended": "1.0.2",
88
88
  "@types/detect-port": "1.3.2",
@@ -101,5 +101,5 @@
101
101
  "vitest": "2.1.9",
102
102
  "vitest-mock-process": "1.0.4"
103
103
  },
104
- "gitHead": "7d40d8a1004c6a1603b9edb2bb906c23799ccae7"
104
+ "gitHead": "a13039de2913c7f9430361c153ea313a3efc4290"
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
- async (argv) => {
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',