@mintlify/cli 4.0.1732 → 4.0.1734
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 +141 -0
- package/__test__/validateEngine.test.ts +46 -0
- package/__test__/validateRules.test.ts +500 -0
- package/bin/cli.js +21 -0
- package/bin/resolveFiles.js +3 -1
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/bin/validate/command.js +58 -0
- package/bin/validate/engine.js +76 -0
- package/bin/validate/files.js +148 -0
- package/bin/validate/mdx.js +201 -0
- package/bin/validate/output.js +28 -0
- package/bin/validate/rules/content/voidTags.js +35 -0
- package/bin/validate/rules/index.js +3 -0
- package/bin/validate/rules/parseError.js +7 -0
- package/bin/validate/site.js +55 -0
- package/bin/validate/types.js +1 -0
- package/package.json +8 -8
- package/src/cli.tsx +24 -0
- package/src/resolveFiles.ts +3 -1
- package/src/validate/command.tsx +71 -0
- package/src/validate/engine.ts +93 -0
- package/src/validate/files.ts +212 -0
- package/src/validate/mdx.ts +252 -0
- package/src/validate/output.tsx +56 -0
- package/src/validate/rules/content/voidTags.ts +35 -0
- package/src/validate/rules/index.ts +5 -0
- package/src/validate/rules/parseError.ts +9 -0
- package/src/validate/site.ts +52 -0
- package/src/validate/types.ts +88 -0
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
11
|
+
import { addLog, clearLogs, ErrorLog, SpinnerLog } from '@mintlify/previewing';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { CMD_EXEC_PATH, terminate } from '../helpers.js';
|
|
14
|
+
import { NoMatchingFilesError, resolveExplicitFiles } from '../resolveFiles.js';
|
|
15
|
+
import { checkSite, selectRules, UsageError } from './engine.js';
|
|
16
|
+
import { logDiagnostics } from './output.js';
|
|
17
|
+
export const EXIT = { passed: 0, failed: 1, crashed: 2 };
|
|
18
|
+
export const splitList = (values) => values.flatMap((value) => value
|
|
19
|
+
.split(',')
|
|
20
|
+
.map((item) => item.trim())
|
|
21
|
+
.filter(Boolean));
|
|
22
|
+
const errorMessage = (error) => (error instanceof Error ? error.message : String(error));
|
|
23
|
+
const resolveSelection = (baseDir, values) => __awaiter(void 0, void 0, void 0, function* () {
|
|
24
|
+
const args = (values !== null && values !== void 0 ? values : []).map(String).filter(Boolean);
|
|
25
|
+
if (args.length === 0)
|
|
26
|
+
return undefined;
|
|
27
|
+
const files = yield resolveExplicitFiles(CMD_EXEC_PATH, args);
|
|
28
|
+
return new Set(files.map((file) => path.relative(baseDir, path.resolve(CMD_EXEC_PATH, file)).split(path.sep).join('/')));
|
|
29
|
+
});
|
|
30
|
+
export const runLintStep = (argv_1, ...args_1) => __awaiter(void 0, [argv_1, ...args_1], void 0, function* (argv, baseDir = CMD_EXEC_PATH) {
|
|
31
|
+
var _a;
|
|
32
|
+
try {
|
|
33
|
+
const rules = selectRules({
|
|
34
|
+
enable: argv.enable,
|
|
35
|
+
disable: (_a = argv.disable) !== null && _a !== void 0 ? _a : [],
|
|
36
|
+
});
|
|
37
|
+
const selected = yield resolveSelection(baseDir, argv.files);
|
|
38
|
+
addLog(_jsx(SpinnerLog, { message: "validating..." }));
|
|
39
|
+
const { diagnostics } = yield checkSite(baseDir, rules, {
|
|
40
|
+
selected,
|
|
41
|
+
localSchema: argv['local-schema'],
|
|
42
|
+
});
|
|
43
|
+
clearLogs();
|
|
44
|
+
const failed = logDiagnostics(diagnostics);
|
|
45
|
+
if (failed) {
|
|
46
|
+
yield terminate(EXIT.failed);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const usage = error instanceof UsageError || error instanceof NoMatchingFilesError;
|
|
53
|
+
clearLogs();
|
|
54
|
+
addLog(_jsx(ErrorLog, { message: errorMessage(error) }));
|
|
55
|
+
yield terminate(usage ? EXIT.failed : EXIT.crashed);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
import { checkFiles } from './files.js';
|
|
11
|
+
import { RULES } from './rules/index.js';
|
|
12
|
+
import { PARSE_ERROR } from './rules/parseError.js';
|
|
13
|
+
import { loadSite } from './site.js';
|
|
14
|
+
import { GROUPS, } from './types.js';
|
|
15
|
+
export class UsageError extends Error {
|
|
16
|
+
}
|
|
17
|
+
const ruleList = () => RULES.map(({ id }) => id).join(', ');
|
|
18
|
+
const matchToken = (token) => {
|
|
19
|
+
if (token === 'all')
|
|
20
|
+
return RULES;
|
|
21
|
+
const rule = RULES.find(({ id }) => id === token);
|
|
22
|
+
if (rule)
|
|
23
|
+
return [rule];
|
|
24
|
+
if (GROUPS.includes(token)) {
|
|
25
|
+
return RULES.filter(({ group }) => group === token);
|
|
26
|
+
}
|
|
27
|
+
throw new UsageError(`unknown rule or group "${token}". Groups: all, ${GROUPS.join(', ')}. Rules: ${ruleList()}`);
|
|
28
|
+
};
|
|
29
|
+
/** Every rule, minus --disable, plus --enable. parse-error is the engine's own, not selectable. */
|
|
30
|
+
export const selectRules = ({ enable = [], disable = [], }) => {
|
|
31
|
+
if ([...disable, ...enable].includes(PARSE_ERROR.id)) {
|
|
32
|
+
throw new UsageError(`${PARSE_ERROR.id} always runs and can't be disabled`);
|
|
33
|
+
}
|
|
34
|
+
const selected = new Set(RULES);
|
|
35
|
+
for (const token of disable)
|
|
36
|
+
for (const rule of matchToken(token))
|
|
37
|
+
selected.delete(rule);
|
|
38
|
+
for (const token of enable)
|
|
39
|
+
for (const rule of matchToken(token))
|
|
40
|
+
selected.add(rule);
|
|
41
|
+
return RULES.filter((rule) => selected.has(rule));
|
|
42
|
+
};
|
|
43
|
+
const toDiagnostic = (source, finding) => {
|
|
44
|
+
var _a;
|
|
45
|
+
return (Object.assign(Object.assign({}, finding), { rule: source.id, group: source.group, severity: (_a = finding.severity) !== null && _a !== void 0 ? _a : source.severity }));
|
|
46
|
+
};
|
|
47
|
+
const RULE_ORDER = new Map(RULES.map(({ id }, index) => [id, index]));
|
|
48
|
+
const sortDiagnostics = (diagnostics) => diagnostics.sort((a, b) => {
|
|
49
|
+
var _a, _b, _c, _d, _e, _f;
|
|
50
|
+
return (a.file < b.file ? -1 : a.file > b.file ? 1 : 0) ||
|
|
51
|
+
((_a = a.line) !== null && _a !== void 0 ? _a : 0) - ((_b = b.line) !== null && _b !== void 0 ? _b : 0) ||
|
|
52
|
+
((_c = a.column) !== null && _c !== void 0 ? _c : 0) - ((_d = b.column) !== null && _d !== void 0 ? _d : 0) ||
|
|
53
|
+
((_e = RULE_ORDER.get(a.rule)) !== null && _e !== void 0 ? _e : 0) - ((_f = RULE_ORDER.get(b.rule)) !== null && _f !== void 0 ? _f : 0);
|
|
54
|
+
});
|
|
55
|
+
/** Runs every selected rule that doesn't need the build. */
|
|
56
|
+
export const runChecks = (site, rules) => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
|
+
const fileRules = rules.filter((rule) => rule.scope === 'file');
|
|
58
|
+
const diagnostics = yield checkFiles(site, fileRules);
|
|
59
|
+
for (const rule of rules) {
|
|
60
|
+
if (rule.scope !== 'site' || rule.needsBuild)
|
|
61
|
+
continue;
|
|
62
|
+
for (const finding of yield rule.check(site))
|
|
63
|
+
diagnostics.push(toDiagnostic(rule, finding));
|
|
64
|
+
}
|
|
65
|
+
return sortDiagnostics(diagnostics);
|
|
66
|
+
});
|
|
67
|
+
/** Loads the site and runs the selected rules. */
|
|
68
|
+
export const checkSite = (baseDir, rules, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
69
|
+
const site = yield loadSite(baseDir, options);
|
|
70
|
+
const { selected } = options;
|
|
71
|
+
if (selected && !site.files.some(({ file }) => selected.has(file))) {
|
|
72
|
+
throw new UsageError(`none of the given files are pages or snippets of this site: ${[...selected].join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
const diagnostics = yield runChecks(site, rules);
|
|
75
|
+
return { site, diagnostics };
|
|
76
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
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 __rest = (this && this.__rest) || function (s, e) {
|
|
11
|
+
var t = {};
|
|
12
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
13
|
+
t[p] = s[p];
|
|
14
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
15
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
16
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
17
|
+
t[p[i]] = s[p[i]];
|
|
18
|
+
}
|
|
19
|
+
return t;
|
|
20
|
+
};
|
|
21
|
+
import { getAST, getFileCategory, parseFrontmatter, replaceVariables } from '@mintlify/common';
|
|
22
|
+
import { categorizeMdxFiles, getErrorPosition, getMintIgnore, listContentFiles, } from '@mintlify/prebuild';
|
|
23
|
+
import fs from 'node:fs/promises';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { applyEdits, parseContent, relocate, selectEdits, unmapEdit } from './mdx.js';
|
|
26
|
+
import { PARSE_ERROR } from './rules/parseError.js';
|
|
27
|
+
const CONCURRENCY = 16;
|
|
28
|
+
const mapLimit = (items, task) => __awaiter(void 0, void 0, void 0, function* () {
|
|
29
|
+
const results = new Array(items.length);
|
|
30
|
+
let next = 0;
|
|
31
|
+
yield Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
32
|
+
while (next < items.length) {
|
|
33
|
+
const index = next++;
|
|
34
|
+
results[index] = yield task(items[index]);
|
|
35
|
+
}
|
|
36
|
+
})));
|
|
37
|
+
return results;
|
|
38
|
+
});
|
|
39
|
+
const CHECKED_CATEGORIES = new Set(['page', 'snippet', 'snippet-v2']);
|
|
40
|
+
export const readSiteFiles = (baseDir) => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
|
+
const { mdxCandidates: candidates } = yield listContentFiles(baseDir, yield getMintIgnore(baseDir));
|
|
42
|
+
const { snippets, snippetsV2 } = yield categorizeMdxFiles(baseDir, candidates);
|
|
43
|
+
const snippetFiles = new Set([...snippets, ...snippetsV2]);
|
|
44
|
+
return candidates
|
|
45
|
+
.filter((name) => /\.mdx?$/.test(name) && CHECKED_CATEGORIES.has(getFileCategory(name)))
|
|
46
|
+
.map((name) => ({
|
|
47
|
+
file: name.replace(/^\//, ''),
|
|
48
|
+
kind: (snippetFiles.has(name) ? 'snippet' : 'page'),
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
const readFrontmatter = (content) => {
|
|
52
|
+
try {
|
|
53
|
+
const { attributes } = parseFrontmatter(content);
|
|
54
|
+
return attributes && typeof attributes === 'object' ? Object.assign({}, attributes) : {};
|
|
55
|
+
}
|
|
56
|
+
catch (_a) {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const REASON_POSITION = /\s*\(\d+:\d+(?:-\d+:\d+)?\)$/;
|
|
61
|
+
export const toParseError = (error) => {
|
|
62
|
+
const reason = error === null || error === void 0 ? void 0 : error.reason;
|
|
63
|
+
const fallback = error instanceof Error ? error.message : 'could not parse this file';
|
|
64
|
+
const text = typeof reason === 'string' ? reason : fallback;
|
|
65
|
+
const position = error && typeof error === 'object' ? getErrorPosition(error) : null;
|
|
66
|
+
return Object.assign(Object.assign(Object.assign({}, ((position === null || position === void 0 ? void 0 : position.line) != null && { line: position.line })), ((position === null || position === void 0 ? void 0 : position.column) != null && { column: position.column })), { message: text.replace(REASON_POSITION, '') });
|
|
67
|
+
};
|
|
68
|
+
const parseErrorDiagnostic = (file, error) => (Object.assign(Object.assign({}, error), { rule: PARSE_ERROR.id, group: PARSE_ERROR.group, severity: PARSE_ERROR.severity, file, message: `${error.message} (other checks were skipped for this file)` }));
|
|
69
|
+
const tryParseContent = (content) => {
|
|
70
|
+
try {
|
|
71
|
+
return parseContent(content);
|
|
72
|
+
}
|
|
73
|
+
catch (_a) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const parseFile = (file, content, baseDir) => {
|
|
78
|
+
const context = { file, content, baseDir, frontmatter: readFrontmatter(content) };
|
|
79
|
+
const tree = tryParseContent(content);
|
|
80
|
+
if (tree)
|
|
81
|
+
return { context: Object.assign(Object.assign({}, context), { tree }), approximate: false };
|
|
82
|
+
try {
|
|
83
|
+
return { context: Object.assign(Object.assign({}, context), { tree: getAST(content, file) }), approximate: true };
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return { error: toParseError(error) };
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const EMPTY_TREE = { type: 'root', children: [] };
|
|
90
|
+
const SOURCE_STAGE = 1;
|
|
91
|
+
const AFTER_RAW_STAGE = 2;
|
|
92
|
+
const VARIABLE = /\{\{\s*([\w.\-]+)\s*\}\}/g;
|
|
93
|
+
const variableEdits = (content, variables = {}) => [...content.matchAll(VARIABLE)]
|
|
94
|
+
.filter((match) => match[1] !== undefined && variables[match[1]] !== undefined)
|
|
95
|
+
.map((match) => ({
|
|
96
|
+
start: match.index,
|
|
97
|
+
end: match.index + match[0].length,
|
|
98
|
+
text: replaceVariables(match[0], variables),
|
|
99
|
+
}));
|
|
100
|
+
const toDiagnostic = (rule, file, _a, stages, stage) => {
|
|
101
|
+
var { edit } = _a, finding = __rest(_a, ["edit"]);
|
|
102
|
+
const original = edit && unmapEdit(stages, stage, edit);
|
|
103
|
+
return Object.assign(Object.assign(Object.assign(Object.assign({}, finding), relocate(stages, stage, finding)), { rule: rule.id, group: rule.group, severity: rule.severity, file }), (original && { fix: Object.assign({ file }, original) }));
|
|
104
|
+
};
|
|
105
|
+
const checkFile = (allRules, { file, kind }, content, baseDir, variables) => {
|
|
106
|
+
const rules = kind === 'snippet' ? allRules.filter((rule) => !rule.pagesOnly) : allRules;
|
|
107
|
+
const raw = rules.filter((rule) => rule.raw);
|
|
108
|
+
const treeRules = rules.filter((rule) => !rule.raw);
|
|
109
|
+
const varEdits = kind === 'snippet' ? variableEdits(content, variables) : [];
|
|
110
|
+
const source = applyEdits(content, varEdits);
|
|
111
|
+
const rawContext = { file, content: source, baseDir, frontmatter: {}, tree: EMPTY_TREE };
|
|
112
|
+
const rawFound = raw.flatMap((rule) => rule.check(rawContext).map((finding) => ({ rule, finding })));
|
|
113
|
+
const rawEdits = selectEdits(rawFound.flatMap(({ finding }) => (finding.edit ? [finding.edit] : [])));
|
|
114
|
+
const afterRaw = applyEdits(source, rawEdits);
|
|
115
|
+
const stages = [
|
|
116
|
+
{ content, edits: varEdits },
|
|
117
|
+
{ content: source, edits: rawEdits },
|
|
118
|
+
{ content: afterRaw, edits: [] },
|
|
119
|
+
];
|
|
120
|
+
const diagnostics = rawFound.map(({ rule, finding }) => toDiagnostic(rule, file, finding, stages, SOURCE_STAGE));
|
|
121
|
+
const parsed = parseFile(file, afterRaw, baseDir);
|
|
122
|
+
if ('error' in parsed) {
|
|
123
|
+
diagnostics.unshift(parseErrorDiagnostic(file, Object.assign(Object.assign({}, parsed.error), relocate(stages, AFTER_RAW_STAGE, parsed.error))));
|
|
124
|
+
return diagnostics;
|
|
125
|
+
}
|
|
126
|
+
for (const rule of treeRules) {
|
|
127
|
+
for (const finding of rule.check(parsed.context)) {
|
|
128
|
+
const found = parsed.approximate ? Object.assign(Object.assign({}, finding), { edit: undefined }) : finding;
|
|
129
|
+
diagnostics.push(toDiagnostic(rule, file, found, stages, AFTER_RAW_STAGE));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return diagnostics;
|
|
133
|
+
};
|
|
134
|
+
export const checkFiles = (site, rules) => __awaiter(void 0, void 0, void 0, function* () {
|
|
135
|
+
const targets = site.files.filter(({ file }) => !site.selected || site.selected.has(file));
|
|
136
|
+
const results = yield mapLimit(targets, (entry) => __awaiter(void 0, void 0, void 0, function* () {
|
|
137
|
+
let content;
|
|
138
|
+
try {
|
|
139
|
+
content = yield fs.readFile(path.join(site.baseDir, entry.file), 'utf-8');
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const message = error instanceof Error ? error.message : 'could not read this file';
|
|
143
|
+
return [parseErrorDiagnostic(entry.file, { message })];
|
|
144
|
+
}
|
|
145
|
+
return checkFile(rules, entry, content, site.baseDir, site.variables);
|
|
146
|
+
}));
|
|
147
|
+
return results.flat();
|
|
148
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { coreRemark, FRONTMATTER_RE, getCodeRanges, HEADING_ID_RE, JSXParser, } from '@mintlify/common';
|
|
2
|
+
const HEADING_ID = new RegExp(HEADING_ID_RE.source, 'gm');
|
|
3
|
+
const ESM_START = /^(?:import|export)(?=[\s{*])/;
|
|
4
|
+
const JSX_TAG = /<[A-Za-z][\w.:-]*(?:"[^"]*"|'[^']*'|\{[^{}]*\}|[^<>"'{}])*>/g;
|
|
5
|
+
const ATTR_VALUE = /"[^"]*"|'[^']*'|\{[^{}]*\}/g;
|
|
6
|
+
export const hasCustomHeadingId = (line) => /\{\s*#[^}]+?\s*\}\s*$/.test(line);
|
|
7
|
+
export const parseContent = (content) => {
|
|
8
|
+
const padded = content.replace(HEADING_ID, (match, indent, hashes, text) => {
|
|
9
|
+
const end = match.indexOf(text, indent.length + hashes.length) + text.length;
|
|
10
|
+
return match.slice(0, end).padEnd(match.length, ' ');
|
|
11
|
+
});
|
|
12
|
+
return coreRemark().parse(padded);
|
|
13
|
+
};
|
|
14
|
+
const blankRanges = (content, ranges) => {
|
|
15
|
+
let result = '';
|
|
16
|
+
let cursor = 0;
|
|
17
|
+
for (const [from, to] of [...ranges].sort((a, b) => a[0] - b[0])) {
|
|
18
|
+
const start = Math.max(from, cursor);
|
|
19
|
+
if (to <= start)
|
|
20
|
+
continue;
|
|
21
|
+
result += content.slice(cursor, start) + content.slice(start, to).replace(/[^\n]/g, ' ');
|
|
22
|
+
cursor = to;
|
|
23
|
+
}
|
|
24
|
+
return result + content.slice(cursor);
|
|
25
|
+
};
|
|
26
|
+
const maskWith = (content, maskers) => maskers.reduce((text, ranges) => blankRanges(text, ranges(text)), content);
|
|
27
|
+
const isCompleteEsm = (text) => {
|
|
28
|
+
try {
|
|
29
|
+
JSXParser.parse(text, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const pos = error.pos;
|
|
34
|
+
return typeof pos !== 'number' || pos < text.trimEnd().length;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const frontmatterRanges = (content) => {
|
|
38
|
+
const match = FRONTMATTER_RE.exec(content);
|
|
39
|
+
return match ? [[match.index, match.index + match[0].length]] : [];
|
|
40
|
+
};
|
|
41
|
+
const esmRanges = (content, codeRanges) => {
|
|
42
|
+
let esm;
|
|
43
|
+
let previousBlank = true;
|
|
44
|
+
const ranges = [];
|
|
45
|
+
let offset = 0;
|
|
46
|
+
for (const line of content.split('\n')) {
|
|
47
|
+
const isBlank = line.trim() === '';
|
|
48
|
+
const wasBlank = previousBlank;
|
|
49
|
+
previousBlank = isBlank;
|
|
50
|
+
if (esm) {
|
|
51
|
+
if (isBlank && isCompleteEsm(esm.join('\n')))
|
|
52
|
+
esm = undefined;
|
|
53
|
+
else {
|
|
54
|
+
esm.push(line);
|
|
55
|
+
ranges.push([offset, offset + line.length]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (wasBlank &&
|
|
59
|
+
ESM_START.test(line) &&
|
|
60
|
+
!codeRanges.some(([start, end]) => start <= offset && offset < end)) {
|
|
61
|
+
esm = [line];
|
|
62
|
+
ranges.push([offset, offset + line.length]);
|
|
63
|
+
}
|
|
64
|
+
offset += line.length + 1;
|
|
65
|
+
}
|
|
66
|
+
return ranges;
|
|
67
|
+
};
|
|
68
|
+
export const maskFrontmatter = (content) => blankRanges(content, frontmatterRanges(content));
|
|
69
|
+
export const maskCode = (content, { keepEsm = false } = {}) => {
|
|
70
|
+
const codeRanges = getCodeRanges(content);
|
|
71
|
+
return blankRanges(content, [
|
|
72
|
+
...frontmatterRanges(content),
|
|
73
|
+
...codeRanges,
|
|
74
|
+
...(keepEsm ? [] : esmRanges(content, codeRanges)),
|
|
75
|
+
]);
|
|
76
|
+
};
|
|
77
|
+
const OPENS = new Set(['{', '${']);
|
|
78
|
+
const maskExpression = (content, start) => {
|
|
79
|
+
const ranges = [];
|
|
80
|
+
let depth = 0;
|
|
81
|
+
try {
|
|
82
|
+
const tokens = JSXParser.tokenizer(content.slice(start), {
|
|
83
|
+
ecmaVersion: 'latest',
|
|
84
|
+
onComment: (block, _text, from, to) => ranges.push([start + from + 2, start + to - (block ? 2 : 0)]),
|
|
85
|
+
});
|
|
86
|
+
for (const token of tokens) {
|
|
87
|
+
const label = token.type.label;
|
|
88
|
+
if (label === 'template')
|
|
89
|
+
ranges.push([start + token.start, start + token.end]);
|
|
90
|
+
else if (label === 'string')
|
|
91
|
+
ranges.push([start + token.start + 1, start + token.end - 1]);
|
|
92
|
+
else if (label === 'regexp') {
|
|
93
|
+
const raw = content.slice(start + token.start, start + token.end);
|
|
94
|
+
ranges.push([start + token.start + 1, start + token.start + raw.lastIndexOf('/')]);
|
|
95
|
+
}
|
|
96
|
+
else if (OPENS.has(label))
|
|
97
|
+
depth++;
|
|
98
|
+
else if (label === '}' && depth-- === 0)
|
|
99
|
+
return { ranges, end: start + token.end };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (!(error instanceof SyntaxError && 'pos' in error && typeof error.pos === 'number')) {
|
|
104
|
+
return { ranges, end: content.length };
|
|
105
|
+
}
|
|
106
|
+
const pos = start + error.pos;
|
|
107
|
+
if (!error.message.startsWith('Unterminated string'))
|
|
108
|
+
return { ranges, end: pos };
|
|
109
|
+
const lineEnd = content.indexOf('\n', pos);
|
|
110
|
+
ranges.push([pos + 1, lineEnd === -1 ? content.length : lineEnd]);
|
|
111
|
+
}
|
|
112
|
+
return { ranges, end: content.length };
|
|
113
|
+
};
|
|
114
|
+
const expressionRanges = (content) => {
|
|
115
|
+
const ranges = [];
|
|
116
|
+
for (let index = 0; index < content.length; index++) {
|
|
117
|
+
const char = content[index];
|
|
118
|
+
if (char === '\\')
|
|
119
|
+
index++;
|
|
120
|
+
else if (char === '{') {
|
|
121
|
+
const expression = maskExpression(content, index + 1);
|
|
122
|
+
ranges.push(...expression.ranges);
|
|
123
|
+
index = expression.end - 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return ranges;
|
|
127
|
+
};
|
|
128
|
+
const attributeRanges = (content) => [...content.matchAll(JSX_TAG)].flatMap(({ index, 0: tag }) => [...tag.matchAll(ATTR_VALUE)].map(({ index: at, 0: value }) => [index + at + 1, index + at + value.length - 1]));
|
|
129
|
+
const escapedTagRanges = (content) => [...content.matchAll(/\\[\s\S]/g)].flatMap(({ index, 0: escape }) => escape === '\\<' ? [[index + 1, index + 2]] : []);
|
|
130
|
+
export const maskNonMarkup = (content) => maskWith(maskCode(content), [attributeRanges, escapedTagRanges, expressionRanges]);
|
|
131
|
+
export const selectEdits = (edits) => {
|
|
132
|
+
const ordered = edits
|
|
133
|
+
.map((edit, index) => ({ edit, index }))
|
|
134
|
+
.sort((a, b) => b.edit.start - a.edit.start || b.edit.end - a.edit.end || b.index - a.index);
|
|
135
|
+
const selected = [];
|
|
136
|
+
let floor = Infinity;
|
|
137
|
+
for (const { edit } of ordered) {
|
|
138
|
+
if (edit.end > floor)
|
|
139
|
+
continue;
|
|
140
|
+
selected.push(edit);
|
|
141
|
+
floor = edit.start;
|
|
142
|
+
}
|
|
143
|
+
return selected;
|
|
144
|
+
};
|
|
145
|
+
export const applyEdits = (content, edits) => selectEdits(edits).reduce((result, edit) => result.slice(0, edit.start) + edit.text + result.slice(edit.end), content);
|
|
146
|
+
const toOffset = (content, line, column = 1) => {
|
|
147
|
+
let offset = 0;
|
|
148
|
+
for (let current = 1; current < line; current++) {
|
|
149
|
+
const next = content.indexOf('\n', offset);
|
|
150
|
+
if (next === -1)
|
|
151
|
+
break;
|
|
152
|
+
offset = next + 1;
|
|
153
|
+
}
|
|
154
|
+
return offset + column - 1;
|
|
155
|
+
};
|
|
156
|
+
export const toPosition = (content, offset) => {
|
|
157
|
+
var _a, _b;
|
|
158
|
+
const before = content.slice(0, offset).split('\n');
|
|
159
|
+
return { line: before.length, column: ((_b = (_a = before.pop()) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) + 1 };
|
|
160
|
+
};
|
|
161
|
+
const byStart = (a, b) => a.start - b.start;
|
|
162
|
+
function toOriginal(edits, offset, inside) {
|
|
163
|
+
let delta = 0;
|
|
164
|
+
for (const edit of edits) {
|
|
165
|
+
const start = edit.start + delta;
|
|
166
|
+
if (inside === 'snap' ? offset < start : offset <= start)
|
|
167
|
+
break;
|
|
168
|
+
if (offset < start + edit.text.length)
|
|
169
|
+
return inside === 'snap' ? edit.start : undefined;
|
|
170
|
+
delta += edit.text.length - (edit.end - edit.start);
|
|
171
|
+
}
|
|
172
|
+
return offset - delta;
|
|
173
|
+
}
|
|
174
|
+
/** Maps an edit made against a later stage back to the original content, or undefined when it touches an earlier fix. */
|
|
175
|
+
export const unmapEdit = (stages, index, edit) => {
|
|
176
|
+
let { start, end } = edit;
|
|
177
|
+
for (const { edits } of stages.slice(0, index).reverse()) {
|
|
178
|
+
const sorted = [...edits].sort(byStart);
|
|
179
|
+
const mappedStart = toOriginal(sorted, start, 'reject');
|
|
180
|
+
const mappedEnd = toOriginal(sorted, end, 'reject');
|
|
181
|
+
if (mappedStart === undefined || mappedEnd === undefined)
|
|
182
|
+
return undefined;
|
|
183
|
+
if (sorted.some((earlier) => earlier.start < mappedEnd && earlier.end > mappedStart)) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
start = mappedStart;
|
|
187
|
+
end = mappedEnd;
|
|
188
|
+
}
|
|
189
|
+
return { start, end, text: edit.text };
|
|
190
|
+
};
|
|
191
|
+
export const relocate = (stages, index, position) => {
|
|
192
|
+
const stage = stages[index];
|
|
193
|
+
const original = stages[0];
|
|
194
|
+
if (!stage || !original || position.line === undefined)
|
|
195
|
+
return {};
|
|
196
|
+
const offset = stages
|
|
197
|
+
.slice(0, index)
|
|
198
|
+
.reduceRight((current, { edits }) => toOriginal([...edits].sort(byStart), current, 'snap'), toOffset(stage.content, position.line, position.column));
|
|
199
|
+
const mapped = toPosition(original.content, offset);
|
|
200
|
+
return position.column === undefined ? { line: mapped.line } : mapped;
|
|
201
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { addLog, ErrorLog } from '@mintlify/previewing';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
const location = ({ line, column }) => line ? `${line}${column ? `:${column}` : ''}` : '';
|
|
5
|
+
const DiagnosticLine = ({ diagnostic }) => {
|
|
6
|
+
const at = location(diagnostic);
|
|
7
|
+
return (_jsxs(Text, { children: [' ', diagnostic.severity === 'error' ? (_jsx(Text, { color: "red", children: "\u2717" })) : (_jsx(Text, { color: "yellow", children: "!" })), ' ', at && _jsxs(Text, { dimColor: true, children: [at, " "] }), _jsx(Text, { dimColor: true, children: diagnostic.rule }), " ", diagnostic.message, diagnostic.fix && _jsx(Text, { dimColor: true, children: " (fixable)" })] }));
|
|
8
|
+
};
|
|
9
|
+
const plural = (count, word) => `${count} ${word}${count === 1 ? '' : 's'}`;
|
|
10
|
+
export const logDiagnostics = (all) => {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
const byFile = new Map();
|
|
13
|
+
for (const diagnostic of all) {
|
|
14
|
+
(_b = (_a = byFile.get(diagnostic.file)) === null || _a === void 0 ? void 0 : _a.push(diagnostic)) !== null && _b !== void 0 ? _b : byFile.set(diagnostic.file, [diagnostic]);
|
|
15
|
+
}
|
|
16
|
+
for (const [file, diagnostics] of byFile) {
|
|
17
|
+
addLog(_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: file }), diagnostics.map((diagnostic, index) => (_jsx(DiagnosticLine, { diagnostic: diagnostic }, index)))] }));
|
|
18
|
+
}
|
|
19
|
+
const errors = all.filter(({ severity }) => severity === 'error').length;
|
|
20
|
+
const warnings = all.length - errors;
|
|
21
|
+
if (errors > 0) {
|
|
22
|
+
addLog(_jsx(ErrorLog, { message: `${plural(errors, 'error')}${warnings > 0 ? `, ${plural(warnings, 'warning')}` : ''}` }));
|
|
23
|
+
}
|
|
24
|
+
else if (warnings > 0) {
|
|
25
|
+
addLog(_jsx(Text, { dimColor: true, children: plural(warnings, 'warning') }));
|
|
26
|
+
}
|
|
27
|
+
return errors > 0;
|
|
28
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { maskNonMarkup } from '../../mdx.js';
|
|
2
|
+
const VOID_TAG = /<(br|hr|img)(?=[\s/>])((?:"[^"]*"|'[^']*'|\{[^{}]*\}|[^<>"'{}])*?)(\/?)>/g;
|
|
3
|
+
export const voidTags = {
|
|
4
|
+
id: 'void-tags',
|
|
5
|
+
group: 'content',
|
|
6
|
+
severity: 'error',
|
|
7
|
+
scope: 'file',
|
|
8
|
+
description: "self-close void HTML tags (<br>, <hr>, <img>); MDX can't parse them unclosed",
|
|
9
|
+
raw: true,
|
|
10
|
+
check: ({ content }) => {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
const masked = maskNonMarkup(content);
|
|
13
|
+
const issues = [];
|
|
14
|
+
for (const match of masked.matchAll(VOID_TAG)) {
|
|
15
|
+
if (match[3])
|
|
16
|
+
continue;
|
|
17
|
+
const start = match.index;
|
|
18
|
+
const end = start + match[0].length;
|
|
19
|
+
if (new RegExp(`^\\s*</${match[1]}\\s*>`).test(masked.slice(end)))
|
|
20
|
+
continue;
|
|
21
|
+
const before = content.slice(0, start).split('\n');
|
|
22
|
+
issues.push({
|
|
23
|
+
line: before.length,
|
|
24
|
+
column: ((_b = (_a = before.pop()) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) + 1,
|
|
25
|
+
message: `<${match[1]}> must be self-closed`,
|
|
26
|
+
edit: {
|
|
27
|
+
start,
|
|
28
|
+
end,
|
|
29
|
+
text: `${content.slice(start, end - 1).trimEnd()} />`,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return issues;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { readSiteFiles } from './files.js';
|
|
13
|
+
import { toPosition } from './mdx.js';
|
|
14
|
+
const readVariables = (config) => {
|
|
15
|
+
try {
|
|
16
|
+
const { variables } = JSON.parse(config);
|
|
17
|
+
return variables && typeof variables === 'object'
|
|
18
|
+
? variables
|
|
19
|
+
: undefined;
|
|
20
|
+
}
|
|
21
|
+
catch (_a) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** Lists the site's pages and snippets; locate reads files from disk on demand. */
|
|
26
|
+
export const loadSite = (baseDir_1, _a) => __awaiter(void 0, [baseDir_1, _a], void 0, function* (baseDir, { selected, localSchema }) {
|
|
27
|
+
const files = yield readSiteFiles(baseDir);
|
|
28
|
+
const byPath = new Map();
|
|
29
|
+
const configFile = existsSync(path.join(baseDir, 'docs.json')) ? 'docs.json' : 'mint.json';
|
|
30
|
+
const read = (file) => {
|
|
31
|
+
var _a;
|
|
32
|
+
if (!byPath.has(file)) {
|
|
33
|
+
try {
|
|
34
|
+
byPath.set(file, readFileSync(path.join(baseDir, file), 'utf-8'));
|
|
35
|
+
}
|
|
36
|
+
catch (_b) {
|
|
37
|
+
byPath.set(file, '');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return (_a = byPath.get(file)) !== null && _a !== void 0 ? _a : '';
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
baseDir,
|
|
44
|
+
files,
|
|
45
|
+
selected,
|
|
46
|
+
configFile,
|
|
47
|
+
variables: readVariables(read(configFile)),
|
|
48
|
+
localSchema,
|
|
49
|
+
locate: (file, text) => {
|
|
50
|
+
const content = read(file);
|
|
51
|
+
const offset = text ? content.indexOf(text) : -1;
|
|
52
|
+
return offset === -1 ? {} : toPosition(content, offset);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const GROUPS = ['content', 'nav', 'config', 'openapi'];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mintlify/cli",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1734",
|
|
4
4
|
"description": "The Mintlify CLI",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@inquirer/prompts": "7.9.0",
|
|
48
|
-
"@mintlify/common": "1.0.
|
|
49
|
-
"@mintlify/link-rot": "3.0.
|
|
48
|
+
"@mintlify/common": "1.0.1306",
|
|
49
|
+
"@mintlify/link-rot": "3.0.1533",
|
|
50
50
|
"@mintlify/models": "0.0.379",
|
|
51
|
-
"@mintlify/og": "0.0.
|
|
52
|
-
"@mintlify/prebuild": "1.0.
|
|
53
|
-
"@mintlify/previewing": "4.0.
|
|
51
|
+
"@mintlify/og": "0.0.105",
|
|
52
|
+
"@mintlify/prebuild": "1.0.1472",
|
|
53
|
+
"@mintlify/previewing": "4.0.1551",
|
|
54
54
|
"@mintlify/validation": "0.1.944",
|
|
55
55
|
"adm-zip": "0.6.0",
|
|
56
56
|
"chalk": "5.2.0",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"keytar": "7.9.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
|
-
"@mintlify/editor": "0.0.
|
|
85
|
+
"@mintlify/editor": "0.0.578",
|
|
86
86
|
"@mintlify/ts-config": "2.0.2",
|
|
87
87
|
"@tsconfig/recommended": "1.0.2",
|
|
88
88
|
"@types/detect-port": "1.3.2",
|
|
@@ -101,5 +101,5 @@
|
|
|
101
101
|
"vitest": "2.1.9",
|
|
102
102
|
"vitest-mock-process": "1.0.4"
|
|
103
103
|
},
|
|
104
|
-
"gitHead": "
|
|
104
|
+
"gitHead": "ae7819bfab79f6c765d39aed34bf33363f4be72a"
|
|
105
105
|
}
|