@mintlify/cli 4.0.1739 → 4.0.1741
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 +422 -39
- package/__test__/validateEngine.test.ts +187 -3
- package/__test__/validateRules.test.ts +64 -49
- package/bin/cli.js +29 -44
- package/bin/helpers.js +1 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/bin/validate/command.js +50 -18
- package/bin/validate/engine.js +93 -8
- package/bin/validate/files.js +1 -1
- package/bin/validate/output.js +29 -13
- package/bin/validate/report.js +11 -0
- package/bin/validate/rules/build.js +94 -0
- package/bin/validate/rules/index.js +3 -1
- package/bin/validate/rules/openapi.js +50 -0
- package/package.json +5 -5
- package/src/cli.tsx +37 -61
- package/src/helpers.tsx +3 -0
- package/src/validate/command.tsx +46 -19
- package/src/validate/engine.ts +132 -9
- package/src/validate/files.ts +1 -1
- package/src/validate/output.tsx +34 -14
- package/src/validate/report.ts +40 -0
- package/src/validate/rules/build.ts +132 -0
- package/src/validate/rules/index.ts +3 -1
- package/src/validate/rules/openapi.ts +59 -0
- package/src/validate/types.ts +10 -0
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { applyFixes, checkSite, selectRules, UsageError } from '../src/validate/engine.js';
|
|
2
7
|
import { RULES } from '../src/validate/rules/index.js';
|
|
3
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
GROUPS,
|
|
10
|
+
type Diagnostic,
|
|
11
|
+
type FileRule,
|
|
12
|
+
type Site,
|
|
13
|
+
type SiteRule,
|
|
14
|
+
} from '../src/validate/types.js';
|
|
4
15
|
|
|
5
16
|
const ids = (rules: { id: string }[]) => rules.map(({ id }) => id);
|
|
6
17
|
|
|
@@ -13,7 +24,7 @@ describe('selectRules', () => {
|
|
|
13
24
|
it('turns on the rules of an enabled group', () => {
|
|
14
25
|
const content = ids(selectRules({ disable: ['all'], enable: ['content'] }));
|
|
15
26
|
|
|
16
|
-
expect(content).toEqual(['void-tags']);
|
|
27
|
+
expect(content).toEqual(['void-tags', 'missing-file', 'import-error']);
|
|
17
28
|
});
|
|
18
29
|
|
|
19
30
|
it('applies --enable after --disable', () => {
|
|
@@ -44,3 +55,176 @@ describe('selectRules', () => {
|
|
|
44
55
|
expect(new Set(ids(RULES)).size).toBe(RULES.length);
|
|
45
56
|
});
|
|
46
57
|
});
|
|
58
|
+
|
|
59
|
+
describe('applyFixes', () => {
|
|
60
|
+
let dir: string;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'validate-engine-'));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
fs.removeSync(dir);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const site = () => ({ baseDir: dir, files: [] }) as unknown as Site;
|
|
71
|
+
|
|
72
|
+
const diagnostic = (fix: Diagnostic['fix'], rule = 'r'): Diagnostic => ({
|
|
73
|
+
rule,
|
|
74
|
+
group: 'content',
|
|
75
|
+
severity: 'error',
|
|
76
|
+
file: 'a.mdx',
|
|
77
|
+
message: 'm',
|
|
78
|
+
fix,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('applies fixes across files, skipping ones that overlap an applied fix', async () => {
|
|
82
|
+
fs.writeFileSync(path.join(dir, 'a.mdx'), 'one two three');
|
|
83
|
+
fs.writeFileSync(path.join(dir, 'b.json'), '{}');
|
|
84
|
+
|
|
85
|
+
const changes = await applyFixes(site(), [
|
|
86
|
+
diagnostic({ file: 'a.mdx', start: 0, end: 3, text: '1' }),
|
|
87
|
+
diagnostic({ file: 'a.mdx', start: 8, end: 13, text: '3' }),
|
|
88
|
+
diagnostic({ file: 'b.json', start: 1, end: 1, text: '"k":1' }),
|
|
89
|
+
// Overlaps the first fix, so it waits for the next pass
|
|
90
|
+
diagnostic({ file: 'a.mdx', start: 2, end: 5, text: 'x' }),
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
expect(fs.readFileSync(path.join(dir, 'a.mdx'), 'utf-8')).toBe('1 two 3');
|
|
94
|
+
expect(fs.readFileSync(path.join(dir, 'b.json'), 'utf-8')).toBe('{"k":1}');
|
|
95
|
+
expect(changes.map(({ file, before, after }) => [file, before, after])).toEqual([
|
|
96
|
+
['a.mdx', 'one', '1'],
|
|
97
|
+
['a.mdx', 'three', '3'],
|
|
98
|
+
['b.json', '', '"k":1'],
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('applies two insertions at the same point', async () => {
|
|
103
|
+
fs.writeFileSync(path.join(dir, 'b.json'), '{}');
|
|
104
|
+
|
|
105
|
+
const changes = await applyFixes(site(), [
|
|
106
|
+
diagnostic({ file: 'b.json', start: 1, end: 1, text: ',"j":2' }),
|
|
107
|
+
diagnostic({ file: 'b.json', start: 1, end: 1, text: '"k":1' }),
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
expect(fs.readFileSync(path.join(dir, 'b.json'), 'utf-8')).toBe('{"k":1,"j":2}');
|
|
111
|
+
expect(changes).toHaveLength(2);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('applies a replacement and an insertion at its start', async () => {
|
|
115
|
+
fs.writeFileSync(path.join(dir, 'a.mdx'), 'a<br>b');
|
|
116
|
+
|
|
117
|
+
const changes = await applyFixes(site(), [
|
|
118
|
+
diagnostic({ file: 'a.mdx', start: 1, end: 5, text: '<br />' }),
|
|
119
|
+
diagnostic({ file: 'a.mdx', start: 1, end: 1, text: 'x' }),
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
expect(fs.readFileSync(path.join(dir, 'a.mdx'), 'utf-8')).toBe('ax<br />b');
|
|
123
|
+
expect(changes.map(({ after }) => after)).toEqual(['<br />', 'x']);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('skips writing a Markdown file whose fixed content no longer parses', async () => {
|
|
127
|
+
fs.writeFileSync(path.join(dir, 'a.mdx'), 'hello world\n');
|
|
128
|
+
fs.writeFileSync(path.join(dir, 'b.mdx'), 'line<br>\n');
|
|
129
|
+
|
|
130
|
+
const changes = await applyFixes(site(), [
|
|
131
|
+
diagnostic({ file: 'a.mdx', start: 0, end: 5, text: '<Note>' }),
|
|
132
|
+
diagnostic({ file: 'b.mdx', start: 4, end: 8, text: '<br />' }),
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
expect(fs.readFileSync(path.join(dir, 'a.mdx'), 'utf-8')).toBe('hello world\n');
|
|
136
|
+
expect(fs.readFileSync(path.join(dir, 'b.mdx'), 'utf-8')).toBe('line<br />\n');
|
|
137
|
+
expect(changes.map(({ file, after }) => [file, after])).toEqual([['b.mdx', '<br />']]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('fixes a page that only parses after the build preprocesses its <MDX> fragments', async () => {
|
|
141
|
+
const fragment = '{true ? <MDX>\n```bash\n<root>/{arch}/app.vpkg\n```\n</MDX> : null}\n';
|
|
142
|
+
fs.writeFileSync(path.join(dir, 'a.mdx'), `${fragment}\nline<br>\n`);
|
|
143
|
+
const start = fragment.length + 5;
|
|
144
|
+
|
|
145
|
+
const changes = await applyFixes(site(), [
|
|
146
|
+
diagnostic({ file: 'a.mdx', start, end: start + 4, text: '<br />' }),
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
expect(fs.readFileSync(path.join(dir, 'a.mdx'), 'utf-8')).toBe(`${fragment}\nline<br />\n`);
|
|
150
|
+
expect(changes).toHaveLength(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('fixes an unparseable page even when another error keeps it unparseable', async () => {
|
|
154
|
+
fs.writeFileSync(path.join(dir, 'a.mdx'), 'line<br>\n');
|
|
155
|
+
fs.writeFileSync(path.join(dir, 'b.mdx'), 'line<br>\n\nText {broken\n');
|
|
156
|
+
|
|
157
|
+
const changes = await applyFixes(site(), [
|
|
158
|
+
diagnostic({ file: 'a.mdx', start: 4, end: 8, text: '<br />' }),
|
|
159
|
+
diagnostic({ file: 'b.mdx', start: 4, end: 8, text: '<br />' }),
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
expect(fs.readFileSync(path.join(dir, 'a.mdx'), 'utf-8')).toBe('line<br />\n');
|
|
163
|
+
expect(fs.readFileSync(path.join(dir, 'b.mdx'), 'utf-8')).toBe('line<br />\n\nText {broken\n');
|
|
164
|
+
expect(changes).toHaveLength(2);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('checkSite with fix', () => {
|
|
169
|
+
let dir: string;
|
|
170
|
+
|
|
171
|
+
beforeEach(() => {
|
|
172
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'validate-fix-passes-'));
|
|
173
|
+
fs.writeJSONSync(path.join(dir, 'docs.json'), {
|
|
174
|
+
name: 'Test',
|
|
175
|
+
navigation: { pages: ['index', 'other'] },
|
|
176
|
+
});
|
|
177
|
+
fs.writeFileSync(path.join(dir, 'index.mdx'), '---\ntitle: Home\n---\n\nText\n');
|
|
178
|
+
fs.writeFileSync(path.join(dir, 'other.mdx'), '---\ntitle: Other\n---\n\nText!!!\n');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
afterEach(() => {
|
|
182
|
+
fs.removeSync(dir);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/** Offers one "!" at a time until a page ends with `limit` of them, so each pass unlocks the next. */
|
|
186
|
+
const bang = (limit: number): FileRule => ({
|
|
187
|
+
id: 'bang',
|
|
188
|
+
group: 'content',
|
|
189
|
+
severity: 'warning',
|
|
190
|
+
scope: 'file',
|
|
191
|
+
raw: true,
|
|
192
|
+
description: 'test rule',
|
|
193
|
+
check: ({ content }) => {
|
|
194
|
+
const end = content.trimEnd().length;
|
|
195
|
+
return content.endsWith(`${'!'.repeat(limit)}\n`)
|
|
196
|
+
? []
|
|
197
|
+
: [{ message: 'needs a bang', edit: { start: end, end, text: '!' } }];
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const siteRule = (): SiteRule => ({
|
|
202
|
+
id: 'site',
|
|
203
|
+
group: 'config',
|
|
204
|
+
severity: 'warning',
|
|
205
|
+
scope: 'site',
|
|
206
|
+
description: 'test rule',
|
|
207
|
+
check: vi.fn(() => []),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('rechecks only changed files between passes and runs site rules once more at the end', async () => {
|
|
211
|
+
const site = siteRule();
|
|
212
|
+
const reads = vi.spyOn(fsp, 'readFile');
|
|
213
|
+
|
|
214
|
+
const result = await checkSite(dir, [bang(3), site], { localSchema: false, fix: true });
|
|
215
|
+
|
|
216
|
+
const otherReads = reads.mock.calls.filter(([file]) => String(file).endsWith('other.mdx'));
|
|
217
|
+
reads.mockRestore();
|
|
218
|
+
expect(result.fixed.map(({ file }) => file)).toEqual(['index.mdx', 'index.mdx', 'index.mdx']);
|
|
219
|
+
expect(fs.readFileSync(path.join(dir, 'index.mdx'), 'utf-8')).toContain('Text!!!\n');
|
|
220
|
+
expect(result.diagnostics).toEqual([]);
|
|
221
|
+
expect(site.check).toHaveBeenCalledTimes(2);
|
|
222
|
+
expect(otherReads).toHaveLength(2);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('applies up to five passes of fixes', async () => {
|
|
226
|
+
const result = await checkSite(dir, [bang(10)], { localSchema: false, fix: true });
|
|
227
|
+
|
|
228
|
+
expect(result.fixed.filter(({ file }) => file === 'index.mdx')).toHaveLength(5);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -20,11 +20,15 @@ const hasErrors = (diagnostics: Diagnostic[]) =>
|
|
|
20
20
|
diagnostics.some(({ severity }) => severity === 'error');
|
|
21
21
|
|
|
22
22
|
/** Runs the file rules (all defaults, or exactly `rules`) the way mint validate --no-build does. */
|
|
23
|
-
const check = async (
|
|
23
|
+
const check = async (
|
|
24
|
+
dir: string,
|
|
25
|
+
{ rules, fix = false, files }: { rules?: string[]; fix?: boolean; files?: string[] }
|
|
26
|
+
) => {
|
|
24
27
|
const selected = selectRules({ disable: ['all'], enable: rules ?? ['content'] });
|
|
25
28
|
const result = await checkSite(dir, selected, {
|
|
26
29
|
selected: files && new Set(files),
|
|
27
30
|
localSchema: false,
|
|
31
|
+
fix,
|
|
28
32
|
});
|
|
29
33
|
const issues = result.diagnostics.map((diagnostic) => ({
|
|
30
34
|
...diagnostic,
|
|
@@ -32,6 +36,8 @@ const check = async (dir: string, { rules, files }: { rules?: string[]; files?:
|
|
|
32
36
|
}));
|
|
33
37
|
return {
|
|
34
38
|
issues,
|
|
39
|
+
fixed: result.fixed,
|
|
40
|
+
filesFixed: [...new Set(result.fixed.map(({ file }) => file))].sort(),
|
|
35
41
|
filesChecked: result.site.files.filter(({ file }) => !files || files.includes(file)).length,
|
|
36
42
|
};
|
|
37
43
|
};
|
|
@@ -59,26 +65,23 @@ describe('validate rules', () => {
|
|
|
59
65
|
|
|
60
66
|
const read = (file: string) => fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
61
67
|
|
|
62
|
-
|
|
63
|
-
const previews = (issues: Diagnostic[]) =>
|
|
64
|
-
issues.map(
|
|
65
|
-
({ file, fix }) => fix && { before: read(file).slice(fix.start, fix.end), after: fix.text }
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
it('flags lowercase void tags with edits, skipping code and components', async () => {
|
|
68
|
+
it('self-closes lowercase void tags, skipping code and components', async () => {
|
|
69
69
|
write(dir, {
|
|
70
70
|
'index.mdx':
|
|
71
71
|
'---\ntitle: Home\n---\n\nline<br>\n\n<hr>\n\n<img src="/a.png" alt="a">\n\n<Link>x</Link>\n\n`<br>`\n\n```html\n<br>\n```\n',
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
const
|
|
74
|
+
const plain = await check(dir, {});
|
|
75
|
+
expect(plain.issues.map(({ rule }) => rule)).toEqual(['void-tags', 'void-tags', 'void-tags']);
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
expect(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
const fixed = await check(dir, { fix: true });
|
|
78
|
+
expect(fixed.issues).toEqual([]);
|
|
79
|
+
const content = read('index.mdx');
|
|
80
|
+
expect(content).toContain('line<br />');
|
|
81
|
+
expect(content).toContain('<hr />');
|
|
82
|
+
expect(content).toContain('<img src="/a.png" alt="a" />');
|
|
83
|
+
expect(content).toContain('`<br>`');
|
|
84
|
+
expect(content).toContain('```html\n<br>\n```');
|
|
82
85
|
});
|
|
83
86
|
|
|
84
87
|
it('skips void tags inside block-quoted code fences', async () => {
|
|
@@ -87,16 +90,13 @@ describe('validate rules', () => {
|
|
|
87
90
|
'other.mdx': '---\ntitle: Other\n---\n\n> ```\n> <br>\n\nafter<br>\n',
|
|
88
91
|
});
|
|
89
92
|
|
|
90
|
-
const result = await check(dir, {});
|
|
93
|
+
const result = await check(dir, { fix: true });
|
|
91
94
|
|
|
92
|
-
expect(result.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
expect(
|
|
97
|
-
{ before: '<br>', after: '<br />' },
|
|
98
|
-
{ before: '<br>', after: '<br />' },
|
|
99
|
-
]);
|
|
95
|
+
expect(result.fixed.map(({ before }) => before).sort()).toEqual(['<br>', '<br>']);
|
|
96
|
+
expect(read('index.mdx')).toContain('> <br>\n');
|
|
97
|
+
expect(read('index.mdx')).toContain('line<br />');
|
|
98
|
+
expect(read('other.mdx')).toContain('> <br>\n');
|
|
99
|
+
expect(read('other.mdx')).toContain('after<br />');
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
it('keeps an info-string fence line from closing an open fence', async () => {
|
|
@@ -105,10 +105,12 @@ describe('validate rules', () => {
|
|
|
105
105
|
'---\ntitle: Home\n---\n\n```bash\n$ one\n\nStep 2\n\n```bash\n$ two <br>\n```\n\nafter<br>\n',
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
const result = await check(dir, {});
|
|
108
|
+
const result = await check(dir, { fix: true });
|
|
109
109
|
|
|
110
|
-
expect(result.
|
|
111
|
-
|
|
110
|
+
expect(result.fixed.map(({ before }) => before)).toEqual(['<br>']);
|
|
111
|
+
const content = read('index.mdx');
|
|
112
|
+
expect(content).toContain('$ two <br>\n');
|
|
113
|
+
expect(content).toContain('after<br />');
|
|
112
114
|
});
|
|
113
115
|
|
|
114
116
|
it('does not close a block-quoted fence with an info-string fence line', async () => {
|
|
@@ -117,10 +119,12 @@ describe('validate rules', () => {
|
|
|
117
119
|
'---\ntitle: Home\n---\n\n> ```\n> <br>\n> ```bash\n> <hr>\n> ```\n\nafter<br>\n',
|
|
118
120
|
});
|
|
119
121
|
|
|
120
|
-
const result = await check(dir, {});
|
|
122
|
+
const result = await check(dir, { fix: true });
|
|
121
123
|
|
|
122
|
-
expect(result.
|
|
123
|
-
|
|
124
|
+
expect(result.fixed.map(({ before }) => before)).toEqual(['<br>']);
|
|
125
|
+
const content = read('index.mdx');
|
|
126
|
+
expect(content).toContain('> <hr>\n');
|
|
127
|
+
expect(content).toContain('after<br />');
|
|
124
128
|
});
|
|
125
129
|
|
|
126
130
|
it('skips custom elements, frontmatter, ESM and attribute strings for void tags', async () => {
|
|
@@ -130,11 +134,15 @@ describe('validate rules', () => {
|
|
|
130
134
|
});
|
|
131
135
|
|
|
132
136
|
const { issues } = await check(dir, { rules: ['void-tags'] });
|
|
133
|
-
|
|
134
137
|
expect(issues.map(({ line }) => line)).toEqual([12]);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
|
|
139
|
+
await check(dir, { rules: ['void-tags'], fix: true });
|
|
140
|
+
const content = read('index.mdx');
|
|
141
|
+
expect(content).toContain('<img src="/a.png" alt="<hr>" />');
|
|
142
|
+
expect(content).toContain('title: "a<br>b"');
|
|
143
|
+
expect(content).toContain('export const html = "<br>";');
|
|
144
|
+
expect(content).toContain('<Card title="one<br>two">');
|
|
145
|
+
expect(content).toContain('`` a`<br>`b ``');
|
|
138
146
|
});
|
|
139
147
|
|
|
140
148
|
it('leaves escaped void tags alone', async () => {
|
|
@@ -159,7 +167,9 @@ describe('validate rules', () => {
|
|
|
159
167
|
expect(issues.map(({ rule, line, column }) => [rule, line, column])).toEqual([
|
|
160
168
|
['void-tags', 1, 23],
|
|
161
169
|
]);
|
|
162
|
-
|
|
170
|
+
|
|
171
|
+
await check(dir, { fix: true });
|
|
172
|
+
expect(read('snippets/note.mdx')).toBe('Hello {{foo.bar}} line<br />\n');
|
|
163
173
|
});
|
|
164
174
|
|
|
165
175
|
it('leaves void tags that already have a closing tag', async () => {
|
|
@@ -167,6 +177,8 @@ describe('validate rules', () => {
|
|
|
167
177
|
write(dir, { 'index.mdx': page });
|
|
168
178
|
|
|
169
179
|
expect((await check(dir, { rules: ['void-tags'] })).issues).toEqual([]);
|
|
180
|
+
await check(dir, { rules: ['void-tags'], fix: true });
|
|
181
|
+
expect(read('index.mdx')).toBe(page);
|
|
170
182
|
});
|
|
171
183
|
|
|
172
184
|
it('accepts <MDX> fragments the build preprocesses', async () => {
|
|
@@ -222,15 +234,17 @@ describe('validate rules', () => {
|
|
|
222
234
|
});
|
|
223
235
|
|
|
224
236
|
const { issues } = await check(dir, { rules: ['void-tags'] });
|
|
225
|
-
|
|
226
237
|
expect(issues.map(({ rule, line }) => [rule, line])).toEqual([
|
|
227
238
|
['void-tags', 7],
|
|
228
239
|
['void-tags', 11],
|
|
229
240
|
]);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
241
|
+
|
|
242
|
+
await check(dir, { rules: ['void-tags'], fix: true });
|
|
243
|
+
const content = read('index.mdx');
|
|
244
|
+
expect(content).toContain("s.replace(/<br>/g, '')");
|
|
245
|
+
expect(content).toContain("replace(/\\/<hr>/, '')");
|
|
246
|
+
expect(content).toContain('and<br />');
|
|
247
|
+
expect(content).toContain('line<br />');
|
|
234
248
|
});
|
|
235
249
|
|
|
236
250
|
it('masks line comments inside expressions', async () => {
|
|
@@ -262,11 +276,11 @@ describe('validate rules', () => {
|
|
|
262
276
|
);
|
|
263
277
|
write(dir, Object.fromEntries(names.map((name) => [name, '---\ntitle: T\n---\n\nline<br>\n'])));
|
|
264
278
|
|
|
265
|
-
const
|
|
279
|
+
const plain = await check(dir, { rules: ['void-tags'] });
|
|
280
|
+
expect(plain.issues.map(({ file }) => file)).toEqual(names);
|
|
266
281
|
|
|
267
|
-
const
|
|
268
|
-
expect(
|
|
269
|
-
expect(flagged).toHaveLength(40);
|
|
282
|
+
const fixed = await check(dir, { rules: ['void-tags'], fix: true });
|
|
283
|
+
expect(fixed.fixed.map(({ file }) => file)).toEqual(names);
|
|
270
284
|
});
|
|
271
285
|
|
|
272
286
|
it('rejects unknown rule ids', async () => {
|
|
@@ -368,8 +382,9 @@ describe('validate rules', () => {
|
|
|
368
382
|
|
|
369
383
|
const { issues } = await check(dir, { rules: ['void-tags'] });
|
|
370
384
|
|
|
371
|
-
expect(issues.map(({ line }) => line)).toEqual([
|
|
372
|
-
|
|
385
|
+
expect(issues.map(({ line, message }) => [line, message])).toEqual([
|
|
386
|
+
[7, '<br> must be self-closed'],
|
|
387
|
+
]);
|
|
373
388
|
});
|
|
374
389
|
|
|
375
390
|
it('does not pair backticks across table cells', async () => {
|
|
@@ -403,9 +418,9 @@ describe('validate rules', () => {
|
|
|
403
418
|
|
|
404
419
|
const { issues } = await check(dir, { rules: ['void-tags'] });
|
|
405
420
|
|
|
406
|
-
expect(
|
|
407
|
-
|
|
408
|
-
|
|
421
|
+
expect(issues.map(({ message }) => message)).toEqual([
|
|
422
|
+
'<br> must be self-closed',
|
|
423
|
+
'<hr> must be self-closed',
|
|
409
424
|
]);
|
|
410
425
|
});
|
|
411
426
|
});
|
package/bin/cli.js
CHANGED
|
@@ -8,11 +8,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
-
import { addLog, dev, exportSite,
|
|
11
|
+
import { addLog, dev, exportSite, ErrorLog, SuccessLog, Logs, WarningLog, } from '@mintlify/previewing';
|
|
12
12
|
import { thumbnailOverrideSchema } from '@mintlify/validation';
|
|
13
13
|
import { render, Text } from 'ink';
|
|
14
|
-
import fs from 'node:fs/promises';
|
|
15
|
-
import path from 'path';
|
|
16
14
|
import yargs from 'yargs';
|
|
17
15
|
import { hideBin } from 'yargs/helpers';
|
|
18
16
|
import { accessibilityCheck } from './accessibilityCheck.js';
|
|
@@ -29,7 +27,7 @@ import { formatGithubSetup, setupGithubDeployment } from './deploy-github-setup.
|
|
|
29
27
|
import { DEPLOY_HELP, deploymentInstructions } from './deploy-instructions.js';
|
|
30
28
|
import { formatDeployment, prepareAgentDeployment } from './deploy.js';
|
|
31
29
|
import { formatHandler } from './format.js';
|
|
32
|
-
import {
|
|
30
|
+
import { checkPort, checkNodeVersion, autoUpgradeIfNeeded, getVersions, isAI, suppressConsoleWarnings, terminate, } from './helpers.js';
|
|
33
31
|
import { init } from './init.js';
|
|
34
32
|
import { getAccessToken } from './keyring.js';
|
|
35
33
|
import { login } from './login.js';
|
|
@@ -37,13 +35,16 @@ import { logout } from './logout.js';
|
|
|
37
35
|
import { mdxLinter } from './mdxLinter.js';
|
|
38
36
|
import { createTelemetryMiddleware } from './middlewares/telemetryMiddleware.js';
|
|
39
37
|
import { ogCommand } from './og.js';
|
|
40
|
-
import { buildOverlayRegistryForCheck, checkOpenApiFile
|
|
38
|
+
import { buildOverlayRegistryForCheck, checkOpenApiFile } from './openApiCheck.js';
|
|
41
39
|
import { scoreHandler } from './score/index.js';
|
|
42
|
-
import {
|
|
40
|
+
import { signup } from './signup.js';
|
|
43
41
|
import { status, getCliSubdomains } from './status.js';
|
|
44
42
|
import { trackTelemetryPreferenceChange } from './telemetry/track.js';
|
|
45
43
|
import { update } from './update.js';
|
|
46
|
-
import {
|
|
44
|
+
import { splitList, validateHandler } from './validate/command.js';
|
|
45
|
+
import { RULES } from './validate/rules/index.js';
|
|
46
|
+
import { PARSE_ERROR } from './validate/rules/parseError.js';
|
|
47
|
+
import { GROUPS } from './validate/types.js';
|
|
47
48
|
import { renderWelcome } from './welcome.js';
|
|
48
49
|
import { workflowsBuilder } from './workflows/index.js';
|
|
49
50
|
// The branded welcome screen replaces yargs' default help, but only for an
|
|
@@ -290,7 +291,7 @@ export const cli = ({ packageName = 'mint' }) => {
|
|
|
290
291
|
yield terminate(1);
|
|
291
292
|
}
|
|
292
293
|
}))
|
|
293
|
-
.command('validate', '
|
|
294
|
+
.command('validate', 'Check the docs: content, OpenAPI specs, then a strict build. Exits 0 with no errors, 1 with errors, 2 when validate itself could not run', (yargs) => yargs
|
|
294
295
|
.option('local-schema', {
|
|
295
296
|
type: 'boolean',
|
|
296
297
|
default: false,
|
|
@@ -305,16 +306,16 @@ export const cli = ({ packageName = 'mint' }) => {
|
|
|
305
306
|
.option('groups', {
|
|
306
307
|
type: 'array',
|
|
307
308
|
description: 'Mock user groups for validation',
|
|
308
|
-
})
|
|
309
|
-
.option('disable-openapi', {
|
|
310
|
-
type: 'boolean',
|
|
311
|
-
default: false,
|
|
312
|
-
description: 'Disable OpenAPI file generation',
|
|
313
309
|
})
|
|
314
310
|
.option('files', {
|
|
315
311
|
type: 'string',
|
|
316
312
|
array: true,
|
|
317
313
|
description: 'Limit page checks to these files or globs; site-wide checks and the build still cover the whole site',
|
|
314
|
+
})
|
|
315
|
+
.option('build', {
|
|
316
|
+
type: 'boolean',
|
|
317
|
+
default: true,
|
|
318
|
+
description: 'Run the strict build after the other checks pass; --no-build skips it, so no client download is needed. Remote OpenAPI specs are still fetched unless openapi rules are disabled',
|
|
318
319
|
})
|
|
319
320
|
.option('enable', {
|
|
320
321
|
type: 'string',
|
|
@@ -328,37 +329,21 @@ export const cli = ({ packageName = 'mint' }) => {
|
|
|
328
329
|
coerce: splitList,
|
|
329
330
|
description: 'Turn off rules or groups (comma-separated), or all; parse errors are still reported',
|
|
330
331
|
})
|
|
331
|
-
.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
catch (error) {
|
|
348
|
-
addLog(_jsx(ErrorLog, { message: error instanceof Error ? error.message : String(error) }));
|
|
349
|
-
yield terminate(1);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
const results = yield Promise.all(openApiFilenames.map((file) => checkOpenApiFile(file, argv['local-schema'], overlayRegistry)));
|
|
353
|
-
if (results.some((valid) => !valid)) {
|
|
354
|
-
yield terminate(1);
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
const { cli: cliVersion } = getVersions(packageName);
|
|
359
|
-
yield validateBuild(Object.assign(Object.assign({}, argv), { packageName,
|
|
360
|
-
cliVersion }), sendPublishPrompt);
|
|
361
|
-
}))
|
|
332
|
+
.option('fix', {
|
|
333
|
+
type: 'boolean',
|
|
334
|
+
default: false,
|
|
335
|
+
description: 'Apply the fixes and check again',
|
|
336
|
+
})
|
|
337
|
+
.usage('usage: mint validate [options]')
|
|
338
|
+
.epilogue([
|
|
339
|
+
`Groups: ${GROUPS.join(', ')}. Rules:`,
|
|
340
|
+
...RULES.map(({ id, description }) => ` ${id} — ${description}`),
|
|
341
|
+
` parse-error — ${PARSE_ERROR.description} (always runs)`,
|
|
342
|
+
].join('\n'))
|
|
343
|
+
.example('mint validate', 'run every check and the build')
|
|
344
|
+
.example('mint validate --files "guides/**/*.mdx" --fix', 'check some pages and apply fixes')
|
|
345
|
+
.example('mint validate --disable all --enable content', 'run the content rules only; missing-file and import-error still run the build')
|
|
346
|
+
.strict(), (argv) => validateHandler(argv, packageName))
|
|
362
347
|
.command('export', 'Export a static site for air-gapped deployment', (yargs) => yargs
|
|
363
348
|
.option('output', {
|
|
364
349
|
type: 'string',
|
package/bin/helpers.js
CHANGED
|
@@ -210,6 +210,7 @@ export const readLocalOpenApiFile = (filename) => __awaiter(void 0, void 0, void
|
|
|
210
210
|
const document = yaml.load(file);
|
|
211
211
|
return document;
|
|
212
212
|
});
|
|
213
|
+
export const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
213
214
|
export const terminate = (code) => __awaiter(void 0, void 0, void 0, function* () {
|
|
214
215
|
// Wait for the logs to be fully rendered before exiting
|
|
215
216
|
yield new Promise((resolve) => setTimeout(resolve, 50));
|