@open-mercato/cli 0.4.9-develop-8c36c096d5 → 0.4.9-develop-be6e1cf49c

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 paths = buildOpenApiPaths(routes);
148
- const pathCount = Object.keys(paths).length;
149
- const doc = {
150
- openapi: "3.1.0",
151
- info: {
152
- title: "Open Mercato API",
153
- version: "1.0.0",
154
- description: "Auto-generated OpenAPI specification"
155
- },
156
- servers: [
157
- { url: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000" }
158
- ],
159
- paths,
160
- components: {
161
- securitySchemes: {
162
- bearerAuth: {
163
- type: "http",
164
- scheme: "bearer",
165
- bearerFormat: "JWT",
166
- description: "Send an `Authorization: Bearer <token>` header with a valid API token."
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-8c36c096d5",
3
+ "version": "0.4.9-develop-be6e1cf49c",
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-8c36c096d5",
61
+ "@open-mercato/shared": "0.4.9-develop-be6e1cf49c",
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-8c36c096d5"
67
+ "@open-mercato/shared": "0.4.9-develop-be6e1cf49c"
68
68
  },
69
69
  "devDependencies": {
70
- "@open-mercato/shared": "0.4.9-develop-8c36c096d5",
70
+ "@open-mercato/shared": "0.4.9-develop-be6e1cf49c",
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
- // Build OpenAPI paths from routes
250
- const paths = buildOpenApiPaths(routes)
251
- const pathCount = Object.keys(paths).length
252
-
253
- // Build OpenAPI document
254
- const doc = {
255
- openapi: '3.1.0',
256
- info: {
257
- title: 'Open Mercato API',
258
- version: '1.0.0',
259
- description: 'Auto-generated OpenAPI specification',
260
- },
261
- servers: [
262
- { url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' },
263
- ],
264
- paths,
265
- components: {
266
- securitySchemes: {
267
- bearerAuth: {
268
- type: 'http',
269
- scheme: 'bearer',
270
- bearerFormat: 'JWT',
271
- description: 'Send an `Authorization: Bearer <token>` header with a valid API token.',
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