@mintlify/cli 4.0.1080 → 4.0.1081

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.
@@ -1,469 +0,0 @@
1
- import { potentiallyParseOpenApiString, parseFrontmatter } from '@mintlify/common';
2
- import { getConfigObj, getConfigPath } from '@mintlify/prebuild';
3
- import { addLog, ErrorLog, SuccessLog } from '@mintlify/previewing';
4
- import {
5
- divisions,
6
- DocsConfig,
7
- NavigationConfig,
8
- validateDocsConfig,
9
- XMint,
10
- XMcp,
11
- } from '@mintlify/validation';
12
- import fs from 'fs';
13
- import { outputFile } from 'fs-extra';
14
- import inquirer from 'inquirer';
15
- import yaml from 'js-yaml';
16
- import { OpenAPI, OpenAPIV3 } from 'openapi-types';
17
- import path from 'path';
18
-
19
- import { CMD_EXEC_PATH } from './constants.js';
20
-
21
- const specCache: Record<string, OpenAPI.Document> = {};
22
-
23
- const candidateSpecCache: Record<string, OpenAPI.Document> = {};
24
-
25
- const specLocks = new Map<string, Promise<void>>();
26
-
27
- async function withSpecLock(specPath: string, task: () => Promise<void>) {
28
- const key = path.resolve(specPath);
29
- const previous = specLocks.get(key) ?? Promise.resolve();
30
- let releaseNext: () => void;
31
- const next = new Promise<void>((resolve) => {
32
- releaseNext = resolve;
33
- });
34
- specLocks.set(key, next);
35
-
36
- await previous;
37
- try {
38
- await task();
39
- } finally {
40
- releaseNext!();
41
- }
42
- }
43
-
44
- let inquirerLockQueue: Promise<void> = Promise.resolve();
45
-
46
- async function withInquirerLock<T>(task: () => Promise<T>): Promise<T> {
47
- const previous = inquirerLockQueue;
48
- let releaseNext: () => void;
49
- const next = new Promise<void>((resolve) => {
50
- releaseNext = resolve;
51
- });
52
- inquirerLockQueue = next;
53
- await previous;
54
- try {
55
- return await task();
56
- } finally {
57
- releaseNext!();
58
- }
59
- }
60
-
61
- export async function migrateMdx() {
62
- const docsConfigPath = await getConfigPath(CMD_EXEC_PATH, 'docs');
63
-
64
- if (!docsConfigPath) {
65
- addLog(<ErrorLog message="docs.json not found in current directory" />);
66
- return;
67
- }
68
-
69
- const rawConfig = JSON.parse(await fs.promises.readFile(docsConfigPath, 'utf-8')) as DocsConfig;
70
-
71
- const docsConfigObj = await getConfigObj(CMD_EXEC_PATH, 'docs');
72
-
73
- const validationResults = await validateDocsConfig(docsConfigObj as DocsConfig);
74
- if (!validationResults.success) {
75
- addLog(<ErrorLog message="docs.json is invalid" />);
76
- return;
77
- }
78
-
79
- const validatedDocsConfig = validationResults.data as DocsConfig;
80
-
81
- await buildCandidateSpecCacheIfNeeded(CMD_EXEC_PATH);
82
-
83
- const updatedNavigation = await processNav(validatedDocsConfig.navigation);
84
-
85
- rawConfig.navigation = updatedNavigation;
86
- await outputFile(docsConfigPath, JSON.stringify(rawConfig, null, 2));
87
- addLog(<SuccessLog message="docs.json updated" />);
88
-
89
- for (const specPath in specCache) {
90
- const specObj = specCache[specPath] as OpenAPI.Document;
91
- const ext = path.extname(specPath).toLowerCase();
92
- const stringified = ext === '.json' ? JSON.stringify(specObj, null, 2) : yaml.dump(specObj);
93
- await outputFile(specPath, stringified);
94
- addLog(<SuccessLog message={`updated ${path.relative(CMD_EXEC_PATH, specPath)}`} />);
95
- }
96
-
97
- addLog(<SuccessLog message="migration complete" />);
98
- }
99
-
100
- async function processNav(nav: NavigationConfig): Promise<NavigationConfig> {
101
- let newNav: NavigationConfig = { ...nav };
102
-
103
- if ('pages' in newNav) {
104
- const newPages = await Promise.all(
105
- newNav.pages.map(async (page) => {
106
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
107
- if (typeof page === 'object' && page !== null && 'group' in page) {
108
- return processNav(page as NavigationConfig);
109
- }
110
- if (typeof page === 'string' && !/\s/.test(page)) {
111
- const mdxCandidatePath = path.join(CMD_EXEC_PATH, `${page}.mdx`);
112
- if (!fs.existsSync(mdxCandidatePath)) {
113
- return page;
114
- }
115
- const fmParsed = parseFrontmatter<Record<string, string>>(
116
- await fs.promises.readFile(mdxCandidatePath, 'utf-8')
117
- );
118
- const frontmatter = fmParsed.attributes;
119
- const content = fmParsed.body;
120
- if (!frontmatter.openapi) {
121
- return page;
122
- }
123
- const parsed = potentiallyParseOpenApiString(frontmatter.openapi);
124
- if (!parsed) {
125
- addLog(
126
- <ErrorLog
127
- message={`invalid openapi frontmatter in ${mdxCandidatePath}: ${frontmatter.openapi}`}
128
- />
129
- );
130
- return page;
131
- }
132
-
133
- const { filename, method, endpoint: endpointPath } = parsed;
134
- let specPath = filename;
135
-
136
- if (specPath && URL.canParse(specPath)) {
137
- return page;
138
- }
139
-
140
- if (!specPath) {
141
- const methodLower = method.toLowerCase();
142
- const matchingSpecs = await findMatchingOpenApiSpecs(
143
- {
144
- method: methodLower,
145
- endpointPath,
146
- },
147
- candidateSpecCache
148
- );
149
-
150
- if (matchingSpecs.length === 0) {
151
- addLog(
152
- <ErrorLog
153
- message={`no OpenAPI spec found for ${method.toUpperCase()} ${endpointPath} in repository`}
154
- />
155
- );
156
- return page;
157
- }
158
-
159
- if (matchingSpecs.length === 1) {
160
- specPath = path.relative(CMD_EXEC_PATH, matchingSpecs[0]!);
161
- } else {
162
- const answer = await withInquirerLock(() =>
163
- inquirer.prompt([
164
- {
165
- type: 'list',
166
- name: 'chosen',
167
- message: `multiple OpenAPI specs found for ${method.toUpperCase()} ${endpointPath}. which one should be used for ${path.relative(
168
- CMD_EXEC_PATH,
169
- mdxCandidatePath
170
- )}?`,
171
- choices: matchingSpecs.map((p) => ({
172
- name: path.relative(CMD_EXEC_PATH, p),
173
- value: path.relative(CMD_EXEC_PATH, p),
174
- })),
175
- },
176
- ])
177
- );
178
- specPath = answer.chosen as string;
179
- }
180
- }
181
-
182
- const href = `/${page}`;
183
- const pageName = specPath ? `${specPath} ${method} ${endpointPath}` : frontmatter.openapi;
184
- delete frontmatter.openapi;
185
- await withSpecLock(path.resolve(specPath), () =>
186
- migrateToXMint({
187
- specPath,
188
- method,
189
- endpointPath,
190
- frontmatter,
191
- content,
192
- href,
193
- })
194
- );
195
-
196
- try {
197
- await fs.promises.unlink(mdxCandidatePath);
198
- } catch (err) {
199
- addLog(
200
- <ErrorLog
201
- message={`failed to delete ${mdxCandidatePath}: ${(err as Error).message}`}
202
- />
203
- );
204
- }
205
-
206
- return pageName;
207
- }
208
- return page;
209
- })
210
- );
211
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
212
- newNav.pages = newPages as any;
213
- }
214
-
215
- for (const division of ['groups', ...divisions]) {
216
- if (division in newNav) {
217
- const items = newNav[division as keyof typeof newNav] as NavigationConfig[];
218
- newNav = {
219
- ...newNav,
220
- [division]: await Promise.all(items.map((item) => processNav(item))),
221
- };
222
- }
223
- }
224
-
225
- return newNav;
226
- }
227
-
228
- async function migrateToXMint(args: {
229
- specPath: string;
230
- method: string;
231
- endpointPath: string;
232
- frontmatter: Record<string, string>;
233
- content: string;
234
- href: string;
235
- }) {
236
- const { specPath, method, endpointPath, frontmatter, content, href } = args;
237
-
238
- if (!fs.existsSync(specPath)) {
239
- addLog(<ErrorLog message={`spec file not found: ${specPath}`} />);
240
- return;
241
- }
242
-
243
- let specObj: OpenAPI.Document;
244
- if (path.resolve(specPath) in specCache) {
245
- specObj = specCache[path.resolve(specPath)] as OpenAPI.Document;
246
- } else {
247
- const pathname = path.join(CMD_EXEC_PATH, specPath);
248
- const file = await fs.promises.readFile(pathname, 'utf-8');
249
- const ext = path.extname(specPath).toLowerCase();
250
- if (ext === '.json') {
251
- specObj = JSON.parse(file) as OpenAPI.Document;
252
- } else if (ext === '.yml' || ext === '.yaml') {
253
- specObj = yaml.load(file) as OpenAPI.Document;
254
- } else {
255
- addLog(<ErrorLog message={`unsupported spec file extension: ${specPath}`} />);
256
- return;
257
- }
258
- }
259
-
260
- const methodLower = method.toLowerCase();
261
-
262
- if (
263
- !editXMint(specObj, endpointPath, methodLower, {
264
- metadata: Object.keys(frontmatter).length > 0 ? frontmatter : undefined,
265
- content: content.length > 0 ? content : undefined,
266
- href,
267
- })
268
- ) {
269
- addLog(
270
- <ErrorLog
271
- message={`operation not found in spec: ${method.toUpperCase()} ${endpointPath} in ${specPath}`}
272
- />
273
- );
274
- return;
275
- }
276
-
277
- specCache[path.resolve(specPath)] = specObj;
278
- }
279
-
280
- function editXMint(
281
- document: OpenAPI.Document,
282
- path: string,
283
- method: string,
284
- newXMint: XMint
285
- ): boolean {
286
- if (method === 'webhook') {
287
- return editWebhookXMint(document, path, newXMint);
288
- }
289
-
290
- if (!document.paths || !document.paths[path]) {
291
- return false;
292
- }
293
-
294
- const pathItem = document.paths[path] as OpenAPIV3.PathItemObject;
295
- const normalizedMethod = method.toLowerCase() as keyof OpenAPIV3.PathItemObject;
296
-
297
- if (!pathItem[normalizedMethod]) {
298
- return false;
299
- }
300
-
301
- const operation = pathItem[normalizedMethod] as OpenAPI.Operation<{
302
- 'x-mint'?: XMint;
303
- 'x-mcp'?: XMcp;
304
- }>;
305
- operation['x-mint'] = newXMint;
306
-
307
- if ('x-mcp' in operation && !('mcp' in operation['x-mint'])) {
308
- operation['x-mint']['mcp'] = operation['x-mcp'];
309
- delete operation['x-mcp'];
310
- }
311
-
312
- return true;
313
- }
314
-
315
- function editWebhookXMint(document: OpenAPI.Document, path: string, newXMint: XMint): boolean {
316
- const webhookObject = (
317
- document as OpenAPIV3.Document & {
318
- webhooks?: Record<string, OpenAPIV3.PathItemObject>;
319
- }
320
- ).webhooks?.[path];
321
- if (!webhookObject || typeof webhookObject !== 'object') {
322
- return false;
323
- }
324
-
325
- if (!webhookObject['post']) {
326
- return false;
327
- }
328
-
329
- const operation = webhookObject['post'] as OpenAPI.Operation<{
330
- 'x-mint'?: XMint;
331
- 'x-mcp'?: XMcp;
332
- }>;
333
- operation['x-mint'] = newXMint;
334
-
335
- if ('x-mcp' in operation && !('mcp' in operation['x-mint'])) {
336
- operation['x-mint']['mcp'] = operation['x-mcp'];
337
- delete operation['x-mcp'];
338
- }
339
- return true;
340
- }
341
-
342
- async function findMatchingOpenApiSpecs(
343
- args: {
344
- method: string;
345
- endpointPath: string;
346
- },
347
- docsByPath?: Record<string, OpenAPI.Document>
348
- ): Promise<string[]> {
349
- const { method, endpointPath } = args;
350
- const docsEntries: Array<[string, OpenAPI.Document | undefined]> = docsByPath
351
- ? Object.entries(docsByPath)
352
- : (await collectOpenApiFiles(CMD_EXEC_PATH)).map((absPath) => [absPath, undefined]);
353
- const normalizedMethod = method.toLowerCase();
354
-
355
- const endpointVariants = new Set<string>([endpointPath]);
356
- if (!endpointPath.startsWith('/')) {
357
- endpointVariants.add(`/${endpointPath}`);
358
- } else {
359
- endpointVariants.add(endpointPath.replace(/^\/+/, ''));
360
- }
361
-
362
- const matches: string[] = [];
363
-
364
- for (const [absPath, maybeDoc] of docsEntries) {
365
- try {
366
- const doc = maybeDoc || (await loadOpenApiDocument(absPath));
367
- if (!doc) continue;
368
-
369
- if (normalizedMethod === 'webhook') {
370
- const webhooks = (
371
- doc as OpenAPIV3.Document & {
372
- webhooks?: Record<string, OpenAPIV3.PathItemObject>;
373
- }
374
- ).webhooks;
375
- if (!webhooks) continue;
376
- for (const key of Object.keys(webhooks)) {
377
- if (endpointVariants.has(key)) {
378
- const pathItem = webhooks[key];
379
- if (pathItem && typeof pathItem === 'object' && 'post' in pathItem && pathItem.post) {
380
- matches.push(absPath);
381
- break;
382
- }
383
- }
384
- }
385
- continue;
386
- }
387
-
388
- if (!doc.paths) continue;
389
- for (const variant of endpointVariants) {
390
- const pathItem = (doc.paths as Record<string, unknown>)[variant] as
391
- | OpenAPIV3.PathItemObject
392
- | undefined;
393
- if (!pathItem) continue;
394
- const hasOperation = !!(pathItem as Record<string, unknown>)[
395
- normalizedMethod as keyof OpenAPIV3.PathItemObject
396
- ];
397
- if (hasOperation) {
398
- matches.push(absPath);
399
- break;
400
- }
401
- }
402
- } catch {}
403
- }
404
-
405
- return matches.map((abs) => path.resolve(abs)).filter((v, i, a) => a.indexOf(v) === i);
406
- }
407
-
408
- async function collectOpenApiFiles(rootDir: string): Promise<string[]> {
409
- const results: string[] = [];
410
- const excludedDirs = new Set([
411
- 'node_modules',
412
- '.git',
413
- 'dist',
414
- 'build',
415
- '.next',
416
- '.vercel',
417
- 'out',
418
- 'coverage',
419
- 'tmp',
420
- 'temp',
421
- ]);
422
-
423
- async function walk(currentDir: string) {
424
- const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
425
- for (const entry of entries) {
426
- const abs = path.join(currentDir, entry.name);
427
- if (entry.isDirectory()) {
428
- if (excludedDirs.has(entry.name)) continue;
429
- await walk(abs);
430
- } else if (entry.isFile()) {
431
- if (/\.(ya?ml|json)$/i.test(entry.name)) {
432
- results.push(abs);
433
- }
434
- }
435
- }
436
- }
437
-
438
- await walk(rootDir);
439
- return results;
440
- }
441
-
442
- async function loadOpenApiDocument(absPath: string): Promise<OpenAPI.Document | undefined> {
443
- try {
444
- const file = await fs.promises.readFile(absPath, 'utf-8');
445
- const ext = path.extname(absPath).toLowerCase();
446
- let doc: OpenAPI.Document | undefined;
447
- if (ext === '.json') {
448
- doc = JSON.parse(file) as OpenAPI.Document;
449
- } else if (ext === '.yml' || ext === '.yaml') {
450
- doc = yaml.load(file) as OpenAPI.Document;
451
- }
452
- return doc;
453
- } catch {
454
- return undefined;
455
- }
456
- }
457
-
458
- async function buildCandidateSpecCacheIfNeeded(rootDir: string) {
459
- if (Object.keys(candidateSpecCache).length > 0) return;
460
- const files = await collectOpenApiFiles(rootDir);
461
- await Promise.all(
462
- files.map(async (abs) => {
463
- const doc = await loadOpenApiDocument(abs);
464
- if (doc) {
465
- candidateSpecCache[path.resolve(abs)] = doc;
466
- }
467
- })
468
- );
469
- }
package/src/scrape.tsx DELETED
@@ -1,122 +0,0 @@
1
- import { MintConfigType } from '@mintlify/models';
2
- import { addLog, ErrorLog, SuccessLog, SpinnerLog, InfoLog } from '@mintlify/previewing';
3
- import {
4
- scrapePageGroup,
5
- scrapeAllSiteTabs,
6
- htmlToHast,
7
- detectFramework,
8
- framework,
9
- fetchPageHtml,
10
- write,
11
- getErrorMessage,
12
- generateOpenApiPages,
13
- FINAL_SUCCESS_MESSAGE,
14
- } from '@mintlify/scraping';
15
- import { upgradeToDocsConfig } from '@mintlify/validation';
16
-
17
- import { terminate } from './helpers.js';
18
-
19
- export async function scrapeSite(url: string, filter?: string) {
20
- try {
21
- const urlObj = new URL(url);
22
- addLog(<SpinnerLog message={`Fetching ${urlObj.toString()}...`} />);
23
- const html = await fetchPageHtml(urlObj);
24
- addLog(<SuccessLog message={`Successfully retrieved HTML from ${urlObj.toString()}`} />);
25
-
26
- addLog(<SpinnerLog message="Scraping site..." />);
27
- const result = await scrapeAllSiteTabs(html, urlObj, { filter });
28
- if (result.success) {
29
- const mintConfig = result.data as MintConfigType;
30
- const docsConfig = upgradeToDocsConfig(mintConfig, {
31
- shouldUpgradeTheme: true,
32
- });
33
- docsConfig.theme = 'aspen';
34
- write('docs.json', JSON.stringify(docsConfig, undefined, 2));
35
- addLog(<SuccessLog message={FINAL_SUCCESS_MESSAGE} />);
36
- } else {
37
- addLog(<ErrorLog message={result.message} />);
38
- await terminate(1);
39
- }
40
- await terminate(0);
41
- } catch (error) {
42
- const errorMessage = getErrorMessage(error);
43
- addLog(<ErrorLog message={errorMessage} />);
44
- await terminate(1);
45
- }
46
- }
47
-
48
- export async function scrapePage(url: string) {
49
- try {
50
- const urlObj = new URL(url);
51
- addLog(<SpinnerLog message={`Fetching ${urlObj.toString()}...`} />);
52
- const html = await fetchPageHtml(urlObj);
53
- addLog(<SuccessLog message={`Successfully retrieved HTML from ${urlObj.toString()}`} />);
54
-
55
- const hast = htmlToHast(html);
56
- detectFramework(hast);
57
-
58
- const needsBrowser = framework.vendor === 'gitbook';
59
- addLog(<SpinnerLog message="Scraping page..." />);
60
- const results = await scrapePageGroup([urlObj], needsBrowser);
61
- const result = results[0] || {
62
- success: false,
63
- message: `An unknown error occurred when scraping ${url}`,
64
- };
65
-
66
- if (result.success) {
67
- addLog(
68
- <SuccessLog
69
- message={`Successfully scraped ${url} ${result.data ? `into ${result.data[1]}` : ''}`}
70
- />
71
- );
72
- } else {
73
- addLog(<ErrorLog message={result.message} />);
74
- await terminate(1);
75
- }
76
- await terminate(0);
77
- } catch (error) {
78
- const errorMessage = getErrorMessage(error);
79
- addLog(<ErrorLog message={errorMessage} />);
80
- await terminate(1);
81
- }
82
- }
83
-
84
- interface ScrapeOpenApiOptions {
85
- openapiLocation: string;
86
- writeFiles: boolean;
87
- outDir?: string;
88
- overwrite: boolean;
89
- }
90
-
91
- export async function scrapeOpenApi({
92
- openapiLocation,
93
- writeFiles,
94
- outDir,
95
- overwrite,
96
- }: ScrapeOpenApiOptions) {
97
- try {
98
- addLog(<SpinnerLog message={`Processing OpenAPI spec from ${openapiLocation}...`} />);
99
- const { nav, isUrl } = await generateOpenApiPages(openapiLocation, {
100
- openApiFilePath: undefined,
101
- version: undefined,
102
- writeFiles,
103
- outDir,
104
- overwrite,
105
- });
106
- addLog(<SuccessLog message="Successfully generated OpenAPI pages" />);
107
- addLog(<InfoLog message="Navigation object suggestion:" />);
108
- addLog(<InfoLog message={JSON.stringify(nav, undefined, 2)} />);
109
- if (isUrl) {
110
- addLog(<InfoLog message="OpenAPI location suggestion:" />);
111
- addLog(<InfoLog message={`openapi: ${openapiLocation}`} />);
112
- }
113
- await terminate(0);
114
- } catch (error) {
115
- if (error instanceof Error) {
116
- addLog(<ErrorLog message={error.message} />);
117
- } else {
118
- addLog(<ErrorLog message={String(error)} />);
119
- }
120
- await terminate(1);
121
- }
122
- }