@mintlify/cli 4.0.1743 → 4.0.1745

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ import {
2
+ collectPatternNames,
3
+ collectExportedNames,
4
+ getAST,
5
+ isBuiltInComponent,
6
+ JSX_EXTENSIONS,
7
+ visitReferences,
8
+ } from '@mintlify/common';
9
+ import { visit } from 'unist-util-visit';
10
+
11
+ import type { FileContext, FileRule, FileFinding } from '../../types.js';
12
+
13
+ const getDeclared = (context: FileContext): Set<string> => {
14
+ const declared = new Set<string>();
15
+ visit(context.tree, 'mdxjsEsm', (node) => {
16
+ for (const statement of node.data?.estree?.body ?? []) {
17
+ if (statement.type === 'ImportDeclaration') {
18
+ for (const specifier of statement.specifiers) {
19
+ collectPatternNames(specifier.local, declared);
20
+ }
21
+ } else if (statement.type === 'ExportNamedDeclaration') {
22
+ const declaration = statement.declaration;
23
+ if (declaration?.type === 'VariableDeclaration') {
24
+ for (const declarator of declaration.declarations) {
25
+ collectPatternNames(declarator.id, declared);
26
+ }
27
+ } else if (declaration) {
28
+ collectPatternNames(declaration.id, declared);
29
+ }
30
+ for (const specifier of statement.specifiers) {
31
+ collectPatternNames(specifier.local, declared);
32
+ collectPatternNames(specifier.exported, declared);
33
+ }
34
+ } else if (statement.type === 'ExportDefaultDeclaration') {
35
+ const declaration = statement.declaration;
36
+ if (declaration.type === 'FunctionDeclaration' || declaration.type === 'ClassDeclaration') {
37
+ collectPatternNames(declaration.id, declared);
38
+ }
39
+ }
40
+ }
41
+ });
42
+ return declared;
43
+ };
44
+
45
+ const getImportPosition = (context: FileContext): number => {
46
+ const frontmatter = context.tree.children.find((child) => ['yaml', 'toml'].includes(child.type));
47
+ return frontmatter?.position?.end.offset ?? 0;
48
+ };
49
+
50
+ const getSpecifier = (snippet: string, file: string, name: string): string | undefined => {
51
+ let exported: Set<string>;
52
+ try {
53
+ exported = collectExportedNames(getAST(snippet, file));
54
+ } catch {
55
+ return undefined;
56
+ }
57
+ if (exported.has(name)) return `{ ${name} }`;
58
+ return exported.has('default') ? name : undefined;
59
+ };
60
+
61
+ export const unknownComponent: FileRule = {
62
+ id: 'unknown-component',
63
+ group: 'content',
64
+ severity: 'warning',
65
+ scope: 'file',
66
+ description: 'JSX components that are not built in, imported or exported',
67
+ pagesOnly: true,
68
+ check: (context) => {
69
+ const declared = getDeclared(context);
70
+ const reported = new Set<string>();
71
+ const issues: FileFinding[] = [];
72
+ const insertAt = getImportPosition(context);
73
+ const prefix = insertAt === 0 ? '' : '\n\n';
74
+ const following = /^\n*/.exec(context.content.slice(insertAt))?.[0].length ?? 0;
75
+ const suffix = '\n'.repeat(Math.max(0, 2 - following));
76
+
77
+ const check = (
78
+ tag: string | null | undefined,
79
+ position: { line?: number; column?: number } | undefined
80
+ ) => {
81
+ const name = tag?.split('.')[0];
82
+ if (!tag || !name || !/^[A-Z]/.test(name)) return;
83
+ if (declared.has(name) || reported.has(tag) || isBuiltInComponent(tag)) return;
84
+ reported.add(tag);
85
+
86
+ const file = JSX_EXTENSIONS.map((extension) => `snippets/${name}${extension}`).find(
87
+ (candidate) => context.readFile?.(candidate) !== undefined
88
+ );
89
+ const source = file && context.readFile?.(file);
90
+ const specifier =
91
+ file && source !== undefined && tag === name ? getSpecifier(source, file, name) : undefined;
92
+ issues.push({
93
+ line: position?.line,
94
+ column: position?.column,
95
+ message: `<${tag}> is not built in, imported or exported on this page`,
96
+ edit: specifier
97
+ ? {
98
+ start: insertAt,
99
+ end: insertAt,
100
+ text: `${prefix}import ${specifier} from "/${file}";${suffix}`,
101
+ }
102
+ : undefined,
103
+ });
104
+ };
105
+
106
+ const checkJsxInExpression = (estree: unknown, fallback?: { line?: number; column?: number }) =>
107
+ visitReferences(estree, new Set(), {
108
+ reference: (identifier) => {
109
+ if (identifier.type !== 'JSXIdentifier') return;
110
+ const loc = identifier.loc as { start?: { line: number; column: number } } | undefined;
111
+ check(String(identifier.name), loc?.start ?? fallback);
112
+ },
113
+ });
114
+
115
+ visit(context.tree, (node) => {
116
+ if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') {
117
+ check(node.name, node.position?.start);
118
+ for (const attribute of node.attributes) {
119
+ const value = attribute.type === 'mdxJsxAttribute' ? attribute.value : attribute;
120
+ if (value && typeof value === 'object') {
121
+ checkJsxInExpression(value.data?.estree, node.position?.start);
122
+ }
123
+ }
124
+ } else if (node.type === 'mdxFlowExpression' || node.type === 'mdxTextExpression') {
125
+ checkJsxInExpression(node.data?.estree, node.position?.start);
126
+ }
127
+ });
128
+ return issues;
129
+ },
130
+ };
@@ -1,7 +1,19 @@
1
1
  import type { Rule } from '../types.js';
