@mintlify/cli 4.0.976 → 4.0.978

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.
@@ -1,4 +1,4 @@
1
- import { getBrokenInternalLinks } from '@mintlify/link-rot';
1
+ import { buildGraph, getBrokenExternalLinks } from '@mintlify/link-rot';
2
2
  import { MdxPath } from '@mintlify/link-rot/dist/graph.js';
3
3
  import * as previewing from '@mintlify/previewing';
4
4
  import { mockProcessExit } from 'vitest-mock-process';
@@ -14,11 +14,18 @@ vi.mock('../src/helpers.js', async () => {
14
14
  };
15
15
  });
16
16
 
17
+ const mockGraph = {
18
+ precomputeFileResolutions: vi.fn(),
19
+ getBrokenInternalLinks: vi.fn().mockReturnValue([]),
20
+ getExternalPathsByNode: vi.fn().mockReturnValue(new Map()),
21
+ };
22
+
17
23
  vi.mock('@mintlify/link-rot', async () => {
18
24
  const actual = await import('@mintlify/link-rot');
19
25
  return {
20
26
  ...actual,
21
- getBrokenInternalLinks: vi.fn(),
27
+ buildGraph: vi.fn(),
28
+ getBrokenExternalLinks: vi.fn(),
22
29
  };
23
30
  });
24
31
 
