@open-mercato/cli 0.4.9-develop-8c36c096d5 → 0.4.9-develop-31d1a87765
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.
|
@@ -103,6 +103,289 @@ function parseOpenApiFromSource(filePath) {
|
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
async function generateOpenApiViaBundle(routes, projectRoot, quiet) {
|
|
107
|
+
let esbuild;
|
|
108
|
+
try {
|
|
109
|
+
esbuild = await import("esbuild");
|
|
110
|
+
} catch {
|
|
111
|
+
if (!quiet) console.log("[OpenAPI] esbuild not available, skipping bundle approach");
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const { execFileSync } = await import("node:child_process");
|
|
115
|
+
const cacheDir = path.join(projectRoot, "node_modules", ".cache");
|
|
116
|
+
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
|
|
117
|
+
const bundlePath = path.join(cacheDir, "_openapi-bundle.mjs");
|
|
118
|
+
const tsconfigPath = path.join(projectRoot, "tsconfig.base.json");
|
|
119
|
+
const generatorPath = path.join(
|
|
120
|
+
projectRoot,
|
|
121
|
+
"packages",
|
|
122
|
+
"shared",
|
|
123
|
+
"src",
|
|
124
|
+
"lib",
|
|
125
|
+
"openapi",
|
|
126
|
+
"generator.ts"
|
|
127
|
+
);
|
|
128
|
+
const importLines = [
|
|
129
|
+
`import { buildOpenApiDocument } from ${JSON.stringify(generatorPath)};`
|
|
130
|
+
];
|
|
131
|
+
const routeMapLines = [];
|
|
132
|
+
for (let i = 0; i < routes.length; i++) {
|
|
133
|
+
const route = routes[i];
|
|
134
|
+
importLines.push(`import * as R${i} from ${JSON.stringify(route.path)};`);
|
|
135
|
+
const bracketPath = route.openApiPath.replace(/\{([^}]+)\}/g, "[$1]");
|
|
136
|
+
routeMapLines.push(` [${JSON.stringify(bracketPath)}, R${i}],`);
|
|
137
|
+
}
|
|
138
|
+
const entryScript = `${importLines.join("\n")}
|
|
139
|
+
|
|
140
|
+
const routeEntries = [
|
|
141
|
+
${routeMapLines.join("\n")}
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const modules = new Map();
|
|
145
|
+
for (const [apiPath, mod] of routeEntries) {
|
|
146
|
+
const moduleId = apiPath.replace(/^\\/api\\//, '').split('/')[0];
|
|
147
|
+
if (!modules.has(moduleId)) modules.set(moduleId, { id: moduleId, apis: [] });
|
|
148
|
+
modules.get(moduleId).apis.push({
|
|
149
|
+
path: apiPath,
|
|
150
|
+
handlers: mod,
|
|
151
|
+
metadata: mod.metadata,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const doc = buildOpenApiDocument([...modules.values()], {
|
|
156
|
+
title: 'Open Mercato API',
|
|
157
|
+
version: '1.0.0',
|
|
158
|
+
description: 'Auto-generated OpenAPI specification',
|
|
159
|
+
servers: [{ url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' }],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Deep-clone to break shared object references before serializing.
|
|
163
|
+
// The zodToJsonSchema memo cache returns the same object instance for
|
|
164
|
+
// fields like currencyCode that appear on both parent and child schemas.
|
|
165
|
+
// A naive WeakSet-based circular-ref guard would drop the second occurrence,
|
|
166
|
+
// causing properties to vanish from the generated spec (while the field
|
|
167
|
+
// still appears in the 'required' array, since those are plain strings).
|
|
168
|
+
const deepClone = (v, ancestors = []) => {
|
|
169
|
+
if (v === null || typeof v !== 'object') return v;
|
|
170
|
+
if (typeof v === 'bigint') return Number(v);
|
|
171
|
+
if (typeof v === 'function') return undefined;
|
|
172
|
+
if (ancestors.includes(v)) return undefined; // true circular ref
|
|
173
|
+
const next = [...ancestors, v];
|
|
174
|
+
if (Array.isArray(v)) return v.map((item) => deepClone(item, next));
|
|
175
|
+
const out = {};
|
|
176
|
+
for (const [k, val] of Object.entries(v)) {
|
|
177
|
+
const cloned = deepClone(val, next);
|
|
178
|
+
if (cloned !== undefined) out[k] = cloned;
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
};
|
|
182
|
+
process.stdout.write(JSON.stringify(deepClone(doc), (_, v) =>
|
|
183
|
+
typeof v === 'bigint' ? Number(v) : v
|
|
184
|
+
));
|
|
185
|
+
`;
|
|
186
|
+
const stubNextPlugin = {
|
|
187
|
+
name: "stub-next",
|
|
188
|
+
setup(build) {
|
|
189
|
+
build.onResolve({ filter: /^next($|\/)/ }, () => ({
|
|
190
|
+
path: "next-stub",
|
|
191
|
+
namespace: "next-stub"
|
|
192
|
+
}));
|
|
193
|
+
build.onLoad({ filter: /.*/, namespace: "next-stub" }, () => ({
|
|
194
|
+
contents: [
|
|
195
|
+
"const p = new Proxy(function(){}, {",
|
|
196
|
+
' get(_, k) { return k === "__esModule" ? true : k === "default" ? p : p; },',
|
|
197
|
+
" apply() { return p; },",
|
|
198
|
+
" construct() { return p; },",
|
|
199
|
+
"});",
|
|
200
|
+
"export default p;",
|
|
201
|
+
"export const NextRequest = p, NextResponse = p, headers = p, cookies = p;",
|
|
202
|
+
"export const redirect = p, notFound = p, useRouter = p, usePathname = p;",
|
|
203
|
+
"export const useSearchParams = p, permanentRedirect = p, revalidatePath = p;"
|
|
204
|
+
].join("\n"),
|
|
205
|
+
loader: "js"
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
const appRoot = path.join(projectRoot, "apps", "mercato");
|
|
210
|
+
const resolveWorkspacePlugin = {
|
|
211
|
+
name: "resolve-workspace",
|
|
212
|
+
setup(build) {
|
|
213
|
+
build.onResolve({ filter: /^@open-mercato\// }, (args) => {
|
|
214
|
+
const withoutScope = args.path.slice("@open-mercato/".length);
|
|
215
|
+
const slashIdx = withoutScope.indexOf("/");
|
|
216
|
+
const pkg = slashIdx === -1 ? withoutScope : withoutScope.slice(0, slashIdx);
|
|
217
|
+
const rest = slashIdx === -1 ? "" : withoutScope.slice(slashIdx + 1);
|
|
218
|
+
const base = rest ? path.join(projectRoot, "packages", pkg, "src", rest) : path.join(projectRoot, "packages", pkg, "src", "index");
|
|
219
|
+
for (const ext of [".ts", ".tsx", "/index.ts", "/index.tsx"]) {
|
|
220
|
+
if (fs.existsSync(base + ext)) return { path: base + ext };
|
|
221
|
+
}
|
|
222
|
+
return void 0;
|
|
223
|
+
});
|
|
224
|
+
build.onResolve({ filter: /^@\/\.mercato\// }, (args) => {
|
|
225
|
+
const rest = args.path.slice("@/".length);
|
|
226
|
+
const base = path.join(appRoot, rest);
|
|
227
|
+
for (const ext of [".ts", ".tsx", "/index.ts", "/index.tsx", ""]) {
|
|
228
|
+
if (fs.existsSync(base + ext)) return { path: base + ext };
|
|
229
|
+
}
|
|
230
|
+
return void 0;
|
|
231
|
+
});
|
|
232
|
+
build.onResolve({ filter: /^@\// }, (args) => {
|
|
233
|
+
const rest = args.path.slice("@/".length);
|
|
234
|
+
const base = path.join(appRoot, "src", rest);
|
|
235
|
+
for (const ext of [".ts", ".tsx", "/index.ts", "/index.tsx"]) {
|
|
236
|
+
if (fs.existsSync(base + ext)) return { path: base + ext };
|
|
237
|
+
}
|
|
238
|
+
return void 0;
|
|
239
|
+
});
|
|
240
|
+
build.onResolve({ filter: /^#generated\// }, (args) => {
|
|
241
|
+
const rest = args.path.slice("#generated/".length);
|
|
242
|
+
const coreGenerated = path.join(projectRoot, "packages", "core", "generated");
|
|
243
|
+
const base = path.join(coreGenerated, rest);
|
|
244
|
+
for (const ext of [".ts", "/index.ts"]) {
|
|
245
|
+
if (fs.existsSync(base + ext)) return { path: base + ext };
|
|
246
|
+
}
|
|
247
|
+
return void 0;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const nodeBuiltins = /* @__PURE__ */ new Set([
|
|
252
|
+
"assert",
|
|
253
|
+
"buffer",
|
|
254
|
+
"child_process",
|
|
255
|
+
"cluster",
|
|
256
|
+
"console",
|
|
257
|
+
"constants",
|
|
258
|
+
"crypto",
|
|
259
|
+
"dgram",
|
|
260
|
+
"dns",
|
|
261
|
+
"domain",
|
|
262
|
+
"events",
|
|
263
|
+
"fs",
|
|
264
|
+
"http",
|
|
265
|
+
"http2",
|
|
266
|
+
"https",
|
|
267
|
+
"module",
|
|
268
|
+
"net",
|
|
269
|
+
"os",
|
|
270
|
+
"path",
|
|
271
|
+
"perf_hooks",
|
|
272
|
+
"process",
|
|
273
|
+
"punycode",
|
|
274
|
+
"querystring",
|
|
275
|
+
"readline",
|
|
276
|
+
"repl",
|
|
277
|
+
"stream",
|
|
278
|
+
"string_decoder",
|
|
279
|
+
"sys",
|
|
280
|
+
"timers",
|
|
281
|
+
"tls",
|
|
282
|
+
"tty",
|
|
283
|
+
"url",
|
|
284
|
+
"util",
|
|
285
|
+
"v8",
|
|
286
|
+
"vm",
|
|
287
|
+
"wasi",
|
|
288
|
+
"worker_threads",
|
|
289
|
+
"zlib",
|
|
290
|
+
"async_hooks",
|
|
291
|
+
"diagnostics_channel",
|
|
292
|
+
"inspector",
|
|
293
|
+
"trace_events"
|
|
294
|
+
]);
|
|
295
|
+
const externalNonWorkspacePlugin = {
|
|
296
|
+
name: "external-non-workspace",
|
|
297
|
+
setup(build) {
|
|
298
|
+
build.onResolve({ filter: /^[^./]/ }, (args) => {
|
|
299
|
+
if (args.path.startsWith("@open-mercato/")) return void 0;
|
|
300
|
+
if (args.path.startsWith("@/")) return void 0;
|
|
301
|
+
if (args.path.startsWith("#generated/")) return void 0;
|
|
302
|
+
if (args.path.startsWith("next")) return void 0;
|
|
303
|
+
if (args.path.startsWith("node:")) return void 0;
|
|
304
|
+
const topLevel = args.path.split("/")[0];
|
|
305
|
+
if (nodeBuiltins.has(topLevel)) return void 0;
|
|
306
|
+
const pkgName = args.path.startsWith("@") ? args.path.split("/").slice(0, 2).join("/") : topLevel;
|
|
307
|
+
const pkgDir = path.join(projectRoot, "node_modules", pkgName);
|
|
308
|
+
if (fs.existsSync(pkgDir)) return { external: true };
|
|
309
|
+
return { path: args.path, namespace: "missing-pkg" };
|
|
310
|
+
});
|
|
311
|
+
build.onLoad({ filter: /.*/, namespace: "missing-pkg" }, () => ({
|
|
312
|
+
contents: 'var h={get:(_,k)=>k==="__esModule"?true:p};var p=new Proxy(function(){return p},{get:h.get,apply:()=>p,construct:()=>p});module.exports=p;',
|
|
313
|
+
loader: "js"
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
try {
|
|
318
|
+
await esbuild.build({
|
|
319
|
+
stdin: {
|
|
320
|
+
contents: entryScript,
|
|
321
|
+
resolveDir: projectRoot,
|
|
322
|
+
sourcefile: "openapi-entry.ts",
|
|
323
|
+
loader: "ts"
|
|
324
|
+
},
|
|
325
|
+
bundle: true,
|
|
326
|
+
format: "esm",
|
|
327
|
+
platform: "node",
|
|
328
|
+
target: "node18",
|
|
329
|
+
outfile: bundlePath,
|
|
330
|
+
write: true,
|
|
331
|
+
tsconfig: tsconfigPath,
|
|
332
|
+
logLevel: "silent",
|
|
333
|
+
jsx: "automatic",
|
|
334
|
+
plugins: [stubNextPlugin, resolveWorkspacePlugin, externalNonWorkspacePlugin]
|
|
335
|
+
});
|
|
336
|
+
const stdout = execFileSync(process.execPath, [bundlePath], {
|
|
337
|
+
timeout: 6e4,
|
|
338
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
339
|
+
encoding: "utf-8",
|
|
340
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
|
341
|
+
cwd: projectRoot
|
|
342
|
+
});
|
|
343
|
+
const lastLine = stdout.trim().split("\n").pop();
|
|
344
|
+
const doc = JSON.parse(lastLine);
|
|
345
|
+
if (!quiet) {
|
|
346
|
+
const pathCount = Object.keys(doc.paths || {}).length;
|
|
347
|
+
const withBody = Object.values(doc.paths || {}).reduce((n, methods) => {
|
|
348
|
+
for (const m of Object.values(methods)) {
|
|
349
|
+
if (m?.requestBody) n++;
|
|
350
|
+
}
|
|
351
|
+
return n;
|
|
352
|
+
}, 0);
|
|
353
|
+
console.log(`[OpenAPI] Bundle approach: ${pathCount} paths, ${withBody} with requestBody schemas`);
|
|
354
|
+
}
|
|
355
|
+
return doc;
|
|
356
|
+
} catch (err) {
|
|
357
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
358
|
+
const stderr = err?.stderr;
|
|
359
|
+
const esbuildErrors = err?.errors;
|
|
360
|
+
if (!quiet) {
|
|
361
|
+
console.log(`[OpenAPI] Bundle approach failed, will use static fallback: ${errMsg.split("\n")[0]}`);
|
|
362
|
+
if (esbuildErrors?.length) {
|
|
363
|
+
const unique = /* @__PURE__ */ new Map();
|
|
364
|
+
for (const e of esbuildErrors) {
|
|
365
|
+
const key = e.text;
|
|
366
|
+
if (!unique.has(key)) unique.set(key, e.location?.file ?? "");
|
|
367
|
+
}
|
|
368
|
+
for (const [text, file] of [...unique.entries()].slice(0, 10)) {
|
|
369
|
+
console.log(`[OpenAPI] ${text}${file ? ` (${path.basename(file)})` : ""}`);
|
|
370
|
+
}
|
|
371
|
+
if (unique.size > 10) console.log(`[OpenAPI] ... and ${unique.size - 10} more`);
|
|
372
|
+
}
|
|
373
|
+
if (stderr) {
|
|
374
|
+
for (const line of String(stderr).trim().split("\n").slice(0, 3)) {
|
|
375
|
+
console.log(`[OpenAPI] ${line}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
} finally {
|
|
381
|
+
for (const file of ["_openapi-register.mjs", "_openapi-loader.mjs", "_next-stub.cjs"]) {
|
|
382
|
+
try {
|
|
383
|
+
fs.unlinkSync(path.join(cacheDir, file));
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
106
389
|
function buildOpenApiPaths(routes) {
|
|
107
390
|
const paths = {};
|
|
108
391
|
for (const route of routes) {
|
|
@@ -144,30 +427,39 @@ async function generateOpenApi(options) {
|
|
|
144
427
|
if (!quiet) {
|
|
145
428
|
console.log(`[OpenAPI] Found ${routes.length} API route files`);
|
|
146
429
|
}
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
430
|
+
const projectRoot = path.resolve(
|
|
431
|
+
path.dirname(new URL(import.meta.url).pathname),
|
|
432
|
+
"../../../../.."
|
|
433
|
+
);
|
|
434
|
+
let doc = await generateOpenApiViaBundle(routes, projectRoot, quiet);
|
|
435
|
+
if (!doc) {
|
|
436
|
+
if (!quiet) {
|
|
437
|
+
console.log("[OpenAPI] Falling back to static regex approach");
|
|
438
|
+
}
|
|
439
|
+
const paths = buildOpenApiPaths(routes);
|
|
440
|
+
doc = {
|
|
441
|
+
openapi: "3.1.0",
|
|
442
|
+
info: {
|
|
443
|
+
title: "Open Mercato API",
|
|
444
|
+
version: "1.0.0",
|
|
445
|
+
description: "Auto-generated OpenAPI specification"
|
|
446
|
+
},
|
|
447
|
+
servers: [
|
|
448
|
+
{ url: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000" }
|
|
449
|
+
],
|
|
450
|
+
paths,
|
|
451
|
+
components: {
|
|
452
|
+
securitySchemes: {
|
|
453
|
+
bearerAuth: {
|
|
454
|
+
type: "http",
|
|
455
|
+
scheme: "bearer",
|
|
456
|
+
bearerFormat: "JWT",
|
|
457
|
+
description: "Send an `Authorization: Bearer <token>` header with a valid API token."
|
|
458
|
+
}
|
|
167
459
|
}
|
|
168
460
|
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
171
463
|
const output = JSON.stringify(doc, null, 2);
|
|
172
464
|
const checksum = calculateChecksum(output);
|
|
173
465
|
const existingChecksums = readChecksumRecord(checksumFile);
|
|
@@ -183,6 +475,7 @@ async function generateOpenApi(options) {
|
|
|
183
475
|
result.filesWritten.push(outFile);
|
|
184
476
|
if (!quiet) {
|
|
185
477
|
logGenerationResult(outFile, true);
|
|
478
|
+
const pathCount = Object.keys(doc.paths || {}).length;
|
|
186
479
|
console.log(`[OpenAPI] Generated ${pathCount} API paths`);
|
|
187
480
|
}
|
|
188
481
|
return result;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/generators/openapi.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * OpenAPI JSON Generator\n *\n * Generates a static openapi.generated.json file at build time.\n * This allows CLI tools (like MCP dev server) to access API endpoint\n * information without requiring a running Next.js app.\n */\n\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport type { PackageResolver } from '../resolver'\nimport {\n calculateChecksum,\n readChecksumRecord,\n writeChecksumRecord,\n logGenerationResult,\n type GeneratorResult,\n createGeneratorResult,\n} from '../utils'\n\nexport interface GenerateOpenApiOptions {\n resolver: PackageResolver\n quiet?: boolean\n}\n\ntype HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n\ninterface ApiRouteInfo {\n path: string\n methods: HttpMethod[]\n openApiPath: string\n}\n\n/**\n * Find all API route files and extract their OpenAPI specs.\n */\nasync function findApiRoutes(resolver: PackageResolver): Promise<ApiRouteInfo[]> {\n const routes: ApiRouteInfo[] = []\n const enabled = resolver.loadEnabledModules()\n\n for (const entry of enabled) {\n const modId = entry.id\n const roots = resolver.getModulePaths(entry)\n\n const apiApp = path.join(roots.appBase, 'api')\n const apiPkg = path.join(roots.pkgBase, 'api')\n\n // Scan route files\n const routeFiles: Array<{ relativePath: string; fullPath: string }> = []\n\n const walkDir = (dir: string, rel: string[] = []) => {\n if (!fs.existsSync(dir)) return\n for (const e of fs.readdirSync(dir, { withFileTypes: true })) {\n if (e.isDirectory()) {\n if (e.name === '__tests__' || e.name === '__mocks__') continue\n walkDir(path.join(dir, e.name), [...rel, e.name])\n } else if (e.isFile() && e.name === 'route.ts') {\n routeFiles.push({\n relativePath: [...rel].join('/'),\n fullPath: path.join(dir, e.name),\n })\n }\n }\n }\n\n // Scan package first, then app (app overrides)\n if (fs.existsSync(apiPkg)) walkDir(apiPkg)\n if (fs.existsSync(apiApp)) walkDir(apiApp)\n\n // Process unique routes (app overrides package)\n const seen = new Set<string>()\n for (const { relativePath, fullPath } of routeFiles) {\n if (seen.has(relativePath)) continue\n seen.add(relativePath)\n\n // Build API path\n const routeSegs = relativePath ? relativePath.split('/') : []\n const apiPath = `/api/${modId}${routeSegs.length ? '/' + routeSegs.join('/') : ''}`\n // Convert [param] to {param} for OpenAPI format\n .replace(/\\[([^\\]]+)\\]/g, '{$1}')\n\n routes.push({\n path: fullPath,\n methods: await detectMethods(fullPath),\n openApiPath: apiPath,\n })\n }\n }\n\n return routes\n}\n\n/**\n * Detect which HTTP methods are exported from a route file.\n */\nasync function detectMethods(filePath: string): Promise<HttpMethod[]> {\n const methods: HttpMethod[] = []\n const content = fs.readFileSync(filePath, 'utf-8')\n\n const methodPatterns: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']\n for (const method of methodPatterns) {\n // Check for export { GET }, export const GET, export async function GET\n const patterns = [\n new RegExp(`export\\\\s+(const|async\\\\s+function|function)\\\\s+${method}\\\\b`),\n new RegExp(`export\\\\s*\\\\{[^}]*\\\\b${method}\\\\b[^}]*\\\\}`),\n ]\n if (patterns.some((p) => p.test(content))) {\n methods.push(method)\n }\n }\n\n return methods\n}\n\n/**\n * Parse openApi export from route file source code statically.\n * This extracts basic operation info without needing to compile the file.\n */\nfunction parseOpenApiFromSource(filePath: string): Record<string, any> | null {\n try {\n const content = fs.readFileSync(filePath, 'utf-8')\n\n // Check if file exports openApi\n if (!content.includes('export const openApi') && !content.includes('export { openApi')) {\n return null\n }\n\n // Extract operationId, summary, description from the source\n const result: Record<string, any> = {}\n const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']\n\n for (const method of methods) {\n // Look for method specs in the openApi object\n // Pattern: GET: { operationId: '...', summary: '...', ... }\n const methodPattern = new RegExp(\n `${method}\\\\s*:\\\\s*\\\\{([^}]+(?:\\\\{[^}]*\\\\}[^}]*)*)\\\\}`,\n 's'\n )\n const methodMatch = content.match(methodPattern)\n\n if (methodMatch) {\n const methodContent = methodMatch[1]\n const spec: Record<string, any> = {}\n\n // Extract operationId\n const opIdMatch = methodContent.match(/operationId\\s*:\\s*['\"]([^'\"]+)['\"]/)\n if (opIdMatch) spec.operationId = opIdMatch[1]\n\n // Extract summary\n const summaryMatch = methodContent.match(/summary\\s*:\\s*['\"]([^'\"]+)['\"]/)\n if (summaryMatch) spec.summary = summaryMatch[1]\n\n // Extract description\n const descMatch = methodContent.match(/description\\s*:\\s*['\"]([^'\"]+)['\"]/)\n if (descMatch) spec.description = descMatch[1]\n\n // Extract tags\n const tagsMatch = methodContent.match(/tags\\s*:\\s*\\[([^\\]]*)\\]/)\n if (tagsMatch) {\n const tagsContent = tagsMatch[1]\n const tags = tagsContent.match(/['\"]([^'\"]+)['\"]/g)\n if (tags) {\n spec.tags = tags.map(t => t.replace(/['\"]/g, ''))\n }\n }\n\n if (Object.keys(spec).length > 0) {\n result[method] = spec\n }\n }\n }\n\n return Object.keys(result).length > 0 ? result : null\n } catch {\n return null\n }\n}\n\n/**\n * Build OpenAPI paths from discovered routes.\n * Extracts basic operation info from route files statically.\n */\nfunction buildOpenApiPaths(routes: ApiRouteInfo[]): Record<string, any> {\n const paths: Record<string, any> = {}\n\n for (const route of routes) {\n const pathEntry: Record<string, any> = {}\n\n // Try to extract OpenAPI specs from source\n const openApiSpec = parseOpenApiFromSource(route.path)\n\n for (const method of route.methods) {\n const methodLower = method.toLowerCase()\n const spec = openApiSpec?.[method]\n\n // Generate a default operationId if not found\n const pathSegments = route.openApiPath\n .replace(/^\\/api\\//, '')\n .replace(/\\{[^}]+\\}/g, 'by_id')\n .split('/')\n .filter(Boolean)\n .join('_')\n const defaultOperationId = `${methodLower}_${pathSegments}`\n\n pathEntry[methodLower] = {\n operationId: spec?.operationId || defaultOperationId,\n summary: spec?.summary || `${method} ${route.openApiPath}`,\n description: spec?.description || `${method} operation for ${route.openApiPath}`,\n tags: spec?.tags || [route.openApiPath.split('/')[2] || 'api'],\n responses: {\n '200': {\n description: 'Successful response',\n },\n },\n }\n }\n\n if (Object.keys(pathEntry).length > 0) {\n paths[route.openApiPath] = pathEntry\n }\n }\n\n return paths\n}\n\n/**\n * Generate the OpenAPI JSON file.\n */\nexport async function generateOpenApi(options: GenerateOpenApiOptions): Promise<GeneratorResult> {\n const { resolver, quiet = false } = options\n const result = createGeneratorResult()\n\n const outputDir = resolver.getOutputDir()\n const outFile = path.join(outputDir, 'openapi.generated.json')\n const checksumFile = path.join(outputDir, 'openapi.generated.checksum')\n\n // Ensure output directory exists\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true })\n }\n\n // Find all API routes\n const routes = await findApiRoutes(resolver)\n\n if (!quiet) {\n console.log(`[OpenAPI] Found ${routes.length} API route files`)\n }\n\n // Build OpenAPI paths from routes\n const paths = buildOpenApiPaths(routes)\n const pathCount = Object.keys(paths).length\n\n // Build OpenAPI document\n const doc = {\n openapi: '3.1.0',\n info: {\n title: 'Open Mercato API',\n version: '1.0.0',\n description: 'Auto-generated OpenAPI specification',\n },\n servers: [\n { url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' },\n ],\n paths,\n components: {\n securitySchemes: {\n bearerAuth: {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n description: 'Send an `Authorization: Bearer <token>` header with a valid API token.',\n },\n },\n },\n }\n\n const output = JSON.stringify(doc, null, 2)\n const checksum = calculateChecksum(output)\n\n // Check if unchanged\n const existingChecksums = readChecksumRecord(checksumFile)\n if (existingChecksums && existingChecksums.content === checksum && fs.existsSync(outFile)) {\n result.filesUnchanged.push(outFile)\n if (!quiet) {\n console.log(`[OpenAPI] Skipped (unchanged): ${outFile}`)\n }\n return result\n }\n\n // Write the file\n fs.writeFileSync(outFile, output)\n writeChecksumRecord(checksumFile, { content: checksum, structure: '' })\n\n result.filesWritten.push(outFile)\n\n if (!quiet) {\n logGenerationResult(outFile, true)\n console.log(`[OpenAPI] Generated ${pathCount} API paths`)\n }\n\n return result\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\n * OpenAPI JSON Generator\n *\n * Generates a static openapi.generated.json file at build time.\n * This allows CLI tools (like MCP dev server) to access API endpoint\n * information without requiring a running Next.js app.\n */\n\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport type { PackageResolver } from '../resolver'\nimport {\n calculateChecksum,\n readChecksumRecord,\n writeChecksumRecord,\n logGenerationResult,\n type GeneratorResult,\n createGeneratorResult,\n} from '../utils'\n\nexport interface GenerateOpenApiOptions {\n resolver: PackageResolver\n quiet?: boolean\n}\n\ntype HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n\ninterface ApiRouteInfo {\n path: string\n methods: HttpMethod[]\n openApiPath: string\n}\n\n/**\n * Find all API route files and extract their OpenAPI specs.\n */\nasync function findApiRoutes(resolver: PackageResolver): Promise<ApiRouteInfo[]> {\n const routes: ApiRouteInfo[] = []\n const enabled = resolver.loadEnabledModules()\n\n for (const entry of enabled) {\n const modId = entry.id\n const roots = resolver.getModulePaths(entry)\n\n const apiApp = path.join(roots.appBase, 'api')\n const apiPkg = path.join(roots.pkgBase, 'api')\n\n // Scan route files\n const routeFiles: Array<{ relativePath: string; fullPath: string }> = []\n\n const walkDir = (dir: string, rel: string[] = []) => {\n if (!fs.existsSync(dir)) return\n for (const e of fs.readdirSync(dir, { withFileTypes: true })) {\n if (e.isDirectory()) {\n if (e.name === '__tests__' || e.name === '__mocks__') continue\n walkDir(path.join(dir, e.name), [...rel, e.name])\n } else if (e.isFile() && e.name === 'route.ts') {\n routeFiles.push({\n relativePath: [...rel].join('/'),\n fullPath: path.join(dir, e.name),\n })\n }\n }\n }\n\n // Scan package first, then app (app overrides)\n if (fs.existsSync(apiPkg)) walkDir(apiPkg)\n if (fs.existsSync(apiApp)) walkDir(apiApp)\n\n // Process unique routes (app overrides package)\n const seen = new Set<string>()\n for (const { relativePath, fullPath } of routeFiles) {\n if (seen.has(relativePath)) continue\n seen.add(relativePath)\n\n // Build API path\n const routeSegs = relativePath ? relativePath.split('/') : []\n const apiPath = `/api/${modId}${routeSegs.length ? '/' + routeSegs.join('/') : ''}`\n // Convert [param] to {param} for OpenAPI format\n .replace(/\\[([^\\]]+)\\]/g, '{$1}')\n\n routes.push({\n path: fullPath,\n methods: await detectMethods(fullPath),\n openApiPath: apiPath,\n })\n }\n }\n\n return routes\n}\n\n/**\n * Detect which HTTP methods are exported from a route file.\n */\nasync function detectMethods(filePath: string): Promise<HttpMethod[]> {\n const methods: HttpMethod[] = []\n const content = fs.readFileSync(filePath, 'utf-8')\n\n const methodPatterns: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']\n for (const method of methodPatterns) {\n // Check for export { GET }, export const GET, export async function GET\n const patterns = [\n new RegExp(`export\\\\s+(const|async\\\\s+function|function)\\\\s+${method}\\\\b`),\n new RegExp(`export\\\\s*\\\\{[^}]*\\\\b${method}\\\\b[^}]*\\\\}`),\n ]\n if (patterns.some((p) => p.test(content))) {\n methods.push(method)\n }\n }\n\n return methods\n}\n\n/**\n * Parse openApi export from route file source code statically.\n * This extracts basic operation info without needing to compile the file.\n */\nfunction parseOpenApiFromSource(filePath: string): Record<string, any> | null {\n try {\n const content = fs.readFileSync(filePath, 'utf-8')\n\n // Check if file exports openApi\n if (!content.includes('export const openApi') && !content.includes('export { openApi')) {\n return null\n }\n\n // Extract operationId, summary, description from the source\n const result: Record<string, any> = {}\n const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']\n\n for (const method of methods) {\n // Look for method specs in the openApi object\n // Pattern: GET: { operationId: '...', summary: '...', ... }\n const methodPattern = new RegExp(\n `${method}\\\\s*:\\\\s*\\\\{([^}]+(?:\\\\{[^}]*\\\\}[^}]*)*)\\\\}`,\n 's'\n )\n const methodMatch = content.match(methodPattern)\n\n if (methodMatch) {\n const methodContent = methodMatch[1]\n const spec: Record<string, any> = {}\n\n // Extract operationId\n const opIdMatch = methodContent.match(/operationId\\s*:\\s*['\"]([^'\"]+)['\"]/)\n if (opIdMatch) spec.operationId = opIdMatch[1]\n\n // Extract summary\n const summaryMatch = methodContent.match(/summary\\s*:\\s*['\"]([^'\"]+)['\"]/)\n if (summaryMatch) spec.summary = summaryMatch[1]\n\n // Extract description\n const descMatch = methodContent.match(/description\\s*:\\s*['\"]([^'\"]+)['\"]/)\n if (descMatch) spec.description = descMatch[1]\n\n // Extract tags\n const tagsMatch = methodContent.match(/tags\\s*:\\s*\\[([^\\]]*)\\]/)\n if (tagsMatch) {\n const tagsContent = tagsMatch[1]\n const tags = tagsContent.match(/['\"]([^'\"]+)['\"]/g)\n if (tags) {\n spec.tags = tags.map(t => t.replace(/['\"]/g, ''))\n }\n }\n\n if (Object.keys(spec).length > 0) {\n result[method] = spec\n }\n }\n }\n\n return Object.keys(result).length > 0 ? result : null\n } catch {\n return null\n }\n}\n\n/**\n * Generate a complete OpenAPI document by bundling route files with esbuild\n * and executing the bundle to call buildOpenApiDocument from @open-mercato/shared.\n *\n * esbuild compiles TypeScript with legacy decorator support (reads experimentalDecorators\n * from tsconfig.json), avoiding the TC39 decorator mismatch that breaks tsx-based imports.\n * External packages (zod, mikro-orm, etc.) are resolved from node_modules at runtime.\n */\nasync function generateOpenApiViaBundle(\n routes: ApiRouteInfo[],\n projectRoot: string,\n quiet: boolean\n): Promise<Record<string, any> | null> {\n let esbuild: typeof import('esbuild')\n try {\n esbuild = await import('esbuild')\n } catch {\n if (!quiet) console.log('[OpenAPI] esbuild not available, skipping bundle approach')\n return null\n }\n\n const { execFileSync } = await import('node:child_process')\n\n const cacheDir = path.join(projectRoot, 'node_modules', '.cache')\n if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true })\n\n const bundlePath = path.join(cacheDir, '_openapi-bundle.mjs')\n const tsconfigPath = path.join(projectRoot, 'tsconfig.base.json')\n const generatorPath = path.join(\n projectRoot, 'packages', 'shared', 'src', 'lib', 'openapi', 'generator.ts'\n )\n\n // Build the entry script that imports all routes and calls buildOpenApiDocument\n const importLines: string[] = [\n `import { buildOpenApiDocument } from ${JSON.stringify(generatorPath)};`,\n ]\n const routeMapLines: string[] = []\n\n for (let i = 0; i < routes.length; i++) {\n const route = routes[i]\n importLines.push(`import * as R${i} from ${JSON.stringify(route.path)};`)\n // Use [param] format so normalizePath in buildOpenApiDocument extracts path params\n const bracketPath = route.openApiPath.replace(/\\{([^}]+)\\}/g, '[$1]')\n routeMapLines.push(` [${JSON.stringify(bracketPath)}, R${i}],`)\n }\n\n const entryScript = `${importLines.join('\\n')}\n\nconst routeEntries = [\n${routeMapLines.join('\\n')}\n];\n\nconst modules = new Map();\nfor (const [apiPath, mod] of routeEntries) {\n const moduleId = apiPath.replace(/^\\\\/api\\\\//, '').split('/')[0];\n if (!modules.has(moduleId)) modules.set(moduleId, { id: moduleId, apis: [] });\n modules.get(moduleId).apis.push({\n path: apiPath,\n handlers: mod,\n metadata: mod.metadata,\n });\n}\n\nconst doc = buildOpenApiDocument([...modules.values()], {\n title: 'Open Mercato API',\n version: '1.0.0',\n description: 'Auto-generated OpenAPI specification',\n servers: [{ url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' }],\n});\n\n// Deep-clone to break shared object references before serializing.\n// The zodToJsonSchema memo cache returns the same object instance for\n// fields like currencyCode that appear on both parent and child schemas.\n// A naive WeakSet-based circular-ref guard would drop the second occurrence,\n// causing properties to vanish from the generated spec (while the field\n// still appears in the 'required' array, since those are plain strings).\nconst deepClone = (v, ancestors = []) => {\n if (v === null || typeof v !== 'object') return v;\n if (typeof v === 'bigint') return Number(v);\n if (typeof v === 'function') return undefined;\n if (ancestors.includes(v)) return undefined; // true circular ref\n const next = [...ancestors, v];\n if (Array.isArray(v)) return v.map((item) => deepClone(item, next));\n const out = {};\n for (const [k, val] of Object.entries(v)) {\n const cloned = deepClone(val, next);\n if (cloned !== undefined) out[k] = cloned;\n }\n return out;\n};\nprocess.stdout.write(JSON.stringify(deepClone(doc), (_, v) =>\n typeof v === 'bigint' ? Number(v) : v\n));\n`\n\n // Plugin: stub next/* imports (not available outside Next.js app context)\n const stubNextPlugin = {\n name: 'stub-next',\n setup(build: any) {\n build.onResolve({ filter: /^next($|\\/)/ }, () => ({\n path: 'next-stub',\n namespace: 'next-stub',\n }))\n build.onLoad({ filter: /.*/, namespace: 'next-stub' }, () => ({\n contents: [\n 'const p = new Proxy(function(){}, {',\n ' get(_, k) { return k === \"__esModule\" ? true : k === \"default\" ? p : p; },',\n ' apply() { return p; },',\n ' construct() { return p; },',\n '});',\n 'export default p;',\n 'export const NextRequest = p, NextResponse = p, headers = p, cookies = p;',\n 'export const redirect = p, notFound = p, useRouter = p, usePathname = p;',\n 'export const useSearchParams = p, permanentRedirect = p, revalidatePath = p;',\n ].join('\\n'),\n loader: 'js' as const,\n }))\n },\n }\n\n // Plugin: resolve workspace imports, aliases, and subpath imports\n const appRoot = path.join(projectRoot, 'apps', 'mercato')\n const resolveWorkspacePlugin = {\n name: 'resolve-workspace',\n setup(build: any) {\n // @open-mercato/<pkg>/<path> \u2192 packages/<pkg>/src/<path>.ts\n build.onResolve({ filter: /^@open-mercato\\// }, (args: any) => {\n const withoutScope = args.path.slice('@open-mercato/'.length)\n const slashIdx = withoutScope.indexOf('/')\n const pkg = slashIdx === -1 ? withoutScope : withoutScope.slice(0, slashIdx)\n const rest = slashIdx === -1 ? '' : withoutScope.slice(slashIdx + 1)\n\n const base = rest\n ? path.join(projectRoot, 'packages', pkg, 'src', rest)\n : path.join(projectRoot, 'packages', pkg, 'src', 'index')\n\n for (const ext of ['.ts', '.tsx', '/index.ts', '/index.tsx']) {\n if (fs.existsSync(base + ext)) return { path: base + ext }\n }\n return undefined\n })\n\n // @/.mercato/* \u2192 apps/mercato/.mercato/* (tsconfig paths)\n build.onResolve({ filter: /^@\\/\\.mercato\\// }, (args: any) => {\n const rest = args.path.slice('@/'.length) // '.mercato/generated/...'\n const base = path.join(appRoot, rest)\n for (const ext of ['.ts', '.tsx', '/index.ts', '/index.tsx', '']) {\n if (fs.existsSync(base + ext)) return { path: base + ext }\n }\n return undefined\n })\n\n // @/* \u2192 apps/mercato/src/* (tsconfig paths)\n build.onResolve({ filter: /^@\\// }, (args: any) => {\n const rest = args.path.slice('@/'.length)\n const base = path.join(appRoot, 'src', rest)\n for (const ext of ['.ts', '.tsx', '/index.ts', '/index.tsx']) {\n if (fs.existsSync(base + ext)) return { path: base + ext }\n }\n return undefined\n })\n\n // #generated/* \u2192 packages/core/generated/* (Node subpath imports)\n build.onResolve({ filter: /^#generated\\// }, (args: any) => {\n const rest = args.path.slice('#generated/'.length)\n const coreGenerated = path.join(projectRoot, 'packages', 'core', 'generated')\n const base = path.join(coreGenerated, rest)\n for (const ext of ['.ts', '/index.ts']) {\n if (fs.existsSync(base + ext)) return { path: base + ext }\n }\n return undefined\n })\n },\n }\n\n // Plugin: externalize installed packages, stub missing ones\n const nodeBuiltins = new Set([\n 'assert', 'buffer', 'child_process', 'cluster', 'console', 'constants',\n 'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'http2',\n 'https', 'module', 'net', 'os', 'path', 'perf_hooks', 'process',\n 'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder',\n 'sys', 'timers', 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'wasi',\n 'worker_threads', 'zlib', 'async_hooks', 'diagnostics_channel', 'inspector',\n 'trace_events',\n ])\n const externalNonWorkspacePlugin = {\n name: 'external-non-workspace',\n setup(build: any) {\n build.onResolve({ filter: /^[^./]/ }, (args: any) => {\n if (args.path.startsWith('@open-mercato/')) return undefined\n if (args.path.startsWith('@/')) return undefined\n if (args.path.startsWith('#generated/')) return undefined\n if (args.path.startsWith('next')) return undefined\n // Let esbuild handle Node builtins (with or without node: prefix)\n if (args.path.startsWith('node:')) return undefined\n const topLevel = args.path.split('/')[0]\n if (nodeBuiltins.has(topLevel)) return undefined\n\n // Extract package name (handle scoped packages like @mikro-orm/core)\n const pkgName = args.path.startsWith('@')\n ? args.path.split('/').slice(0, 2).join('/')\n : topLevel\n const pkgDir = path.join(projectRoot, 'node_modules', pkgName)\n if (fs.existsSync(pkgDir)) return { external: true }\n\n // Package not installed \u2014 provide CJS stub (allows any named import)\n return { path: args.path, namespace: 'missing-pkg' }\n })\n build.onLoad({ filter: /.*/, namespace: 'missing-pkg' }, () => ({\n contents: 'var h={get:(_,k)=>k===\"__esModule\"?true:p};var p=new Proxy(function(){return p},{get:h.get,apply:()=>p,construct:()=>p});module.exports=p;',\n loader: 'js' as const,\n }))\n },\n }\n\n try {\n await esbuild.build({\n stdin: {\n contents: entryScript,\n resolveDir: projectRoot,\n sourcefile: 'openapi-entry.ts',\n loader: 'ts',\n },\n bundle: true,\n format: 'esm',\n platform: 'node',\n target: 'node18',\n outfile: bundlePath,\n write: true,\n tsconfig: tsconfigPath,\n logLevel: 'silent',\n jsx: 'automatic',\n plugins: [stubNextPlugin, resolveWorkspacePlugin, externalNonWorkspacePlugin],\n })\n\n const stdout = execFileSync(process.execPath, [bundlePath], {\n timeout: 60_000,\n maxBuffer: 20 * 1024 * 1024,\n encoding: 'utf-8',\n env: { ...process.env, NODE_NO_WARNINGS: '1' },\n cwd: projectRoot,\n })\n\n const lastLine = stdout.trim().split('\\n').pop()!\n const doc = JSON.parse(lastLine) as Record<string, any>\n\n if (!quiet) {\n const pathCount = Object.keys(doc.paths || {}).length\n const withBody = Object.values(doc.paths || {}).reduce((n: number, methods: any) => {\n for (const m of Object.values(methods)) {\n if ((m as any)?.requestBody) n++\n }\n return n\n }, 0)\n console.log(`[OpenAPI] Bundle approach: ${pathCount} paths, ${withBody} with requestBody schemas`)\n }\n\n return doc\n } catch (err) {\n const errMsg = err instanceof Error ? err.message : String(err)\n const stderr = (err as any)?.stderr\n const esbuildErrors = (err as any)?.errors as Array<{ text: string; location?: { file: string } }> | undefined\n if (!quiet) {\n console.log(`[OpenAPI] Bundle approach failed, will use static fallback: ${errMsg.split('\\n')[0]}`)\n if (esbuildErrors?.length) {\n const unique = new Map<string, string>()\n for (const e of esbuildErrors) {\n const key = e.text\n if (!unique.has(key)) unique.set(key, e.location?.file ?? '')\n }\n for (const [text, file] of [...unique.entries()].slice(0, 10)) {\n console.log(`[OpenAPI] ${text}${file ? ` (${path.basename(file)})` : ''}`)\n }\n if (unique.size > 10) console.log(`[OpenAPI] ... and ${unique.size - 10} more`)\n }\n if (stderr) {\n for (const line of String(stderr).trim().split('\\n').slice(0, 3)) {\n console.log(`[OpenAPI] ${line}`)\n }\n }\n }\n return null\n } finally {\n // Clean up old files from previous tsx-based approach\n for (const file of ['_openapi-register.mjs', '_openapi-loader.mjs', '_next-stub.cjs']) {\n try { fs.unlinkSync(path.join(cacheDir, file)) } catch {}\n }\n }\n}\n\n/**\n * Build OpenAPI paths from discovered routes.\n * Extracts basic operation info from route files statically.\n */\nfunction buildOpenApiPaths(routes: ApiRouteInfo[]): Record<string, any> {\n const paths: Record<string, any> = {}\n\n for (const route of routes) {\n const pathEntry: Record<string, any> = {}\n\n // Try to extract OpenAPI specs from source\n const openApiSpec = parseOpenApiFromSource(route.path)\n\n for (const method of route.methods) {\n const methodLower = method.toLowerCase()\n const spec = openApiSpec?.[method]\n\n // Generate a default operationId if not found\n const pathSegments = route.openApiPath\n .replace(/^\\/api\\//, '')\n .replace(/\\{[^}]+\\}/g, 'by_id')\n .split('/')\n .filter(Boolean)\n .join('_')\n const defaultOperationId = `${methodLower}_${pathSegments}`\n\n pathEntry[methodLower] = {\n operationId: spec?.operationId || defaultOperationId,\n summary: spec?.summary || `${method} ${route.openApiPath}`,\n description: spec?.description || `${method} operation for ${route.openApiPath}`,\n tags: spec?.tags || [route.openApiPath.split('/')[2] || 'api'],\n responses: {\n '200': {\n description: 'Successful response',\n },\n },\n }\n }\n\n if (Object.keys(pathEntry).length > 0) {\n paths[route.openApiPath] = pathEntry\n }\n }\n\n return paths\n}\n\n/**\n * Generate the OpenAPI JSON file.\n */\nexport async function generateOpenApi(options: GenerateOpenApiOptions): Promise<GeneratorResult> {\n const { resolver, quiet = false } = options\n const result = createGeneratorResult()\n\n const outputDir = resolver.getOutputDir()\n const outFile = path.join(outputDir, 'openapi.generated.json')\n const checksumFile = path.join(outputDir, 'openapi.generated.checksum')\n\n // Ensure output directory exists\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true })\n }\n\n // Find all API routes\n const routes = await findApiRoutes(resolver)\n\n if (!quiet) {\n console.log(`[OpenAPI] Found ${routes.length} API route files`)\n }\n\n // Determine project root (cli package is at packages/cli/src/lib/generators/)\n const projectRoot = path.resolve(\n path.dirname(new URL(import.meta.url).pathname),\n '../../../../..'\n )\n\n // Try esbuild bundle approach first \u2014 produces full requestBody/response schemas\n let doc: Record<string, any> | null = await generateOpenApiViaBundle(routes, projectRoot, quiet)\n\n // Fallback to static regex approach (extracts operationId/summary/tags but no schemas)\n if (!doc) {\n if (!quiet) {\n console.log('[OpenAPI] Falling back to static regex approach')\n }\n const paths = buildOpenApiPaths(routes)\n doc = {\n openapi: '3.1.0',\n info: {\n title: 'Open Mercato API',\n version: '1.0.0',\n description: 'Auto-generated OpenAPI specification',\n },\n servers: [\n { url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' },\n ],\n paths,\n components: {\n securitySchemes: {\n bearerAuth: {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n description: 'Send an `Authorization: Bearer <token>` header with a valid API token.',\n },\n },\n },\n }\n }\n\n const output = JSON.stringify(doc, null, 2)\n const checksum = calculateChecksum(output)\n\n // Check if unchanged\n const existingChecksums = readChecksumRecord(checksumFile)\n if (existingChecksums && existingChecksums.content === checksum && fs.existsSync(outFile)) {\n result.filesUnchanged.push(outFile)\n if (!quiet) {\n console.log(`[OpenAPI] Skipped (unchanged): ${outFile}`)\n }\n return result\n }\n\n // Write the file\n fs.writeFileSync(outFile, output)\n writeChecksumRecord(checksumFile, { content: checksum, structure: '' })\n\n result.filesWritten.push(outFile)\n\n if (!quiet) {\n logGenerationResult(outFile, true)\n const pathCount = Object.keys(doc.paths || {}).length\n console.log(`[OpenAPI] Generated ${pathCount} API paths`)\n }\n\n return result\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.4.9-develop-
|
|
3
|
+
"version": "0.4.9-develop-31d1a87765",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -58,16 +58,16 @@
|
|
|
58
58
|
"@mikro-orm/core": "^6.6.2",
|
|
59
59
|
"@mikro-orm/migrations": "^6.6.2",
|
|
60
60
|
"@mikro-orm/postgresql": "^6.6.2",
|
|
61
|
-
"@open-mercato/shared": "0.4.9-develop-
|
|
61
|
+
"@open-mercato/shared": "0.4.9-develop-31d1a87765",
|
|
62
62
|
"pg": "8.20.0",
|
|
63
63
|
"testcontainers": "^11.12.0",
|
|
64
64
|
"typescript": "^5.9.3"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@open-mercato/shared": "0.4.9-develop-
|
|
67
|
+
"@open-mercato/shared": "0.4.9-develop-31d1a87765"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@open-mercato/shared": "0.4.9-develop-
|
|
70
|
+
"@open-mercato/shared": "0.4.9-develop-31d1a87765",
|
|
71
71
|
"@types/jest": "^30.0.0",
|
|
72
72
|
"jest": "^30.2.0",
|
|
73
73
|
"ts-jest": "^29.4.6"
|
|
@@ -176,6 +176,296 @@ function parseOpenApiFromSource(filePath: string): Record<string, any> | null {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Generate a complete OpenAPI document by bundling route files with esbuild
|
|
181
|
+
* and executing the bundle to call buildOpenApiDocument from @open-mercato/shared.
|
|
182
|
+
*
|
|
183
|
+
* esbuild compiles TypeScript with legacy decorator support (reads experimentalDecorators
|
|
184
|
+
* from tsconfig.json), avoiding the TC39 decorator mismatch that breaks tsx-based imports.
|
|
185
|
+
* External packages (zod, mikro-orm, etc.) are resolved from node_modules at runtime.
|
|
186
|
+
*/
|
|
187
|
+
async function generateOpenApiViaBundle(
|
|
188
|
+
routes: ApiRouteInfo[],
|
|
189
|
+
projectRoot: string,
|
|
190
|
+
quiet: boolean
|
|
191
|
+
): Promise<Record<string, any> | null> {
|
|
192
|
+
let esbuild: typeof import('esbuild')
|
|
193
|
+
try {
|
|
194
|
+
esbuild = await import('esbuild')
|
|
195
|
+
} catch {
|
|
196
|
+
if (!quiet) console.log('[OpenAPI] esbuild not available, skipping bundle approach')
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const { execFileSync } = await import('node:child_process')
|
|
201
|
+
|
|
202
|
+
const cacheDir = path.join(projectRoot, 'node_modules', '.cache')
|
|
203
|
+
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true })
|
|
204
|
+
|
|
205
|
+
const bundlePath = path.join(cacheDir, '_openapi-bundle.mjs')
|
|
206
|
+
const tsconfigPath = path.join(projectRoot, 'tsconfig.base.json')
|
|
207
|
+
const generatorPath = path.join(
|
|
208
|
+
projectRoot, 'packages', 'shared', 'src', 'lib', 'openapi', 'generator.ts'
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
// Build the entry script that imports all routes and calls buildOpenApiDocument
|
|
212
|
+
const importLines: string[] = [
|
|
213
|
+
`import { buildOpenApiDocument } from ${JSON.stringify(generatorPath)};`,
|
|
214
|
+
]
|
|
215
|
+
const routeMapLines: string[] = []
|
|
216
|
+
|
|
217
|
+
for (let i = 0; i < routes.length; i++) {
|
|
218
|
+
const route = routes[i]
|
|
219
|
+
importLines.push(`import * as R${i} from ${JSON.stringify(route.path)};`)
|
|
220
|
+
// Use [param] format so normalizePath in buildOpenApiDocument extracts path params
|
|
221
|
+
const bracketPath = route.openApiPath.replace(/\{([^}]+)\}/g, '[$1]')
|
|
222
|
+
routeMapLines.push(` [${JSON.stringify(bracketPath)}, R${i}],`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const entryScript = `${importLines.join('\n')}
|
|
226
|
+
|
|
227
|
+
const routeEntries = [
|
|
228
|
+
${routeMapLines.join('\n')}
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const modules = new Map();
|
|
232
|
+
for (const [apiPath, mod] of routeEntries) {
|
|
233
|
+
const moduleId = apiPath.replace(/^\\/api\\//, '').split('/')[0];
|
|
234
|
+
if (!modules.has(moduleId)) modules.set(moduleId, { id: moduleId, apis: [] });
|
|
235
|
+
modules.get(moduleId).apis.push({
|
|
236
|
+
path: apiPath,
|
|
237
|
+
handlers: mod,
|
|
238
|
+
metadata: mod.metadata,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const doc = buildOpenApiDocument([...modules.values()], {
|
|
243
|
+
title: 'Open Mercato API',
|
|
244
|
+
version: '1.0.0',
|
|
245
|
+
description: 'Auto-generated OpenAPI specification',
|
|
246
|
+
servers: [{ url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' }],
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Deep-clone to break shared object references before serializing.
|
|
250
|
+
// The zodToJsonSchema memo cache returns the same object instance for
|
|
251
|
+
// fields like currencyCode that appear on both parent and child schemas.
|
|
252
|
+
// A naive WeakSet-based circular-ref guard would drop the second occurrence,
|
|
253
|
+
// causing properties to vanish from the generated spec (while the field
|
|
254
|
+
// still appears in the 'required' array, since those are plain strings).
|
|
255
|
+
const deepClone = (v, ancestors = []) => {
|
|
256
|
+
if (v === null || typeof v !== 'object') return v;
|
|
257
|
+
if (typeof v === 'bigint') return Number(v);
|
|
258
|
+
if (typeof v === 'function') return undefined;
|
|
259
|
+
if (ancestors.includes(v)) return undefined; // true circular ref
|
|
260
|
+
const next = [...ancestors, v];
|
|
261
|
+
if (Array.isArray(v)) return v.map((item) => deepClone(item, next));
|
|
262
|
+
const out = {};
|
|
263
|
+
for (const [k, val] of Object.entries(v)) {
|
|
264
|
+
const cloned = deepClone(val, next);
|
|
265
|
+
if (cloned !== undefined) out[k] = cloned;
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
};
|
|
269
|
+
process.stdout.write(JSON.stringify(deepClone(doc), (_, v) =>
|
|
270
|
+
typeof v === 'bigint' ? Number(v) : v
|
|
271
|
+
));
|
|
272
|
+
`
|
|
273
|
+
|
|
274
|
+
// Plugin: stub next/* imports (not available outside Next.js app context)
|
|
275
|
+
const stubNextPlugin = {
|
|
276
|
+
name: 'stub-next',
|
|
277
|
+
setup(build: any) {
|
|
278
|
+
build.onResolve({ filter: /^next($|\/)/ }, () => ({
|
|
279
|
+
path: 'next-stub',
|
|
280
|
+
namespace: 'next-stub',
|
|
281
|
+
}))
|
|
282
|
+
build.onLoad({ filter: /.*/, namespace: 'next-stub' }, () => ({
|
|
283
|
+
contents: [
|
|
284
|
+
'const p = new Proxy(function(){}, {',
|
|
285
|
+
' get(_, k) { return k === "__esModule" ? true : k === "default" ? p : p; },',
|
|
286
|
+
' apply() { return p; },',
|
|
287
|
+
' construct() { return p; },',
|
|
288
|
+
'});',
|
|
289
|
+
'export default p;',
|
|
290
|
+
'export const NextRequest = p, NextResponse = p, headers = p, cookies = p;',
|
|
291
|
+
'export const redirect = p, notFound = p, useRouter = p, usePathname = p;',
|
|
292
|
+
'export const useSearchParams = p, permanentRedirect = p, revalidatePath = p;',
|
|
293
|
+
].join('\n'),
|
|
294
|
+
loader: 'js' as const,
|
|
295
|
+
}))
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Plugin: resolve workspace imports, aliases, and subpath imports
|
|
300
|
+
const appRoot = path.join(projectRoot, 'apps', 'mercato')
|
|
301
|
+
const resolveWorkspacePlugin = {
|
|
302
|
+
name: 'resolve-workspace',
|
|
303
|
+
setup(build: any) {
|
|
304
|
+
// @open-mercato/<pkg>/<path> → packages/<pkg>/src/<path>.ts
|
|
305
|
+
build.onResolve({ filter: /^@open-mercato\// }, (args: any) => {
|
|
306
|
+
const withoutScope = args.path.slice('@open-mercato/'.length)
|
|
307
|
+
const slashIdx = withoutScope.indexOf('/')
|
|
308
|
+
const pkg = slashIdx === -1 ? withoutScope : withoutScope.slice(0, slashIdx)
|
|
309
|
+
const rest = slashIdx === -1 ? '' : withoutScope.slice(slashIdx + 1)
|
|
310
|
+
|
|
311
|
+
const base = rest
|
|
312
|
+
? path.join(projectRoot, 'packages', pkg, 'src', rest)
|
|
313
|
+
: path.join(projectRoot, 'packages', pkg, 'src', 'index')
|
|
314
|
+
|
|
315
|
+
for (const ext of ['.ts', '.tsx', '/index.ts', '/index.tsx']) {
|
|
316
|
+
if (fs.existsSync(base + ext)) return { path: base + ext }
|
|
317
|
+
}
|
|
318
|
+
return undefined
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
// @/.mercato/* → apps/mercato/.mercato/* (tsconfig paths)
|
|
322
|
+
build.onResolve({ filter: /^@\/\.mercato\// }, (args: any) => {
|
|
323
|
+
const rest = args.path.slice('@/'.length) // '.mercato/generated/...'
|
|
324
|
+
const base = path.join(appRoot, rest)
|
|
325
|
+
for (const ext of ['.ts', '.tsx', '/index.ts', '/index.tsx', '']) {
|
|
326
|
+
if (fs.existsSync(base + ext)) return { path: base + ext }
|
|
327
|
+
}
|
|
328
|
+
return undefined
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// @/* → apps/mercato/src/* (tsconfig paths)
|
|
332
|
+
build.onResolve({ filter: /^@\// }, (args: any) => {
|
|
333
|
+
const rest = args.path.slice('@/'.length)
|
|
334
|
+
const base = path.join(appRoot, 'src', rest)
|
|
335
|
+
for (const ext of ['.ts', '.tsx', '/index.ts', '/index.tsx']) {
|
|
336
|
+
if (fs.existsSync(base + ext)) return { path: base + ext }
|
|
337
|
+
}
|
|
338
|
+
return undefined
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
// #generated/* → packages/core/generated/* (Node subpath imports)
|
|
342
|
+
build.onResolve({ filter: /^#generated\// }, (args: any) => {
|
|
343
|
+
const rest = args.path.slice('#generated/'.length)
|
|
344
|
+
const coreGenerated = path.join(projectRoot, 'packages', 'core', 'generated')
|
|
345
|
+
const base = path.join(coreGenerated, rest)
|
|
346
|
+
for (const ext of ['.ts', '/index.ts']) {
|
|
347
|
+
if (fs.existsSync(base + ext)) return { path: base + ext }
|
|
348
|
+
}
|
|
349
|
+
return undefined
|
|
350
|
+
})
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Plugin: externalize installed packages, stub missing ones
|
|
355
|
+
const nodeBuiltins = new Set([
|
|
356
|
+
'assert', 'buffer', 'child_process', 'cluster', 'console', 'constants',
|
|
357
|
+
'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'http2',
|
|
358
|
+
'https', 'module', 'net', 'os', 'path', 'perf_hooks', 'process',
|
|
359
|
+
'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder',
|
|
360
|
+
'sys', 'timers', 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'wasi',
|
|
361
|
+
'worker_threads', 'zlib', 'async_hooks', 'diagnostics_channel', 'inspector',
|
|
362
|
+
'trace_events',
|
|
363
|
+
])
|
|
364
|
+
const externalNonWorkspacePlugin = {
|
|
365
|
+
name: 'external-non-workspace',
|
|
366
|
+
setup(build: any) {
|
|
367
|
+
build.onResolve({ filter: /^[^./]/ }, (args: any) => {
|
|
368
|
+
if (args.path.startsWith('@open-mercato/')) return undefined
|
|
369
|
+
if (args.path.startsWith('@/')) return undefined
|
|
370
|
+
if (args.path.startsWith('#generated/')) return undefined
|
|
371
|
+
if (args.path.startsWith('next')) return undefined
|
|
372
|
+
// Let esbuild handle Node builtins (with or without node: prefix)
|
|
373
|
+
if (args.path.startsWith('node:')) return undefined
|
|
374
|
+
const topLevel = args.path.split('/')[0]
|
|
375
|
+
if (nodeBuiltins.has(topLevel)) return undefined
|
|
376
|
+
|
|
377
|
+
// Extract package name (handle scoped packages like @mikro-orm/core)
|
|
378
|
+
const pkgName = args.path.startsWith('@')
|
|
379
|
+
? args.path.split('/').slice(0, 2).join('/')
|
|
380
|
+
: topLevel
|
|
381
|
+
const pkgDir = path.join(projectRoot, 'node_modules', pkgName)
|
|
382
|
+
if (fs.existsSync(pkgDir)) return { external: true }
|
|
383
|
+
|
|
384
|
+
// Package not installed — provide CJS stub (allows any named import)
|
|
385
|
+
return { path: args.path, namespace: 'missing-pkg' }
|
|
386
|
+
})
|
|
387
|
+
build.onLoad({ filter: /.*/, namespace: 'missing-pkg' }, () => ({
|
|
388
|
+
contents: 'var h={get:(_,k)=>k==="__esModule"?true:p};var p=new Proxy(function(){return p},{get:h.get,apply:()=>p,construct:()=>p});module.exports=p;',
|
|
389
|
+
loader: 'js' as const,
|
|
390
|
+
}))
|
|
391
|
+
},
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
await esbuild.build({
|
|
396
|
+
stdin: {
|
|
397
|
+
contents: entryScript,
|
|
398
|
+
resolveDir: projectRoot,
|
|
399
|
+
sourcefile: 'openapi-entry.ts',
|
|
400
|
+
loader: 'ts',
|
|
401
|
+
},
|
|
402
|
+
bundle: true,
|
|
403
|
+
format: 'esm',
|
|
404
|
+
platform: 'node',
|
|
405
|
+
target: 'node18',
|
|
406
|
+
outfile: bundlePath,
|
|
407
|
+
write: true,
|
|
408
|
+
tsconfig: tsconfigPath,
|
|
409
|
+
logLevel: 'silent',
|
|
410
|
+
jsx: 'automatic',
|
|
411
|
+
plugins: [stubNextPlugin, resolveWorkspacePlugin, externalNonWorkspacePlugin],
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
const stdout = execFileSync(process.execPath, [bundlePath], {
|
|
415
|
+
timeout: 60_000,
|
|
416
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
417
|
+
encoding: 'utf-8',
|
|
418
|
+
env: { ...process.env, NODE_NO_WARNINGS: '1' },
|
|
419
|
+
cwd: projectRoot,
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const lastLine = stdout.trim().split('\n').pop()!
|
|
423
|
+
const doc = JSON.parse(lastLine) as Record<string, any>
|
|
424
|
+
|
|
425
|
+
if (!quiet) {
|
|
426
|
+
const pathCount = Object.keys(doc.paths || {}).length
|
|
427
|
+
const withBody = Object.values(doc.paths || {}).reduce((n: number, methods: any) => {
|
|
428
|
+
for (const m of Object.values(methods)) {
|
|
429
|
+
if ((m as any)?.requestBody) n++
|
|
430
|
+
}
|
|
431
|
+
return n
|
|
432
|
+
}, 0)
|
|
433
|
+
console.log(`[OpenAPI] Bundle approach: ${pathCount} paths, ${withBody} with requestBody schemas`)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return doc
|
|
437
|
+
} catch (err) {
|
|
438
|
+
const errMsg = err instanceof Error ? err.message : String(err)
|
|
439
|
+
const stderr = (err as any)?.stderr
|
|
440
|
+
const esbuildErrors = (err as any)?.errors as Array<{ text: string; location?: { file: string } }> | undefined
|
|
441
|
+
if (!quiet) {
|
|
442
|
+
console.log(`[OpenAPI] Bundle approach failed, will use static fallback: ${errMsg.split('\n')[0]}`)
|
|
443
|
+
if (esbuildErrors?.length) {
|
|
444
|
+
const unique = new Map<string, string>()
|
|
445
|
+
for (const e of esbuildErrors) {
|
|
446
|
+
const key = e.text
|
|
447
|
+
if (!unique.has(key)) unique.set(key, e.location?.file ?? '')
|
|
448
|
+
}
|
|
449
|
+
for (const [text, file] of [...unique.entries()].slice(0, 10)) {
|
|
450
|
+
console.log(`[OpenAPI] ${text}${file ? ` (${path.basename(file)})` : ''}`)
|
|
451
|
+
}
|
|
452
|
+
if (unique.size > 10) console.log(`[OpenAPI] ... and ${unique.size - 10} more`)
|
|
453
|
+
}
|
|
454
|
+
if (stderr) {
|
|
455
|
+
for (const line of String(stderr).trim().split('\n').slice(0, 3)) {
|
|
456
|
+
console.log(`[OpenAPI] ${line}`)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return null
|
|
461
|
+
} finally {
|
|
462
|
+
// Clean up old files from previous tsx-based approach
|
|
463
|
+
for (const file of ['_openapi-register.mjs', '_openapi-loader.mjs', '_next-stub.cjs']) {
|
|
464
|
+
try { fs.unlinkSync(path.join(cacheDir, file)) } catch {}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
179
469
|
/**
|
|
180
470
|
* Build OpenAPI paths from discovered routes.
|
|
181
471
|
* Extracts basic operation info from route files statically.
|
|
@@ -246,32 +536,43 @@ export async function generateOpenApi(options: GenerateOpenApiOptions): Promise<
|
|
|
246
536
|
console.log(`[OpenAPI] Found ${routes.length} API route files`)
|
|
247
537
|
}
|
|
248
538
|
|
|
249
|
-
//
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
539
|
+
// Determine project root (cli package is at packages/cli/src/lib/generators/)
|
|
540
|
+
const projectRoot = path.resolve(
|
|
541
|
+
path.dirname(new URL(import.meta.url).pathname),
|
|
542
|
+
'../../../../..'
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
// Try esbuild bundle approach first — produces full requestBody/response schemas
|
|
546
|
+
let doc: Record<string, any> | null = await generateOpenApiViaBundle(routes, projectRoot, quiet)
|
|
547
|
+
|
|
548
|
+
// Fallback to static regex approach (extracts operationId/summary/tags but no schemas)
|
|
549
|
+
if (!doc) {
|
|
550
|
+
if (!quiet) {
|
|
551
|
+
console.log('[OpenAPI] Falling back to static regex approach')
|
|
552
|
+
}
|
|
553
|
+
const paths = buildOpenApiPaths(routes)
|
|
554
|
+
doc = {
|
|
555
|
+
openapi: '3.1.0',
|
|
556
|
+
info: {
|
|
557
|
+
title: 'Open Mercato API',
|
|
558
|
+
version: '1.0.0',
|
|
559
|
+
description: 'Auto-generated OpenAPI specification',
|
|
560
|
+
},
|
|
561
|
+
servers: [
|
|
562
|
+
{ url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' },
|
|
563
|
+
],
|
|
564
|
+
paths,
|
|
565
|
+
components: {
|
|
566
|
+
securitySchemes: {
|
|
567
|
+
bearerAuth: {
|
|
568
|
+
type: 'http',
|
|
569
|
+
scheme: 'bearer',
|
|
570
|
+
bearerFormat: 'JWT',
|
|
571
|
+
description: 'Send an `Authorization: Bearer <token>` header with a valid API token.',
|
|
572
|
+
},
|
|
272
573
|
},
|
|
273
574
|
},
|
|
274
|
-
}
|
|
575
|
+
}
|
|
275
576
|
}
|
|
276
577
|
|
|
277
578
|
const output = JSON.stringify(doc, null, 2)
|
|
@@ -295,6 +596,7 @@ export async function generateOpenApi(options: GenerateOpenApiOptions): Promise<
|
|
|
295
596
|
|
|
296
597
|
if (!quiet) {
|
|
297
598
|
logGenerationResult(outFile, true)
|
|
599
|
+
const pathCount = Object.keys(doc.paths || {}).length
|
|
298
600
|
console.log(`[OpenAPI] Generated ${pathCount} API paths`)
|
|
299
601
|
}
|
|
300
602
|
|