@mintlify/cli 4.0.1742 → 4.0.1744

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.
@@ -97,20 +97,13 @@ const parseLevel = (file, content, baseDir) => {
97
97
  /** Applies every fix that doesn't overlap an earlier fix in the same file, writing each Markdown file only when its fixed content parses at least as well as before. */
98
98
  export const applyFixes = (site, diagnostics) => __awaiter(void 0, void 0, void 0, function* () {
99
99
  var _a;
100
- const contents = new Map();
101
- const read = (file) => __awaiter(void 0, void 0, void 0, function* () {
102
- var _a;
103
- if (!contents.has(file)) {
104
- contents.set(file, yield fs.readFile(path.join(site.baseDir, file), 'utf-8'));
105
- }
106
- return (_a = contents.get(file)) !== null && _a !== void 0 ? _a : '';
107
- });
100
+ const read = (file) => { var _a; return (_a = site.read(file)) !== null && _a !== void 0 ? _a : ''; };
108
101
  const chosen = new Map();
109
102
  const overlaps = ({ file, start, end }) => { var _a; return ((_a = chosen.get(file)) !== null && _a !== void 0 ? _a : []).some(({ fix }) => start < fix.end && fix.start < end); };
110
103
  for (const { rule, fix } of diagnostics) {
111
104
  if (!fix || overlaps(fix))
112
105
  continue;
113
- const content = yield read(fix.file);
106
+ const content = read(fix.file);
114
107
  const change = Object.assign(Object.assign({ rule, file: fix.file }, toPosition(content, fix.start)), { before: content.slice(fix.start, fix.end), after: fix.text });
115
108
  chosen.set(fix.file, [...((_a = chosen.get(fix.file)) !== null && _a !== void 0 ? _a : []), { fix, change }]);
116
109
  }
@@ -118,7 +111,7 @@ export const applyFixes = (site, diagnostics) => __awaiter(void 0, void 0, void
118
111
  const level = (file, content) => parseLevel(file, snippets.has(file) ? replaceVariables(content, site.variables) : content, site.baseDir);
119
112
  const applied = [];
120
113
  for (const [file, picks] of chosen) {
121
- const content = yield read(file);
114
+ const content = read(file);
122
115
  const updated = applyEdits(content, selectEdits(picks.map(({ fix }) => fix)));
123
116
  if (/\.mdx?$/.test(file) && level(file, updated) < level(file, content)) {
124
117
  continue;
@@ -136,7 +129,7 @@ const MAX_FIX_PASSES = 5;
136
129
  */
137
130
  export const checkSite = (baseDir, rules, options) => __awaiter(void 0, void 0, void 0, function* () {
138
131
  const fixed = [];
139
- const site = yield loadSite(baseDir, options);
132
+ let site = yield loadSite(baseDir, options);
140
133
  const { selected } = options;
141
134
  if (selected && !site.files.some(({ file }) => selected.has(file))) {
142
135
  throw new UsageError(`none of the given files are pages or snippets of this site: ${[...selected].join(', ')}`);
@@ -151,11 +144,11 @@ export const checkSite = (baseDir, rules, options) => __awaiter(void 0, void 0,
151
144
  if (changes.length === 0)
152
145
  break;
153
146
  fixed.push(...changes);
147
+ site = yield loadSite(baseDir, options);
154
148
  const changed = new Set(changes.map(({ file }) => file));
155
149
  pending = sortDiagnostics(yield checkFiles(Object.assign(Object.assign({}, site), { selected: changed }), fileRulesOf(rules)));
156
150
  }
157
151
  if (fixed.length === 0)
158
152
  return { site, diagnostics, fixed };
159
- const fixedSite = yield loadSite(baseDir, options);
160
- return { site: fixedSite, diagnostics: yield runChecks(fixedSite, rules), fixed };
153
+ return { site, diagnostics: yield runChecks(site, rules), fixed };
161
154
  });
@@ -20,22 +20,8 @@ var __rest = (this && this.__rest) || function (s, e) {
20
20
  };
21
21
  import { getAST, getFileCategory, parseFrontmatter, replaceVariables } from '@mintlify/common';
22
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';
23
+ import { applyEdits, collectAnchorLinks, parseContent, relocate, selectEdits, unmapEdit, } from './mdx.js';
26
24
  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
25
  const CHECKED_CATEGORIES = new Set(['page', 'snippet', 'snippet-v2']);
40
26
  export const readSiteFiles = (baseDir) => __awaiter(void 0, void 0, void 0, function* () {
41
27
  const { mdxCandidates: candidates } = yield listContentFiles(baseDir, yield getMintIgnore(baseDir));
@@ -74,8 +60,8 @@ const tryParseContent = (content) => {
74
60
  return undefined;
75
61
  }
76
62
  };
77
- export const parseFile = (file, content, baseDir) => {
78
- const context = { file, content, baseDir, frontmatter: readFrontmatter(content) };
63
+ export const parseFile = (file, content, baseDir, extras = {}) => {
64
+ const context = Object.assign({ file, content, baseDir, frontmatter: readFrontmatter(content) }, extras);
79
65
  const tree = tryParseContent(content);
80
66
  if (tree)
81
67
  return { context: Object.assign(Object.assign({}, context), { tree }), approximate: false };
@@ -102,7 +88,7 @@ const toDiagnostic = (rule, file, _a, stages, stage) => {
102
88
  const original = edit && unmapEdit(stages, stage, edit);
103
89
  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
90
  };
105
- const checkFile = (allRules, { file, kind }, content, baseDir, variables) => {
91
+ const checkFile = (allRules, { file, kind }, content, baseDir, extras, variables) => {
106
92
  const rules = kind === 'snippet' ? allRules.filter((rule) => !rule.pagesOnly) : allRules;
107
93
  const raw = rules.filter((rule) => rule.raw);
108
94
  const treeRules = rules.filter((rule) => !rule.raw);
@@ -118,7 +104,7 @@ const checkFile = (allRules, { file, kind }, content, baseDir, variables) => {
118
104
  { content: afterRaw, edits: [] },
119
105
  ];
120
106
  const diagnostics = rawFound.map(({ rule, finding }) => toDiagnostic(rule, file, finding, stages, SOURCE_STAGE));
121
- const parsed = parseFile(file, afterRaw, baseDir);
107
+ const parsed = parseFile(file, afterRaw, baseDir, extras);
122
108
  if ('error' in parsed) {
123
109
  diagnostics.unshift(parseErrorDiagnostic(file, Object.assign(Object.assign({}, parsed.error), relocate(stages, AFTER_RAW_STAGE, parsed.error))));
124
110
  return diagnostics;
@@ -131,18 +117,28 @@ const checkFile = (allRules, { file, kind }, content, baseDir, variables) => {
131
117
  }
132
118
  return diagnostics;
133
119
  };
120
+ const collectSiteAnchorLinks = (site) => __awaiter(void 0, void 0, void 0, function* () {
121
+ const pages = yield Promise.all(site.files.map(({ file }) => { var _a; return collectAnchorLinks(file, (_a = site.read(file)) !== null && _a !== void 0 ? _a : ''); }));
122
+ return new Set(pages.flat());
123
+ });
134
124
  export const checkFiles = (site, rules) => __awaiter(void 0, void 0, void 0, function* () {
125
+ const anchorRules = new Set(rules.filter((rule) => rule.needsAnchorLinks).map(({ id }) => id));
135
126
  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');
127
+ const checkTarget = (entry, extras) => {
128
+ const content = site.read(entry.file);
129
+ if (content === undefined) {
130
+ return [parseErrorDiagnostic(entry.file, { message: 'could not read this file' })];
140
131
  }
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();
132
+ return checkFile(rules, entry, content, site.baseDir, extras, site.variables);
133
+ };
134
+ const needsLinks = (diagnostics) => diagnostics.some(({ rule }) => anchorRules.has(rule));
135
+ const checked = targets.map((entry) => checkTarget(entry, { readFile: site.read }));
136
+ if (!checked.some(needsLinks))
137
+ return checked.flat();
138
+ const extras = { readFile: site.read, anchorLinks: yield collectSiteAnchorLinks(site) };
139
+ return targets.flatMap((entry, index) => {
140
+ var _a;
141
+ const diagnostics = (_a = checked[index]) !== null && _a !== void 0 ? _a : [];
142
+ return needsLinks(diagnostics) ? checkTarget(entry, extras) : diagnostics;
143
+ });
148
144
  });
@@ -1,4 +1,14 @@
1
- import { coreRemark, FRONTMATTER_RE, getCodeRanges, HEADING_ID_RE, JSXParser, } from '@mintlify/common';
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 { coreRemark, FRONTMATTER_RE, getCodeRanges, HEADING_ID_RE, JSXParser, pagePathToSlug, replaceSlashIndex, safeCleanHeadingId, toPosixPath, } from '@mintlify/common';
11
+ import { decorateGraphNodeFromPageContent, Node, PathType } from '@mintlify/link-rot';
2
12
  const HEADING_ID = new RegExp(HEADING_ID_RE.source, 'gm');
3
13
  const ESM_START = /^(?:import|export)(?=[\s{*])/;
4
14
  const JSX_TAG = /<[A-Za-z][\w.:-]*(?:"[^"]*"|'[^']*'|\{[^{}]*\}|[^<>"'{}])*>/g;
@@ -199,3 +209,30 @@ export const relocate = (stages, index, position) => {
199
209
  const mapped = toPosition(original.content, offset);
200
210
  return position.column === undefined ? { line: mapped.line } : mapped;
201
211
  };
212
+ const pagePath = (file) => replaceSlashIndex(pagePathToSlug(file)).replace(/^\.$/, '');
213
+ export const collectAnchorLinks = (file, content) => __awaiter(void 0, void 0, void 0, function* () {
214
+ const node = new Node(file);
215
+ try {
216
+ yield decorateGraphNodeFromPageContent(node, content);
217
+ }
218
+ catch (_a) {
219
+ return [];
220
+ }
221
+ return node.paths.flatMap((link) => link.pathType === PathType.INTERNAL && link.anchorLink
222
+ ? [
223
+ `${pagePath(toPosixPath(link.toString()))}#${safeCleanHeadingId(link.anchorLink.slice(1))}`,
224
+ ]
225
+ : []);
226
+ });
227
+ export const linkedAnchor = (links, file, slug) => {
228
+ const prefix = `${pagePath(file)}#`;
229
+ for (const link of links !== null && links !== void 0 ? links : []) {
230
+ if (!link.startsWith(prefix))
231
+ continue;
232
+ const anchor = link.slice(prefix.length);
233
+ const suffix = anchor.startsWith(`${slug}-`) && /^\d/.test(anchor.slice(slug.length + 1));
234
+ if (anchor === slug || suffix)
235
+ return anchor;
236
+ }
237
+ return undefined;
238
+ };
@@ -0,0 +1,57 @@
1
+ import { AVOIDED_PAGE_MODES, getTableOfContentsTitle, safeCleanHeadingId, slugify, } from '@mintlify/common';
2
+ import remarkSmartypants from 'remark-smartypants';
3
+ import { unified } from 'unified';
4
+ import { hasCustomHeadingId, linkedAnchor } from '../../mdx.js';
5
+ const normalize = (value) => value.trim().replace(/\s+/g, ' ');
6
+ const smartypants = unified().use(remarkSmartypants);
7
+ const firstHeading = (tree) => tree.children.find((node) => node.type === 'heading');
8
+ const renderedText = (heading) => {
9
+ const clone = structuredClone(heading);
10
+ const root = { type: 'root', children: [clone] };
11
+ smartypants.runSync(root);
12
+ return getTableOfContentsTitle(clone);
13
+ };
14
+ export const duplicateH1 = {
15
+ id: 'duplicate-h1',
16
+ group: 'content',
17
+ severity: 'warning',
18
+ scope: 'file',
19
+ description: 'remove a first heading that repeats the frontmatter title',
20
+ pagesOnly: true,
21
+ needsAnchorLinks: true,
22
+ check: ({ tree, frontmatter, content, file, anchorLinks }) => {
23
+ var _a, _b, _c, _d, _e, _f, _g, _h;
24
+ const { title, mode } = frontmatter;
25
+ if (typeof title !== 'string' || AVOIDED_PAGE_MODES.includes(String(mode)))
26
+ return [];
27
+ const first = firstHeading(tree);
28
+ if (!first || first.depth !== 1)
29
+ return [];
30
+ if (normalize(getTableOfContentsTitle(first)) !== normalize(title))
31
+ return [];
32
+ const start = (_b = (_a = first.position) === null || _a === void 0 ? void 0 : _a.start.offset) !== null && _b !== void 0 ? _b : 0;
33
+ const end = (_d = (_c = first.position) === null || _c === void 0 ? void 0 : _c.end.offset) !== null && _d !== void 0 ? _d : start;
34
+ const location = {
35
+ line: (_e = first.position) === null || _e === void 0 ? void 0 : _e.start.line,
36
+ column: (_f = first.position) === null || _f === void 0 ? void 0 : _f.start.column,
37
+ };
38
+ const lineEnd = content.indexOf('\n', start);
39
+ if (hasCustomHeadingId(content.slice(start, lineEnd === -1 ? undefined : lineEnd))) {
40
+ return [
41
+ Object.assign(Object.assign({}, location), { message: 'first heading duplicates the frontmatter title; it has a custom {#id}, so remove it by hand and update links to it' }),
42
+ ];
43
+ }
44
+ const anchor = [renderedText(first), getTableOfContentsTitle(first)]
45
+ .map((text) => linkedAnchor(anchorLinks, file, safeCleanHeadingId(slugify(text))))
46
+ .find((linked) => linked !== undefined);
47
+ if (anchor !== undefined) {
48
+ return [
49
+ Object.assign(Object.assign({}, location), { message: `first heading duplicates the frontmatter title; links point to #${anchor}, so remove it by hand and update them` }),
50
+ ];
51
+ }
52
+ const trailing = (_h = (_g = /^\n*/.exec(content.slice(end))) === null || _g === void 0 ? void 0 : _g[0].length) !== null && _h !== void 0 ? _h : 0;
53
+ return [
54
+ Object.assign(Object.assign({}, location), { message: 'first heading duplicates the frontmatter title', edit: { start, end: end + trailing, text: '' } }),
55
+ ];
56
+ },
57
+ };
@@ -0,0 +1,120 @@
1
+ import { maskCode, maskFrontmatter, maskNonMarkup, toPosition } from '../../mdx.js';
2
+ const CALLOUTS = {
3
+ note: '<Note>',
4
+ info: '<Info>',
5
+ tip: '<Tip>',
6
+ hint: '<Tip>',
7
+ success: '<Check>',
8
+ important: '<Note>',
9
+ warning: '<Warning>',
10
+ caution: '<Warning>',
11
+ danger: '<Warning>',
12
+ error: '<Warning>',
13
+ details: '<Accordion>',
14
+ };
15
+ const callout = (name) => { var _a, _b; return (_b = CALLOUTS[(_a = name === null || name === void 0 ? void 0 : name.toLowerCase()) !== null && _a !== void 0 ? _a : '']) !== null && _b !== void 0 ? _b : 'a callout (<Note>, <Info>, <Tip> or <Warning>)'; };
16
+ const LIQUID_TAGS = {
17
+ hint: 'a callout (<Note>, <Info>, <Tip> or <Warning>)',
18
+ tabs: '<Tabs>',
19
+ tab: '<Tab title="…">',
20
+ stepper: '<Steps>',
21
+ step: '<Step title="…">',
22
+ embed: 'a link or <Frame>',
23
+ 'content-ref': '<Card>',
24
+ include: 'a snippet import',
25
+ code: 'a fenced code block',
26
+ file: 'a link to the file',
27
+ swagger: 'an OpenAPI page',
28
+ openapi: 'an OpenAPI page',
29
+ };
30
+ const PATTERNS = [
31
+ {
32
+ regex: /\{%-?\s*(?!end)([\w-]+)[^%\n]*?-?%\}/g,
33
+ message: ([tag, name]) => {
34
+ var _a, _b;
35
+ const style = name === 'hint' ? (_a = /style=["']?(\w+)/.exec(tag)) === null || _a === void 0 ? void 0 : _a[1] : undefined;
36
+ const target = style
37
+ ? callout(style)
38
+ : ((_b = LIQUID_TAGS[name !== null && name !== void 0 ? name : '']) !== null && _b !== void 0 ? _b : 'Markdown or a Mintlify component');
39
+ return `${tag} is a GitBook or Liquid tag, which Mintlify does not support; use ${target}`;
40
+ },
41
+ },
42
+ {
43
+ regex: /\{\{[<%]\s*([\w/-]+)[^}\n]*[>%]\}\}/g,
44
+ message: ([tag]) => `${tag} is a Hugo shortcode, which Mintlify does not support; use Markdown or a Mintlify component`,
45
+ },
46
+ {
47
+ regex: /\{\{-?\s*(?:(?:site|page|layout|include|jekyll|paginator)\.[^}\n]*|[^\s|{}]+\s*\|\s*\w+[^}\n]*)-?\}\}/g,
48
+ message: ([tag]) => `${tag} is a Liquid variable, which Mintlify does not support; write the value, or use a snippet or a variable`,
49
+ },
50
+ {
51
+ regex: /^ {0,3}:{3,}\s*([A-Za-z][\w-]*)/gm,
52
+ message: ([, name]) => `:::${name} is a Docusaurus or VitePress admonition, which Mintlify does not support; use ${callout(name)}`,
53
+ },
54
+ {
55
+ regex: /^ {0,3}::(?!:)([A-Za-z][\w-]*)/gm,
56
+ message: ([, name]) => `::${name} is an MDC (Nuxt Content, Docus) block, which Mintlify does not support; use the matching Mintlify component`,
57
+ },
58
+ {
59
+ regex: /^ {0,3}(!!!|\?\?\?\+?)[ \t]+([A-Za-z][\w-]*)/gm,
60
+ message: ([, marker, name]) => `${marker} ${name} is an MkDocs admonition, which Mintlify does not support; use ${marker === '!!!' ? callout(name) : '<Accordion>'}`,
61
+ },
62
+ {
63
+ regex: /^ {0,3}===\+?[ \t]+["']/gm,
64
+ message: () => `=== "…" is an MkDocs content tab, which Mintlify does not support; use <Tabs> with <Tab title="…">`,
65
+ },
66
+ {
67
+ regex: /^[ \t]*;?[ \t]*--8<--/gm,
68
+ scope: 'everything',
69
+ message: () => '--8<-- is an MkDocs snippet include, which Mintlify does not support; use a snippet import or paste the content',
70
+ },
71
+ {
72
+ regex: /\{:\s*[.#\w][^}\n]*\}/g,
73
+ message: ([list]) => `${list} is a Kramdown attribute list, which Mintlify does not support; remove it, use a component, or use {#id} for a heading anchor`,
74
+ },
75
+ {
76
+ regex: /(?<![[{\w])\[\[([^[\]\n]+)\]\](?![(:[])/g,
77
+ message: ([link]) => `${link} is a wiki link, which Mintlify does not support; use a Markdown link such as [text](/path)`,
78
+ },
79
+ {
80
+ regex: /(?:\]\(|href=["'])((?:doc|ref|blog|changelog|page):[^)\s"']+)/g,
81
+ scope: 'links',
82
+ message: ([, link]) => {
83
+ var _a;
84
+ return `${link} is a ReadMe link, which Mintlify does not support; link to the page path instead, such as /${(_a = link === null || link === void 0 ? void 0 : link.split(':')[1]) !== null && _a !== void 0 ? _a : ''}`;
85
+ },
86
+ },
87
+ {
88
+ regex: /["'(]@site\/[^"')\s]*/g,
89
+ scope: 'prose-and-esm',
90
+ message: ([path]) => `${path.slice(1)} uses the Docusaurus @site alias, which Mintlify does not resolve; use a root-relative path such as /images/… or /snippets/…`,
91
+ },
92
+ ];
93
+ export const foreignSyntax = {
94
+ id: 'foreign-syntax',
95
+ group: 'content',
96
+ severity: 'warning',
97
+ scope: 'file',
98
+ description: "syntax from another docs framework that Mintlify doesn't render",
99
+ raw: true,
100
+ check: ({ content }) => {
101
+ const prose = maskCode(content);
102
+ const literal = maskNonMarkup(content);
103
+ const views = {
104
+ prose,
105
+ links: prose,
106
+ 'prose-and-esm': maskCode(content, { keepEsm: true }),
107
+ everything: maskFrontmatter(content),
108
+ };
109
+ const issues = [];
110
+ for (const { regex, scope = 'prose', message } of PATTERNS) {
111
+ for (const match of views[scope].matchAll(regex)) {
112
+ const start = match.index + (match[0].length - match[0].trimStart().length);
113
+ if (scope === 'prose' && literal[start] !== prose[start])
114
+ continue;
115
+ issues.push(Object.assign(Object.assign({}, toPosition(content, start)), { message: message(match) }));
116
+ }
117
+ }
118
+ return issues;
119
+ },
120
+ };
@@ -0,0 +1,34 @@
1
+ import { isCurrencyMath } from '@mintlify/common';
2
+ import katex from 'katex';
3
+ import { visit } from 'unist-util-visit';
4
+ export const katexRule = {
5
+ id: 'katex',
6
+ group: 'content',
7
+ severity: 'warning',
8
+ scope: 'file',
9
+ description: "math that KaTeX can't render",
10
+ check: ({ tree, content }) => {
11
+ const issues = [];
12
+ visit(tree, (node) => {
13
+ var _a, _b;
14
+ if (node.type !== 'math' && node.type !== 'inlineMath')
15
+ return;
16
+ if (node.type === 'inlineMath' && isCurrencyMath(node, content))
17
+ return;
18
+ try {
19
+ katex.renderToString(node.value, {
20
+ throwOnError: true,
21
+ displayMode: node.type === 'math',
22
+ });
23
+ }
24
+ catch (error) {
25
+ issues.push({
26
+ line: (_a = node.position) === null || _a === void 0 ? void 0 : _a.start.line,
27
+ column: (_b = node.position) === null || _b === void 0 ? void 0 : _b.start.column,
28
+ message: error instanceof Error ? error.message : 'invalid math',
29
+ });
30
+ }
31
+ });
32
+ return issues;
33
+ },
34
+ };
@@ -0,0 +1,121 @@
1
+ import { collectPatternNames, collectExportedNames, getAST, isBuiltInComponent, JSX_EXTENSIONS, visitReferences, } from '@mintlify/common';
2
+ import { visit } from 'unist-util-visit';
3
+ const getDeclared = (context) => {
4
+ const declared = new Set();
5
+ visit(context.tree, 'mdxjsEsm', (node) => {
6
+ var _a, _b, _c;
7
+ for (const statement of (_c = (_b = (_a = node.data) === null || _a === void 0 ? void 0 : _a.estree) === null || _b === void 0 ? void 0 : _b.body) !== null && _c !== void 0 ? _c : []) {
8
+ if (statement.type === 'ImportDeclaration') {
9
+ for (const specifier of statement.specifiers) {
10
+ collectPatternNames(specifier.local, declared);
11
+ }
12
+ }
13
+ else if (statement.type === 'ExportNamedDeclaration') {
14
+ const declaration = statement.declaration;
15
+ if ((declaration === null || declaration === void 0 ? void 0 : declaration.type) === 'VariableDeclaration') {
16
+ for (const declarator of declaration.declarations) {
17
+ collectPatternNames(declarator.id, declared);
18
+ }
19
+ }
20
+ else if (declaration) {
21
+ collectPatternNames(declaration.id, declared);
22
+ }
23
+ for (const specifier of statement.specifiers) {
24
+ collectPatternNames(specifier.local, declared);
25
+ collectPatternNames(specifier.exported, declared);
26
+ }
27
+ }
28
+ else if (statement.type === 'ExportDefaultDeclaration') {
29
+ const declaration = statement.declaration;
30
+ if (declaration.type === 'FunctionDeclaration' || declaration.type === 'ClassDeclaration') {
31
+ collectPatternNames(declaration.id, declared);
32
+ }
33
+ }
34
+ }
35
+ });
36
+ return declared;
37
+ };
38
+ const getImportPosition = (context) => {
39
+ var _a, _b;
40
+ const frontmatter = context.tree.children.find((child) => ['yaml', 'toml'].includes(child.type));
41
+ return (_b = (_a = frontmatter === null || frontmatter === void 0 ? void 0 : frontmatter.position) === null || _a === void 0 ? void 0 : _a.end.offset) !== null && _b !== void 0 ? _b : 0;
42
+ };
43
+ const getSpecifier = (snippet, file, name) => {
44
+ let exported;
45
+ try {
46
+ exported = collectExportedNames(getAST(snippet, file));
47
+ }
48
+ catch (_a) {
49
+ return undefined;
50
+ }
51
+ if (exported.has(name))
52
+ return `{ ${name} }`;
53
+ return exported.has('default') ? name : undefined;
54
+ };
55
+ export const unknownComponent = {
56
+ id: 'unknown-component',
57
+ group: 'content',
58
+ severity: 'warning',
59
+ scope: 'file',
60
+ description: 'JSX components that are not built in, imported or exported',
61
+ pagesOnly: true,
62
+ check: (context) => {
63
+ var _a, _b;
64
+ const declared = getDeclared(context);
65
+ const reported = new Set();
66
+ const issues = [];
67
+ const insertAt = getImportPosition(context);
68
+ const prefix = insertAt === 0 ? '' : '\n\n';
69
+ const following = (_b = (_a = /^\n*/.exec(context.content.slice(insertAt))) === null || _a === void 0 ? void 0 : _a[0].length) !== null && _b !== void 0 ? _b : 0;
70
+ const suffix = '\n'.repeat(Math.max(0, 2 - following));
71
+ const check = (tag, position) => {
72
+ var _a;
73
+ const name = tag === null || tag === void 0 ? void 0 : tag.split('.')[0];
74
+ if (!tag || !name || !/^[A-Z]/.test(name))
75
+ return;
76
+ if (declared.has(name) || reported.has(tag) || isBuiltInComponent(tag))
77
+ return;
78
+ reported.add(tag);
79
+ const file = JSX_EXTENSIONS.map((extension) => `snippets/${name}${extension}`).find((candidate) => { var _a; return ((_a = context.readFile) === null || _a === void 0 ? void 0 : _a.call(context, candidate)) !== undefined; });
80
+ const source = file && ((_a = context.readFile) === null || _a === void 0 ? void 0 : _a.call(context, file));
81
+ const specifier = file && source !== undefined && tag === name ? getSpecifier(source, file, name) : undefined;
82
+ issues.push({
83
+ line: position === null || position === void 0 ? void 0 : position.line,
84
+ column: position === null || position === void 0 ? void 0 : position.column,
85
+ message: `<${tag}> is not built in, imported or exported on this page`,
86
+ edit: specifier
87
+ ? {
88
+ start: insertAt,
89
+ end: insertAt,
90
+ text: `${prefix}import ${specifier} from "/${file}";${suffix}`,
91
+ }
92
+ : undefined,
93
+ });
94
+ };
95
+ const checkJsxInExpression = (estree, fallback) => visitReferences(estree, new Set(), {
96
+ reference: (identifier) => {
97
+ var _a;
98
+ if (identifier.type !== 'JSXIdentifier')
99
+ return;
100
+ const loc = identifier.loc;
101
+ check(String(identifier.name), (_a = loc === null || loc === void 0 ? void 0 : loc.start) !== null && _a !== void 0 ? _a : fallback);
102
+ },
103
+ });
104
+ visit(context.tree, (node) => {
105
+ var _a, _b, _c, _d, _e;
106
+ if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') {
107
+ check(node.name, (_a = node.position) === null || _a === void 0 ? void 0 : _a.start);
108
+ for (const attribute of node.attributes) {
109
+ const value = attribute.type === 'mdxJsxAttribute' ? attribute.value : attribute;
110
+ if (value && typeof value === 'object') {
111
+ checkJsxInExpression((_b = value.data) === null || _b === void 0 ? void 0 : _b.estree, (_c = node.position) === null || _c === void 0 ? void 0 : _c.start);
112
+ }
113
+ }
114
+ }
115
+ else if (node.type === 'mdxFlowExpression' || node.type === 'mdxTextExpression') {
116
+ checkJsxInExpression((_d = node.data) === null || _d === void 0 ? void 0 : _d.estree, (_e = node.position) === null || _e === void 0 ? void 0 : _e.start);
117
+ }
118
+ });
119
+ return issues;
120
+ },
121
+ };
@@ -1,5 +1,17 @@
1
1
  import { BUILD_RULES } from './build.js';
2
+ import { duplicateH1 } from './content/duplicateH1.js';
3
+ import { foreignSyntax } from './content/foreignSyntax.js';
4
+ import { katexRule } from './content/katex.js';
5
+ import { unknownComponent } from './content/unknownComponent.js';
2
6
  import { voidTags } from './content/voidTags.js';
3
7
  import { openApiSpec } from './openapi.js';
4
8
  /** Every check mint validate runs, in report order. */
5
- export const RULES = [voidTags, openApiSpec, ...BUILD_RULES];
9
+ export const RULES = [
10
+ voidTags,
11
+ duplicateH1,
12
+ unknownComponent,
13
+ katexRule,
14
+ foreignSyntax,
15
+ openApiSpec,
16
+ ...BUILD_RULES,
17
+ ];
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { existsSync, readFileSync } from 'node:fs';
10
+ import fs from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { readSiteFiles } from './files.js';
13
13
  import { toPosition } from './mdx.js';
@@ -24,30 +24,32 @@ const readVariables = (config) => {
24
24
  };
25
25
  /** Lists the site's pages and snippets; locate reads files from disk on demand. */
26
26
  export const loadSite = (baseDir_1, _a) => __awaiter(void 0, [baseDir_1, _a], void 0, function* (baseDir, { selected, localSchema }) {
27
+ var _b;
27
28
  const files = yield readSiteFiles(baseDir);
28
29
  const byPath = new Map();
29
- const configFile = existsSync(path.join(baseDir, 'docs.json')) ? 'docs.json' : 'mint.json';
30
+ const configFile = fs.existsSync(path.join(baseDir, 'docs.json')) ? 'docs.json' : 'mint.json';
30
31
  const read = (file) => {
31
- var _a;
32
32
  if (!byPath.has(file)) {
33
33
  try {
34
- byPath.set(file, readFileSync(path.join(baseDir, file), 'utf-8'));
34
+ byPath.set(file, fs.readFileSync(path.join(baseDir, file), 'utf-8'));
35
35
  }
36
- catch (_b) {
37
- byPath.set(file, '');
36
+ catch (_a) {
37
+ byPath.set(file, undefined);
38
38
  }
39
39
  }
40
- return (_a = byPath.get(file)) !== null && _a !== void 0 ? _a : '';
40
+ return byPath.get(file);
41
41
  };
42
42
  return {
43
43
  baseDir,
44
44
  files,
45
45
  selected,
46
46
  configFile,
47
- variables: readVariables(read(configFile)),
47
+ variables: readVariables((_b = read(configFile)) !== null && _b !== void 0 ? _b : ''),
48
48
  localSchema,
49
+ read,
49
50
  locate: (file, text) => {
50
- const content = read(file);
51
+ var _a;
52
+ const content = (_a = read(file)) !== null && _a !== void 0 ? _a : '';
51
53
  const offset = text ? content.indexOf(text) : -1;
52
54
  return offset === -1 ? {} : toPosition(content, offset);
53
55
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mintlify/cli",
3
- "version": "4.0.1742",
3
+ "version": "4.0.1744",
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.1307",
49
- "@mintlify/link-rot": "3.0.1538",
48
+ "@mintlify/common": "1.0.1309",
49
+ "@mintlify/link-rot": "3.0.1540",
50
50
  "@mintlify/models": "0.0.379",
51
- "@mintlify/og": "0.0.106",
52
- "@mintlify/prebuild": "1.0.1474",
53
- "@mintlify/previewing": "4.0.1556",
51
+ "@mintlify/og": "0.0.108",
52
+ "@mintlify/prebuild": "1.0.1476",
53
+ "@mintlify/previewing": "4.0.1558",
54
54
  "@mintlify/validation": "0.1.944",
55
55
  "adm-zip": "0.6.0",
56
56
  "chalk": "5.2.0",
@@ -60,6 +60,7 @@
60
60
  "ink": "6.3.0",
61
61
  "inquirer": "12.3.0",
62
62
  "js-yaml": "4.3.1",
63
+ "katex": "0.16.47",
63
64
  "mdast-util-mdx-jsx": "3.2.0",
64
65
  "open": "8.4.2",
65
66
  "openid-client": "6.8.2",
@@ -71,6 +72,7 @@
71
72
  "remark-gfm": "4.0.1",
72
73
  "remark-math": "6.0.0",
73
74
  "remark-mdx": "3.1.1",
75
+ "remark-smartypants": "3.0.2",
74
76
  "remark-stringify": "11.0.0",
75
77
  "unified": "11.0.5",
76
78
  "unist-util-visit": "5.0.0",
@@ -82,7 +84,7 @@
82
84
  "keytar": "7.9.0"
83
85
  },
84
86
  "devDependencies": {
85
- "@mintlify/editor": "0.0.580",
87
+ "@mintlify/editor": "0.0.582",
86
88
  "@mintlify/ts-config": "2.0.2",
87
89
  "@tsconfig/recommended": "1.0.2",
88
90
  "@types/detect-port": "1.3.2",
@@ -101,5 +103,5 @@
101
103
  "vitest": "2.1.9",
102
104
  "vitest-mock-process": "1.0.4"
103
105
  },
104
- "gitHead": "d186b90eab824aece48724f71d3b71f2cdebdbe2"
106
+ "gitHead": "c32639354792b7678f023b42e6e85cfd1d942d42"
105
107
  }