@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.
@@ -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: 2, warnings: 0, byRule: { 'parse-error': 1, 'void-tags': 1 } },
118
+ summary: { errors: 1, warnings: 1, byRule: { 'duplicate-h1': 1, 'void-tags': 1 } },
122
119
  diagnostics: [
123
120
  expect.objectContaining({
124
- rule: 'void-tags',
121
+ rule: 'duplicate-h1',
125
122
  group: 'content',
126
- severity: 'error',
123
+ severity: 'warning',
127
124
  file: 'index.mdx',
128
125
  line: 5,
129
- fix: { file: 'index.mdx', start: 25, end: 29, text: '<br />' },
126
+ fix: { file: 'index.mdx', start: 21, end: 29, text: '' },
130
127
  }),
131
128
  expect.objectContaining({
132
- rule: 'parse-error',
129
+ rule: 'void-tags',
133
130
  severity: 'error',
134
- file: 'other.mdx',
135
- message: expect.stringContaining('(other checks were skipped for this file)'),
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': HOME });
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 lists them', async () => {
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, missing-nav-page'))
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\nmore<hr>\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 fsp from 'node:fs/promises';
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(['void-tags', 'missing-file', 'import-error']);
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 = () => ({ baseDir: dir, files: [] }) as unknown as 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(fsp, 'readFile');
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