2
2
  import { BUILD_RULES } from './build.js';
3
+ import { duplicateH1 } from './content/duplicateH1.js';
4
+ import { foreignSyntax } from './content/foreignSyntax.js';
5
+ import { katexRule } from './content/katex.js';
6
+ import { unknownComponent } from './content/unknownComponent.js';
3
7
  import { voidTags } from './content/voidTags.js';
4
8
  import { openApiSpec } from './openapi.js';
5
9
 
6
10
  /** Every check mint validate runs, in report order. */
7
- export const RULES: Rule[] = [voidTags, openApiSpec, ...BUILD_RULES];
11
+ export const RULES: Rule[] = [
12
+ voidTags,
13
+ duplicateH1,
14
+ unknownComponent,
15
+ katexRule,
16
+ foreignSyntax,
17
+ openApiSpec,
18
+ ...BUILD_RULES,
19
+ ];
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { readSiteFiles } from './files.js';
@@ -22,18 +22,18 @@ export const loadSite = async (
22
22
  { selected, localSchema }: { selected?: ReadonlySet<string>; localSchema: boolean }
23
23
  ): Promise<Site> => {
24
24
  const files = await readSiteFiles(baseDir);
25
- const byPath = new Map<string, string>();
26
- const configFile = existsSync(path.join(baseDir, 'docs.json')) ? 'docs.json' : 'mint.json';
25
+ const byPath = new Map<string, string | undefined>();
26
+ const configFile = fs.existsSync(path.join(baseDir, 'docs.json')) ? 'docs.json' : 'mint.json';
27
27
 
28
28
  const read = (file: string) => {
29
29
  if (!byPath.has(file)) {
30
30
  try {
31
- byPath.set(file, readFileSync(path.join(baseDir, file), 'utf-8'));
31
+ byPath.set(file, fs.readFileSync(path.join(baseDir, file), 'utf-8'));
32
32
  } catch {
33
- byPath.set(file, '');
33
+ byPath.set(file, undefined);
34
34
  }
35
35
  }
36
- return byPath.get(file) ?? '';
36
+ return byPath.get(file);
37
37
  };
38
38
 
39
39
  return {
@@ -41,10 +41,11 @@ export const loadSite = async (
41
41
  files,
42
42
  selected,
43
43
  configFile,
44
- variables: readVariables(read(configFile)),
44
+ variables: readVariables(read(configFile) ?? ''),
45
45
  localSchema,
46
+ read,
46
47
  locate: (file, text) => {
47
- const content = read(file);
48
+ const content = read(file) ?? '';
48
49
  const offset = text ? content.indexOf(text) : -1;
49
50
  return offset === -1 ? {} : toPosition(content, offset);
50
51
  },
@@ -53,6 +53,8 @@ export type FileContext = {
53
53
  baseDir: string;
54
54
  frontmatter: Record<string, unknown>;
55
55
  tree: Root;
56
+ readFile?: (file: string) => string | undefined;
57
+ anchorLinks?: ReadonlySet<string>;
56
58
  };
57
59
 
58
60
  /** A finding in one file, with an optional edit against the content the rule saw. */
@@ -86,6 +88,7 @@ export type Site = {
86
88
  build?: BuildResult;
87
89
  /** The line and column of the first occurrence of text in a file. */
88
90
  locate: (file: string, text: string) => { line?: number; column?: number };
91
+ read: (file: string) => string | undefined;
89
92
  localSchema: boolean;
90
93
  };
91
94