openapi-ai-generator 0.1.0
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/LICENSE +21 -0
- package/README.md +579 -0
- package/dist/cli.js +526 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.mts +92 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +476 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +467 -0
- package/dist/index.mjs.map +1 -0
- package/dist/plugin.d.mts +14 -0
- package/dist/plugin.d.ts +14 -0
- package/dist/plugin.js +572 -0
- package/dist/plugin.js.map +1 -0
- package/dist/plugin.mjs +570 -0
- package/dist/plugin.mjs.map +1 -0
- package/package.json +63 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
type Provider = 'azure' | 'openai' | 'anthropic';
|
|
2
|
+
type JSDocMode = 'context' | 'exact';
|
|
3
|
+
interface OpenAPIGenConfig {
|
|
4
|
+
provider: Provider;
|
|
5
|
+
output: {
|
|
6
|
+
specPath: string;
|
|
7
|
+
scalarDocs?: boolean;
|
|
8
|
+
scalarPath?: string;
|
|
9
|
+
};
|
|
10
|
+
openapi: {
|
|
11
|
+
title: string;
|
|
12
|
+
version: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
servers?: Array<{
|
|
15
|
+
url: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
jsdocMode?: JSDocMode;
|
|
20
|
+
cache?: boolean;
|
|
21
|
+
cacheDir?: string;
|
|
22
|
+
include?: string[];
|
|
23
|
+
exclude?: string[];
|
|
24
|
+
}
|
|
25
|
+
interface ResolvedConfig extends Required<OpenAPIGenConfig> {
|
|
26
|
+
output: Required<OpenAPIGenConfig['output']>;
|
|
27
|
+
openapi: Required<OpenAPIGenConfig['openapi']>;
|
|
28
|
+
}
|
|
29
|
+
declare function resolveConfig(config: OpenAPIGenConfig): ResolvedConfig;
|
|
30
|
+
declare function loadConfig(configPath?: string): Promise<ResolvedConfig>;
|
|
31
|
+
|
|
32
|
+
interface RouteInfo {
|
|
33
|
+
filePath: string;
|
|
34
|
+
relativePath: string;
|
|
35
|
+
urlPath: string;
|
|
36
|
+
sourceCode: string;
|
|
37
|
+
jsdocComments: string[];
|
|
38
|
+
hasExactJsdoc: boolean;
|
|
39
|
+
exactPathItem?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
declare function scanRoutes(include: string[], exclude: string[], cwd?: string): Promise<RouteInfo[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Convert a Next.js route file path to an OpenAPI URL path.
|
|
44
|
+
* e.g. src/app/api/users/[id]/route.ts -> /api/users/{id}
|
|
45
|
+
*/
|
|
46
|
+
declare function filePathToUrlPath(filePath: string): string;
|
|
47
|
+
|
|
48
|
+
interface AnalyzeOptions {
|
|
49
|
+
provider: Provider;
|
|
50
|
+
jsdocMode: JSDocMode;
|
|
51
|
+
cache: boolean;
|
|
52
|
+
cacheDir: string;
|
|
53
|
+
}
|
|
54
|
+
interface AnalyzedRoute {
|
|
55
|
+
urlPath: string;
|
|
56
|
+
pathItem: Record<string, unknown>;
|
|
57
|
+
fromCache: boolean;
|
|
58
|
+
skippedLLM: boolean;
|
|
59
|
+
}
|
|
60
|
+
declare function analyzeRoutes(routes: RouteInfo[], options: AnalyzeOptions): Promise<AnalyzedRoute[]>;
|
|
61
|
+
|
|
62
|
+
interface OpenAPISpec {
|
|
63
|
+
openapi: '3.1.0';
|
|
64
|
+
info: {
|
|
65
|
+
title: string;
|
|
66
|
+
version: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
};
|
|
69
|
+
servers?: Array<{
|
|
70
|
+
url: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
}>;
|
|
73
|
+
paths: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
declare function assembleSpec(config: ResolvedConfig, routes: AnalyzedRoute[]): OpenAPISpec;
|
|
76
|
+
declare function writeOutputFiles(config: ResolvedConfig, spec: OpenAPISpec, cwd?: string): void;
|
|
77
|
+
|
|
78
|
+
interface GenerateOptions {
|
|
79
|
+
config?: string;
|
|
80
|
+
provider?: OpenAPIGenConfig['provider'];
|
|
81
|
+
cache?: boolean;
|
|
82
|
+
cwd?: string;
|
|
83
|
+
}
|
|
84
|
+
interface GenerateResult {
|
|
85
|
+
routesAnalyzed: number;
|
|
86
|
+
routesFromCache: number;
|
|
87
|
+
routesSkippedLLM: number;
|
|
88
|
+
specPath: string;
|
|
89
|
+
}
|
|
90
|
+
declare function generate(options?: GenerateOptions): Promise<GenerateResult>;
|
|
91
|
+
|
|
92
|
+
export { type GenerateOptions, type GenerateResult, type JSDocMode, type OpenAPIGenConfig, type Provider, type ResolvedConfig, analyzeRoutes, assembleSpec, filePathToUrlPath, generate, loadConfig, resolveConfig, scanRoutes, writeOutputFiles };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
type Provider = 'azure' | 'openai' | 'anthropic';
|
|
2
|
+
type JSDocMode = 'context' | 'exact';
|
|
3
|
+
interface OpenAPIGenConfig {
|
|
4
|
+
provider: Provider;
|
|
5
|
+
output: {
|
|
6
|
+
specPath: string;
|
|
7
|
+
scalarDocs?: boolean;
|
|
8
|
+
scalarPath?: string;
|
|
9
|
+
};
|
|
10
|
+
openapi: {
|
|
11
|
+
title: string;
|
|
12
|
+
version: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
servers?: Array<{
|
|
15
|
+
url: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
jsdocMode?: JSDocMode;
|
|
20
|
+
cache?: boolean;
|
|
21
|
+
cacheDir?: string;
|
|
22
|
+
include?: string[];
|
|
23
|
+
exclude?: string[];
|
|
24
|
+
}
|
|
25
|
+
interface ResolvedConfig extends Required<OpenAPIGenConfig> {
|
|
26
|
+
output: Required<OpenAPIGenConfig['output']>;
|
|
27
|
+
openapi: Required<OpenAPIGenConfig['openapi']>;
|
|
28
|
+
}
|
|
29
|
+
declare function resolveConfig(config: OpenAPIGenConfig): ResolvedConfig;
|
|
30
|
+
declare function loadConfig(configPath?: string): Promise<ResolvedConfig>;
|
|
31
|
+
|
|
32
|
+
interface RouteInfo {
|
|
33
|
+
filePath: string;
|
|
34
|
+
relativePath: string;
|
|
35
|
+
urlPath: string;
|
|
36
|
+
sourceCode: string;
|
|
37
|
+
jsdocComments: string[];
|
|
38
|
+
hasExactJsdoc: boolean;
|
|
39
|
+
exactPathItem?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
declare function scanRoutes(include: string[], exclude: string[], cwd?: string): Promise<RouteInfo[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Convert a Next.js route file path to an OpenAPI URL path.
|
|
44
|
+
* e.g. src/app/api/users/[id]/route.ts -> /api/users/{id}
|
|
45
|
+
*/
|
|
46
|
+
declare function filePathToUrlPath(filePath: string): string;
|
|
47
|
+
|
|
48
|
+
interface AnalyzeOptions {
|
|
49
|
+
provider: Provider;
|
|
50
|
+
jsdocMode: JSDocMode;
|
|
51
|
+
cache: boolean;
|
|
52
|
+
cacheDir: string;
|
|
53
|
+
}
|
|
54
|
+
interface AnalyzedRoute {
|
|
55
|
+
urlPath: string;
|
|
56
|
+
pathItem: Record<string, unknown>;
|
|
57
|
+
fromCache: boolean;
|
|
58
|
+
skippedLLM: boolean;
|
|
59
|
+
}
|
|
60
|
+
declare function analyzeRoutes(routes: RouteInfo[], options: AnalyzeOptions): Promise<AnalyzedRoute[]>;
|
|
61
|
+
|
|
62
|
+
interface OpenAPISpec {
|
|
63
|
+
openapi: '3.1.0';
|
|
64
|
+
info: {
|
|
65
|
+
title: string;
|
|
66
|
+
version: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
};
|
|
69
|
+
servers?: Array<{
|
|
70
|
+
url: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
}>;
|
|
73
|
+
paths: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
declare function assembleSpec(config: ResolvedConfig, routes: AnalyzedRoute[]): OpenAPISpec;
|
|
76
|
+
declare function writeOutputFiles(config: ResolvedConfig, spec: OpenAPISpec, cwd?: string): void;
|
|
77
|
+
|
|
78
|
+
interface GenerateOptions {
|
|
79
|
+
config?: string;
|
|
80
|
+
provider?: OpenAPIGenConfig['provider'];
|
|
81
|
+
cache?: boolean;
|
|
82
|
+
cwd?: string;
|
|
83
|
+
}
|
|
84
|
+
interface GenerateResult {
|
|
85
|
+
routesAnalyzed: number;
|
|
86
|
+
routesFromCache: number;
|
|
87
|
+
routesSkippedLLM: number;
|
|
88
|
+
specPath: string;
|
|
89
|
+
}
|
|
90
|
+
declare function generate(options?: GenerateOptions): Promise<GenerateResult>;
|
|
91
|
+
|
|
92
|
+
export { type GenerateOptions, type GenerateResult, type JSDocMode, type OpenAPIGenConfig, type Provider, type ResolvedConfig, analyzeRoutes, assembleSpec, filePathToUrlPath, generate, loadConfig, resolveConfig, scanRoutes, writeOutputFiles };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var url = require('url');
|
|
6
|
+
var glob = require('glob');
|
|
7
|
+
var ai = require('ai');
|
|
8
|
+
var crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
11
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
12
|
+
}) : x)(function(x) {
|
|
13
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
14
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
15
|
+
});
|
|
16
|
+
var defaults = {
|
|
17
|
+
jsdocMode: "context",
|
|
18
|
+
cache: true,
|
|
19
|
+
cacheDir: ".openapi-cache",
|
|
20
|
+
include: ["src/app/api/**/route.ts"],
|
|
21
|
+
exclude: []
|
|
22
|
+
};
|
|
23
|
+
function resolveConfig(config) {
|
|
24
|
+
return {
|
|
25
|
+
...defaults,
|
|
26
|
+
...config,
|
|
27
|
+
output: {
|
|
28
|
+
scalarDocs: false,
|
|
29
|
+
scalarPath: "src/app/api/docs/route.ts",
|
|
30
|
+
...config.output
|
|
31
|
+
},
|
|
32
|
+
openapi: {
|
|
33
|
+
description: "",
|
|
34
|
+
servers: [],
|
|
35
|
+
...config.openapi
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function loadConfig(configPath) {
|
|
40
|
+
const searchPaths = configPath ? [configPath] : [
|
|
41
|
+
"openapi-gen.config.ts",
|
|
42
|
+
"openapi-gen.config.js",
|
|
43
|
+
"openapi-gen.config.mjs",
|
|
44
|
+
"openapi-gen.config.cjs"
|
|
45
|
+
];
|
|
46
|
+
for (const p of searchPaths) {
|
|
47
|
+
const abs = path.resolve(process.cwd(), p);
|
|
48
|
+
if (fs.existsSync(abs)) {
|
|
49
|
+
const mod = await importConfig(abs);
|
|
50
|
+
const config = mod.default ?? mod;
|
|
51
|
+
return resolveConfig(config);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new Error(
|
|
55
|
+
"No openapi-gen.config.ts found. Create one at your project root."
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
async function importConfig(filePath) {
|
|
59
|
+
if (filePath.endsWith(".ts")) {
|
|
60
|
+
return importTypeScriptConfig(filePath);
|
|
61
|
+
}
|
|
62
|
+
const url$1 = url.pathToFileURL(filePath).href;
|
|
63
|
+
return import(url$1);
|
|
64
|
+
}
|
|
65
|
+
async function importTypeScriptConfig(filePath) {
|
|
66
|
+
try {
|
|
67
|
+
const { register } = await import('module');
|
|
68
|
+
const url$1 = url.pathToFileURL(filePath).href;
|
|
69
|
+
return await import(url$1);
|
|
70
|
+
} catch {
|
|
71
|
+
try {
|
|
72
|
+
__require("ts-node/register");
|
|
73
|
+
return __require(filePath);
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Cannot load TypeScript config file: ${filePath}. Install tsx or ts-node, or use a .js config file.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function scanRoutes(include, exclude, cwd = process.cwd()) {
|
|
82
|
+
const files = await glob.glob(include, {
|
|
83
|
+
cwd,
|
|
84
|
+
ignore: exclude,
|
|
85
|
+
absolute: true
|
|
86
|
+
});
|
|
87
|
+
return files.map((filePath) => parseRoute(filePath, cwd));
|
|
88
|
+
}
|
|
89
|
+
function parseRoute(filePath, cwd) {
|
|
90
|
+
const relativePath = path.relative(cwd, filePath);
|
|
91
|
+
const sourceCode = fs.readFileSync(filePath, "utf8");
|
|
92
|
+
const urlPath = filePathToUrlPath(relativePath);
|
|
93
|
+
const { jsdocComments, hasExactJsdoc, exactPathItem } = extractJsdoc(sourceCode);
|
|
94
|
+
return {
|
|
95
|
+
filePath,
|
|
96
|
+
relativePath,
|
|
97
|
+
urlPath,
|
|
98
|
+
sourceCode,
|
|
99
|
+
jsdocComments,
|
|
100
|
+
hasExactJsdoc,
|
|
101
|
+
exactPathItem
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function filePathToUrlPath(filePath) {
|
|
105
|
+
let path = filePath.replace(/\\/g, "/");
|
|
106
|
+
path = path.replace(/^(src\/)?app\//, "");
|
|
107
|
+
path = path.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
|
|
108
|
+
path = path.replace(/\[([^\]]+)\]/g, (_, param) => {
|
|
109
|
+
if (param.startsWith("...")) {
|
|
110
|
+
return `{${param.slice(3)}}`;
|
|
111
|
+
}
|
|
112
|
+
return `{${param}}`;
|
|
113
|
+
});
|
|
114
|
+
if (!path.startsWith("/")) {
|
|
115
|
+
path = "/" + path;
|
|
116
|
+
}
|
|
117
|
+
return path;
|
|
118
|
+
}
|
|
119
|
+
function extractJsdoc(sourceCode) {
|
|
120
|
+
const jsdocRegex = /\/\*\*([\s\S]*?)\*\//g;
|
|
121
|
+
const jsdocComments = [];
|
|
122
|
+
let hasExactJsdoc = false;
|
|
123
|
+
let exactPathItem;
|
|
124
|
+
let match;
|
|
125
|
+
while ((match = jsdocRegex.exec(sourceCode)) !== null) {
|
|
126
|
+
const comment = match[0];
|
|
127
|
+
jsdocComments.push(comment);
|
|
128
|
+
if (/@openapi-exact/.test(comment)) {
|
|
129
|
+
hasExactJsdoc = true;
|
|
130
|
+
const openapiMatch = comment.match(/@openapi\s+([\s\S]*?)(?=\s*\*\/|\s*\*\s*@)/);
|
|
131
|
+
if (openapiMatch) {
|
|
132
|
+
try {
|
|
133
|
+
const jsonStr = openapiMatch[1].split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n").trim();
|
|
134
|
+
exactPathItem = JSON.parse(jsonStr);
|
|
135
|
+
} catch {
|
|
136
|
+
hasExactJsdoc = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { jsdocComments, hasExactJsdoc, exactPathItem };
|
|
142
|
+
}
|
|
143
|
+
function computeHash(content, provider, modelId) {
|
|
144
|
+
return crypto.createHash("sha256").update(content).update(provider).update(modelId).digest("hex");
|
|
145
|
+
}
|
|
146
|
+
var RouteCache = class {
|
|
147
|
+
cacheDir;
|
|
148
|
+
constructor(cacheDir) {
|
|
149
|
+
this.cacheDir = cacheDir;
|
|
150
|
+
}
|
|
151
|
+
ensureDir() {
|
|
152
|
+
if (!fs.existsSync(this.cacheDir)) {
|
|
153
|
+
fs.mkdirSync(this.cacheDir, { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
get(hash) {
|
|
157
|
+
const filePath = path.join(this.cacheDir, `${hash}.json`);
|
|
158
|
+
if (!fs.existsSync(filePath)) return null;
|
|
159
|
+
try {
|
|
160
|
+
const entry = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
161
|
+
return entry.pathItem;
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
set(hash, pathItem) {
|
|
167
|
+
this.ensureDir();
|
|
168
|
+
const entry = {
|
|
169
|
+
hash,
|
|
170
|
+
pathItem,
|
|
171
|
+
cachedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
172
|
+
};
|
|
173
|
+
const filePath = path.join(this.cacheDir, `${hash}.json`);
|
|
174
|
+
fs.writeFileSync(filePath, JSON.stringify(entry, null, 2), "utf8");
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/providers/index.ts
|
|
179
|
+
function createModel(provider) {
|
|
180
|
+
switch (provider) {
|
|
181
|
+
case "azure":
|
|
182
|
+
return createAzureModel();
|
|
183
|
+
case "openai":
|
|
184
|
+
return createOpenAIModel();
|
|
185
|
+
case "anthropic":
|
|
186
|
+
return createAnthropicModel();
|
|
187
|
+
default: {
|
|
188
|
+
const _exhaustive = provider;
|
|
189
|
+
throw new Error(`Unknown provider: ${_exhaustive}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function createAzureModel() {
|
|
194
|
+
const endpoint = requireEnv("AZURE_OPENAI_ENDPOINT");
|
|
195
|
+
const apiKey = requireEnv("AZURE_OPENAI_API_KEY");
|
|
196
|
+
const deployment = requireEnv("AZURE_OPENAI_DEPLOYMENT");
|
|
197
|
+
const { createAzure } = __require("@ai-sdk/azure");
|
|
198
|
+
const azure = createAzure({ endpoint, apiKey });
|
|
199
|
+
return azure(deployment);
|
|
200
|
+
}
|
|
201
|
+
function createOpenAIModel() {
|
|
202
|
+
const apiKey = requireEnv("OPENAI_API_KEY");
|
|
203
|
+
const model = process.env.OPENAI_MODEL ?? "gpt-4o";
|
|
204
|
+
const { createOpenAI } = __require("@ai-sdk/openai");
|
|
205
|
+
const openai = createOpenAI({ apiKey });
|
|
206
|
+
return openai(model);
|
|
207
|
+
}
|
|
208
|
+
function createAnthropicModel() {
|
|
209
|
+
const apiKey = requireEnv("ANTHROPIC_API_KEY");
|
|
210
|
+
const model = process.env.ANTHROPIC_MODEL ?? "claude-sonnet-4-6";
|
|
211
|
+
const { createAnthropic } = __require("@ai-sdk/anthropic");
|
|
212
|
+
const anthropic = createAnthropic({ apiKey });
|
|
213
|
+
return anthropic(model);
|
|
214
|
+
}
|
|
215
|
+
function requireEnv(name) {
|
|
216
|
+
const val = process.env[name];
|
|
217
|
+
if (!val) {
|
|
218
|
+
throw new Error(`Required environment variable ${name} is not set.`);
|
|
219
|
+
}
|
|
220
|
+
return val;
|
|
221
|
+
}
|
|
222
|
+
function getModelId(provider) {
|
|
223
|
+
switch (provider) {
|
|
224
|
+
case "azure":
|
|
225
|
+
return process.env.AZURE_OPENAI_DEPLOYMENT ?? "unknown";
|
|
226
|
+
case "openai":
|
|
227
|
+
return process.env.OPENAI_MODEL ?? "gpt-4o";
|
|
228
|
+
case "anthropic":
|
|
229
|
+
return process.env.ANTHROPIC_MODEL ?? "claude-sonnet-4-6";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/analyzer.ts
|
|
234
|
+
async function analyzeRoutes(routes, options) {
|
|
235
|
+
const modelId = getModelId(options.provider);
|
|
236
|
+
const cache = options.cache ? new RouteCache(options.cacheDir) : null;
|
|
237
|
+
let model = null;
|
|
238
|
+
const getModel = () => {
|
|
239
|
+
if (!model) model = createModel(options.provider);
|
|
240
|
+
return model;
|
|
241
|
+
};
|
|
242
|
+
const results = [];
|
|
243
|
+
for (const route of routes) {
|
|
244
|
+
const result = await analyzeRoute(route, options, modelId, cache, getModel);
|
|
245
|
+
results.push(result);
|
|
246
|
+
}
|
|
247
|
+
return results;
|
|
248
|
+
}
|
|
249
|
+
async function analyzeRoute(route, options, modelId, cache, getModel) {
|
|
250
|
+
if (route.hasExactJsdoc && route.exactPathItem) {
|
|
251
|
+
return {
|
|
252
|
+
urlPath: route.urlPath,
|
|
253
|
+
pathItem: route.exactPathItem,
|
|
254
|
+
fromCache: false,
|
|
255
|
+
skippedLLM: true
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (options.jsdocMode === "exact") {
|
|
259
|
+
if (route.hasExactJsdoc && route.exactPathItem) {
|
|
260
|
+
return {
|
|
261
|
+
urlPath: route.urlPath,
|
|
262
|
+
pathItem: route.exactPathItem,
|
|
263
|
+
fromCache: false,
|
|
264
|
+
skippedLLM: true
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const hash = computeHash(route.sourceCode, options.provider, modelId);
|
|
269
|
+
if (cache) {
|
|
270
|
+
const cached = cache.get(hash);
|
|
271
|
+
if (cached) {
|
|
272
|
+
return {
|
|
273
|
+
urlPath: route.urlPath,
|
|
274
|
+
pathItem: cached,
|
|
275
|
+
fromCache: true,
|
|
276
|
+
skippedLLM: true
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const pathItem = await callLLM(route, options.jsdocMode, getModel());
|
|
281
|
+
if (cache) {
|
|
282
|
+
cache.set(hash, pathItem);
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
urlPath: route.urlPath,
|
|
286
|
+
pathItem,
|
|
287
|
+
fromCache: false,
|
|
288
|
+
skippedLLM: false
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function buildPrompt(route, jsdocMode) {
|
|
292
|
+
const jsDocSection = route.jsdocComments.length > 0 ? `JSDoc COMMENTS (use as ${jsdocMode === "context" ? "additional context" : "primary source"}):
|
|
293
|
+
${route.jsdocComments.join("\n\n")}` : "No JSDoc comments found.";
|
|
294
|
+
return `You are an OpenAPI 3.1 specification generator. Analyze the following Next.js API route and extract a complete OpenAPI PathItem object.
|
|
295
|
+
|
|
296
|
+
FILE PATH: ${route.relativePath}
|
|
297
|
+
INFERRED URL PATH: ${route.urlPath}
|
|
298
|
+
|
|
299
|
+
SOURCE CODE:
|
|
300
|
+
\`\`\`typescript
|
|
301
|
+
${route.sourceCode}
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
${jsDocSection}
|
|
305
|
+
|
|
306
|
+
Extract the following for EACH exported HTTP method handler (GET, POST, PUT, PATCH, DELETE):
|
|
307
|
+
- operationId (camelCase, unique)
|
|
308
|
+
- summary (short description)
|
|
309
|
+
- description (detailed description)
|
|
310
|
+
- path parameters (from URL segments like [id])
|
|
311
|
+
- query parameters (from NextRequest.nextUrl.searchParams usage)
|
|
312
|
+
- request body schema (from request.json() usage and TypeScript types)
|
|
313
|
+
- response schemas (per status code, from NextResponse.json() calls and return types)
|
|
314
|
+
- tags (infer from path segments)
|
|
315
|
+
- security requirements (if auth middleware or token checks are present)
|
|
316
|
+
|
|
317
|
+
Return ONLY a valid JSON object matching the OpenAPI 3.1 PathItem schema. No explanation, no markdown, no code blocks. Just the raw JSON object.`;
|
|
318
|
+
}
|
|
319
|
+
async function callLLM(route, jsdocMode, model) {
|
|
320
|
+
const prompt = buildPrompt(route, jsdocMode);
|
|
321
|
+
const { text } = await ai.generateText({
|
|
322
|
+
model,
|
|
323
|
+
prompt,
|
|
324
|
+
temperature: 0
|
|
325
|
+
});
|
|
326
|
+
return parsePathItem(text, route.urlPath);
|
|
327
|
+
}
|
|
328
|
+
function parsePathItem(text, urlPath) {
|
|
329
|
+
let json = text.trim();
|
|
330
|
+
if (json.startsWith("```")) {
|
|
331
|
+
json = json.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim();
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const parsed = JSON.parse(json);
|
|
335
|
+
if (typeof parsed !== "object" || Array.isArray(parsed) || parsed === null) {
|
|
336
|
+
throw new Error("Response is not a JSON object");
|
|
337
|
+
}
|
|
338
|
+
return parsed;
|
|
339
|
+
} catch (err) {
|
|
340
|
+
console.warn(
|
|
341
|
+
`Warning: Failed to parse LLM response for ${urlPath}. Using empty PathItem.`,
|
|
342
|
+
err
|
|
343
|
+
);
|
|
344
|
+
return {};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function assembleSpec(config, routes) {
|
|
348
|
+
const paths = {};
|
|
349
|
+
for (const route of routes) {
|
|
350
|
+
if (Object.keys(route.pathItem).length > 0) {
|
|
351
|
+
paths[route.urlPath] = route.pathItem;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const spec = {
|
|
355
|
+
openapi: "3.1.0",
|
|
356
|
+
info: {
|
|
357
|
+
title: config.openapi.title,
|
|
358
|
+
version: config.openapi.version,
|
|
359
|
+
...config.openapi.description ? { description: config.openapi.description } : {}
|
|
360
|
+
},
|
|
361
|
+
paths
|
|
362
|
+
};
|
|
363
|
+
if (config.openapi.servers && config.openapi.servers.length > 0) {
|
|
364
|
+
spec.servers = config.openapi.servers;
|
|
365
|
+
}
|
|
366
|
+
return spec;
|
|
367
|
+
}
|
|
368
|
+
function writeOutputFiles(config, spec, cwd = process.cwd()) {
|
|
369
|
+
writeSpecFiles(config, spec, cwd);
|
|
370
|
+
if (config.output.scalarDocs) {
|
|
371
|
+
writeScalarRoute(config, cwd);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function writeSpecFiles(config, spec, cwd) {
|
|
375
|
+
const specRoutePath = path.resolve(cwd, config.output.specPath);
|
|
376
|
+
const specDir = path.dirname(specRoutePath);
|
|
377
|
+
ensureDir(specDir);
|
|
378
|
+
const specJsonPath = path.join(specDir, "spec.json");
|
|
379
|
+
fs.writeFileSync(specJsonPath, JSON.stringify(spec, null, 2), "utf8");
|
|
380
|
+
const routeContent = `import spec from './spec.json';
|
|
381
|
+
|
|
382
|
+
export const dynamic = 'force-static';
|
|
383
|
+
|
|
384
|
+
export function GET() {
|
|
385
|
+
return Response.json(spec);
|
|
386
|
+
}
|
|
387
|
+
`;
|
|
388
|
+
fs.writeFileSync(specRoutePath, routeContent, "utf8");
|
|
389
|
+
}
|
|
390
|
+
function writeScalarRoute(config, cwd) {
|
|
391
|
+
const scalarRoutePath = path.resolve(cwd, config.output.scalarPath);
|
|
392
|
+
ensureDir(path.dirname(scalarRoutePath));
|
|
393
|
+
const specUrl = filePathToApiUrl(config.output.specPath);
|
|
394
|
+
const routeContent = `export const dynamic = 'force-static';
|
|
395
|
+
|
|
396
|
+
export function GET() {
|
|
397
|
+
return new Response(
|
|
398
|
+
\`<!doctype html>
|
|
399
|
+
<html>
|
|
400
|
+
<head>
|
|
401
|
+
<title>API Docs</title>
|
|
402
|
+
<meta charset="utf-8" />
|
|
403
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
404
|
+
</head>
|
|
405
|
+
<body>
|
|
406
|
+
<script
|
|
407
|
+
id="api-reference"
|
|
408
|
+
data-url="${specUrl}"
|
|
409
|
+
></script>
|
|
410
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
411
|
+
</body>
|
|
412
|
+
</html>\`,
|
|
413
|
+
{ headers: { 'Content-Type': 'text/html' } }
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
`;
|
|
417
|
+
fs.writeFileSync(scalarRoutePath, routeContent, "utf8");
|
|
418
|
+
}
|
|
419
|
+
function filePathToApiUrl(filePath) {
|
|
420
|
+
let path = filePath.replace(/\\/g, "/");
|
|
421
|
+
path = path.replace(/^(src\/)?app\//, "");
|
|
422
|
+
path = path.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
|
|
423
|
+
if (!path.startsWith("/")) path = "/" + path;
|
|
424
|
+
return path;
|
|
425
|
+
}
|
|
426
|
+
function ensureDir(dir) {
|
|
427
|
+
if (!fs.existsSync(dir)) {
|
|
428
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/index.ts
|
|
433
|
+
async function generate(options = {}) {
|
|
434
|
+
const cwd = options.cwd ?? process.cwd();
|
|
435
|
+
const config = await loadConfig(options.config);
|
|
436
|
+
if (options.provider) config.provider = options.provider;
|
|
437
|
+
if (options.cache === false) config.cache = false;
|
|
438
|
+
console.log(`[openapi-ai-generator] Scanning routes...`);
|
|
439
|
+
const routes = await scanRoutes(config.include, config.exclude, cwd);
|
|
440
|
+
console.log(`[openapi-ai-generator] Found ${routes.length} route(s)`);
|
|
441
|
+
console.log(`[openapi-ai-generator] Analyzing routes with provider: ${config.provider}`);
|
|
442
|
+
const analyzed = await analyzeRoutes(routes, {
|
|
443
|
+
provider: config.provider,
|
|
444
|
+
jsdocMode: config.jsdocMode,
|
|
445
|
+
cache: config.cache,
|
|
446
|
+
cacheDir: config.cacheDir
|
|
447
|
+
});
|
|
448
|
+
const fromCache = analyzed.filter((r) => r.fromCache).length;
|
|
449
|
+
const skippedLLM = analyzed.filter((r) => r.skippedLLM).length;
|
|
450
|
+
console.log(
|
|
451
|
+
`[openapi-ai-generator] ${analyzed.length} routes analyzed (${fromCache} from cache, ${skippedLLM - fromCache} exact JSDoc)`
|
|
452
|
+
);
|
|
453
|
+
const spec = assembleSpec(config, analyzed);
|
|
454
|
+
writeOutputFiles(config, spec, cwd);
|
|
455
|
+
console.log(`[openapi-ai-generator] Spec written to ${config.output.specPath}`);
|
|
456
|
+
if (config.output.scalarDocs) {
|
|
457
|
+
console.log(`[openapi-ai-generator] Scalar docs written to ${config.output.scalarPath}`);
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
routesAnalyzed: analyzed.length,
|
|
461
|
+
routesFromCache: fromCache,
|
|
462
|
+
routesSkippedLLM: skippedLLM,
|
|
463
|
+
specPath: config.output.specPath
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
exports.analyzeRoutes = analyzeRoutes;
|
|
468
|
+
exports.assembleSpec = assembleSpec;
|
|
469
|
+
exports.filePathToUrlPath = filePathToUrlPath;
|
|
470
|
+
exports.generate = generate;
|
|
471
|
+
exports.loadConfig = loadConfig;
|
|
472
|
+
exports.resolveConfig = resolveConfig;
|
|
473
|
+
exports.scanRoutes = scanRoutes;
|
|
474
|
+
exports.writeOutputFiles = writeOutputFiles;
|
|
475
|
+
//# sourceMappingURL=index.js.map
|
|
476
|
+
//# sourceMappingURL=index.js.map
|