openapi-ai-generator 0.1.0 → 0.1.2
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/dist/cli.js +212 -181
- package/dist/cli.js.map +1 -1
- package/dist/index.d.mts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +179 -150
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +180 -151
- package/dist/index.mjs.map +1 -1
- package/dist/plugin.js +205 -180
- package/dist/plugin.js.map +1 -1
- package/dist/plugin.mjs +205 -180
- package/dist/plugin.mjs.map +1 -1
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -26,150 +26,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
// src/cli.ts
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
|
|
29
|
-
// src/config.ts
|
|
30
|
-
var import_fs = require("fs");
|
|
31
|
-
var import_path = require("path");
|
|
32
|
-
var import_url = require("url");
|
|
33
|
-
var defaults = {
|
|
34
|
-
jsdocMode: "context",
|
|
35
|
-
cache: true,
|
|
36
|
-
cacheDir: ".openapi-cache",
|
|
37
|
-
include: ["src/app/api/**/route.ts"],
|
|
38
|
-
exclude: []
|
|
39
|
-
};
|
|
40
|
-
function resolveConfig(config) {
|
|
41
|
-
return {
|
|
42
|
-
...defaults,
|
|
43
|
-
...config,
|
|
44
|
-
output: {
|
|
45
|
-
scalarDocs: false,
|
|
46
|
-
scalarPath: "src/app/api/docs/route.ts",
|
|
47
|
-
...config.output
|
|
48
|
-
},
|
|
49
|
-
openapi: {
|
|
50
|
-
description: "",
|
|
51
|
-
servers: [],
|
|
52
|
-
...config.openapi
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
async function loadConfig(configPath) {
|
|
57
|
-
const searchPaths = configPath ? [configPath] : [
|
|
58
|
-
"openapi-gen.config.ts",
|
|
59
|
-
"openapi-gen.config.js",
|
|
60
|
-
"openapi-gen.config.mjs",
|
|
61
|
-
"openapi-gen.config.cjs"
|
|
62
|
-
];
|
|
63
|
-
for (const p of searchPaths) {
|
|
64
|
-
const abs = (0, import_path.resolve)(process.cwd(), p);
|
|
65
|
-
if ((0, import_fs.existsSync)(abs)) {
|
|
66
|
-
const mod = await importConfig(abs);
|
|
67
|
-
const config = mod.default ?? mod;
|
|
68
|
-
return resolveConfig(config);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
throw new Error(
|
|
72
|
-
"No openapi-gen.config.ts found. Create one at your project root."
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
async function importConfig(filePath) {
|
|
76
|
-
if (filePath.endsWith(".ts")) {
|
|
77
|
-
return importTypeScriptConfig(filePath);
|
|
78
|
-
}
|
|
79
|
-
const url = (0, import_url.pathToFileURL)(filePath).href;
|
|
80
|
-
return import(url);
|
|
81
|
-
}
|
|
82
|
-
async function importTypeScriptConfig(filePath) {
|
|
83
|
-
try {
|
|
84
|
-
const { register } = await import("module");
|
|
85
|
-
const url = (0, import_url.pathToFileURL)(filePath).href;
|
|
86
|
-
return await import(url);
|
|
87
|
-
} catch {
|
|
88
|
-
try {
|
|
89
|
-
require("ts-node/register");
|
|
90
|
-
return require(filePath);
|
|
91
|
-
} catch {
|
|
92
|
-
throw new Error(
|
|
93
|
-
`Cannot load TypeScript config file: ${filePath}. Install tsx or ts-node, or use a .js config file.`
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// src/scanner.ts
|
|
100
|
-
var import_fs2 = require("fs");
|
|
101
|
-
var import_glob = require("glob");
|
|
102
|
-
var import_path2 = require("path");
|
|
103
|
-
async function scanRoutes(include, exclude, cwd = process.cwd()) {
|
|
104
|
-
const files = await (0, import_glob.glob)(include, {
|
|
105
|
-
cwd,
|
|
106
|
-
ignore: exclude,
|
|
107
|
-
absolute: true
|
|
108
|
-
});
|
|
109
|
-
return files.map((filePath) => parseRoute(filePath, cwd));
|
|
110
|
-
}
|
|
111
|
-
function parseRoute(filePath, cwd) {
|
|
112
|
-
const relativePath = (0, import_path2.relative)(cwd, filePath);
|
|
113
|
-
const sourceCode = (0, import_fs2.readFileSync)(filePath, "utf8");
|
|
114
|
-
const urlPath = filePathToUrlPath(relativePath);
|
|
115
|
-
const { jsdocComments, hasExactJsdoc, exactPathItem } = extractJsdoc(sourceCode);
|
|
116
|
-
return {
|
|
117
|
-
filePath,
|
|
118
|
-
relativePath,
|
|
119
|
-
urlPath,
|
|
120
|
-
sourceCode,
|
|
121
|
-
jsdocComments,
|
|
122
|
-
hasExactJsdoc,
|
|
123
|
-
exactPathItem
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
function filePathToUrlPath(filePath) {
|
|
127
|
-
let path = filePath.replace(/\\/g, "/");
|
|
128
|
-
path = path.replace(/^(src\/)?app\//, "");
|
|
129
|
-
path = path.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
|
|
130
|
-
path = path.replace(/\[([^\]]+)\]/g, (_, param) => {
|
|
131
|
-
if (param.startsWith("...")) {
|
|
132
|
-
return `{${param.slice(3)}}`;
|
|
133
|
-
}
|
|
134
|
-
return `{${param}}`;
|
|
135
|
-
});
|
|
136
|
-
if (!path.startsWith("/")) {
|
|
137
|
-
path = "/" + path;
|
|
138
|
-
}
|
|
139
|
-
return path;
|
|
140
|
-
}
|
|
141
|
-
function extractJsdoc(sourceCode) {
|
|
142
|
-
const jsdocRegex = /\/\*\*([\s\S]*?)\*\//g;
|
|
143
|
-
const jsdocComments = [];
|
|
144
|
-
let hasExactJsdoc = false;
|
|
145
|
-
let exactPathItem;
|
|
146
|
-
let match;
|
|
147
|
-
while ((match = jsdocRegex.exec(sourceCode)) !== null) {
|
|
148
|
-
const comment = match[0];
|
|
149
|
-
jsdocComments.push(comment);
|
|
150
|
-
if (/@openapi-exact/.test(comment)) {
|
|
151
|
-
hasExactJsdoc = true;
|
|
152
|
-
const openapiMatch = comment.match(/@openapi\s+([\s\S]*?)(?=\s*\*\/|\s*\*\s*@)/);
|
|
153
|
-
if (openapiMatch) {
|
|
154
|
-
try {
|
|
155
|
-
const jsonStr = openapiMatch[1].split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n").trim();
|
|
156
|
-
exactPathItem = JSON.parse(jsonStr);
|
|
157
|
-
} catch {
|
|
158
|
-
hasExactJsdoc = false;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return { jsdocComments, hasExactJsdoc, exactPathItem };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
29
|
// src/analyzer.ts
|
|
167
30
|
var import_ai = require("ai");
|
|
168
31
|
|
|
169
32
|
// src/cache.ts
|
|
170
33
|
var import_crypto = require("crypto");
|
|
171
|
-
var
|
|
172
|
-
var
|
|
34
|
+
var import_fs = require("fs");
|
|
35
|
+
var import_path = require("path");
|
|
173
36
|
function computeHash(content, provider, modelId) {
|
|
174
37
|
return (0, import_crypto.createHash)("sha256").update(content).update(provider).update(modelId).digest("hex");
|
|
175
38
|
}
|
|
@@ -179,15 +42,15 @@ var RouteCache = class {
|
|
|
179
42
|
this.cacheDir = cacheDir;
|
|
180
43
|
}
|
|
181
44
|
ensureDir() {
|
|
182
|
-
if (!(0,
|
|
183
|
-
(0,
|
|
45
|
+
if (!(0, import_fs.existsSync)(this.cacheDir)) {
|
|
46
|
+
(0, import_fs.mkdirSync)(this.cacheDir, { recursive: true });
|
|
184
47
|
}
|
|
185
48
|
}
|
|
186
49
|
get(hash) {
|
|
187
|
-
const filePath = (0,
|
|
188
|
-
if (!(0,
|
|
50
|
+
const filePath = (0, import_path.join)(this.cacheDir, `${hash}.json`);
|
|
51
|
+
if (!(0, import_fs.existsSync)(filePath)) return null;
|
|
189
52
|
try {
|
|
190
|
-
const entry = JSON.parse((0,
|
|
53
|
+
const entry = JSON.parse((0, import_fs.readFileSync)(filePath, "utf8"));
|
|
191
54
|
return entry.pathItem;
|
|
192
55
|
} catch {
|
|
193
56
|
return null;
|
|
@@ -200,8 +63,8 @@ var RouteCache = class {
|
|
|
200
63
|
pathItem,
|
|
201
64
|
cachedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
202
65
|
};
|
|
203
|
-
const filePath = (0,
|
|
204
|
-
(0,
|
|
66
|
+
const filePath = (0, import_path.join)(this.cacheDir, `${hash}.json`);
|
|
67
|
+
(0, import_fs.writeFileSync)(filePath, JSON.stringify(entry, null, 2), "utf8");
|
|
205
68
|
}
|
|
206
69
|
};
|
|
207
70
|
|
|
@@ -222,10 +85,11 @@ function createModel(provider) {
|
|
|
222
85
|
}
|
|
223
86
|
function createAzureModel() {
|
|
224
87
|
const endpoint = requireEnv("AZURE_OPENAI_ENDPOINT");
|
|
88
|
+
const resourceName = endpoint?.match(/https?:\/\/([^.]+)/)?.[1];
|
|
225
89
|
const apiKey = requireEnv("AZURE_OPENAI_API_KEY");
|
|
226
90
|
const deployment = requireEnv("AZURE_OPENAI_DEPLOYMENT");
|
|
227
91
|
const { createAzure } = require("@ai-sdk/azure");
|
|
228
|
-
const azure = createAzure({
|
|
92
|
+
const azure = createAzure({ resourceName, apiKey });
|
|
229
93
|
return azure(deployment);
|
|
230
94
|
}
|
|
231
95
|
function createOpenAIModel() {
|
|
@@ -307,7 +171,25 @@ async function analyzeRoute(route, options, modelId, cache, getModel) {
|
|
|
307
171
|
};
|
|
308
172
|
}
|
|
309
173
|
}
|
|
310
|
-
|
|
174
|
+
let pathItem;
|
|
175
|
+
try {
|
|
176
|
+
pathItem = await callLLM(route, options.jsdocMode, getModel());
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
179
|
+
const isContentFilter = message.includes("content filtering policy") || message.includes("content_filter") || err?.status === 400;
|
|
180
|
+
if (isContentFilter) {
|
|
181
|
+
console.warn(
|
|
182
|
+
`Warning: Content filter blocked response for ${route.urlPath}. Skipping route. Use @openapi-exact JSDoc to provide the spec manually.`
|
|
183
|
+
);
|
|
184
|
+
return {
|
|
185
|
+
urlPath: route.urlPath,
|
|
186
|
+
pathItem: {},
|
|
187
|
+
fromCache: false,
|
|
188
|
+
skippedLLM: false
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
311
193
|
if (cache) {
|
|
312
194
|
cache.set(hash, pathItem);
|
|
313
195
|
}
|
|
@@ -321,30 +203,40 @@ async function analyzeRoute(route, options, modelId, cache, getModel) {
|
|
|
321
203
|
function buildPrompt(route, jsdocMode) {
|
|
322
204
|
const jsDocSection = route.jsdocComments.length > 0 ? `JSDoc COMMENTS (use as ${jsdocMode === "context" ? "additional context" : "primary source"}):
|
|
323
205
|
${route.jsdocComments.join("\n\n")}` : "No JSDoc comments found.";
|
|
324
|
-
return `You are
|
|
206
|
+
return `You are a technical documentation tool that reads existing source code and produces OpenAPI 3.1 documentation data. You do not write or execute code \u2014 you only read and describe it.
|
|
207
|
+
|
|
208
|
+
## Task
|
|
325
209
|
|
|
326
|
-
|
|
327
|
-
|
|
210
|
+
Read the Next.js API route source file below and produce a JSON object that documents its HTTP endpoints according to the OpenAPI 3.1 PathItem schema.
|
|
211
|
+
|
|
212
|
+
## Route metadata
|
|
213
|
+
|
|
214
|
+
- File: ${route.relativePath}
|
|
215
|
+
- URL path: ${route.urlPath}
|
|
216
|
+
|
|
217
|
+
## Source file contents
|
|
328
218
|
|
|
329
|
-
SOURCE CODE:
|
|
330
219
|
\`\`\`typescript
|
|
331
220
|
${route.sourceCode}
|
|
332
221
|
\`\`\`
|
|
333
222
|
|
|
334
223
|
${jsDocSection}
|
|
335
224
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
-
|
|
340
|
-
-
|
|
341
|
-
-
|
|
342
|
-
-
|
|
343
|
-
-
|
|
344
|
-
-
|
|
345
|
-
-
|
|
225
|
+
## Instructions
|
|
226
|
+
|
|
227
|
+
For each exported function named GET, POST, PUT, PATCH, or DELETE, document:
|
|
228
|
+
- operationId: a unique camelCase identifier
|
|
229
|
+
- summary: a short one-line description
|
|
230
|
+
- description: a fuller explanation of what the endpoint does
|
|
231
|
+
- parameters: path params from URL segments like {id}, and query params from searchParams usage
|
|
232
|
+
- requestBody: schema inferred from request.json() calls and TypeScript types (POST/PUT/PATCH only)
|
|
233
|
+
- responses: per status code, inferred from NextResponse.json() calls and return type annotations
|
|
234
|
+
- tags: inferred from the URL path segments
|
|
235
|
+
- security: noted if the code checks for auth tokens, session cookies, or middleware guards
|
|
346
236
|
|
|
347
|
-
|
|
237
|
+
## Output format
|
|
238
|
+
|
|
239
|
+
Return a single raw JSON object matching the OpenAPI 3.1 PathItem schema. No explanation, no markdown fences, no extra text \u2014 only the JSON object.`;
|
|
348
240
|
}
|
|
349
241
|
async function callLLM(route, jsdocMode, model) {
|
|
350
242
|
const prompt = buildPrompt(route, jsdocMode);
|
|
@@ -375,9 +267,75 @@ function parsePathItem(text, urlPath) {
|
|
|
375
267
|
}
|
|
376
268
|
}
|
|
377
269
|
|
|
270
|
+
// src/config.ts
|
|
271
|
+
var import_fs2 = require("fs");
|
|
272
|
+
var import_path2 = require("path");
|
|
273
|
+
var import_url = require("url");
|
|
274
|
+
var defaults = {
|
|
275
|
+
jsdocMode: "context",
|
|
276
|
+
cache: true,
|
|
277
|
+
cacheDir: ".openapi-cache",
|
|
278
|
+
include: ["src/app/api/**/route.ts"],
|
|
279
|
+
exclude: [],
|
|
280
|
+
envFile: [".env", ".env.local"]
|
|
281
|
+
};
|
|
282
|
+
function resolveConfig(config) {
|
|
283
|
+
let envFile;
|
|
284
|
+
if (config.envFile === false) {
|
|
285
|
+
envFile = false;
|
|
286
|
+
} else if (typeof config.envFile === "string") {
|
|
287
|
+
envFile = [config.envFile];
|
|
288
|
+
} else {
|
|
289
|
+
envFile = config.envFile ?? defaults.envFile;
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
...defaults,
|
|
293
|
+
...config,
|
|
294
|
+
envFile,
|
|
295
|
+
output: {
|
|
296
|
+
scalarDocs: false,
|
|
297
|
+
scalarPath: "src/app/api/docs/route.ts",
|
|
298
|
+
...config.output
|
|
299
|
+
},
|
|
300
|
+
openapi: {
|
|
301
|
+
description: "",
|
|
302
|
+
servers: [],
|
|
303
|
+
...config.openapi
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async function loadConfig(configPath) {
|
|
308
|
+
const searchPaths = configPath ? [configPath] : [
|
|
309
|
+
"openapi-gen.config.ts",
|
|
310
|
+
"openapi-gen.config.js",
|
|
311
|
+
"openapi-gen.config.mjs",
|
|
312
|
+
"openapi-gen.config.cjs"
|
|
313
|
+
];
|
|
314
|
+
for (const p of searchPaths) {
|
|
315
|
+
const abs = (0, import_path2.resolve)(process.cwd(), p);
|
|
316
|
+
if ((0, import_fs2.existsSync)(abs)) {
|
|
317
|
+
const mod = await importConfig(abs);
|
|
318
|
+
const config = mod.default ?? mod;
|
|
319
|
+
return resolveConfig(config);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
throw new Error("No openapi-gen.config.ts found. Create one at your project root.");
|
|
323
|
+
}
|
|
324
|
+
async function importConfig(filePath) {
|
|
325
|
+
if (filePath.endsWith(".ts")) {
|
|
326
|
+
return importTypeScriptConfig(filePath);
|
|
327
|
+
}
|
|
328
|
+
const url = (0, import_url.pathToFileURL)(filePath).href;
|
|
329
|
+
return import(url);
|
|
330
|
+
}
|
|
331
|
+
async function importTypeScriptConfig(filePath) {
|
|
332
|
+
require("tsx/cjs");
|
|
333
|
+
return require(filePath);
|
|
334
|
+
}
|
|
335
|
+
|
|
378
336
|
// src/generator.ts
|
|
379
|
-
var
|
|
380
|
-
var
|
|
337
|
+
var import_fs3 = require("fs");
|
|
338
|
+
var import_path3 = require("path");
|
|
381
339
|
function assembleSpec(config, routes) {
|
|
382
340
|
const paths = {};
|
|
383
341
|
for (const route of routes) {
|
|
@@ -406,11 +364,11 @@ function writeOutputFiles(config, spec, cwd = process.cwd()) {
|
|
|
406
364
|
}
|
|
407
365
|
}
|
|
408
366
|
function writeSpecFiles(config, spec, cwd) {
|
|
409
|
-
const specRoutePath = (0,
|
|
410
|
-
const specDir = (0,
|
|
367
|
+
const specRoutePath = (0, import_path3.resolve)(cwd, config.output.specPath);
|
|
368
|
+
const specDir = (0, import_path3.dirname)(specRoutePath);
|
|
411
369
|
ensureDir(specDir);
|
|
412
|
-
const specJsonPath = (0,
|
|
413
|
-
(0,
|
|
370
|
+
const specJsonPath = (0, import_path3.join)(specDir, "spec.json");
|
|
371
|
+
(0, import_fs3.writeFileSync)(specJsonPath, JSON.stringify(spec, null, 2), "utf8");
|
|
414
372
|
const routeContent = `import spec from './spec.json';
|
|
415
373
|
|
|
416
374
|
export const dynamic = 'force-static';
|
|
@@ -419,11 +377,11 @@ export function GET() {
|
|
|
419
377
|
return Response.json(spec);
|
|
420
378
|
}
|
|
421
379
|
`;
|
|
422
|
-
(0,
|
|
380
|
+
(0, import_fs3.writeFileSync)(specRoutePath, routeContent, "utf8");
|
|
423
381
|
}
|
|
424
382
|
function writeScalarRoute(config, cwd) {
|
|
425
|
-
const scalarRoutePath = (0,
|
|
426
|
-
ensureDir((0,
|
|
383
|
+
const scalarRoutePath = (0, import_path3.resolve)(cwd, config.output.scalarPath);
|
|
384
|
+
ensureDir((0, import_path3.dirname)(scalarRoutePath));
|
|
427
385
|
const specUrl = filePathToApiUrl(config.output.specPath);
|
|
428
386
|
const routeContent = `export const dynamic = 'force-static';
|
|
429
387
|
|
|
@@ -448,27 +406,101 @@ export function GET() {
|
|
|
448
406
|
);
|
|
449
407
|
}
|
|
450
408
|
`;
|
|
451
|
-
(0,
|
|
409
|
+
(0, import_fs3.writeFileSync)(scalarRoutePath, routeContent, "utf8");
|
|
452
410
|
}
|
|
453
411
|
function filePathToApiUrl(filePath) {
|
|
454
412
|
let path = filePath.replace(/\\/g, "/");
|
|
455
413
|
path = path.replace(/^(src\/)?app\//, "");
|
|
456
414
|
path = path.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
|
|
457
|
-
if (!path.startsWith("/")) path =
|
|
415
|
+
if (!path.startsWith("/")) path = `/${path}`;
|
|
458
416
|
return path;
|
|
459
417
|
}
|
|
460
418
|
function ensureDir(dir) {
|
|
461
|
-
if (!(0,
|
|
462
|
-
(0,
|
|
419
|
+
if (!(0, import_fs3.existsSync)(dir)) {
|
|
420
|
+
(0, import_fs3.mkdirSync)(dir, { recursive: true });
|
|
463
421
|
}
|
|
464
422
|
}
|
|
465
423
|
|
|
424
|
+
// src/scanner.ts
|
|
425
|
+
var import_fs4 = require("fs");
|
|
426
|
+
var import_path4 = require("path");
|
|
427
|
+
async function scanRoutes(include, exclude, cwd = process.cwd()) {
|
|
428
|
+
const { default: fg } = await import("fast-glob");
|
|
429
|
+
const files = await fg(include, {
|
|
430
|
+
cwd,
|
|
431
|
+
ignore: exclude,
|
|
432
|
+
absolute: true
|
|
433
|
+
});
|
|
434
|
+
return files.map((filePath) => parseRoute(filePath, cwd));
|
|
435
|
+
}
|
|
436
|
+
function parseRoute(filePath, cwd) {
|
|
437
|
+
const relativePath = (0, import_path4.relative)(cwd, filePath);
|
|
438
|
+
const sourceCode = (0, import_fs4.readFileSync)(filePath, "utf8");
|
|
439
|
+
const urlPath = filePathToUrlPath(relativePath);
|
|
440
|
+
const { jsdocComments, hasExactJsdoc, exactPathItem } = extractJsdoc(sourceCode);
|
|
441
|
+
return {
|
|
442
|
+
filePath,
|
|
443
|
+
relativePath,
|
|
444
|
+
urlPath,
|
|
445
|
+
sourceCode,
|
|
446
|
+
jsdocComments,
|
|
447
|
+
hasExactJsdoc,
|
|
448
|
+
exactPathItem
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function filePathToUrlPath(filePath) {
|
|
452
|
+
let path = filePath.replace(/\\/g, "/");
|
|
453
|
+
path = path.replace(/^(src\/)?app\//, "");
|
|
454
|
+
path = path.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
|
|
455
|
+
path = path.replace(/\[([^\]]+)\]/g, (_, param) => {
|
|
456
|
+
if (param.startsWith("...")) {
|
|
457
|
+
return `{${param.slice(3)}}`;
|
|
458
|
+
}
|
|
459
|
+
return `{${param}}`;
|
|
460
|
+
});
|
|
461
|
+
if (!path.startsWith("/")) {
|
|
462
|
+
path = `/${path}`;
|
|
463
|
+
}
|
|
464
|
+
return path;
|
|
465
|
+
}
|
|
466
|
+
function extractJsdoc(sourceCode) {
|
|
467
|
+
const jsdocRegex = /\/\*\*([\s\S]*?)\*\//g;
|
|
468
|
+
const jsdocComments = [];
|
|
469
|
+
let hasExactJsdoc = false;
|
|
470
|
+
let exactPathItem;
|
|
471
|
+
const match = jsdocRegex.exec(sourceCode);
|
|
472
|
+
while (match !== null) {
|
|
473
|
+
const comment = match[0];
|
|
474
|
+
jsdocComments.push(comment);
|
|
475
|
+
if (/@openapi-exact/.test(comment)) {
|
|
476
|
+
hasExactJsdoc = true;
|
|
477
|
+
const openapiMatch = comment.match(/@openapi\s+([\s\S]*?)(?=\s*\*\/|\s*\*\s*@)/);
|
|
478
|
+
if (openapiMatch) {
|
|
479
|
+
try {
|
|
480
|
+
const jsonStr = openapiMatch[1].split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n").trim();
|
|
481
|
+
exactPathItem = JSON.parse(jsonStr);
|
|
482
|
+
} catch {
|
|
483
|
+
hasExactJsdoc = false;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return { jsdocComments, hasExactJsdoc, exactPathItem };
|
|
489
|
+
}
|
|
490
|
+
|
|
466
491
|
// src/index.ts
|
|
492
|
+
var import_node_path = require("path");
|
|
467
493
|
async function generate(options = {}) {
|
|
468
494
|
const cwd = options.cwd ?? process.cwd();
|
|
469
495
|
const config = await loadConfig(options.config);
|
|
470
496
|
if (options.provider) config.provider = options.provider;
|
|
471
497
|
if (options.cache === false) config.cache = false;
|
|
498
|
+
if (config.envFile !== false) {
|
|
499
|
+
const { config: dotenvConfig } = await import("dotenv");
|
|
500
|
+
for (const file of config.envFile) {
|
|
501
|
+
dotenvConfig({ path: (0, import_node_path.resolve)(cwd, file), override: false });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
472
504
|
console.log(`[openapi-ai-generator] Scanning routes...`);
|
|
473
505
|
const routes = await scanRoutes(config.include, config.exclude, cwd);
|
|
474
506
|
console.log(`[openapi-ai-generator] Found ${routes.length} route(s)`);
|
|
@@ -501,10 +533,7 @@ async function generate(options = {}) {
|
|
|
501
533
|
// src/cli.ts
|
|
502
534
|
var program = new import_commander.Command();
|
|
503
535
|
program.name("openapi-ai-generator").description("Generate OpenAPI 3.1 specs from Next.js API routes using AI").version("0.1.0");
|
|
504
|
-
program.command("generate").description("Scan Next.js API routes and generate an OpenAPI spec").option("-c, --config <path>", "Path to config file (default: openapi-gen.config.ts)").option(
|
|
505
|
-
"-p, --provider <provider>",
|
|
506
|
-
"Override provider (azure | openai | anthropic)"
|
|
507
|
-
).option("--no-cache", "Disable caching (always re-analyze all routes)").action(async (opts) => {
|
|
536
|
+
program.command("generate").description("Scan Next.js API routes and generate an OpenAPI spec").option("-c, --config <path>", "Path to config file (default: openapi-gen.config.ts)").option("-p, --provider <provider>", "Override provider (azure | openai | anthropic)").option("--no-cache", "Disable caching (always re-analyze all routes)").action(async (opts) => {
|
|
508
537
|
try {
|
|
509
538
|
const result = await generate({
|
|
510
539
|
config: opts.config,
|
|
@@ -515,7 +544,9 @@ program.command("generate").description("Scan Next.js API routes and generate an
|
|
|
515
544
|
\u2713 OpenAPI spec generated successfully`);
|
|
516
545
|
console.log(` Routes analyzed: ${result.routesAnalyzed}`);
|
|
517
546
|
console.log(` From cache: ${result.routesFromCache}`);
|
|
518
|
-
console.log(
|
|
547
|
+
console.log(
|
|
548
|
+
` LLM calls made: ${result.routesAnalyzed - result.routesFromCache - (result.routesSkippedLLM - result.routesFromCache)}`
|
|
549
|
+
);
|
|
519
550
|
console.log(` Spec written to: ${result.specPath}`);
|
|
520
551
|
} catch (err) {
|
|
521
552
|
console.error("[openapi-ai-generator] Error:", err instanceof Error ? err.message : err);
|