@@ -29,15 +36,12 @@ const processExitMock = mockProcessExit();
29
36
  describe('brokenLinks', () => {
30
37
  beforeEach(() => {
31
38
  vi.clearAllMocks();
32
- });
33
-
34
- afterEach(() => {
35
- vi.clearAllMocks();
39
+ vi.mocked(buildGraph).mockResolvedValue(mockGraph as never);
40
+ mockGraph.getBrokenInternalLinks.mockReturnValue([]);
36
41
  });
37
42
 
38
43
  it('success with no broken links', async () => {
39
44
  vi.mocked(checkForMintJson).mockResolvedValueOnce(true);
40
- vi.mocked(getBrokenInternalLinks).mockResolvedValueOnce([]);
41
45
 
42
46
  await runCommand('broken-links');
43
47
 
@@ -56,7 +60,7 @@ describe('brokenLinks', () => {
56
60
 
57
61
  it('fails with broken links', async () => {
58
62
  vi.mocked(checkForMintJson).mockResolvedValueOnce(true);
59
- vi.mocked(getBrokenInternalLinks).mockResolvedValueOnce([
63
+ mockGraph.getBrokenInternalLinks.mockReturnValue([
60
64
  {
61
65
  relativeDir: '.',
62
66
  filename: 'introduction.mdx',
@@ -87,7 +91,7 @@ describe('brokenLinks', () => {
87
91
 
88
92
  it('fails when checking throws error', async () => {
89
93
  vi.mocked(checkForMintJson).mockResolvedValueOnce(true);
90
- vi.mocked(getBrokenInternalLinks).mockRejectedValueOnce(new Error('some error'));
94
+ vi.mocked(buildGraph).mockRejectedValueOnce(new Error('some error'));
91
95
 
92
96
  await runCommand('broken-links');
93
97
 
@@ -104,3 +108,73 @@ describe('brokenLinks', () => {
104
108
  expect(processExitMock).toHaveBeenCalledWith(1);
105
109
  });
106
110
  });
111
+
112
+ describe('brokenLinks --check-external', () => {
113
+ beforeEach(() => {
114
+ vi.clearAllMocks();
115
+ vi.mocked(buildGraph).mockResolvedValue(mockGraph as never);
116
+ mockGraph.getBrokenInternalLinks.mockReturnValue([]);
117
+ });
118
+
119
+ it('success with no broken external links', async () => {
120
+ vi.mocked(checkForMintJson).mockResolvedValueOnce(true);
121
+ vi.mocked(getBrokenExternalLinks).mockResolvedValueOnce([]);
122
+
123
+ await runCommand('broken-links', '--check-external');
124
+
125
+ expect(addLogSpy).toHaveBeenCalledWith(
126
+ expect.objectContaining({
127
+ props: { message: 'no broken links found' },
128
+ })
129
+ );
130
+ expect(processExitMock).toHaveBeenCalledWith(0);
131
+ });
132
+
133
+ it('fails with broken external links', async () => {
134
+ vi.mocked(checkForMintJson).mockResolvedValueOnce(true);
135
+ vi.mocked(getBrokenExternalLinks).mockResolvedValueOnce([
136
+ {
137
+ url: 'https://example.com/broken',
138
+ status: 404,
139
+ sources: [{ file: 'introduction.mdx', originalPath: 'https://example.com/broken' }],
140
+ },
141
+ ]);
142
+
143
+ await runCommand('broken-links', '--check-external');
144
+
145
+ expect(clearLogsSpy).toHaveBeenCalled();
146
+ expect(addLogSpy).toHaveBeenCalledWith(
147
+ expect.objectContaining({
148
+ props: {
149
+ brokenLinksByFile: {
150
+ 'introduction.mdx': ['https://example.com/broken (404)'],
151
+ },
152
+ },
153
+ })
154
+ );
155
+ expect(processExitMock).toHaveBeenCalledWith(1);
156
+ });
157
+
158
+ it('fails when external link checking throws error', async () => {
159
+ vi.mocked(checkForMintJson).mockResolvedValueOnce(true);
160
+ vi.mocked(getBrokenExternalLinks).mockRejectedValueOnce(new Error('network error'));
161
+
162
+ await runCommand('broken-links', '--check-external');
163
+
164
+ expect(addLogSpy).toHaveBeenCalledWith(
165
+ expect.objectContaining({
166
+ props: { message: 'network error' },
167
+ })
168
+ );
169
+ expect(processExitMock).toHaveBeenCalledWith(1);
170
+ });
171
+
172
+ it('does not check external links without --check-external flag', async () => {
173
+ vi.mocked(checkForMintJson).mockResolvedValueOnce(true);
174
+
175
+ await runCommand('broken-links');
176
+
177
+ expect(getBrokenExternalLinks).not.toHaveBeenCalled();
178
+ expect(processExitMock).toHaveBeenCalledWith(0);
179
+ });
180
+ });
package/bin/cli.js CHANGED
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  import { validate, getOpenApiDocumentFromUrl, isAllowedLocalSchemaUrl } from '@mintlify/common';
12
- import { getBrokenInternalLinks, renameFilesAndUpdateLinksInContent } from '@mintlify/link-rot';
12
+ import { buildGraph, getBrokenExternalLinks, renameFilesAndUpdateLinksInContent, } from '@mintlify/link-rot';
13
13
  import { addLog, dev, validateBuild, ErrorLog, SpinnerLog, SuccessLog, Logs, clearLogs, BrokenLinksLog, WarningLog, } from '@mintlify/previewing';
14
14
  import { checkUrl } from '@mintlify/scraping';
15
15
  import { render, Text } from 'ink';
@@ -158,10 +158,21 @@ export const cli = ({ packageName = 'mint' }) => {
158
158
  }
159
159
  yield terminate(0);
160
160
  }))
161
- .command('broken-links', 'check for invalid internal links', (yargs) => yargs.option('check-anchors', {
161
+ .command('broken-links', 'check for broken links', (yargs) => yargs
162
+ .option('check-anchors', {
162
163
  type: 'boolean',
163
164
  default: false,
164
165
  description: 'also validate anchor links (e.g. #section) against heading slugs',
166
+ })
167
+ .option('check-external', {
168
+ type: 'boolean',
169
+ default: false,
170
+ description: 'also check external links for broken URLs',
171
+ })
172
+ .option('check-snippets', {
173
+ type: 'boolean',
174
+ default: false,
175
+ description: 'also check links inside <Snippet> components',
165
176
  }), (argv) => __awaiter(void 0, void 0, void 0, function* () {
166
177
  const hasMintJson = yield checkForMintJson();
167
178
  if (!hasMintJson) {
@@ -169,25 +180,46 @@ export const cli = ({ packageName = 'mint' }) => {
169
180
  }
170
181
  addLog(_jsx(SpinnerLog, { message: "checking for broken links..." }));
171
182
  try {
172
- const brokenLinks = yield getBrokenInternalLinks(undefined, {
183
+ const graph = yield buildGraph(undefined, {
184
+ checkSnippets: argv['check-snippets'],
185
+ });
186
+ graph.precomputeFileResolutions();
187
+ const brokenInternalLinks = graph.getBrokenInternalLinks({
173
188
  checkAnchors: argv['check-anchors'],
174
189
  });
175
- if (brokenLinks.length === 0) {
176
- clearLogs();
177
- addLog(_jsx(SuccessLog, { message: "no broken links found" }));
178
- yield terminate(0);
179
- }
180
190
  const brokenLinksByFile = {};
181
- brokenLinks.forEach((mdxPath) => {
191
+ brokenInternalLinks.forEach((mdxPath) => {
182
192
  const filename = path.join(mdxPath.relativeDir, mdxPath.filename);
183
- const brokenLinksForFile = brokenLinksByFile[filename];
184
- if (brokenLinksForFile) {
185
- brokenLinksForFile.push(mdxPath.originalPath);
193
+ const existing = brokenLinksByFile[filename];
194
+ if (existing) {
195
+ existing.push(mdxPath.originalPath);
186
196
  }
187
197
  else {
188
198
  brokenLinksByFile[filename] = [mdxPath.originalPath];
189
199
  }
190
200
  });
201
+ if (argv['check-external']) {
202
+ const brokenExternalLinks = yield getBrokenExternalLinks(graph);
203
+ for (const result of brokenExternalLinks) {
204
+ for (const source of result.sources) {
205
+ const label = result.status
206
+ ? `${result.url} (${result.status})`
207
+ : `${result.url} (${result.error})`;
208
+ const existing = brokenLinksByFile[source.file];
209
+ if (existing) {
210
+ existing.push(label);
211
+ }
212
+ else {
213
+ brokenLinksByFile[source.file] = [label];
214
+ }
215
+ }
216
+ }
217
+ }
218
+ if (Object.keys(brokenLinksByFile).length === 0) {
219
+ clearLogs();
220
+ addLog(_jsx(SuccessLog, { message: "no broken links found" }));
221
+ yield terminate(0);
222
+ }
191
223
  clearLogs();
192
224
  addLog(_jsx(BrokenLinksLog, { brokenLinksByFile: brokenLinksByFile }));
193
225
  }