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