@mintlify/cli 4.0.1743 → 4.0.1745
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__/foreignSyntax.test.ts +73 -0
- package/__test__/validateCommand.test.ts +64 -20
- package/__test__/validateEngine.test.ts +17 -4
- package/__test__/validateRules.test.ts +659 -8
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/bin/validate/engine.js +6 -13
- package/bin/validate/files.js +26 -30
- package/bin/validate/mdx.js +38 -1
- package/bin/validate/rules/build.js +16 -8
- package/bin/validate/rules/content/duplicateH1.js +57 -0
- package/bin/validate/rules/content/foreignSyntax.js +120 -0
- package/bin/validate/rules/content/katex.js +34 -0
- package/bin/validate/rules/content/unknownComponent.js +121 -0
- package/bin/validate/rules/index.js +13 -1
- package/bin/validate/site.js +11 -9
- package/package.json +10 -8
- package/src/validate/engine.ts +6 -12
- package/src/validate/files.ts +41 -31
- package/src/validate/mdx.ts +38 -0
- package/src/validate/rules/build.ts +27 -13
- package/src/validate/rules/content/duplicateH1.ts +78 -0
- package/src/validate/rules/content/foreignSyntax.ts +144 -0
- package/src/validate/rules/content/katex.ts +33 -0
- package/src/validate/rules/content/unknownComponent.ts +130 -0
- package/src/validate/rules/index.ts +13 -1
- package/src/validate/site.ts +9 -8
- package/src/validate/types.ts +3 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { foreignSyntax } from '../src/validate/rules/content/foreignSyntax.js';
|
|
2
|
+
|
|
3
|
+
const check = (content: string) =>
|
|
4
|
+
foreignSyntax.check({
|
|
5
|
+
file: 'page.mdx',
|
|
6
|
+
content,
|
|
7
|
+
baseDir: '/',
|
|
8
|
+
frontmatter: {},
|
|
9
|
+
tree: { type: 'root', children: [] },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const messages = (content: string) => check(content).map(({ message }) => message);
|
|
13
|
+
|
|
14
|
+
describe('foreign-syntax', () => {
|
|
15
|
+
it.each([
|
|
16
|
+
['GitBook hint', '{% hint style="warning" %}\nCareful\n{% endhint %}\n', '<Warning>'],
|
|
17
|
+
['Liquid tag', '{% include note.html %}\n', 'snippet import'],
|
|
18
|
+
['Hugo shortcode', '{{< figure src="a.png" >}}\n', 'Hugo shortcode'],
|
|
19
|
+
['Liquid variable', 'Version {{ site.version }}\n', 'Liquid variable'],
|
|
20
|
+
['Liquid filter', '[Home]({{ "/" | relative_url }})\n', 'Liquid variable'],
|
|
21
|
+
['Docusaurus admonition', ':::tip\nUse it\n:::\n', '<Tip>'],
|
|
22
|
+
['VitePress admonition with title', '::: warning Heads up\nx\n:::\n', '<Warning>'],
|
|
23
|
+
['MDC block', '::note\nx\n::\n', 'MDC'],
|
|
24
|
+
['MkDocs admonition', '!!! note "Title"\n Body\n', '<Note>'],
|
|
25
|
+
['MkDocs collapsible', '??? info\n Body\n', '<Accordion>'],
|
|
26
|
+
['MkDocs tab', '=== "Python"\n code\n', '<Tabs>'],
|
|
27
|
+
['MkDocs snippet in a fence', '```py\n--8<-- "example.py"\n```\n', 'snippet include'],
|
|
28
|
+
['Kramdown IAL', 'Text\n{: .note }\n', 'Kramdown'],
|
|
29
|
+
['wiki link', 'See [[Getting Started]].\n', 'wiki link'],
|
|
30
|
+
['wiki link with label', 'See [[setup|Install]].\n', 'wiki link'],
|
|
31
|
+
['ReadMe doc link', 'See [setup](doc:setup).\n', 'such as /setup'],
|
|
32
|
+
['ReadMe ref link in href', '<a href="ref:create-user">x</a>\n', 'such as /create-user'],
|
|
33
|
+
[
|
|
34
|
+
'Docusaurus @site import',
|
|
35
|
+
"import Img from '@site/static/img/a.png';\n\n<img src={Img} />\n",
|
|
36
|
+
'@site',
|
|
37
|
+
],
|
|
38
|
+
])('flags %s', (_name, content, expected) => {
|
|
39
|
+
const found = messages(content);
|
|
40
|
+
expect(found).toHaveLength(1);
|
|
41
|
+
expect(found[0]).toContain(expected);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('flags each opening GitBook tag once, not its closing tag', () => {
|
|
45
|
+
expect(messages('{% tabs %}\n{% tab title="A" %}\nx\n{% endtab %}\n{% endtabs %}\n')).toEqual([
|
|
46
|
+
expect.stringContaining('use <Tabs>'),
|
|
47
|
+
expect.stringContaining('use <Tab title="…">'),
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it.each([
|
|
52
|
+
['fenced code', '```liquid\n{% hint style="info" %}\n{{ site.title }}\n:::note\n```\n'],
|
|
53
|
+
['inline code', 'Write `{% hint %}`, `:::note` and `[[Page]]` like this.\n'],
|
|
54
|
+
['frontmatter', '---\ntitle: "{% hint %}"\ndescription: "[[Page]]"\n---\n\nBody\n'],
|
|
55
|
+
['MDX expressions', 'Total: {a | b} and {{ x: 1 }.x} and {[[1, 2]].length} and {{page}}\n'],
|
|
56
|
+
['closing tags and bare fences', ':::\n\n::\n'],
|
|
57
|
+
['Markdown link after a bracket', '[[1]](#note-1) and [a][ref]\n'],
|
|
58
|
+
['ordinary prose', 'Use doc: the guide. Wow!!! Equal === sign.\n'],
|
|
59
|
+
['custom heading ids', '## Install {#install}\n'],
|
|
60
|
+
])('ignores %s', (_name, content) => {
|
|
61
|
+
expect(messages(content)).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('reports positions in the original file', () => {
|
|
65
|
+
expect(check('---\ntitle: A\n---\n\nText\n :::note\nx\n:::\n')).toEqual([
|
|
66
|
+
expect.objectContaining({ line: 6, column: 3 }),
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('never offers edits', () => {
|
|
71
|
+
expect(check('{% hint %}\n').every(({ edit }) => edit === undefined)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -109,30 +109,27 @@ describe('mint validate', () => {
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
it('prints one JSON report with a summary and every diagnostic', async () => {
|
|
112
|
-
writeSite({
|
|
113
|
-
'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n',
|
|
114
|
-
'other.mdx': '---\ntitle: Other\n---\n\nText {broken\n',
|
|
115
|
-
});
|
|
112
|
+
writeSite({ 'index.mdx': '---\ntitle: Home\n---\n\n# Home\n\nline<br>\n' });
|
|
116
113
|
|
|
117
114
|
const result = await report('validate', '--no-build');
|
|
118
115
|
|
|
119
116
|
expect(result).toEqual({
|
|
120
117
|
passed: false,
|
|
121
|
-
summary: { errors:
|
|
118
|
+
summary: { errors: 1, warnings: 1, byRule: { 'duplicate-h1': 1, 'void-tags': 1 } },
|
|
122
119
|
diagnostics: [
|
|
123
120
|
expect.objectContaining({
|
|
124
|
-
rule: '
|
|
121
|
+
rule: 'duplicate-h1',
|
|
125
122
|
group: 'content',
|
|
126
|
-
severity: '
|
|
123
|
+
severity: 'warning',
|
|
127
124
|
file: 'index.mdx',
|
|
128
125
|
line: 5,
|
|
129
|
-
fix: { file: 'index.mdx', start:
|
|
126
|
+
fix: { file: 'index.mdx', start: 21, end: 29, text: '' },
|
|
130
127
|
}),
|
|
131
128
|
expect.objectContaining({
|
|
132
|
-
rule: '
|
|
129
|
+
rule: 'void-tags',
|
|
133
130
|
severity: 'error',
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
line: 7,
|
|
132
|
+
fix: { file: 'index.mdx', start: 33, end: 37, text: '<br />' },
|
|
136
133
|
}),
|
|
137
134
|
],
|
|
138
135
|
build: { ran: false, reason: 'disabled' },
|
|
@@ -141,13 +138,13 @@ describe('mint validate', () => {
|
|
|
141
138
|
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
142
139
|
});
|
|
143
140
|
|
|
144
|
-
it('passes and runs the build', async () => {
|
|
145
|
-
writeSite({ 'index.mdx':
|
|
141
|
+
it('passes with warnings and runs the build', async () => {
|
|
142
|
+
writeSite({ 'index.mdx': '---\ntitle: Home\n---\n\n# Home\n' });
|
|
146
143
|
|
|
147
144
|
const result = await report('validate');
|
|
148
145
|
|
|
149
146
|
expect(result).toMatchObject({ passed: true, build: { ran: true, passed: true } });
|
|
150
|
-
expect(rules(result)).toEqual([]);
|
|
147
|
+
expect(rules(result)).toEqual(['duplicate-h1']);
|
|
151
148
|
expect(checkBuild).toHaveBeenCalled();
|
|
152
149
|
expect(sendPublishPrompt).not.toHaveBeenCalled();
|
|
153
150
|
expect(processExitMock).toHaveBeenCalledWith(0);
|
|
@@ -301,6 +298,53 @@ describe('mint validate', () => {
|
|
|
301
298
|
]);
|
|
302
299
|
});
|
|
303
300
|
|
|
301
|
+
it('prefers structured warning fields over the message text', async () => {
|
|
302
|
+
writeSite(
|
|
303
|
+
{ 'index.mdx': HOME, 'guides/real.mdx': HOME },
|
|
304
|
+
{ navigation: { pages: ['index', 'guides/real', 'gone'] } }
|
|
305
|
+
);
|
|
306
|
+
vi.mocked(checkBuild).mockResolvedValue({
|
|
307
|
+
passed: false,
|
|
308
|
+
error: 'Build validation failed with 2 warning(s). See above for details.',
|
|
309
|
+
strict: true,
|
|
310
|
+
warnings: [
|
|
311
|
+
{
|
|
312
|
+
type: 'parse-error',
|
|
313
|
+
message: '\u001b[31m./wrong.mdx:9:9 Unexpected token\u001b[39m',
|
|
314
|
+
file: 'guides/real.mdx',
|
|
315
|
+
line: 3,
|
|
316
|
+
column: 7,
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
type: 'missing-nav-page',
|
|
320
|
+
message: '"gone" is referenced in the docs.json navigation but the file does not exist.',
|
|
321
|
+
file: 'docs.json',
|
|
322
|
+
ref: 'gone',
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const result = await report('validate');
|
|
328
|
+
|
|
329
|
+
expect(result.diagnostics).toHaveLength(2);
|
|
330
|
+
expect(result.diagnostics).toEqual(
|
|
331
|
+
expect.arrayContaining([
|
|
332
|
+
expect.objectContaining({
|
|
333
|
+
rule: 'parse-error',
|
|
334
|
+
file: 'guides/real.mdx',
|
|
335
|
+
line: 3,
|
|
336
|
+
column: 7,
|
|
337
|
+
message: './wrong.mdx:9:9 Unexpected token',
|
|
338
|
+
}),
|
|
339
|
+
expect.objectContaining({
|
|
340
|
+
rule: 'missing-nav-page',
|
|
341
|
+
file: 'docs.json',
|
|
342
|
+
line: expect.any(Number),
|
|
343
|
+
}),
|
|
344
|
+
])
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
304
348
|
it('fails when the build fails on a parse error outside --files', async () => {
|
|
305
349
|
writeSite({ 'index.mdx': HOME, 'other.mdx': HOME });
|
|
306
350
|
vi.mocked(checkBuild).mockResolvedValue({
|
|
@@ -417,8 +461,8 @@ describe('mint validate', () => {
|
|
|
417
461
|
expect(errors).toContain('progress line\n');
|
|
418
462
|
});
|
|
419
463
|
|
|
420
|
-
it('applies fixes with --fix and
|
|
421
|
-
writeSite({ 'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n' });
|
|
464
|
+
it('applies fixes with --fix, lists them and leaves unfixable issues', async () => {
|
|
465
|
+
writeSite({ 'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n\n<Missing />\n' });
|
|
422
466
|
|
|
423
467
|
const result = await report('validate', '--no-build', '--fix');
|
|
424
468
|
|
|
@@ -433,13 +477,13 @@ describe('mint validate', () => {
|
|
|
433
477
|
after: '<br />',
|
|
434
478
|
},
|
|
435
479
|
]);
|
|
436
|
-
expect(rules(result)).toEqual([]);
|
|
480
|
+
expect(rules(result)).toEqual(['unknown-component']);
|
|
437
481
|
expect(processExitMock).toHaveBeenCalledWith(0);
|
|
438
482
|
});
|
|
439
483
|
|
|
440
484
|
it('turns rules and groups on and off', async () => {
|
|
441
485
|
writeSite({
|
|
442
|
-
'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n',
|
|
486
|
+
'index.mdx': '---\ntitle: Home\n---\n\n# Home\n\nline<br>\n',
|
|
443
487
|
});
|
|
444
488
|
|
|
445
489
|
// Without void-tags, nothing makes the page parseable
|
|
@@ -453,7 +497,7 @@ describe('mint validate', () => {
|
|
|
453
497
|
rules(await report('validate', '--no-build', '--disable', 'all', '--enable', 'void-tags'))
|
|
454
498
|
).toEqual(['void-tags']);
|
|
455
499
|
expect(
|
|
456
|
-
rules(await report('validate', '--no-build', '--disable', 'openapi,
|
|
500
|
+
rules(await report('validate', '--no-build', '--disable', 'openapi, duplicate-h1'))
|
|
457
501
|
).toEqual(['void-tags']);
|
|
458
502
|
});
|
|
459
503
|
|
|
@@ -474,7 +518,7 @@ describe('mint validate', () => {
|
|
|
474
518
|
writeSite(
|
|
475
519
|
{
|
|
476
520
|
'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n',
|
|
477
|
-
'other.mdx': '---\ntitle: Other\n---\n\nline<br>\n
|
|
521
|
+
'other.mdx': '---\ntitle: Other\n---\n\n# Other\n\nline<br>\n',
|
|
478
522
|
},
|
|
479
523
|
{ navigation: { pages: ['index', 'other'] } }
|
|
480
524
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
-
import
|
|
2
|
+
import fsSync from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
@@ -24,7 +24,15 @@ describe('selectRules', () => {
|
|
|
24
24
|
it('turns on the rules of an enabled group', () => {
|
|
25
25
|
const content = ids(selectRules({ disable: ['all'], enable: ['content'] }));
|
|
26
26
|
|
|
27
|
-
expect(content).toEqual([
|
|
27
|
+
expect(content).toEqual([
|
|
28
|
+
'void-tags',
|
|
29
|
+
'duplicate-h1',
|
|
30
|
+
'unknown-component',
|
|
31
|
+
'katex',
|
|
32
|
+
'foreign-syntax',
|
|
33
|
+
'missing-file',
|
|
34
|
+
'import-error',
|
|
35
|
+
]);
|
|
28
36
|
});
|
|
29
37
|
|
|
30
38
|
it('applies --enable after --disable', () => {
|
|
@@ -67,7 +75,12 @@ describe('applyFixes', () => {
|
|
|
67
75
|
fs.removeSync(dir);
|
|
68
76
|
});
|
|
69
77
|
|
|
70
|
-
const site = () =>
|
|
78
|
+
const site = () =>
|
|
79
|
+
({
|
|
80
|
+
baseDir: dir,
|
|
81
|
+
files: [],
|
|
82
|
+
read: (file: string) => fs.readFileSync(path.join(dir, file), 'utf-8'),
|
|
83
|
+
}) as unknown as Site;
|
|
71
84
|
|
|
72
85
|
const diagnostic = (fix: Diagnostic['fix'], rule = 'r'): Diagnostic => ({
|
|
73
86
|
rule,
|
|
@@ -209,7 +222,7 @@ describe('checkSite with fix', () => {
|
|
|
209
222
|
|
|
210
223
|
it('rechecks only changed files between passes and runs site rules once more at the end', async () => {
|
|
211
224
|
const site = siteRule();
|
|
212
|
-
const reads = vi.spyOn(
|
|
225
|
+
const reads = vi.spyOn(fsSync, 'readFileSync');
|
|
213
226
|
|
|
214
227
|
const result = await checkSite(dir, [bang(3), site], { localSchema: false, fix: true });
|
|
215
228
|
|