@mintlify/cli 4.0.1338 → 4.0.1340

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,9 +1,11 @@
1
1
  import { buildGraph, getBrokenExternalLinks } from '@mintlify/link-rot';
2
2
  import { MdxPath } from '@mintlify/link-rot/dist/graph.js';
3
3
  import { addLog, clearLogs } from '@mintlify/previewing';
4
+ import path from 'node:path';
4
5
  import { mockProcessExit } from 'vitest-mock-process';
5
6
 
6
- import { checkForMintJson } from '../src/helpers.js';
7
+ import { resolveExplicitFiles } from '../src/deslop/resolveFiles.js';
8
+ import { checkForMintJson, CMD_EXEC_PATH } from '../src/helpers.js';
7
9
  import { runCommand } from './utils.js';
8
10
 
9
11
  vi.mock('@mintlify/previewing', async (importOriginal) => {
@@ -19,6 +21,11 @@ vi.mock('../src/helpers.js', async (importOriginal) => {
19
21
  };
20
22
  });
21
23
 
24
+ vi.mock('../src/deslop/resolveFiles.js', () => ({
25
+ resolveChangedFiles: vi.fn(),
26
+ resolveExplicitFiles: vi.fn(),
27
+ }));
28
+
22
29
  const mockGraph = {
23
30
  precomputeFileResolutions: vi.fn(),
24
31
  getBrokenInternalLinks: vi.fn().mockReturnValue([]),
@@ -42,6 +49,7 @@ describe('brokenLinks', () => {
42
49
  beforeEach(() => {
43
50
  vi.clearAllMocks();
44
51
  vi.mocked(buildGraph).mockResolvedValue(mockGraph as never);
52
+ vi.mocked(resolveExplicitFiles).mockResolvedValue([]);
45
53
  mockGraph.getBrokenInternalLinks.mockReturnValue([]);
46
54
  });
47
55
 
@@ -94,6 +102,42 @@ describe('brokenLinks', () => {
94
102
  expect(processExitMock).toHaveBeenCalledWith(1);
95
103
  });
96
104
 
105
+ it('checks only files matching the provided option', async () => {
106
+ vi.mocked(checkForMintJson).mockResolvedValueOnce(true);
107
+ vi.mocked(resolveExplicitFiles).mockResolvedValueOnce(['guides/selected.mdx']);
108
+ mockGraph.getBrokenInternalLinks.mockReturnValue([
109
+ {
110
+ relativeDir: 'guides',
111
+ filename: 'selected.mdx',
112
+ originalPath: '/api/selected-invalid',
113
+ pathType: 'internal',
114
+ } as MdxPath,
115
+ {
116
+ relativeDir: 'guides',
117
+ filename: 'other.mdx',
118
+ originalPath: '/api/other-invalid',
119
+ pathType: 'internal',
120
+ } as MdxPath,
121
+ ]);
122
+
123
+ await runCommand('broken-links', '--files', 'guides/selected.mdx', 'guides/**/*.mdx');
124
+
125
+ expect(resolveExplicitFiles).toHaveBeenCalledWith(CMD_EXEC_PATH, [
126
+ 'guides/selected.mdx',
127
+ 'guides/**/*.mdx',
128
+ ]);
129
+ expect(addLogSpy).toHaveBeenCalledWith(
130
+ expect.objectContaining({
131
+ props: {
132
+ brokenLinksByFile: {
133
+ [path.join('guides', 'selected.mdx')]: ['/api/selected-invalid'],
134
+ },
135
+ },
136
+ })
137
+ );
138
+ expect(processExitMock).toHaveBeenCalledWith(1);
139
+ });
140
+
97
141
  it('fails when checking throws error', async () => {
98
142
  vi.mocked(checkForMintJson).mockResolvedValueOnce(true);
99
143
  vi.mocked(buildGraph).mockRejectedValueOnce(new Error('some error'));
@@ -118,6 +162,7 @@ describe('brokenLinks --check-external', () => {
118
162
  beforeEach(() => {
119
163
  vi.clearAllMocks();
120
164
  vi.mocked(buildGraph).mockResolvedValue(mockGraph as never);
165
+ vi.mocked(resolveExplicitFiles).mockResolvedValue([]);
121
166
  mockGraph.getBrokenInternalLinks.mockReturnValue([]);
122
167
  });
123
168
 
@@ -160,6 +205,19 @@ describe('brokenLinks --check-external', () => {
160
205
  expect(processExitMock).toHaveBeenCalledWith(1);
161
206
  });
162
207
 
208
+ it('checks external links only from matching files', async () => {
209
+ vi.mocked(checkForMintJson).mockResolvedValueOnce(true);
210
+ vi.mocked(resolveExplicitFiles).mockResolvedValueOnce(['selected.mdx']);
211
+ vi.mocked(getBrokenExternalLinks).mockResolvedValueOnce([]);
212
+
213
+ await runCommand('broken-links', '--files', 'selected.mdx', '--check-external');
214
+
215
+ expect(getBrokenExternalLinks).toHaveBeenCalledWith(mockGraph, {
216
+ sourceFiles: new Set(['selected.mdx']),
217
+ });
218
+ expect(processExitMock).toHaveBeenCalledWith(0);
219
+ });
220
+
163
221
  it('fails when external link checking throws error', async () => {
164
222
  vi.mocked(checkForMintJson).mockResolvedValueOnce(true);
165
223
  vi.mocked(getBrokenExternalLinks).mockRejectedValueOnce(new Error('network error'));
package/bin/cli.js CHANGED
@@ -21,6 +21,8 @@ import { setTelemetryEnabled } from './config.js';
21
21
  import { getConfigValue, setConfigValue, clearConfigValue } from './config.js';
22
22
  import { API_URL } from './constants.js';
23
23
  import { deslopHandler } from './deslop/index.js';
24
+ import { resolveExplicitFiles } from './deslop/resolveFiles.js';
25
+ import { formatHandler } from './format.js';
24
26
  import { CMD_EXEC_PATH, checkPort, checkNodeVersion, autoUpgradeIfNeeded, getVersions, isAI, suppressConsoleWarnings, terminate, } from './helpers.js';
25
27
  import { init } from './init.js';
26
28
  import { getAccessToken } from './keyring.js';
@@ -232,6 +234,11 @@ export const cli = ({ packageName = 'mint' }) => {
232
234
  yield terminate(valid ? 0 : 1);
233
235
  }))
234
236
  .command('broken-links', 'Check for broken links', (yargs) => yargs
237
+ .option('files', {
238
+ type: 'string',
239
+ array: true,
240
+ description: 'Files or globs to check (defaults to the whole site)',
241
+ })
235
242
  .option('check-anchors', {
236
243
  type: 'boolean',
237
244
  default: false,
@@ -251,17 +258,26 @@ export const cli = ({ packageName = 'mint' }) => {
251
258
  type: 'boolean',
252
259
  default: false,
253
260
  description: 'also check that docs.json redirect destinations resolve to valid paths',
254
- }), (argv) => __awaiter(void 0, void 0, void 0, function* () {
261
+ })
262
+ .example('mint broken-links --files introduction.mdx', 'Check a specific page')
263
+ .example('mint broken-links --files "guides/**/*.mdx"', 'Check pages matching a glob'), (argv) => __awaiter(void 0, void 0, void 0, function* () {
264
+ var _a;
255
265
  yield autoUpgradeIfNeeded();
256
266
  addLog(_jsx(SpinnerLog, { message: "checking for broken links..." }));
257
267
  try {
268
+ const fileArgs = ((_a = argv.files) !== null && _a !== void 0 ? _a : []).map(String).filter(Boolean);
269
+ const sourceFiles = fileArgs.length > 0
270
+ ? new Set((yield resolveExplicitFiles(CMD_EXEC_PATH, fileArgs)).map((file) => path.normalize(file)))
271
+ : undefined;
258
272
  const graph = yield buildGraph(undefined, {
259
273
  checkSnippets: argv['check-snippets'],
260
274
  });
261
275
  graph.precomputeFileResolutions();
262
- const brokenInternalLinks = graph.getBrokenInternalLinks({
276
+ const brokenInternalLinks = graph
277
+ .getBrokenInternalLinks({
263
278
  checkAnchors: argv['check-anchors'],
264
- });
279
+ })
280
+ .filter(({ relativeDir, filename }) => !sourceFiles || sourceFiles.has(path.join(relativeDir, filename)));
265
281
  const brokenLinksByFile = {};
266
282
  brokenInternalLinks.forEach((mdxPath) => {
267
283
  const filename = path.join(mdxPath.relativeDir, mdxPath.filename);
@@ -274,7 +290,7 @@ export const cli = ({ packageName = 'mint' }) => {
274
290
  }
275
291
  });
276
292
  if (argv['check-external']) {
277
- const brokenExternalLinks = yield getBrokenExternalLinks(graph);
293
+ const brokenExternalLinks = yield getBrokenExternalLinks(graph, sourceFiles ? { sourceFiles } : undefined);
278
294
  for (const result of brokenExternalLinks) {
279
295
  for (const source of result.sources) {
280
296
  const label = result.status
@@ -522,6 +538,9 @@ export const cli = ({ packageName = 'mint' }) => {
522
538
  .example('mint deslop', 'Check git-changed pages for AI-sounding prose')
523
539
  .example('mint deslop docs/guide.mdx', 'Check a specific page')
524
540
  .example('mint deslop "docs/**/*.mdx" --format json', 'Check pages by glob with agent-friendly JSON output'), deslopHandler)
541
+ .command('format', 'Format MDX files in the current directory', (yargs) => yargs
542
+ .usage('usage: mint format')
543
+ .example('mint format', 'format all MDX files in the current directory'), formatHandler)
525
544
  // Coming soon commands — visible in help, tracked via telemetry to gauge interest.
526
545
  .command('ai', '[Coming soon] AI-powered documentation (run mint ai to vote)', () => undefined, comingSoon('ai', packageName))
527
546
  .command('test', '[Coming soon] Test your documentation (run mint test to vote)', () => undefined, comingSoon('test', packageName))
package/bin/format.js ADDED
@@ -0,0 +1,110 @@
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
+ var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
11
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
12
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
13
+ var m = o[Symbol.asyncIterator], i;
14
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
15
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
16
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
17
+ };
18
+ var __asyncDelegator = (this && this.__asyncDelegator) || function (o) {
19
+ var i, p;
20
+ return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i;
21
+ function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }
22
+ };
23
+ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
24
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
25
+ var g = generator.apply(thisArg, _arguments || []), i, q = [];
26
+ return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
27
+ function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
28
+ function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
29
+ function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
30
+ function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
31
+ function fulfill(value) { resume("next", value); }
32
+ function reject(value) { resume("throw", value); }
33
+ function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
34
+ };
35
+ import { jsx as _jsx } from "react/jsx-runtime";
36
+ import { isMintIgnored, processMintIgnoreString } from '@mintlify/common';
37
+ import { getMintIgnore } from '@mintlify/prebuild';
38
+ import { addLog, ErrorLog, SuccessLog } from '@mintlify/previewing';
39
+ import fs from 'node:fs/promises';
40
+ import path from 'node:path';
41
+ import { CMD_EXEC_PATH, terminate } from './helpers.js';
42
+ function getGitIgnore() {
43
+ return __awaiter(this, void 0, void 0, function* () {
44
+ try {
45
+ const content = yield fs.readFile(path.join(CMD_EXEC_PATH, '.gitignore'), 'utf-8');
46
+ return processMintIgnoreString(content);
47
+ }
48
+ catch (_a) {
49
+ return [];
50
+ }
51
+ });
52
+ }
53
+ function walk(dir, ignores) {
54
+ return __asyncGenerator(this, arguments, function* walk_1() {
55
+ const entries = yield __await(fs.readdir(dir, { withFileTypes: true }));
56
+ for (const entry of entries) {
57
+ const full = path.join(dir, entry.name);
58
+ const relative = path.relative(CMD_EXEC_PATH, full).split(path.sep).join('/');
59
+ if (entry.isDirectory()) {
60
+ if (isMintIgnored(`${relative}/`, ignores))
61
+ continue;
62
+ yield __await(yield* __asyncDelegator(__asyncValues(walk(full, ignores))));
63
+ }
64
+ else if (entry.name.endsWith('.mdx')) {
65
+ if (isMintIgnored(relative, ignores))
66
+ continue;
67
+ yield yield __await(full);
68
+ }
69
+ }
70
+ });
71
+ }
72
+ export const formatHandler = () => __awaiter(void 0, void 0, void 0, function* () {
73
+ var _a, e_1, _b, _c;
74
+ const { mdxToPm, pmToMdx } = yield import('./vendor/converter.js');
75
+ const ignores = [...(yield getGitIgnore()), ...(yield getMintIgnore(CMD_EXEC_PATH))];
76
+ let total = 0;
77
+ let changed = 0;
78
+ let failed = 0;
79
+ try {
80
+ for (var _d = true, _e = __asyncValues(walk(CMD_EXEC_PATH, ignores)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
81
+ _c = _f.value;
82
+ _d = false;
83
+ const file = _c;
84
+ total++;
85
+ const relative = path.relative(CMD_EXEC_PATH, file);
86
+ try {
87
+ const raw = yield fs.readFile(file, 'utf-8');
88
+ const { doc, frontmatter } = mdxToPm(raw, { singleDollarTextMath: false });
89
+ const formatted = `${pmToMdx(doc, { frontmatter })}\n`;
90
+ if (formatted !== raw) {
91
+ yield fs.writeFile(file, formatted);
92
+ changed++;
93
+ }
94
+ }
95
+ catch (error) {
96
+ failed++;
97
+ addLog(_jsx(ErrorLog, { message: `${relative}: ${error instanceof Error ? error.message : 'unknown error'}` }));
98
+ }
99
+ }
100
+ }
101
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
102
+ finally {
103
+ try {
104
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
105
+ }
106
+ finally { if (e_1) throw e_1.error; }
107
+ }
108
+ addLog(_jsx(SuccessLog, { message: `formatted ${changed} of ${total} mdx file${total === 1 ? '' : 's'}${failed > 0 ? ` (${failed} failed)` : ''}` }));
109
+ yield terminate(failed > 0 ? 1 : 0);
110
+ });