@mintlify/cli 4.0.1741 → 4.0.1742
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__/validateCommand.test.ts +106 -27
- package/bin/cli.js +7 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/bin/validate/command.js +45 -8
- package/package.json +4 -4
- package/src/cli.tsx +11 -0
- package/src/validate/command.tsx +44 -7
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { mockProcessExit } from 'vitest-mock-process';
|
|
5
5
|
|
|
6
6
|
import { sendPublishPrompt } from '../src/signup.js';
|
|
7
|
-
import {
|
|
7
|
+
import type { Report } from '../src/validate/report.js';
|
|
8
8
|
import { runCommand } from './utils.js';
|
|
9
9
|
|
|
10
10
|
const { dir } = vi.hoisted(() => ({
|
|
@@ -26,11 +26,6 @@ vi.mock('@mintlify/previewing', async (importOriginal) => {
|
|
|
26
26
|
return { ...mod, addLog: vi.fn(), checkBuild: vi.fn() };
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
vi.mock('../src/validate/report.js', async (importOriginal) => {
|
|
30
|
-
const mod = await importOriginal<typeof import('../src/validate/report.js')>();
|
|
31
|
-
return { ...mod, toReport: vi.fn(mod.toReport) };
|
|
32
|
-
});
|
|
33
|
-
|
|
34
29
|
const processExitMock = mockProcessExit();
|
|
35
30
|
|
|
36
31
|
const HOME = '---\ntitle: Home\n---\n\nBody\n';
|
|
@@ -49,14 +44,35 @@ const writeSite = (files: Record<string, string>, config: Record<string, unknown
|
|
|
49
44
|
}
|
|
50
45
|
};
|
|
51
46
|
|
|
47
|
+
const runJson = async (...args: string[]) => {
|
|
48
|
+
const write = vi.spyOn(process.stdout, 'write').mockImplementation((...params: unknown[]) => {
|
|
49
|
+
params.find((param): param is () => void => typeof param === 'function')?.();
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
await runCommand(...args, '--format', 'json');
|
|
54
|
+
return write.mock.calls.map(([chunk]) => String(chunk));
|
|
55
|
+
} finally {
|
|
56
|
+
write.mockRestore();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
52
60
|
const report = async (...args: string[]) => {
|
|
53
|
-
await
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return result?.value as ReturnType<typeof toReport>;
|
|
61
|
+
const chunks = await runJson(...args);
|
|
62
|
+
expect(chunks).toHaveLength(1);
|
|
63
|
+
return JSON.parse(chunks[0] ?? '') as Report;
|
|
57
64
|
};
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
/** Runs in json mode expecting a failure: nothing on stdout, the error on stderr. */
|
|
67
|
+
const jsonError = async (...args: string[]) => {
|
|
68
|
+
const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
69
|
+
try {
|
|
70
|
+
expect(await runJson(...args)).toEqual([]);
|
|
71
|
+
return stderr.mock.calls.map(([chunk]) => String(chunk)).join('');
|
|
72
|
+
} finally {
|
|
73
|
+
stderr.mockRestore();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
60
76
|
|
|
61
77
|
const rules = (result: { diagnostics: { rule: string }[] }) =>
|
|
62
78
|
result.diagnostics.map(({ rule }) => rule);
|
|
@@ -71,7 +87,28 @@ describe('mint validate', () => {
|
|
|
71
87
|
fs.removeSync(dir);
|
|
72
88
|
});
|
|
73
89
|
|
|
74
|
-
it('
|
|
90
|
+
it('exits only after stdout has flushed the JSON report', async () => {
|
|
91
|
+
writeSite({ 'index.mdx': HOME });
|
|
92
|
+
let flush: (() => void) | undefined;
|
|
93
|
+
const write = vi.spyOn(process.stdout, 'write').mockImplementation((...params: unknown[]) => {
|
|
94
|
+
flush = params.find((param): param is () => void => typeof param === 'function');
|
|
95
|
+
return true;
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
const run = runCommand('validate', '--no-build', '--format', 'json');
|
|
99
|
+
await vi.waitFor(() => expect(write).toHaveBeenCalled());
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
101
|
+
expect(processExitMock).not.toHaveBeenCalled();
|
|
102
|
+
|
|
103
|
+
flush?.();
|
|
104
|
+
await run;
|
|
105
|
+
expect(processExitMock).toHaveBeenCalledWith(0);
|
|
106
|
+
} finally {
|
|
107
|
+
write.mockRestore();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('prints one JSON report with a summary and every diagnostic', async () => {
|
|
75
112
|
writeSite({
|
|
76
113
|
'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n',
|
|
77
114
|
'other.mdx': '---\ntitle: Other\n---\n\nText {broken\n',
|
|
@@ -100,6 +137,7 @@ describe('mint validate', () => {
|
|
|
100
137
|
],
|
|
101
138
|
build: { ran: false, reason: 'disabled' },
|
|
102
139
|
});
|
|
140
|
+
expect(addLog).not.toHaveBeenCalled();
|
|
103
141
|
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
104
142
|
});
|
|
105
143
|
|
|
@@ -111,7 +149,7 @@ describe('mint validate', () => {
|
|
|
111
149
|
expect(result).toMatchObject({ passed: true, build: { ran: true, passed: true } });
|
|
112
150
|
expect(rules(result)).toEqual([]);
|
|
113
151
|
expect(checkBuild).toHaveBeenCalled();
|
|
114
|
-
expect(sendPublishPrompt).toHaveBeenCalled();
|
|
152
|
+
expect(sendPublishPrompt).not.toHaveBeenCalled();
|
|
115
153
|
expect(processExitMock).toHaveBeenCalledWith(0);
|
|
116
154
|
});
|
|
117
155
|
|
|
@@ -208,7 +246,6 @@ describe('mint validate', () => {
|
|
|
208
246
|
|
|
209
247
|
expect(result).toMatchObject({ passed: true, build: { ran: true, passed: false } });
|
|
210
248
|
expect(rules(result)).toEqual([]);
|
|
211
|
-
expect(sendPublishPrompt).not.toHaveBeenCalled();
|
|
212
249
|
expect(processExitMock).toHaveBeenCalledWith(0);
|
|
213
250
|
});
|
|
214
251
|
|
|
@@ -307,7 +344,7 @@ describe('mint validate', () => {
|
|
|
307
344
|
|
|
308
345
|
const result = await report('validate');
|
|
309
346
|
|
|
310
|
-
expect(result.diagnostics.map(({ file }) => file)).toEqual([
|
|
347
|
+
expect(result.diagnostics.map(({ file }: { file: string }) => file)).toEqual([
|
|
311
348
|
'index.mdx',
|
|
312
349
|
'index.mdx',
|
|
313
350
|
'index.mdx',
|
|
@@ -360,12 +397,26 @@ describe('mint validate', () => {
|
|
|
360
397
|
writeSite({ 'index.mdx': HOME });
|
|
361
398
|
vi.mocked(checkBuild).mockRejectedValue(new Error('requires an internet connection'));
|
|
362
399
|
|
|
363
|
-
await
|
|
364
|
-
|
|
365
|
-
expect(logged()).toContain('requires an internet connection');
|
|
400
|
+
expect(await jsonError('validate')).toBe('Error: requires an internet connection\n');
|
|
366
401
|
expect(processExitMock).toHaveBeenCalledWith(2);
|
|
367
402
|
});
|
|
368
403
|
|
|
404
|
+
it('keeps console output during the build off stdout in json mode', async () => {
|
|
405
|
+
writeSite({ 'index.mdx': HOME });
|
|
406
|
+
vi.mocked(checkBuild).mockImplementation(async () => {
|
|
407
|
+
console.log('progress line');
|
|
408
|
+
return { passed: true, warnings: [] };
|
|
409
|
+
});
|
|
410
|
+
const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
411
|
+
|
|
412
|
+
const result = await report('validate');
|
|
413
|
+
|
|
414
|
+
const errors = stderr.mock.calls.map(([chunk]) => String(chunk));
|
|
415
|
+
stderr.mockRestore();
|
|
416
|
+
expect(result.passed).toBe(true);
|
|
417
|
+
expect(errors).toContain('progress line\n');
|
|
418
|
+
});
|
|
419
|
+
|
|
369
420
|
it('applies fixes with --fix and lists them', async () => {
|
|
370
421
|
writeSite({ 'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n' });
|
|
371
422
|
|
|
@@ -409,13 +460,14 @@ describe('mint validate', () => {
|
|
|
409
460
|
it('rejects unknown rules and disabling parse-error', async () => {
|
|
410
461
|
writeSite({ 'index.mdx': HOME });
|
|
411
462
|
|
|
412
|
-
await
|
|
413
|
-
|
|
463
|
+
expect(await jsonError('validate', '--disable', 'nope')).toContain(
|
|
464
|
+
'Error: unknown rule or group "nope"'
|
|
465
|
+
);
|
|
414
466
|
expect(processExitMock).toHaveBeenLastCalledWith(2);
|
|
415
467
|
|
|
416
|
-
await
|
|
417
|
-
|
|
418
|
-
|
|
468
|
+
expect(await jsonError('validate', '--disable', 'parse-error')).toBe(
|
|
469
|
+
"Error: parse-error always runs and can't be disabled\n"
|
|
470
|
+
);
|
|
419
471
|
});
|
|
420
472
|
|
|
421
473
|
it('limits page findings to --files', async () => {
|
|
@@ -493,9 +545,9 @@ describe('mint validate', () => {
|
|
|
493
545
|
it('rejects --files that match nothing', async () => {
|
|
494
546
|
writeSite({ 'index.mdx': HOME });
|
|
495
547
|
|
|
496
|
-
await
|
|
497
|
-
|
|
498
|
-
|
|
548
|
+
expect(await jsonError('validate', '--files', 'missing.mdx')).toContain(
|
|
549
|
+
'Error: No matching files'
|
|
550
|
+
);
|
|
499
551
|
expect(processExitMock).toHaveBeenCalledWith(2);
|
|
500
552
|
expect(checkBuild).not.toHaveBeenCalled();
|
|
501
553
|
});
|
|
@@ -515,10 +567,37 @@ describe('mint validate', () => {
|
|
|
515
567
|
|
|
516
568
|
await runCommand('validate', '--no-build');
|
|
517
569
|
|
|
518
|
-
const text =
|
|
570
|
+
const text = JSON.stringify(vi.mocked(addLog).mock.calls);
|
|
519
571
|
expect(text).toContain('index.mdx');
|
|
520
572
|
expect(text).toContain('must be self-closed');
|
|
521
573
|
expect(text).toContain('1 error');
|
|
522
574
|
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
523
575
|
});
|
|
576
|
+
|
|
577
|
+
it('prompts to publish in text mode only when the build passed', async () => {
|
|
578
|
+
writeSite({ 'index.mdx': HOME });
|
|
579
|
+
|
|
580
|
+
await runCommand('validate');
|
|
581
|
+
expect(sendPublishPrompt).toHaveBeenCalledTimes(1);
|
|
582
|
+
|
|
583
|
+
vi.mocked(checkBuild).mockResolvedValue({
|
|
584
|
+
passed: false,
|
|
585
|
+
error: 'Build validation failed with 1 warning(s). See above for details.',
|
|
586
|
+
strict: true,
|
|
587
|
+
warnings: [{ type: 'missing-nav-page', message: '"gone" is missing.' }],
|
|
588
|
+
});
|
|
589
|
+
await runCommand('validate', '--disable', 'missing-nav-page');
|
|
590
|
+
expect(sendPublishPrompt).toHaveBeenCalledTimes(1);
|
|
591
|
+
expect(processExitMock).toHaveBeenLastCalledWith(0);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('rejects --format values other than text and json', async () => {
|
|
595
|
+
writeSite({ 'index.mdx': HOME });
|
|
596
|
+
const error = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
597
|
+
|
|
598
|
+
await expect(runCommand('validate', '--format', 'table')).rejects.toThrow();
|
|
599
|
+
|
|
600
|
+
expect(error).toHaveBeenCalledWith(expect.stringContaining('Invalid values'));
|
|
601
|
+
error.mockRestore();
|
|
602
|
+
});
|
|
524
603
|
});
|
package/bin/cli.js
CHANGED
|
@@ -333,6 +333,12 @@ export const cli = ({ packageName = 'mint' }) => {
|
|
|
333
333
|
type: 'boolean',
|
|
334
334
|
default: false,
|
|
335
335
|
description: 'Apply the fixes and check again',
|
|
336
|
+
})
|
|
337
|
+
.option('format', {
|
|
338
|
+
type: 'string',
|
|
339
|
+
choices: ['text', 'json'],
|
|
340
|
+
default: 'text',
|
|
341
|
+
description: 'Output format; json prints one report ({ passed, summary, diagnostics, build }) and nothing else on stdout, and errors to stderr',
|
|
336
342
|
})
|
|
337
343
|
.usage('usage: mint validate [options]')
|
|
338
344
|
.epilogue([
|
|
@@ -341,6 +347,7 @@ export const cli = ({ packageName = 'mint' }) => {
|
|
|
341
347
|
` parse-error — ${PARSE_ERROR.description} (always runs)`,
|
|
342
348
|
].join('\n'))
|
|
343
349
|
.example('mint validate', 'run every check and the build')
|
|
350
|
+
.example('mint validate --no-build --format json', 'fast offline check for scripts and agents')
|
|
344
351
|
.example('mint validate --files "guides/**/*.mdx" --fix', 'check some pages and apply fixes')
|
|
345
352
|
.example('mint validate --disable all --enable content', 'run the content rules only; missing-file and import-error still run the build')
|
|
346
353
|
.strict(), (argv) => validateHandler(argv, packageName))
|