@mintlify/cli 4.0.975 → 4.0.977
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/__test__/brokenLinks.test.ts +83 -9
- package/bin/cli.js +44 -12
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/cli.tsx +57 -17
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
191
|
+
brokenInternalLinks.forEach((mdxPath) => {
|
|
182
192
|
const filename = path.join(mdxPath.relativeDir, mdxPath.filename);
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
185
|
-
|
|
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
|
}
|