@open-mercato/cli 0.10.1-develop.8004.1.a8a79ee43f → 0.10.1-develop.8024.1.02dbc16b49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentic/guides/module-facts.json +162 -162
- package/dist/agentic/guides/module-facts.v2.json +162 -162
- package/dist/agentic/guides/modules/agent_orchestrator/index.md +1 -1
- package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
- package/dist/agentic/guides/modules/api_docs/index.md +1 -1
- package/dist/agentic/guides/modules/api_keys/index.md +1 -1
- package/dist/agentic/guides/modules/attachments/index.md +1 -1
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/availability/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/carrier_inpost/index.md +1 -1
- package/dist/agentic/guides/modules/cart/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/index.md +1 -1
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
- package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
- package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
- package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
- package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
- package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
- package/dist/agentic/guides/modules/channel_smtp/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
- package/dist/agentic/guides/modules/configs/index.md +1 -1
- package/dist/agentic/guides/modules/content/index.md +1 -1
- package/dist/agentic/guides/modules/currencies/index.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
- package/dist/agentic/guides/modules/customer_groups/index.md +1 -1
- package/dist/agentic/guides/modules/customers/index.md +1 -1
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- package/dist/agentic/guides/modules/design_system/index.md +1 -1
- package/dist/agentic/guides/modules/devices/index.md +1 -1
- package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
- package/dist/agentic/guides/modules/directory/index.md +1 -1
- package/dist/agentic/guides/modules/documents/index.md +1 -1
- package/dist/agentic/guides/modules/ecommerce/index.md +1 -1
- package/dist/agentic/guides/modules/entities/index.md +1 -1
- package/dist/agentic/guides/modules/eudr/index.md +1 -1
- package/dist/agentic/guides/modules/events/index.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
- package/dist/agentic/guides/modules/forms/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_autopay/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_tpay/index.md +1 -1
- package/dist/agentic/guides/modules/generators/index.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
- package/dist/agentic/guides/modules/integrations/index.md +1 -1
- package/dist/agentic/guides/modules/messages/index.md +1 -1
- package/dist/agentic/guides/modules/notifications/index.md +1 -1
- package/dist/agentic/guides/modules/onboarding/index.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
- package/dist/agentic/guides/modules/perspectives/index.md +1 -1
- package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
- package/dist/agentic/guides/modules/planner/index.md +1 -1
- package/dist/agentic/guides/modules/portal/index.md +1 -1
- package/dist/agentic/guides/modules/progress/index.md +1 -1
- package/dist/agentic/guides/modules/promotions/index.md +1 -1
- package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
- package/dist/agentic/guides/modules/query_index/index.md +1 -1
- package/dist/agentic/guides/modules/record_locks/index.md +1 -1
- package/dist/agentic/guides/modules/resources/index.md +1 -1
- package/dist/agentic/guides/modules/sales/index.md +1 -1
- package/dist/agentic/guides/modules/scheduler/index.md +1 -1
- package/dist/agentic/guides/modules/search/index.md +1 -1
- package/dist/agentic/guides/modules/security/index.md +1 -1
- package/dist/agentic/guides/modules/seeds/index.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
- package/dist/agentic/guides/modules/sso/index.md +1 -1
- package/dist/agentic/guides/modules/staff/index.md +1 -1
- package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
- package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
- package/dist/agentic/guides/modules/telemetry/index.md +1 -1
- package/dist/agentic/guides/modules/tillio/index.md +1 -1
- package/dist/agentic/guides/modules/translations/index.md +1 -1
- package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
- package/dist/agentic/guides/modules/webhooks/index.md +1 -1
- package/dist/agentic/guides/modules/wms/index.md +1 -1
- package/dist/agentic/guides/modules/workflows/index.md +1 -1
- package/dist/agentic/guides/reference-module-facts.json +1 -1
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +4 -0
- package/dist/agentic/guides/upstream/manifest.json +2 -2
- package/dist/agentic/shared/ai/harness/cases.json +2 -2
- package/dist/agentic/shared/ai/trackers/github.md +257 -2
- package/dist/agentic/shared/scripts/execution-sandbox.mjs +1 -0
- package/dist/lib/generators/openapi.js +7 -2
- package/dist/lib/generators/openapi.js.map +2 -2
- package/dist/lib/telemetry-init.js +1 -1
- package/dist/lib/telemetry-init.js.map +1 -1
- package/dist/lib/testing/integration.js +22 -0
- package/dist/lib/testing/integration.js.map +2 -2
- package/package.json +7 -7
- package/src/lib/__tests__/telemetry-init.test.ts +1 -1
- package/src/lib/generators/__tests__/openapi.test.ts +24 -0
- package/src/lib/generators/openapi.ts +7 -2
- package/src/lib/telemetry-init.ts +1 -1
- package/src/lib/testing/__tests__/integration.test.ts +24 -0
- package/src/lib/testing/integration.ts +23 -0
|
@@ -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 { ModuleEntry, PackageResolver } from '../resolver'\nimport { getEnabledModuleIdentities } from '../module-identity'\nimport { isModuleRouteFile } from './scanner'\nimport {\n calculateChecksum,\n calculateStructureChecksum,\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\ninterface OpenApiInputManifest {\n version: 1\n inputPaths: string[]\n}\n\ninterface OpenApiInputContext {\n enabled: ModuleEntry[]\n apiRoots: string[]\n moduleIdentity: Array<{\n id: string\n from: string\n appBase: string\n pkgBase: string\n }>\n}\n\ninterface OpenApiBundleResult {\n doc: Record<string, any> | null\n inputPaths: string[]\n}\n\nconst OPENAPI_INPUT_MANIFEST_VERSION = 1\n\nfunction createOpenApiInputContext(resolver: PackageResolver): OpenApiInputContext {\n const enabled = resolver.loadEnabledModules()\n const apiRoots: string[] = []\n const moduleIdentity = enabled.map((entry) => {\n const roots = resolver.getModulePaths(entry)\n apiRoots.push(path.join(roots.appBase, 'api'), path.join(roots.pkgBase, 'api'))\n return {\n id: entry.id,\n from: entry.from ?? '@open-mercato/core',\n appBase: path.resolve(roots.appBase),\n pkgBase: path.resolve(roots.pkgBase),\n }\n })\n return { enabled, apiRoots, moduleIdentity }\n}\n\nfunction readOpenApiInputManifest(filePath: string): OpenApiInputManifest | null {\n try {\n const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Partial<OpenApiInputManifest>\n if (\n parsed.version !== OPENAPI_INPUT_MANIFEST_VERSION\n || !Array.isArray(parsed.inputPaths)\n || parsed.inputPaths.some((entry) => typeof entry !== 'string')\n ) {\n return null\n }\n return {\n version: OPENAPI_INPUT_MANIFEST_VERSION,\n inputPaths: Array.from(new Set(parsed.inputPaths.map((entry) => path.resolve(entry))))\n .sort((a, b) => a.localeCompare(b)),\n }\n } catch {\n return null\n }\n}\n\nfunction trackedPathRecord(filePath: string): string {\n try {\n const stat = fs.statSync(filePath)\n if (stat.isFile()) {\n return `${path.resolve(filePath)}:${calculateChecksum(fs.readFileSync(filePath, 'utf8'))}`\n }\n return `${path.resolve(filePath)}:${stat.size}:${stat.mtimeMs}`\n } catch {\n return `missing:${path.resolve(filePath)}`\n }\n}\n\nfunction calculateOpenApiInputFingerprint(\n resolver: PackageResolver,\n context: OpenApiInputContext,\n manifest: OpenApiInputManifest,\n): string {\n const rootDir = resolver.getRootDir()\n const appDir = resolver.getAppDir()\n const configPaths = [\n resolver.getModulesConfigPath(),\n path.join(rootDir, 'package.json'),\n path.join(rootDir, 'yarn.lock'),\n path.join(rootDir, 'package-lock.json'),\n path.join(rootDir, 'pnpm-lock.yaml'),\n path.join(rootDir, 'tsconfig.json'),\n path.join(rootDir, 'tsconfig.base.json'),\n path.join(appDir, 'package.json'),\n path.join(appDir, 'tsconfig.json'),\n ]\n const trackedRecords = [...manifest.inputPaths, ...configPaths]\n .map(trackedPathRecord)\n .sort((a, b) => a.localeCompare(b))\n return calculateChecksum(JSON.stringify({\n version: OPENAPI_INPUT_MANIFEST_VERSION,\n modules: context.moduleIdentity,\n routeStructure: calculateStructureChecksum(context.apiRoots),\n trackedRecords,\n environment: {\n node: process.version,\n nodeEnv: process.env.NODE_ENV ?? '',\n publicAppUrl: process.env.NEXT_PUBLIC_APP_URL ?? '',\n },\n }))\n}\n\nfunction resolveExistingPath(paths: string[]): string | null {\n for (const candidate of paths) {\n if (fs.existsSync(candidate)) return candidate\n }\n return null\n}\n\n/**\n * Find all API route files and extract their OpenAPI specs.\n */\nasync function findApiRoutes(\n resolver: PackageResolver,\n enabled: ModuleEntry[],\n): Promise<ApiRouteInfo[]> {\n const routes: ApiRouteInfo[] = []\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() && isModuleRouteFile(e.name)) {\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 (later app files overwrite earlier package files)\n const filesByRelativePath = new Map<string, string>()\n for (const { relativePath, fullPath } of routeFiles) {\n filesByRelativePath.set(relativePath, fullPath)\n }\n\n for (const [relativePath, fullPath] of filesByRelativePath) {\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 resolver: PackageResolver,\n quiet: boolean\n): Promise<OpenApiBundleResult> {\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 { doc: null, inputPaths: [] }\n }\n\n const { execFileSync } = await import('node:child_process')\n\n const rootDir = resolver.getRootDir()\n const appDir = resolver.getAppDir()\n const sharedPackageRoot = resolver.getPackageRoot('@open-mercato/shared')\n const corePackageRoot = resolver.getPackageRoot('@open-mercato/core')\n const tsconfigPath = resolveExistingPath([\n path.join(appDir, 'tsconfig.json'),\n path.join(rootDir, 'tsconfig.base.json'),\n path.join(rootDir, 'tsconfig.json'),\n ])\n const generatorPath = path.join(sharedPackageRoot, 'src', 'lib', 'openapi', 'generator.ts')\n const coreGeneratedRoot = path.join(corePackageRoot, 'generated')\n\n if (!fs.existsSync(generatorPath)) {\n if (!quiet) {\n console.log(`[OpenAPI] Generator source not found at ${generatorPath}, skipping bundle approach`)\n }\n return { doc: null, inputPaths: [] }\n }\n\n const cacheDir = path.join(rootDir, 'node_modules', '.cache')\n if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true })\n\n const bundlePath = path.join(cacheDir, '_openapi-bundle.mjs')\n let inputPaths: string[] = []\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 resolveProjectImportsPlugin = {\n name: 'resolve-project-imports',\n setup(build: any) {\n // @open-mercato/<pkg>/<path> \u2192 packageRoot/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 const packageName = `@open-mercato/${pkg}`\n const packageRoot = resolver.getPackageRoot(packageName)\n\n const base = rest\n ? path.join(packageRoot, 'src', rest)\n : path.join(packageRoot, '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 app/.mercato/* (tsconfig paths)\n build.onResolve({ filter: /^@\\/\\.mercato\\// }, (args: any) => {\n const rest = args.path.slice('@/'.length) // '.mercato/generated/...'\n const base = path.join(appDir, 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 app/src/* (tsconfig paths)\n build.onResolve({ filter: /^@\\// }, (args: any) => {\n const rest = args.path.slice('@/'.length)\n const base = path.join(appDir, '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 core package generated/* (Node subpath imports)\n build.onResolve({ filter: /^#generated\\// }, (args: any) => {\n const rest = args.path.slice('#generated/'.length)\n const base = path.join(coreGeneratedRoot, 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 (path.isAbsolute(args.path)) return undefined\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(rootDir, '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 const buildResult = await esbuild.build({\n stdin: {\n contents: entryScript,\n resolveDir: appDir,\n sourcefile: 'openapi-entry.ts',\n loader: 'ts',\n },\n bundle: true,\n metafile: true,\n absWorkingDir: rootDir,\n format: 'esm',\n platform: 'node',\n target: 'node18',\n outfile: bundlePath,\n write: true,\n ...(tsconfigPath ? { tsconfig: tsconfigPath } : {}),\n logLevel: 'silent',\n jsx: 'automatic',\n plugins: [stubNextPlugin, resolveProjectImportsPlugin, externalNonWorkspacePlugin],\n })\n\n inputPaths = Object.keys(buildResult.metafile?.inputs ?? {})\n .filter((inputPath) => !inputPath.startsWith('<'))\n .map((inputPath) => path.resolve(rootDir, inputPath))\n .filter((inputPath) => fs.existsSync(inputPath))\n .sort((a, b) => a.localeCompare(b))\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: rootDir,\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, inputPaths }\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 { doc: null, inputPaths }\n } finally {\n // Shut down esbuild's persistent Go service so it does not deadlock at\n // process exit when a plugin request is still in flight.\n try { await esbuild.stop() } catch {}\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 const inputManifestFile = path.join(outputDir, 'openapi.generated.inputs.json')\n\n getEnabledModuleIdentities(resolver, resolver.loadEnabledModules())\n\n // Ensure output directory exists\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true })\n }\n\n const inputContext = createOpenApiInputContext(resolver)\n const existingChecksums = readChecksumRecord(checksumFile)\n const existingManifest = readOpenApiInputManifest(inputManifestFile)\n\n if (\n existingChecksums?.structure\n && existingManifest\n && fs.existsSync(outFile)\n && calculateChecksum(fs.readFileSync(outFile, 'utf8')) === existingChecksums.content\n && calculateOpenApiInputFingerprint(resolver, inputContext, existingManifest) === existingChecksums.structure\n ) {\n result.filesUnchanged.push(outFile)\n if (!quiet) {\n console.log(`[OpenAPI] Skipped (inputs unchanged): ${outFile}`)\n }\n return result\n }\n\n // Find all API routes\n const routes = await findApiRoutes(resolver, inputContext.enabled)\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 // Try esbuild bundle approach first \u2014 produces full requestBody/response schemas\n const bundleResult = await generateOpenApiViaBundle(routes, resolver, quiet)\n let doc: Record<string, any> | null = bundleResult.doc\n\n // Fallback to static regex approach (extracts operationId/summary/tags but no schemas)\n if (!doc) {\n if (!quiet) {\n console.warn(\n '[OpenAPI] \u26A0 Bundling failed; falling back to static regex extraction \u2014 '\n + 'request/response schemas are OMITTED from the generated document. '\n + 'The usual cause is a route file importing a module specifier that does not resolve.',\n )\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 let structureChecksum = ''\n\n if (bundleResult.doc && bundleResult.inputPaths.length > 0) {\n const manifest: OpenApiInputManifest = {\n version: OPENAPI_INPUT_MANIFEST_VERSION,\n inputPaths: Array.from(new Set(bundleResult.inputPaths)).sort((a, b) => a.localeCompare(b)),\n }\n fs.writeFileSync(inputManifestFile, `${JSON.stringify(manifest, null, 2)}\\n`)\n structureChecksum = calculateOpenApiInputFingerprint(resolver, inputContext, manifest)\n } else {\n try { fs.unlinkSync(inputManifestFile) } catch {}\n }\n\n // Check if unchanged\n const existingOutputMatches = fs.existsSync(outFile)\n && calculateChecksum(fs.readFileSync(outFile, 'utf8')) === checksum\n if (existingChecksums && existingChecksums.content === checksum && existingOutputMatches) {\n if (existingChecksums.structure !== structureChecksum) {\n writeChecksumRecord(checksumFile, { content: checksum, structure: structureChecksum })\n }\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: structureChecksum })\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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",
|
|
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 { randomBytes } from 'node:crypto'\nimport * as path from 'node:path'\nimport type { ModuleEntry, PackageResolver } from '../resolver'\nimport { getEnabledModuleIdentities } from '../module-identity'\nimport { isModuleRouteFile } from './scanner'\nimport {\n calculateChecksum,\n calculateStructureChecksum,\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\ninterface OpenApiInputManifest {\n version: 1\n inputPaths: string[]\n}\n\ninterface OpenApiInputContext {\n enabled: ModuleEntry[]\n apiRoots: string[]\n moduleIdentity: Array<{\n id: string\n from: string\n appBase: string\n pkgBase: string\n }>\n}\n\ninterface OpenApiBundleResult {\n doc: Record<string, any> | null\n inputPaths: string[]\n}\n\nconst OPENAPI_INPUT_MANIFEST_VERSION = 1\n\nfunction createOpenApiInputContext(resolver: PackageResolver): OpenApiInputContext {\n const enabled = resolver.loadEnabledModules()\n const apiRoots: string[] = []\n const moduleIdentity = enabled.map((entry) => {\n const roots = resolver.getModulePaths(entry)\n apiRoots.push(path.join(roots.appBase, 'api'), path.join(roots.pkgBase, 'api'))\n return {\n id: entry.id,\n from: entry.from ?? '@open-mercato/core',\n appBase: path.resolve(roots.appBase),\n pkgBase: path.resolve(roots.pkgBase),\n }\n })\n return { enabled, apiRoots, moduleIdentity }\n}\n\nfunction readOpenApiInputManifest(filePath: string): OpenApiInputManifest | null {\n try {\n const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Partial<OpenApiInputManifest>\n if (\n parsed.version !== OPENAPI_INPUT_MANIFEST_VERSION\n || !Array.isArray(parsed.inputPaths)\n || parsed.inputPaths.some((entry) => typeof entry !== 'string')\n ) {\n return null\n }\n return {\n version: OPENAPI_INPUT_MANIFEST_VERSION,\n inputPaths: Array.from(new Set(parsed.inputPaths.map((entry) => path.resolve(entry))))\n .sort((a, b) => a.localeCompare(b)),\n }\n } catch {\n return null\n }\n}\n\nfunction trackedPathRecord(filePath: string): string {\n try {\n const stat = fs.statSync(filePath)\n if (stat.isFile()) {\n return `${path.resolve(filePath)}:${calculateChecksum(fs.readFileSync(filePath, 'utf8'))}`\n }\n return `${path.resolve(filePath)}:${stat.size}:${stat.mtimeMs}`\n } catch {\n return `missing:${path.resolve(filePath)}`\n }\n}\n\nfunction calculateOpenApiInputFingerprint(\n resolver: PackageResolver,\n context: OpenApiInputContext,\n manifest: OpenApiInputManifest,\n): string {\n const rootDir = resolver.getRootDir()\n const appDir = resolver.getAppDir()\n const configPaths = [\n resolver.getModulesConfigPath(),\n path.join(rootDir, 'package.json'),\n path.join(rootDir, 'yarn.lock'),\n path.join(rootDir, 'package-lock.json'),\n path.join(rootDir, 'pnpm-lock.yaml'),\n path.join(rootDir, 'tsconfig.json'),\n path.join(rootDir, 'tsconfig.base.json'),\n path.join(appDir, 'package.json'),\n path.join(appDir, 'tsconfig.json'),\n ]\n const trackedRecords = [...manifest.inputPaths, ...configPaths]\n .map(trackedPathRecord)\n .sort((a, b) => a.localeCompare(b))\n return calculateChecksum(JSON.stringify({\n version: OPENAPI_INPUT_MANIFEST_VERSION,\n modules: context.moduleIdentity,\n routeStructure: calculateStructureChecksum(context.apiRoots),\n trackedRecords,\n environment: {\n node: process.version,\n nodeEnv: process.env.NODE_ENV ?? '',\n publicAppUrl: process.env.NEXT_PUBLIC_APP_URL ?? '',\n },\n }))\n}\n\nfunction resolveExistingPath(paths: string[]): string | null {\n for (const candidate of paths) {\n if (fs.existsSync(candidate)) return candidate\n }\n return null\n}\n\n/**\n * Find all API route files and extract their OpenAPI specs.\n */\nasync function findApiRoutes(\n resolver: PackageResolver,\n enabled: ModuleEntry[],\n): Promise<ApiRouteInfo[]> {\n const routes: ApiRouteInfo[] = []\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() && isModuleRouteFile(e.name)) {\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 (later app files overwrite earlier package files)\n const filesByRelativePath = new Map<string, string>()\n for (const { relativePath, fullPath } of routeFiles) {\n filesByRelativePath.set(relativePath, fullPath)\n }\n\n for (const [relativePath, fullPath] of filesByRelativePath) {\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 resolver: PackageResolver,\n quiet: boolean\n): Promise<OpenApiBundleResult> {\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 { doc: null, inputPaths: [] }\n }\n\n const { execFileSync } = await import('node:child_process')\n\n const rootDir = resolver.getRootDir()\n const appDir = resolver.getAppDir()\n const sharedPackageRoot = resolver.getPackageRoot('@open-mercato/shared')\n const corePackageRoot = resolver.getPackageRoot('@open-mercato/core')\n const tsconfigPath = resolveExistingPath([\n path.join(appDir, 'tsconfig.json'),\n path.join(rootDir, 'tsconfig.base.json'),\n path.join(rootDir, 'tsconfig.json'),\n ])\n const generatorPath = path.join(sharedPackageRoot, 'src', 'lib', 'openapi', 'generator.ts')\n const coreGeneratedRoot = path.join(corePackageRoot, 'generated')\n\n if (!fs.existsSync(generatorPath)) {\n if (!quiet) {\n console.log(`[OpenAPI] Generator source not found at ${generatorPath}, skipping bundle approach`)\n }\n return { doc: null, inputPaths: [] }\n }\n\n const cacheDir = path.join(rootDir, 'node_modules', '.cache')\n if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true })\n\n // Per-run file: `yarn dev`'s watcher and a CLI `generate` (e.g. after\n // create-mercato-module init) can bundle concurrently, and a shared path let\n // one process execute the other's half-written bundle (\"Unexpected end of input\").\n const bundlePath = path.join(cacheDir, `_openapi-bundle-${process.pid}-${randomBytes(6).toString('hex')}.mjs`)\n let inputPaths: string[] = []\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 resolveProjectImportsPlugin = {\n name: 'resolve-project-imports',\n setup(build: any) {\n // @open-mercato/<pkg>/<path> \u2192 packageRoot/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 const packageName = `@open-mercato/${pkg}`\n const packageRoot = resolver.getPackageRoot(packageName)\n\n const base = rest\n ? path.join(packageRoot, 'src', rest)\n : path.join(packageRoot, '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 app/.mercato/* (tsconfig paths)\n build.onResolve({ filter: /^@\\/\\.mercato\\// }, (args: any) => {\n const rest = args.path.slice('@/'.length) // '.mercato/generated/...'\n const base = path.join(appDir, 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 app/src/* (tsconfig paths)\n build.onResolve({ filter: /^@\\// }, (args: any) => {\n const rest = args.path.slice('@/'.length)\n const base = path.join(appDir, '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 core package generated/* (Node subpath imports)\n build.onResolve({ filter: /^#generated\\// }, (args: any) => {\n const rest = args.path.slice('#generated/'.length)\n const base = path.join(coreGeneratedRoot, 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 (path.isAbsolute(args.path)) return undefined\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(rootDir, '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 const buildResult = await esbuild.build({\n stdin: {\n contents: entryScript,\n resolveDir: appDir,\n sourcefile: 'openapi-entry.ts',\n loader: 'ts',\n },\n bundle: true,\n metafile: true,\n absWorkingDir: rootDir,\n format: 'esm',\n platform: 'node',\n target: 'node18',\n outfile: bundlePath,\n write: true,\n ...(tsconfigPath ? { tsconfig: tsconfigPath } : {}),\n logLevel: 'silent',\n jsx: 'automatic',\n plugins: [stubNextPlugin, resolveProjectImportsPlugin, externalNonWorkspacePlugin],\n })\n\n inputPaths = Object.keys(buildResult.metafile?.inputs ?? {})\n .filter((inputPath) => !inputPath.startsWith('<'))\n .map((inputPath) => path.resolve(rootDir, inputPath))\n .filter((inputPath) => fs.existsSync(inputPath))\n .sort((a, b) => a.localeCompare(b))\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: rootDir,\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, inputPaths }\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 { doc: null, inputPaths }\n } finally {\n // Shut down esbuild's persistent Go service so it does not deadlock at\n // process exit when a plugin request is still in flight.\n try { await esbuild.stop() } catch {}\n // Clean up old files from previous tsx-based approach\n try { fs.unlinkSync(bundlePath) } catch {}\n for (const file of ['_openapi-bundle.mjs', '_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 const inputManifestFile = path.join(outputDir, 'openapi.generated.inputs.json')\n\n getEnabledModuleIdentities(resolver, resolver.loadEnabledModules())\n\n // Ensure output directory exists\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true })\n }\n\n const inputContext = createOpenApiInputContext(resolver)\n const existingChecksums = readChecksumRecord(checksumFile)\n const existingManifest = readOpenApiInputManifest(inputManifestFile)\n\n if (\n existingChecksums?.structure\n && existingManifest\n && fs.existsSync(outFile)\n && calculateChecksum(fs.readFileSync(outFile, 'utf8')) === existingChecksums.content\n && calculateOpenApiInputFingerprint(resolver, inputContext, existingManifest) === existingChecksums.structure\n ) {\n result.filesUnchanged.push(outFile)\n if (!quiet) {\n console.log(`[OpenAPI] Skipped (inputs unchanged): ${outFile}`)\n }\n return result\n }\n\n // Find all API routes\n const routes = await findApiRoutes(resolver, inputContext.enabled)\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 // Try esbuild bundle approach first \u2014 produces full requestBody/response schemas\n const bundleResult = await generateOpenApiViaBundle(routes, resolver, quiet)\n let doc: Record<string, any> | null = bundleResult.doc\n\n // Fallback to static regex approach (extracts operationId/summary/tags but no schemas)\n if (!doc) {\n if (!quiet) {\n console.warn(\n '[OpenAPI] \u26A0 Bundling failed; falling back to static regex extraction \u2014 '\n + 'request/response schemas are OMITTED from the generated document. '\n + 'The usual cause is a route file importing a module specifier that does not resolve.',\n )\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 let structureChecksum = ''\n\n if (bundleResult.doc && bundleResult.inputPaths.length > 0) {\n const manifest: OpenApiInputManifest = {\n version: OPENAPI_INPUT_MANIFEST_VERSION,\n inputPaths: Array.from(new Set(bundleResult.inputPaths)).sort((a, b) => a.localeCompare(b)),\n }\n fs.writeFileSync(inputManifestFile, `${JSON.stringify(manifest, null, 2)}\\n`)\n structureChecksum = calculateOpenApiInputFingerprint(resolver, inputContext, manifest)\n } else {\n try { fs.unlinkSync(inputManifestFile) } catch {}\n }\n\n // Check if unchanged\n const existingOutputMatches = fs.existsSync(outFile)\n && calculateChecksum(fs.readFileSync(outFile, 'utf8')) === checksum\n if (existingChecksums && existingChecksums.content === checksum && existingOutputMatches) {\n if (existingChecksums.structure !== structureChecksum) {\n writeChecksumRecord(checksumFile, { content: checksum, structure: structureChecksum })\n }\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: structureChecksum })\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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
const BULLMQ_OTEL_VERSION = "^
|
|
3
|
+
const BULLMQ_OTEL_VERSION = "^2.0.1";
|
|
4
4
|
const ENV_CORE_BLOCK = `
|
|
5
5
|
# --- Telemetry & Observability (vendor-neutral OTLP: traces, logs, metrics, errors) ---
|
|
6
6
|
# Off by default: leave TELEMETRY_BACKEND unset (or =noop) for a hard no-op
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/telemetry-init.ts"],
|
|
4
|
-
"sourcesContent": ["import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\n\n/**\n * `mercato telemetry init` \u2014 wire `@open-mercato/telemetry` into an app that was\n * scaffolded before telemetry existed. Deterministic, idempotent, and safe to\n * re-run: each step detects whether it is already applied and skips.\n *\n * The web-tier wiring lives in app-owned source files (a dependency bump cannot\n * touch them), so this patches them in place using the same idioms the rest of\n * the CLI uses: JSON edits, detect-before-append for `.env`, a ts-morph edit for\n * `next.config.ts`, and anchored insertion for `instrumentation.ts`, the API\n * dispatcher, `src/modules.ts`, and the backoffice layout. When a file's shape is\n * not recognized (a customized dispatcher, an unusual next.config, a rewritten\n * layout), the step degrades to printing the exact snippet and flags it as a\n * manual step rather than editing code it does not understand.\n *\n * \"Already wired\" is decided per capability, not per file: an app initialized\n * before browser RUM shipped has the core telemetry block and none of the RUM\n * wiring, and re-running the command is how it adopts the difference.\n *\n * Worker/scheduler telemetry is not handled here \u2014 it ships transitively via\n * `@open-mercato/cli`'s telemetry dependency once the app updates its packages.\n */\n\nconst BULLMQ_OTEL_VERSION = '^1.3.1'\n\ntype StepStatus = 'created' | 'patched' | 'skipped' | 'manual'\n\ntype StepResult = {\n file: string\n status: StepStatus\n detail: string\n /** Snippet to show when the step needs manual work. */\n manualSnippet?: string\n}\n\ntype TelemetryInitOptions = {\n dryRun: boolean\n}\n\nconst ENV_CORE_BLOCK = `\n# --- Telemetry & Observability (vendor-neutral OTLP: traces, logs, metrics, errors) ---\n# Off by default: leave TELEMETRY_BACKEND unset (or =noop) for a hard no-op\n# (the OpenTelemetry SDK is never loaded). Set to 'console' for local span/metric\n# output, or an OTLP backend to export: 'signoz' | 'newrelic' | 'otlp' (generic).\n# All three OTLP names use the same exporter \u2014 they differ only by the endpoint +\n# headers below. Any OTLP backend is a one-line swap; no code change.\n# TELEMETRY_BACKEND=otlp\n# Logs use the shared logger facade and its OM_LOG_LEVEL / OM_LOG_PRETTY /\n# OM_LOG_DESTINATION controls. Telemetry adds remote export; it does not create\n# a second local output path.\n# TELEMETRY_SAMPLING_RATIO=1.0 # 0.0\u20131.0 (default 1.0 dev / 0.1 prod)\n# TELEMETRY_TRUST_INBOUND_TRACE=false # default false: root-per-request (ignore an\n# # inbound traceparent from a proxy/LB). Set true\n# # only behind a trusted upstream whose trace continues.\n# # This also enables richer bullmq-otel spans;\n# # otherwise async queues use metadata._trace.\n#\n# OTLP endpoint + auth \u2014 pick ONE vendor (use your region's host):\n# SigNoz Cloud:\n# OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.<region>.signoz.cloud:443\n# OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key=<your-ingestion-key>\n# New Relic (OTLP-native; EU host shown, US is https://otlp.nr-data.net):\n# OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.eu01.nr-data.net\n# OTEL_EXPORTER_OTLP_HEADERS=api-key=<your-ingest-license-key>\n#\n# Resource identity: keep one stable service name across environments and\n# separate envs via the deployment.environment resource attribute.\n# OTEL_SERVICE_NAME=open-mercato\n# OTEL_RESOURCE_ATTRIBUTES=deployment.environment=local\n`\n\n/**\n * Appended on its own when an app already carries the core block \u2014 an app\n * initialized before browser RUM shipped must be able to adopt it by re-running\n * the command.\n */\nconst ENV_BROWSER_BLOCK = `\n# --- Browser RUM (client-side telemetry) ---\n# Document-load and fetch spans exported from the backoffice through a\n# same-origin proxy (/api/telemetry/browser-traces) that adds the collector\n# credential server-side. Off by default; requires an active\n# TELEMETRY_BACKEND + OTLP endpoint above.\n# Read at request time (not NEXT_PUBLIC_*), so toggling needs no rebuild.\n# Browser spans join their server spans only when TELEMETRY_TRUST_INBOUND_TRACE=true\n# above; without it RUM still works, but each page yields two separate traces.\n# Ingest budget: each open tab exports at most 20 batches/min (a 3s batch delay),\n# and the proxy allows 400/min per authenticated user \u2014 roughly 20 concurrent tabs.\n# Over that, batches are dropped with 204 rather than a retryable status, so the\n# only symptom is missing spans for that one user, never a client retry storm.\n# TELEMETRY_BROWSER_ENABLED=false\n# TELEMETRY_BROWSER_SAMPLING_RATIO=1.0 # 0.0-1.0 (default 1.0)\n# TELEMETRY_BROWSER_SERVICE_NAME= # default: <OTEL_SERVICE_NAME>-browser\n`\n\nconst ENV_BLOCK = `${ENV_CORE_BLOCK}${ENV_BROWSER_BLOCK}`\n\nconst INSTRUMENTATION_TS = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\n\nexport async function register(): Promise<void> {\n // Initialize telemetry (no-op unless TELEMETRY_BACKEND is set). OTEL's NodeSDK\n // is Node-only and incompatible with the edge runtime, so the telemetry\n // bootstrap \u2014 which can pull in the SDK \u2014 is imported only on the Node.js\n // runtime. The helper owns init + graceful degrade + shutdown flush; a\n // deliberate configuration failure bubbles here so the host exits loudly.\n if (\n process.env.NEXT_RUNTIME === 'nodejs'\n && isTelemetryBackendEnabled()\n ) {\n const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')\n try {\n await registerTelemetryForNextjs()\n } catch (err) {\n const nodeProcess = process\n nodeProcess.stderr.write(\\`\\${err instanceof Error ? err.message : String(err)}\\\\n\\`)\n nodeProcess.exit(1)\n }\n }\n}\n`\n\nconst MULTILINE_TELEMETRY_GUARD = ` if (\n process.env.NEXT_RUNTIME === 'nodejs'\n && isTelemetryBackendEnabled()\n ) {`\nconst INLINE_TELEMETRY_GUARD = ` if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {`\nconst LEGACY_NODE_TELEMETRY_GUARD = ` if (process.env.NEXT_RUNTIME === 'nodejs') {`\nconst TELEMETRY_DYNAMIC_IMPORT = ` const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')`\nconst BARE_TELEMETRY_REGISTRATION = ` await registerTelemetryForNextjs()`\nconst GUARDED_TELEMETRY_REGISTRATION = ` try {\n await registerTelemetryForNextjs()\n } catch (err) {\n const nodeProcess = process\n nodeProcess.stderr.write(\\`\\${err instanceof Error ? err.message : String(err)}\\\\n\\`)\n nodeProcess.exit(1)\n }`\n\nconst DISPATCHER_SNIPPET = ` // add near the other imports:\n import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\n // in the dispatcher's success path, just before returning the response:\n getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, finalResponse.status, startedAt)\n\n // in the dispatcher's catch (error) block, before re-throwing:\n getTelemetryRuntime()?.reportError(error, {\n attributes: { 'http.request.method': method, 'http.route': match.route.path, 'http.response.status_code': 500 },\n })\n getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, 500, startedAt)`\n\nfunction parseArgs(args: string[]): TelemetryInitOptions {\n return {\n dryRun: args.includes('--dry-run') || args.includes('-n'),\n }\n}\n\n/** Read the version an app already pins its `@open-mercato/*` deps to (lockstep). */\nfunction resolveOpenMercatoVersion(pkg: Record<string, unknown>): string {\n for (const field of ['dependencies', 'devDependencies'] as const) {\n const deps = pkg[field]\n if (deps && typeof deps === 'object') {\n for (const [name, version] of Object.entries(deps as Record<string, string>)) {\n if (name.startsWith('@open-mercato/') && typeof version === 'string') return version\n }\n }\n }\n return '*'\n}\n\nfunction patchPackageJson(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'package.json'\n const path = join(appDir, file)\n const raw = readFileSync(path, 'utf8')\n const pkg = JSON.parse(raw) as {\n dependencies?: Record<string, string>\n optionalDependencies?: Record<string, string>\n }\n const version = resolveOpenMercatoVersion(pkg)\n const hasTelemetry = Boolean(pkg.dependencies?.['@open-mercato/telemetry'])\n const hasBullmqOtel = Boolean(pkg.optionalDependencies?.['bullmq-otel'])\n if (hasTelemetry && hasBullmqOtel) {\n return { file, status: 'skipped', detail: '@open-mercato/telemetry already installed' }\n }\n\n const deps = { ...(pkg.dependencies ?? {}) }\n if (!hasTelemetry) deps['@open-mercato/telemetry'] = version\n const optDeps = { ...(pkg.optionalDependencies ?? {}) }\n if (!hasBullmqOtel) optDeps['bullmq-otel'] = BULLMQ_OTEL_VERSION\n\n const next = {\n ...pkg,\n dependencies: sortRecord(deps),\n optionalDependencies: sortRecord(optDeps),\n }\n if (!options.dryRun) writeFileSync(path, `${JSON.stringify(next, null, 2)}\\n`)\n return {\n file,\n status: 'patched',\n detail: `+@open-mercato/telemetry@${version}${hasBullmqOtel ? '' : ` +bullmq-otel@${BULLMQ_OTEL_VERSION} (optional)`} \u2014 run \\`yarn install\\``,\n }\n}\n\nfunction sortRecord(record: Record<string, string>): Record<string, string> {\n return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)))\n}\n\nfunction patchEnvFile(appDir: string, relativePath: string, options: TelemetryInitOptions): StepResult | null {\n const path = join(appDir, relativePath)\n if (!existsSync(path)) return null\n const content = readFileSync(path, 'utf8')\n // Checked key by key, not block by block: an app initialized before browser RUM\n // shipped has the core keys and none of the browser ones, and re-running the\n // command is how it adopts them.\n const hasCore = content.includes('TELEMETRY_BACKEND')\n const hasBrowser = content.includes('TELEMETRY_BROWSER_ENABLED')\n if (hasCore && hasBrowser) {\n return { file: relativePath, status: 'skipped', detail: 'TELEMETRY_* block already present' }\n }\n const next = `${content.replace(/\\s*$/, '')}\\n${hasCore ? ENV_BROWSER_BLOCK : ENV_BLOCK}`\n if (!options.dryRun) writeFileSync(path, next)\n return {\n file: relativePath,\n status: 'patched',\n detail: hasCore\n ? 'appended the missing TELEMETRY_BROWSER_* block'\n : 'appended commented TELEMETRY_* / OTEL_* block',\n }\n}\n\nconst MODULES_SNIPPET = ` // in the enabledModules array:\n { id: 'telemetry', from: '@open-mercato/telemetry' },`\n\n/**\n * Register the `telemetry` module, which owns the same-origin OTLP proxy the\n * browser exporter posts to. Without it `TELEMETRY_BROWSER_ENABLED` produces a\n * client that exports into a 404.\n */\nfunction patchModules(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'src/modules.ts'\n const path = join(appDir, file)\n const content = readFileSync(path, 'utf8')\n if (/id:\\s*'telemetry'/.test(content)) {\n return { file, status: 'skipped', detail: 'telemetry module already enabled' }\n }\n const anchor = content.match(/export\\s+const\\s+enabledModules[^=]*=\\s*\\[/)\n if (!anchor || anchor.index === undefined) {\n return {\n file,\n status: 'manual',\n detail: 'enabledModules array not found \u2014 add the entry by hand',\n manualSnippet: MODULES_SNIPPET,\n }\n }\n const insertAt = anchor.index + anchor[0].length\n const block =\n `\\n // Same-origin OTLP proxy for browser RUM spans; inert unless` +\n `\\n // TELEMETRY_BROWSER_ENABLED is set alongside an active telemetry backend.` +\n `\\n { id: 'telemetry', from: '@open-mercato/telemetry' },`\n if (!options.dryRun) writeFileSync(path, content.slice(0, insertAt) + block + content.slice(insertAt))\n return { file, status: 'patched', detail: \"enabled the 'telemetry' module (browser-traces proxy route)\" }\n}\n\nconst LAYOUT_FILE = 'src/app/(backend)/backend/layout.tsx'\n\nconst LAYOUT_SNIPPET = ` // add near the other imports:\n import { BrowserTelemetry } from '@open-mercato/telemetry/browser'\n import { resolveBrowserTelemetryConfig } from '@open-mercato/telemetry/browser/server'\n\n // in the component body, before the returned JSX:\n // pass the request's Cookie header when the layout already reads cookies\n // (only consulted by the integration-test opt-in; production is env-only):\n const browserTelemetryConfig = resolveBrowserTelemetryConfig({ cookieHeader: cookieStore.toString() })\n\n // as the last child of <AppShell>:\n <BrowserTelemetry config={browserTelemetryConfig} />`\n\n/**\n * The config resolver takes the request's `Cookie` header so an integration spec can opt one page\n * into browser RUM; production resolution is env-only and never reads it. A scaffolded layout\n * already awaits `cookies()`, so reuse that binding \u2014 but an app that removed it must still get a\n * layout that compiles, hence the argument-free fallback. Both forms behave identically outside an\n * `OM_TEST_MODE` environment.\n */\nfunction resolveArgument(layout: string): string {\n const binding = layout.match(/const\\s+(\\w+)\\s*=\\s*await\\s+cookies\\(\\)/)\n return binding ? `{ cookieHeader: ${binding[1]}.toString() }` : ''\n}\n\n/**\n * Render the client bootstrap in the backoffice shell. The config is resolved\n * per request (the layout is `force-dynamic`), so RUM toggles per environment\n * without a rebuild.\n *\n * A backoffice layout is the file apps customize most, so this only edits one\n * that still matches the scaffold shape; anything else gets the snippet.\n */\nfunction patchBackendLayout(appDir: string, options: TelemetryInitOptions): StepResult {\n const path = join(appDir, LAYOUT_FILE)\n if (!existsSync(path)) {\n return { file: LAYOUT_FILE, status: 'manual', detail: 'backend layout not found', manualSnippet: LAYOUT_SNIPPET }\n }\n const content = readFileSync(path, 'utf8')\n if (content.includes('<BrowserTelemetry')) {\n return { file: LAYOUT_FILE, status: 'skipped', detail: 'BrowserTelemetry already rendered' }\n }\n const closingShell = content.lastIndexOf('</AppShell>')\n const returnMatch = content.match(/^([ \\t]*)return \\(\\s*$/m)\n const lastImport = [...content.matchAll(/^import[^\\n]*$/gm)].at(-1)\n if (closingShell === -1 || !returnMatch || returnMatch.index === undefined || !lastImport) {\n return {\n file: LAYOUT_FILE,\n status: 'manual',\n detail: 'layout shape not recognized \u2014 apply the snippet by hand',\n manualSnippet: LAYOUT_SNIPPET,\n }\n }\n\n // Edit back to front so each earlier anchor's offset stays valid.\n const shellIndent = content.slice(0, closingShell).match(/\\n([ \\t]*)$/)?.[1] ?? ' '\n let next =\n content.slice(0, closingShell) +\n `<BrowserTelemetry config={browserTelemetryConfig} />\\n${shellIndent}` +\n content.slice(closingShell)\n\n const returnIndent = returnMatch[1]\n next =\n next.slice(0, returnMatch.index) +\n `${returnIndent}// Resolved per request (this layout is force-dynamic), so browser RUM can be\\n` +\n `${returnIndent}// toggled per environment without a rebuild. Null keeps the SDK chunk from\\n` +\n `${returnIndent}// ever being requested.\\n` +\n `${returnIndent}const browserTelemetryConfig = resolveBrowserTelemetryConfig(${resolveArgument(content)})\\n\\n` +\n next.slice(returnMatch.index)\n\n const importInsertAt = (lastImport.index ?? 0) + lastImport[0].length\n next =\n next.slice(0, importInsertAt) +\n `\\nimport { BrowserTelemetry } from '@open-mercato/telemetry/browser'` +\n `\\nimport { resolveBrowserTelemetryConfig } from '@open-mercato/telemetry/browser/server'` +\n next.slice(importInsertAt)\n\n if (!options.dryRun) writeFileSync(path, next)\n return { file: LAYOUT_FILE, status: 'patched', detail: 'rendered <BrowserTelemetry /> in the backoffice shell' }\n}\n\nfunction patchInstrumentation(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'src/instrumentation.ts'\n const path = join(appDir, file)\n if (!existsSync(path)) {\n if (!options.dryRun) {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, INSTRUMENTATION_TS)\n }\n return { file, status: 'created', detail: 'Node-runtime-guarded registerTelemetryForNextjs() bootstrap' }\n }\n const content = readFileSync(path, 'utf8')\n if (content.includes('registerTelemetryForNextjs')) {\n const guardedBootstraps = [MULTILINE_TELEMETRY_GUARD, INLINE_TELEMETRY_GUARD]\n const safeBootstrap = guardedBootstraps\n .map((guard) => `${guard}\\n${TELEMETRY_DYNAMIC_IMPORT}\\n${GUARDED_TELEMETRY_REGISTRATION}`)\n .find((bootstrap) => content.includes(bootstrap))\n if (safeBootstrap) {\n return { file, status: 'skipped', detail: 'telemetry already wired' }\n }\n const guardedLegacyBootstrap = guardedBootstraps\n .map((guard) => `${guard}\\n${TELEMETRY_DYNAMIC_IMPORT}\\n${BARE_TELEMETRY_REGISTRATION}`)\n .find((bootstrap) => content.includes(bootstrap))\n if (guardedLegacyBootstrap) {\n const migrated = content.replace(\n guardedLegacyBootstrap,\n guardedLegacyBootstrap.replace(BARE_TELEMETRY_REGISTRATION, GUARDED_TELEMETRY_REGISTRATION),\n )\n if (!options.dryRun) writeFileSync(path, migrated)\n return { file, status: 'patched', detail: 'wrapped OTLP dependency failures with host termination' }\n }\n const nodeOnlyLegacyBootstrap = `${LEGACY_NODE_TELEMETRY_GUARD}\\n${TELEMETRY_DYNAMIC_IMPORT}\\n${BARE_TELEMETRY_REGISTRATION}`\n if (content.includes(nodeOnlyLegacyBootstrap)) {\n const guardedBootstrap = nodeOnlyLegacyBootstrap\n .replace(LEGACY_NODE_TELEMETRY_GUARD, INLINE_TELEMETRY_GUARD)\n .replace(BARE_TELEMETRY_REGISTRATION, GUARDED_TELEMETRY_REGISTRATION)\n const importLine = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\\n`\n const withBootstrap = content.replace(nodeOnlyLegacyBootstrap, guardedBootstrap)\n const migrated = withBootstrap.includes(importLine) ? withBootstrap : importLine + withBootstrap\n if (!options.dryRun) writeFileSync(path, migrated)\n return {\n file,\n status: 'patched',\n detail: 'added the explicit backend guard and wrapped OTLP dependency failures with host termination',\n }\n }\n return {\n file,\n status: 'manual',\n detail: 'existing telemetry bootstrap needs the canonical Node guard and fail-closed host wrapper',\n manualSnippet: INSTRUMENTATION_TS,\n }\n }\n const anchor = content.match(/export\\s+async\\s+function\\s+register\\s*\\([^)]*\\)\\s*:\\s*Promise<void>\\s*\\{/)\n if (!anchor || anchor.index === undefined) {\n return {\n file,\n status: 'manual',\n detail: 'register() not found \u2014 add the telemetry bootstrap manually',\n manualSnippet: INSTRUMENTATION_TS,\n }\n }\n const insertAt = anchor.index + anchor[0].length\n const block =\n `\\n // Initialize telemetry (no-op unless TELEMETRY_BACKEND is set); Node-only.` +\n `\\n if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {` +\n `\\n const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')` +\n `\\n try {` +\n `\\n await registerTelemetryForNextjs()` +\n `\\n } catch (err) {` +\n `\\n const nodeProcess = process` +\n `\\n nodeProcess.stderr.write(\\`\\${err instanceof Error ? err.message : String(err)}\\\\n\\`)` +\n `\\n nodeProcess.exit(1)` +\n `\\n }` +\n `\\n }`\n const importLine = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\\n`\n const withImport = content.includes('isTelemetryBackendEnabled')\n ? content\n : importLine + content\n const shiftedInsertAt = insertAt + (withImport.length - content.length)\n const next = withImport.slice(0, shiftedInsertAt) + block + withImport.slice(shiftedInsertAt)\n if (!options.dryRun) writeFileSync(path, next)\n return { file, status: 'patched', detail: 'inserted registerTelemetryForNextjs() into register()' }\n}\n\nasync function patchNextConfig(appDir: string, options: TelemetryInitOptions): Promise<StepResult> {\n const file = 'next.config.ts'\n const path = join(appDir, file)\n if (!existsSync(path)) {\n return { file, status: 'manual', detail: 'next.config.ts not found', manualSnippet: nextConfigSnippet() }\n }\n // Use ts-morph to *detect* the real `serverExternalPackages` array literal\n // (robust against a match inside a comment/string), then do the *insertion*\n // via a formatting-preserving text splice so the user's config stays tidy.\n const { Project, SyntaxKind } = await import('ts-morph')\n const project = new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true })\n const sf = project.addSourceFileAtPath(path)\n\n const telemetryConfigImport = sf\n .getImportDeclarations()\n .find((decl) => [\n '@open-mercato/telemetry/nextjs',\n '@open-mercato/telemetry/nextjs-config',\n ].includes(decl.getModuleSpecifierValue()))\n const hasImport = Boolean(telemetryConfigImport)\n const hasCanonicalImport =\n telemetryConfigImport?.getModuleSpecifierValue() === '@open-mercato/telemetry/nextjs-config'\n\n const arrayProp = sf\n .getDescendantsOfKind(SyntaxKind.PropertyAssignment)\n .find((prop) => prop.getName() === 'serverExternalPackages')\n const arrayInit = arrayProp?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)\n if (!arrayInit) {\n return {\n file,\n status: 'manual',\n detail: 'serverExternalPackages array not found',\n manualSnippet: nextConfigSnippet(),\n }\n }\n const hasSpread = arrayInit.getElements().some((el) => el.getText().includes('telemetryServerExternalPackages'))\n if (hasCanonicalImport && hasSpread) {\n return { file, status: 'skipped', detail: 'telemetryServerExternalPackages already wired' }\n }\n\n let text = readFileSync(path, 'utf8')\n // Splice the array first, using the AST node's byte offsets (formatting-agnostic:\n // handles both single-line and multi-line arrays). Do this before modifying\n // imports, because either an inserted import or the legacy import migration\n // shifts the array offsets captured from the original source.\n if (!hasSpread) {\n const start = arrayInit.getStart()\n const end = arrayInit.getEnd()\n text = text.slice(0, start) + insertSpread(text.slice(start, end)) + text.slice(end)\n }\n text = text.replace(\n \"from '@open-mercato/telemetry/nextjs'\",\n \"from '@open-mercato/telemetry/nextjs-config'\",\n )\n if (!hasImport) {\n const importRegex = /^import[^\\n]*$/gm\n let lastImport: RegExpExecArray | null = null\n let match: RegExpExecArray | null\n while ((match = importRegex.exec(text)) !== null) lastImport = match\n const insertAt = lastImport ? lastImport.index + lastImport[0].length : 0\n text =\n text.slice(0, insertAt) +\n `\\nimport { telemetryServerExternalPackages } from '@open-mercato/telemetry/nextjs-config'` +\n text.slice(insertAt)\n }\n if (!options.dryRun) writeFileSync(path, text)\n return { file, status: 'patched', detail: 'spread telemetryServerExternalPackages into serverExternalPackages' }\n}\n\n/** Add `...telemetryServerExternalPackages` as the last element of an array literal, preserving its style. */\nfunction insertSpread(arrayText: string): string {\n const inner = arrayText.slice(1, -1)\n if (inner.includes('\\n')) {\n const elementIndent = inner.match(/\\n([ \\t]+)\\S/)?.[1] ?? ' '\n const closeIndent = arrayText.match(/\\n([ \\t]*)\\]$/)?.[1] ?? ''\n const trimmed = inner.replace(/\\s*$/, '')\n const separator = trimmed === '' || trimmed.endsWith(',') ? '' : ','\n return `[${trimmed}${separator}\\n${elementIndent}...telemetryServerExternalPackages,\\n${closeIndent}]`\n }\n const flat = inner.trim()\n return flat === '' ? '[...telemetryServerExternalPackages]' : `[${flat}, ...telemetryServerExternalPackages]`\n}\n\nfunction nextConfigSnippet(): string {\n return (\n `import { telemetryServerExternalPackages } from '@open-mercato/telemetry/nextjs-config'\\n` +\n `// serverExternalPackages: [ ...existing, ...telemetryServerExternalPackages ]`\n )\n}\n\n/**\n * Auto-patch the catch-all API dispatcher \u2014 but only when it still matches the\n * known scaffold shape (all anchors present, telemetry not yet wired). On any\n * mismatch, print the snippet and mark it manual rather than editing an\n * unrecognized (customized) handler.\n */\nfunction patchDispatcher(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'src/app/api/[...slug]/route.ts'\n const path = join(appDir, file)\n if (!existsSync(path)) {\n return { file, status: 'manual', detail: 'dispatcher not found', manualSnippet: DISPATCHER_SNIPPET }\n }\n let content = readFileSync(path, 'utf8')\n if (\n content.includes(\"from '@open-mercato/shared/lib/telemetry/runtime'\")\n && content.includes('getTelemetryRuntime()?.recordHttpDuration')\n ) {\n return { file, status: 'skipped', detail: 'telemetry already wired' }\n }\n if (\n content.includes(\"import { reportError } from '@open-mercato/telemetry'\")\n && content.includes(\"import { recordHttpDuration } from '@open-mercato/telemetry/nextjs'\")\n ) {\n content = content\n .replace(\n \"import { reportError } from '@open-mercato/telemetry'\\nimport { recordHttpDuration } from '@open-mercato/telemetry/nextjs'\",\n \"import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\",\n )\n .replace(/\\brecordHttpDuration\\(/g, 'getTelemetryRuntime()?.recordHttpDuration(')\n .replace(/\\breportError\\(/g, 'getTelemetryRuntime()?.reportError(')\n if (!options.dryRun) writeFileSync(path, content)\n return { file, status: 'patched', detail: 'migrated dispatcher to the default-off runtime bridge' }\n }\n\n const importAnchor = \"import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\"\n const returnMatch = content.match(/^([ \\t]*)return finalResponse\\s*$/m)\n // The handleRequest catch is the first `} catch (error) {` after the success return.\n const catchRegex = /^([ \\t]*)\\}\\s*catch\\s*\\(error\\)\\s*\\{[ \\t]*$/gm\n const catchMatch = findCatchAfter(content, catchRegex, returnMatch?.index ?? -1)\n\n if (!content.includes(importAnchor) || !returnMatch || returnMatch.index === undefined || !catchMatch) {\n return {\n file,\n status: 'manual',\n detail: 'dispatcher shape not recognized \u2014 apply the snippet by hand',\n manualSnippet: DISPATCHER_SNIPPET,\n }\n }\n\n // 1) imports\n content = content.replace(\n importAnchor,\n `${importAnchor}\\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'`,\n )\n\n // 2) success-path metric (re-match after the import edit shifted offsets)\n const ret = content.match(/^([ \\t]*)return finalResponse\\s*$/m)\n if (!ret || ret.index === undefined) {\n return { file, status: 'manual', detail: 'success-path anchor lost', manualSnippet: DISPATCHER_SNIPPET }\n }\n const retIndent = ret[1]\n content =\n content.slice(0, ret.index) +\n `${retIndent}getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, finalResponse.status, startedAt)\\n` +\n content.slice(ret.index)\n\n // 3) catch-block error funnel + 500 metric\n const catch2 = findCatchAfter(content, /^([ \\t]*)\\}\\s*catch\\s*\\(error\\)\\s*\\{[ \\t]*$/gm, content.indexOf('return finalResponse'))\n if (!catch2) {\n return { file, status: 'manual', detail: 'catch anchor lost', manualSnippet: DISPATCHER_SNIPPET }\n }\n const bodyIndent = `${catch2.indent} `\n const insertAt = catch2.index + catch2.length\n const block =\n `\\n${bodyIndent}getTelemetryRuntime()?.reportError(error, {` +\n `\\n${bodyIndent} attributes: { 'http.request.method': method, 'http.route': match.route.path, 'http.response.status_code': 500 },` +\n `\\n${bodyIndent}})` +\n `\\n${bodyIndent}getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, 500, startedAt)`\n content = content.slice(0, insertAt) + block + content.slice(insertAt)\n\n if (!options.dryRun) writeFileSync(path, content)\n return { file, status: 'patched', detail: 'wired reportError (5xx) + http.server.request.duration metric' }\n}\n\nfunction findCatchAfter(\n content: string,\n regex: RegExp,\n afterIndex: number,\n): { index: number; length: number; indent: string } | null {\n regex.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = regex.exec(content)) !== null) {\n if (match.index > afterIndex) {\n return { index: match.index, length: match[0].length, indent: match[1] }\n }\n }\n return null\n}\n\nexport async function runTelemetryInit(args: string[]): Promise<number> {\n const appDir = resolve('.')\n const options = parseArgs(args)\n\n if (!existsSync(join(appDir, 'src', 'modules.ts'))) {\n console.error('\u274C Not an Open Mercato app directory (src/modules.ts not found)')\n return 1\n }\n if (!existsSync(join(appDir, 'package.json'))) {\n console.error('\u274C package.json not found')\n return 1\n }\n\n console.log('')\n console.log(options.dryRun ? '\uD83D\uDD2D Telemetry init (dry run \u2014 no files written)\\n' : '\uD83D\uDD2D Wiring @open-mercato/telemetry into this app\\n')\n\n const results: StepResult[] = []\n results.push(patchPackageJson(appDir, options))\n const envExample = patchEnvFile(appDir, '.env.example', options)\n if (envExample) results.push(envExample)\n const env = patchEnvFile(appDir, '.env', options)\n if (env) results.push(env)\n results.push(patchInstrumentation(appDir, options))\n results.push(await patchNextConfig(appDir, options))\n results.push(patchDispatcher(appDir, options))\n results.push(patchModules(appDir, options))\n results.push(patchBackendLayout(appDir, options))\n\n const icon: Record<StepStatus, string> = { created: '\u270D\uFE0F ', patched: '\u2705', skipped: '\u23ED\uFE0F ', manual: '\u26A0\uFE0F ' }\n for (const result of results) {\n console.log(`${icon[result.status]} ${result.file} \u2014 ${result.detail}`)\n }\n\n const manual = results.filter((result) => result.status === 'manual')\n for (const result of manual) {\n if (!result.manualSnippet) continue\n console.log('')\n console.log(`\u2500\u2500 manual step for ${result.file} \u2500\u2500`)\n console.log(result.manualSnippet)\n }\n\n console.log('')\n const changed = results.some((result) => result.status === 'patched' || result.status === 'created')\n if (options.dryRun) {\n console.log('Dry run complete \u2014 re-run without --dry-run to apply.')\n } else if (changed) {\n console.log('Next steps:')\n console.log(' 1. yarn install (resolve @open-mercato/telemetry + the OTEL SDK)')\n console.log(' 2. set TELEMETRY_BACKEND + OTEL_EXPORTER_OTLP_* in .env')\n console.log(' 3. yarn typecheck && yarn build (verify the wiring)')\n } else {\n console.log('Nothing to do \u2014 telemetry is already wired.')\n }\n if (manual.length > 0) {\n console.log('')\n console.log(`\u26A0\uFE0F ${manual.length} file(s) need a manual edit (shown above) \u2014 the rest were applied automatically.`)\n }\n return 0\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\n\n/**\n * `mercato telemetry init` \u2014 wire `@open-mercato/telemetry` into an app that was\n * scaffolded before telemetry existed. Deterministic, idempotent, and safe to\n * re-run: each step detects whether it is already applied and skips.\n *\n * The web-tier wiring lives in app-owned source files (a dependency bump cannot\n * touch them), so this patches them in place using the same idioms the rest of\n * the CLI uses: JSON edits, detect-before-append for `.env`, a ts-morph edit for\n * `next.config.ts`, and anchored insertion for `instrumentation.ts`, the API\n * dispatcher, `src/modules.ts`, and the backoffice layout. When a file's shape is\n * not recognized (a customized dispatcher, an unusual next.config, a rewritten\n * layout), the step degrades to printing the exact snippet and flags it as a\n * manual step rather than editing code it does not understand.\n *\n * \"Already wired\" is decided per capability, not per file: an app initialized\n * before browser RUM shipped has the core telemetry block and none of the RUM\n * wiring, and re-running the command is how it adopts the difference.\n *\n * Worker/scheduler telemetry is not handled here \u2014 it ships transitively via\n * `@open-mercato/cli`'s telemetry dependency once the app updates its packages.\n */\n\nconst BULLMQ_OTEL_VERSION = '^2.0.1'\n\ntype StepStatus = 'created' | 'patched' | 'skipped' | 'manual'\n\ntype StepResult = {\n file: string\n status: StepStatus\n detail: string\n /** Snippet to show when the step needs manual work. */\n manualSnippet?: string\n}\n\ntype TelemetryInitOptions = {\n dryRun: boolean\n}\n\nconst ENV_CORE_BLOCK = `\n# --- Telemetry & Observability (vendor-neutral OTLP: traces, logs, metrics, errors) ---\n# Off by default: leave TELEMETRY_BACKEND unset (or =noop) for a hard no-op\n# (the OpenTelemetry SDK is never loaded). Set to 'console' for local span/metric\n# output, or an OTLP backend to export: 'signoz' | 'newrelic' | 'otlp' (generic).\n# All three OTLP names use the same exporter \u2014 they differ only by the endpoint +\n# headers below. Any OTLP backend is a one-line swap; no code change.\n# TELEMETRY_BACKEND=otlp\n# Logs use the shared logger facade and its OM_LOG_LEVEL / OM_LOG_PRETTY /\n# OM_LOG_DESTINATION controls. Telemetry adds remote export; it does not create\n# a second local output path.\n# TELEMETRY_SAMPLING_RATIO=1.0 # 0.0\u20131.0 (default 1.0 dev / 0.1 prod)\n# TELEMETRY_TRUST_INBOUND_TRACE=false # default false: root-per-request (ignore an\n# # inbound traceparent from a proxy/LB). Set true\n# # only behind a trusted upstream whose trace continues.\n# # This also enables richer bullmq-otel spans;\n# # otherwise async queues use metadata._trace.\n#\n# OTLP endpoint + auth \u2014 pick ONE vendor (use your region's host):\n# SigNoz Cloud:\n# OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.<region>.signoz.cloud:443\n# OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key=<your-ingestion-key>\n# New Relic (OTLP-native; EU host shown, US is https://otlp.nr-data.net):\n# OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.eu01.nr-data.net\n# OTEL_EXPORTER_OTLP_HEADERS=api-key=<your-ingest-license-key>\n#\n# Resource identity: keep one stable service name across environments and\n# separate envs via the deployment.environment resource attribute.\n# OTEL_SERVICE_NAME=open-mercato\n# OTEL_RESOURCE_ATTRIBUTES=deployment.environment=local\n`\n\n/**\n * Appended on its own when an app already carries the core block \u2014 an app\n * initialized before browser RUM shipped must be able to adopt it by re-running\n * the command.\n */\nconst ENV_BROWSER_BLOCK = `\n# --- Browser RUM (client-side telemetry) ---\n# Document-load and fetch spans exported from the backoffice through a\n# same-origin proxy (/api/telemetry/browser-traces) that adds the collector\n# credential server-side. Off by default; requires an active\n# TELEMETRY_BACKEND + OTLP endpoint above.\n# Read at request time (not NEXT_PUBLIC_*), so toggling needs no rebuild.\n# Browser spans join their server spans only when TELEMETRY_TRUST_INBOUND_TRACE=true\n# above; without it RUM still works, but each page yields two separate traces.\n# Ingest budget: each open tab exports at most 20 batches/min (a 3s batch delay),\n# and the proxy allows 400/min per authenticated user \u2014 roughly 20 concurrent tabs.\n# Over that, batches are dropped with 204 rather than a retryable status, so the\n# only symptom is missing spans for that one user, never a client retry storm.\n# TELEMETRY_BROWSER_ENABLED=false\n# TELEMETRY_BROWSER_SAMPLING_RATIO=1.0 # 0.0-1.0 (default 1.0)\n# TELEMETRY_BROWSER_SERVICE_NAME= # default: <OTEL_SERVICE_NAME>-browser\n`\n\nconst ENV_BLOCK = `${ENV_CORE_BLOCK}${ENV_BROWSER_BLOCK}`\n\nconst INSTRUMENTATION_TS = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\n\nexport async function register(): Promise<void> {\n // Initialize telemetry (no-op unless TELEMETRY_BACKEND is set). OTEL's NodeSDK\n // is Node-only and incompatible with the edge runtime, so the telemetry\n // bootstrap \u2014 which can pull in the SDK \u2014 is imported only on the Node.js\n // runtime. The helper owns init + graceful degrade + shutdown flush; a\n // deliberate configuration failure bubbles here so the host exits loudly.\n if (\n process.env.NEXT_RUNTIME === 'nodejs'\n && isTelemetryBackendEnabled()\n ) {\n const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')\n try {\n await registerTelemetryForNextjs()\n } catch (err) {\n const nodeProcess = process\n nodeProcess.stderr.write(\\`\\${err instanceof Error ? err.message : String(err)}\\\\n\\`)\n nodeProcess.exit(1)\n }\n }\n}\n`\n\nconst MULTILINE_TELEMETRY_GUARD = ` if (\n process.env.NEXT_RUNTIME === 'nodejs'\n && isTelemetryBackendEnabled()\n ) {`\nconst INLINE_TELEMETRY_GUARD = ` if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {`\nconst LEGACY_NODE_TELEMETRY_GUARD = ` if (process.env.NEXT_RUNTIME === 'nodejs') {`\nconst TELEMETRY_DYNAMIC_IMPORT = ` const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')`\nconst BARE_TELEMETRY_REGISTRATION = ` await registerTelemetryForNextjs()`\nconst GUARDED_TELEMETRY_REGISTRATION = ` try {\n await registerTelemetryForNextjs()\n } catch (err) {\n const nodeProcess = process\n nodeProcess.stderr.write(\\`\\${err instanceof Error ? err.message : String(err)}\\\\n\\`)\n nodeProcess.exit(1)\n }`\n\nconst DISPATCHER_SNIPPET = ` // add near the other imports:\n import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\n // in the dispatcher's success path, just before returning the response:\n getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, finalResponse.status, startedAt)\n\n // in the dispatcher's catch (error) block, before re-throwing:\n getTelemetryRuntime()?.reportError(error, {\n attributes: { 'http.request.method': method, 'http.route': match.route.path, 'http.response.status_code': 500 },\n })\n getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, 500, startedAt)`\n\nfunction parseArgs(args: string[]): TelemetryInitOptions {\n return {\n dryRun: args.includes('--dry-run') || args.includes('-n'),\n }\n}\n\n/** Read the version an app already pins its `@open-mercato/*` deps to (lockstep). */\nfunction resolveOpenMercatoVersion(pkg: Record<string, unknown>): string {\n for (const field of ['dependencies', 'devDependencies'] as const) {\n const deps = pkg[field]\n if (deps && typeof deps === 'object') {\n for (const [name, version] of Object.entries(deps as Record<string, string>)) {\n if (name.startsWith('@open-mercato/') && typeof version === 'string') return version\n }\n }\n }\n return '*'\n}\n\nfunction patchPackageJson(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'package.json'\n const path = join(appDir, file)\n const raw = readFileSync(path, 'utf8')\n const pkg = JSON.parse(raw) as {\n dependencies?: Record<string, string>\n optionalDependencies?: Record<string, string>\n }\n const version = resolveOpenMercatoVersion(pkg)\n const hasTelemetry = Boolean(pkg.dependencies?.['@open-mercato/telemetry'])\n const hasBullmqOtel = Boolean(pkg.optionalDependencies?.['bullmq-otel'])\n if (hasTelemetry && hasBullmqOtel) {\n return { file, status: 'skipped', detail: '@open-mercato/telemetry already installed' }\n }\n\n const deps = { ...(pkg.dependencies ?? {}) }\n if (!hasTelemetry) deps['@open-mercato/telemetry'] = version\n const optDeps = { ...(pkg.optionalDependencies ?? {}) }\n if (!hasBullmqOtel) optDeps['bullmq-otel'] = BULLMQ_OTEL_VERSION\n\n const next = {\n ...pkg,\n dependencies: sortRecord(deps),\n optionalDependencies: sortRecord(optDeps),\n }\n if (!options.dryRun) writeFileSync(path, `${JSON.stringify(next, null, 2)}\\n`)\n return {\n file,\n status: 'patched',\n detail: `+@open-mercato/telemetry@${version}${hasBullmqOtel ? '' : ` +bullmq-otel@${BULLMQ_OTEL_VERSION} (optional)`} \u2014 run \\`yarn install\\``,\n }\n}\n\nfunction sortRecord(record: Record<string, string>): Record<string, string> {\n return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)))\n}\n\nfunction patchEnvFile(appDir: string, relativePath: string, options: TelemetryInitOptions): StepResult | null {\n const path = join(appDir, relativePath)\n if (!existsSync(path)) return null\n const content = readFileSync(path, 'utf8')\n // Checked key by key, not block by block: an app initialized before browser RUM\n // shipped has the core keys and none of the browser ones, and re-running the\n // command is how it adopts them.\n const hasCore = content.includes('TELEMETRY_BACKEND')\n const hasBrowser = content.includes('TELEMETRY_BROWSER_ENABLED')\n if (hasCore && hasBrowser) {\n return { file: relativePath, status: 'skipped', detail: 'TELEMETRY_* block already present' }\n }\n const next = `${content.replace(/\\s*$/, '')}\\n${hasCore ? ENV_BROWSER_BLOCK : ENV_BLOCK}`\n if (!options.dryRun) writeFileSync(path, next)\n return {\n file: relativePath,\n status: 'patched',\n detail: hasCore\n ? 'appended the missing TELEMETRY_BROWSER_* block'\n : 'appended commented TELEMETRY_* / OTEL_* block',\n }\n}\n\nconst MODULES_SNIPPET = ` // in the enabledModules array:\n { id: 'telemetry', from: '@open-mercato/telemetry' },`\n\n/**\n * Register the `telemetry` module, which owns the same-origin OTLP proxy the\n * browser exporter posts to. Without it `TELEMETRY_BROWSER_ENABLED` produces a\n * client that exports into a 404.\n */\nfunction patchModules(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'src/modules.ts'\n const path = join(appDir, file)\n const content = readFileSync(path, 'utf8')\n if (/id:\\s*'telemetry'/.test(content)) {\n return { file, status: 'skipped', detail: 'telemetry module already enabled' }\n }\n const anchor = content.match(/export\\s+const\\s+enabledModules[^=]*=\\s*\\[/)\n if (!anchor || anchor.index === undefined) {\n return {\n file,\n status: 'manual',\n detail: 'enabledModules array not found \u2014 add the entry by hand',\n manualSnippet: MODULES_SNIPPET,\n }\n }\n const insertAt = anchor.index + anchor[0].length\n const block =\n `\\n // Same-origin OTLP proxy for browser RUM spans; inert unless` +\n `\\n // TELEMETRY_BROWSER_ENABLED is set alongside an active telemetry backend.` +\n `\\n { id: 'telemetry', from: '@open-mercato/telemetry' },`\n if (!options.dryRun) writeFileSync(path, content.slice(0, insertAt) + block + content.slice(insertAt))\n return { file, status: 'patched', detail: \"enabled the 'telemetry' module (browser-traces proxy route)\" }\n}\n\nconst LAYOUT_FILE = 'src/app/(backend)/backend/layout.tsx'\n\nconst LAYOUT_SNIPPET = ` // add near the other imports:\n import { BrowserTelemetry } from '@open-mercato/telemetry/browser'\n import { resolveBrowserTelemetryConfig } from '@open-mercato/telemetry/browser/server'\n\n // in the component body, before the returned JSX:\n // pass the request's Cookie header when the layout already reads cookies\n // (only consulted by the integration-test opt-in; production is env-only):\n const browserTelemetryConfig = resolveBrowserTelemetryConfig({ cookieHeader: cookieStore.toString() })\n\n // as the last child of <AppShell>:\n <BrowserTelemetry config={browserTelemetryConfig} />`\n\n/**\n * The config resolver takes the request's `Cookie` header so an integration spec can opt one page\n * into browser RUM; production resolution is env-only and never reads it. A scaffolded layout\n * already awaits `cookies()`, so reuse that binding \u2014 but an app that removed it must still get a\n * layout that compiles, hence the argument-free fallback. Both forms behave identically outside an\n * `OM_TEST_MODE` environment.\n */\nfunction resolveArgument(layout: string): string {\n const binding = layout.match(/const\\s+(\\w+)\\s*=\\s*await\\s+cookies\\(\\)/)\n return binding ? `{ cookieHeader: ${binding[1]}.toString() }` : ''\n}\n\n/**\n * Render the client bootstrap in the backoffice shell. The config is resolved\n * per request (the layout is `force-dynamic`), so RUM toggles per environment\n * without a rebuild.\n *\n * A backoffice layout is the file apps customize most, so this only edits one\n * that still matches the scaffold shape; anything else gets the snippet.\n */\nfunction patchBackendLayout(appDir: string, options: TelemetryInitOptions): StepResult {\n const path = join(appDir, LAYOUT_FILE)\n if (!existsSync(path)) {\n return { file: LAYOUT_FILE, status: 'manual', detail: 'backend layout not found', manualSnippet: LAYOUT_SNIPPET }\n }\n const content = readFileSync(path, 'utf8')\n if (content.includes('<BrowserTelemetry')) {\n return { file: LAYOUT_FILE, status: 'skipped', detail: 'BrowserTelemetry already rendered' }\n }\n const closingShell = content.lastIndexOf('</AppShell>')\n const returnMatch = content.match(/^([ \\t]*)return \\(\\s*$/m)\n const lastImport = [...content.matchAll(/^import[^\\n]*$/gm)].at(-1)\n if (closingShell === -1 || !returnMatch || returnMatch.index === undefined || !lastImport) {\n return {\n file: LAYOUT_FILE,\n status: 'manual',\n detail: 'layout shape not recognized \u2014 apply the snippet by hand',\n manualSnippet: LAYOUT_SNIPPET,\n }\n }\n\n // Edit back to front so each earlier anchor's offset stays valid.\n const shellIndent = content.slice(0, closingShell).match(/\\n([ \\t]*)$/)?.[1] ?? ' '\n let next =\n content.slice(0, closingShell) +\n `<BrowserTelemetry config={browserTelemetryConfig} />\\n${shellIndent}` +\n content.slice(closingShell)\n\n const returnIndent = returnMatch[1]\n next =\n next.slice(0, returnMatch.index) +\n `${returnIndent}// Resolved per request (this layout is force-dynamic), so browser RUM can be\\n` +\n `${returnIndent}// toggled per environment without a rebuild. Null keeps the SDK chunk from\\n` +\n `${returnIndent}// ever being requested.\\n` +\n `${returnIndent}const browserTelemetryConfig = resolveBrowserTelemetryConfig(${resolveArgument(content)})\\n\\n` +\n next.slice(returnMatch.index)\n\n const importInsertAt = (lastImport.index ?? 0) + lastImport[0].length\n next =\n next.slice(0, importInsertAt) +\n `\\nimport { BrowserTelemetry } from '@open-mercato/telemetry/browser'` +\n `\\nimport { resolveBrowserTelemetryConfig } from '@open-mercato/telemetry/browser/server'` +\n next.slice(importInsertAt)\n\n if (!options.dryRun) writeFileSync(path, next)\n return { file: LAYOUT_FILE, status: 'patched', detail: 'rendered <BrowserTelemetry /> in the backoffice shell' }\n}\n\nfunction patchInstrumentation(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'src/instrumentation.ts'\n const path = join(appDir, file)\n if (!existsSync(path)) {\n if (!options.dryRun) {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, INSTRUMENTATION_TS)\n }\n return { file, status: 'created', detail: 'Node-runtime-guarded registerTelemetryForNextjs() bootstrap' }\n }\n const content = readFileSync(path, 'utf8')\n if (content.includes('registerTelemetryForNextjs')) {\n const guardedBootstraps = [MULTILINE_TELEMETRY_GUARD, INLINE_TELEMETRY_GUARD]\n const safeBootstrap = guardedBootstraps\n .map((guard) => `${guard}\\n${TELEMETRY_DYNAMIC_IMPORT}\\n${GUARDED_TELEMETRY_REGISTRATION}`)\n .find((bootstrap) => content.includes(bootstrap))\n if (safeBootstrap) {\n return { file, status: 'skipped', detail: 'telemetry already wired' }\n }\n const guardedLegacyBootstrap = guardedBootstraps\n .map((guard) => `${guard}\\n${TELEMETRY_DYNAMIC_IMPORT}\\n${BARE_TELEMETRY_REGISTRATION}`)\n .find((bootstrap) => content.includes(bootstrap))\n if (guardedLegacyBootstrap) {\n const migrated = content.replace(\n guardedLegacyBootstrap,\n guardedLegacyBootstrap.replace(BARE_TELEMETRY_REGISTRATION, GUARDED_TELEMETRY_REGISTRATION),\n )\n if (!options.dryRun) writeFileSync(path, migrated)\n return { file, status: 'patched', detail: 'wrapped OTLP dependency failures with host termination' }\n }\n const nodeOnlyLegacyBootstrap = `${LEGACY_NODE_TELEMETRY_GUARD}\\n${TELEMETRY_DYNAMIC_IMPORT}\\n${BARE_TELEMETRY_REGISTRATION}`\n if (content.includes(nodeOnlyLegacyBootstrap)) {\n const guardedBootstrap = nodeOnlyLegacyBootstrap\n .replace(LEGACY_NODE_TELEMETRY_GUARD, INLINE_TELEMETRY_GUARD)\n .replace(BARE_TELEMETRY_REGISTRATION, GUARDED_TELEMETRY_REGISTRATION)\n const importLine = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\\n`\n const withBootstrap = content.replace(nodeOnlyLegacyBootstrap, guardedBootstrap)\n const migrated = withBootstrap.includes(importLine) ? withBootstrap : importLine + withBootstrap\n if (!options.dryRun) writeFileSync(path, migrated)\n return {\n file,\n status: 'patched',\n detail: 'added the explicit backend guard and wrapped OTLP dependency failures with host termination',\n }\n }\n return {\n file,\n status: 'manual',\n detail: 'existing telemetry bootstrap needs the canonical Node guard and fail-closed host wrapper',\n manualSnippet: INSTRUMENTATION_TS,\n }\n }\n const anchor = content.match(/export\\s+async\\s+function\\s+register\\s*\\([^)]*\\)\\s*:\\s*Promise<void>\\s*\\{/)\n if (!anchor || anchor.index === undefined) {\n return {\n file,\n status: 'manual',\n detail: 'register() not found \u2014 add the telemetry bootstrap manually',\n manualSnippet: INSTRUMENTATION_TS,\n }\n }\n const insertAt = anchor.index + anchor[0].length\n const block =\n `\\n // Initialize telemetry (no-op unless TELEMETRY_BACKEND is set); Node-only.` +\n `\\n if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {` +\n `\\n const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')` +\n `\\n try {` +\n `\\n await registerTelemetryForNextjs()` +\n `\\n } catch (err) {` +\n `\\n const nodeProcess = process` +\n `\\n nodeProcess.stderr.write(\\`\\${err instanceof Error ? err.message : String(err)}\\\\n\\`)` +\n `\\n nodeProcess.exit(1)` +\n `\\n }` +\n `\\n }`\n const importLine = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\\n`\n const withImport = content.includes('isTelemetryBackendEnabled')\n ? content\n : importLine + content\n const shiftedInsertAt = insertAt + (withImport.length - content.length)\n const next = withImport.slice(0, shiftedInsertAt) + block + withImport.slice(shiftedInsertAt)\n if (!options.dryRun) writeFileSync(path, next)\n return { file, status: 'patched', detail: 'inserted registerTelemetryForNextjs() into register()' }\n}\n\nasync function patchNextConfig(appDir: string, options: TelemetryInitOptions): Promise<StepResult> {\n const file = 'next.config.ts'\n const path = join(appDir, file)\n if (!existsSync(path)) {\n return { file, status: 'manual', detail: 'next.config.ts not found', manualSnippet: nextConfigSnippet() }\n }\n // Use ts-morph to *detect* the real `serverExternalPackages` array literal\n // (robust against a match inside a comment/string), then do the *insertion*\n // via a formatting-preserving text splice so the user's config stays tidy.\n const { Project, SyntaxKind } = await import('ts-morph')\n const project = new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true })\n const sf = project.addSourceFileAtPath(path)\n\n const telemetryConfigImport = sf\n .getImportDeclarations()\n .find((decl) => [\n '@open-mercato/telemetry/nextjs',\n '@open-mercato/telemetry/nextjs-config',\n ].includes(decl.getModuleSpecifierValue()))\n const hasImport = Boolean(telemetryConfigImport)\n const hasCanonicalImport =\n telemetryConfigImport?.getModuleSpecifierValue() === '@open-mercato/telemetry/nextjs-config'\n\n const arrayProp = sf\n .getDescendantsOfKind(SyntaxKind.PropertyAssignment)\n .find((prop) => prop.getName() === 'serverExternalPackages')\n const arrayInit = arrayProp?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)\n if (!arrayInit) {\n return {\n file,\n status: 'manual',\n detail: 'serverExternalPackages array not found',\n manualSnippet: nextConfigSnippet(),\n }\n }\n const hasSpread = arrayInit.getElements().some((el) => el.getText().includes('telemetryServerExternalPackages'))\n if (hasCanonicalImport && hasSpread) {\n return { file, status: 'skipped', detail: 'telemetryServerExternalPackages already wired' }\n }\n\n let text = readFileSync(path, 'utf8')\n // Splice the array first, using the AST node's byte offsets (formatting-agnostic:\n // handles both single-line and multi-line arrays). Do this before modifying\n // imports, because either an inserted import or the legacy import migration\n // shifts the array offsets captured from the original source.\n if (!hasSpread) {\n const start = arrayInit.getStart()\n const end = arrayInit.getEnd()\n text = text.slice(0, start) + insertSpread(text.slice(start, end)) + text.slice(end)\n }\n text = text.replace(\n \"from '@open-mercato/telemetry/nextjs'\",\n \"from '@open-mercato/telemetry/nextjs-config'\",\n )\n if (!hasImport) {\n const importRegex = /^import[^\\n]*$/gm\n let lastImport: RegExpExecArray | null = null\n let match: RegExpExecArray | null\n while ((match = importRegex.exec(text)) !== null) lastImport = match\n const insertAt = lastImport ? lastImport.index + lastImport[0].length : 0\n text =\n text.slice(0, insertAt) +\n `\\nimport { telemetryServerExternalPackages } from '@open-mercato/telemetry/nextjs-config'` +\n text.slice(insertAt)\n }\n if (!options.dryRun) writeFileSync(path, text)\n return { file, status: 'patched', detail: 'spread telemetryServerExternalPackages into serverExternalPackages' }\n}\n\n/** Add `...telemetryServerExternalPackages` as the last element of an array literal, preserving its style. */\nfunction insertSpread(arrayText: string): string {\n const inner = arrayText.slice(1, -1)\n if (inner.includes('\\n')) {\n const elementIndent = inner.match(/\\n([ \\t]+)\\S/)?.[1] ?? ' '\n const closeIndent = arrayText.match(/\\n([ \\t]*)\\]$/)?.[1] ?? ''\n const trimmed = inner.replace(/\\s*$/, '')\n const separator = trimmed === '' || trimmed.endsWith(',') ? '' : ','\n return `[${trimmed}${separator}\\n${elementIndent}...telemetryServerExternalPackages,\\n${closeIndent}]`\n }\n const flat = inner.trim()\n return flat === '' ? '[...telemetryServerExternalPackages]' : `[${flat}, ...telemetryServerExternalPackages]`\n}\n\nfunction nextConfigSnippet(): string {\n return (\n `import { telemetryServerExternalPackages } from '@open-mercato/telemetry/nextjs-config'\\n` +\n `// serverExternalPackages: [ ...existing, ...telemetryServerExternalPackages ]`\n )\n}\n\n/**\n * Auto-patch the catch-all API dispatcher \u2014 but only when it still matches the\n * known scaffold shape (all anchors present, telemetry not yet wired). On any\n * mismatch, print the snippet and mark it manual rather than editing an\n * unrecognized (customized) handler.\n */\nfunction patchDispatcher(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'src/app/api/[...slug]/route.ts'\n const path = join(appDir, file)\n if (!existsSync(path)) {\n return { file, status: 'manual', detail: 'dispatcher not found', manualSnippet: DISPATCHER_SNIPPET }\n }\n let content = readFileSync(path, 'utf8')\n if (\n content.includes(\"from '@open-mercato/shared/lib/telemetry/runtime'\")\n && content.includes('getTelemetryRuntime()?.recordHttpDuration')\n ) {\n return { file, status: 'skipped', detail: 'telemetry already wired' }\n }\n if (\n content.includes(\"import { reportError } from '@open-mercato/telemetry'\")\n && content.includes(\"import { recordHttpDuration } from '@open-mercato/telemetry/nextjs'\")\n ) {\n content = content\n .replace(\n \"import { reportError } from '@open-mercato/telemetry'\\nimport { recordHttpDuration } from '@open-mercato/telemetry/nextjs'\",\n \"import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\",\n )\n .replace(/\\brecordHttpDuration\\(/g, 'getTelemetryRuntime()?.recordHttpDuration(')\n .replace(/\\breportError\\(/g, 'getTelemetryRuntime()?.reportError(')\n if (!options.dryRun) writeFileSync(path, content)\n return { file, status: 'patched', detail: 'migrated dispatcher to the default-off runtime bridge' }\n }\n\n const importAnchor = \"import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\"\n const returnMatch = content.match(/^([ \\t]*)return finalResponse\\s*$/m)\n // The handleRequest catch is the first `} catch (error) {` after the success return.\n const catchRegex = /^([ \\t]*)\\}\\s*catch\\s*\\(error\\)\\s*\\{[ \\t]*$/gm\n const catchMatch = findCatchAfter(content, catchRegex, returnMatch?.index ?? -1)\n\n if (!content.includes(importAnchor) || !returnMatch || returnMatch.index === undefined || !catchMatch) {\n return {\n file,\n status: 'manual',\n detail: 'dispatcher shape not recognized \u2014 apply the snippet by hand',\n manualSnippet: DISPATCHER_SNIPPET,\n }\n }\n\n // 1) imports\n content = content.replace(\n importAnchor,\n `${importAnchor}\\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'`,\n )\n\n // 2) success-path metric (re-match after the import edit shifted offsets)\n const ret = content.match(/^([ \\t]*)return finalResponse\\s*$/m)\n if (!ret || ret.index === undefined) {\n return { file, status: 'manual', detail: 'success-path anchor lost', manualSnippet: DISPATCHER_SNIPPET }\n }\n const retIndent = ret[1]\n content =\n content.slice(0, ret.index) +\n `${retIndent}getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, finalResponse.status, startedAt)\\n` +\n content.slice(ret.index)\n\n // 3) catch-block error funnel + 500 metric\n const catch2 = findCatchAfter(content, /^([ \\t]*)\\}\\s*catch\\s*\\(error\\)\\s*\\{[ \\t]*$/gm, content.indexOf('return finalResponse'))\n if (!catch2) {\n return { file, status: 'manual', detail: 'catch anchor lost', manualSnippet: DISPATCHER_SNIPPET }\n }\n const bodyIndent = `${catch2.indent} `\n const insertAt = catch2.index + catch2.length\n const block =\n `\\n${bodyIndent}getTelemetryRuntime()?.reportError(error, {` +\n `\\n${bodyIndent} attributes: { 'http.request.method': method, 'http.route': match.route.path, 'http.response.status_code': 500 },` +\n `\\n${bodyIndent}})` +\n `\\n${bodyIndent}getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, 500, startedAt)`\n content = content.slice(0, insertAt) + block + content.slice(insertAt)\n\n if (!options.dryRun) writeFileSync(path, content)\n return { file, status: 'patched', detail: 'wired reportError (5xx) + http.server.request.duration metric' }\n}\n\nfunction findCatchAfter(\n content: string,\n regex: RegExp,\n afterIndex: number,\n): { index: number; length: number; indent: string } | null {\n regex.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = regex.exec(content)) !== null) {\n if (match.index > afterIndex) {\n return { index: match.index, length: match[0].length, indent: match[1] }\n }\n }\n return null\n}\n\nexport async function runTelemetryInit(args: string[]): Promise<number> {\n const appDir = resolve('.')\n const options = parseArgs(args)\n\n if (!existsSync(join(appDir, 'src', 'modules.ts'))) {\n console.error('\u274C Not an Open Mercato app directory (src/modules.ts not found)')\n return 1\n }\n if (!existsSync(join(appDir, 'package.json'))) {\n console.error('\u274C package.json not found')\n return 1\n }\n\n console.log('')\n console.log(options.dryRun ? '\uD83D\uDD2D Telemetry init (dry run \u2014 no files written)\\n' : '\uD83D\uDD2D Wiring @open-mercato/telemetry into this app\\n')\n\n const results: StepResult[] = []\n results.push(patchPackageJson(appDir, options))\n const envExample = patchEnvFile(appDir, '.env.example', options)\n if (envExample) results.push(envExample)\n const env = patchEnvFile(appDir, '.env', options)\n if (env) results.push(env)\n results.push(patchInstrumentation(appDir, options))\n results.push(await patchNextConfig(appDir, options))\n results.push(patchDispatcher(appDir, options))\n results.push(patchModules(appDir, options))\n results.push(patchBackendLayout(appDir, options))\n\n const icon: Record<StepStatus, string> = { created: '\u270D\uFE0F ', patched: '\u2705', skipped: '\u23ED\uFE0F ', manual: '\u26A0\uFE0F ' }\n for (const result of results) {\n console.log(`${icon[result.status]} ${result.file} \u2014 ${result.detail}`)\n }\n\n const manual = results.filter((result) => result.status === 'manual')\n for (const result of manual) {\n if (!result.manualSnippet) continue\n console.log('')\n console.log(`\u2500\u2500 manual step for ${result.file} \u2500\u2500`)\n console.log(result.manualSnippet)\n }\n\n console.log('')\n const changed = results.some((result) => result.status === 'patched' || result.status === 'created')\n if (options.dryRun) {\n console.log('Dry run complete \u2014 re-run without --dry-run to apply.')\n } else if (changed) {\n console.log('Next steps:')\n console.log(' 1. yarn install (resolve @open-mercato/telemetry + the OTEL SDK)')\n console.log(' 2. set TELEMETRY_BACKEND + OTEL_EXPORTER_OTLP_* in .env')\n console.log(' 3. yarn typecheck && yarn build (verify the wiring)')\n } else {\n console.log('Nothing to do \u2014 telemetry is already wired.')\n }\n if (manual.length > 0) {\n console.log('')\n console.log(`\u26A0\uFE0F ${manual.length} file(s) need a manual edit (shown above) \u2014 the rest were applied automatically.`)\n }\n return 0\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["importLine"]
|
|
7
7
|
}
|
|
@@ -10,6 +10,7 @@ import spawn from "cross-spawn";
|
|
|
10
10
|
import { fetchWithTimeout } from "@open-mercato/shared/lib/http/fetchWithTimeout";
|
|
11
11
|
import { isUnsafeJwtSecret } from "@open-mercato/shared/lib/auth/jwt";
|
|
12
12
|
import { resolveEnvironment } from "../resolver.js";
|
|
13
|
+
import { resolveInitSuperadminCredentials } from "../init-secrets.js";
|
|
13
14
|
import { resolveSpawnCommand } from "../spawn.js";
|
|
14
15
|
import { discoverIntegrationSpecFiles as discoverIntegrationSpecFilesShared } from "./integration-discovery.js";
|
|
15
16
|
import { resolveDockerHostFromContext, runCommandAndCapture } from "./runtime-utils.js";
|
|
@@ -52,6 +53,21 @@ function resolveEphemeralJwtSecret(env2 = process.env) {
|
|
|
52
53
|
}
|
|
53
54
|
return DEFAULT_EPHEMERAL_JWT_SECRET;
|
|
54
55
|
}
|
|
56
|
+
const DEFAULT_EPHEMERAL_SUPERADMIN_PASSWORD = "Om-Ephemeral-Superadmin-2026!";
|
|
57
|
+
function resolveEphemeralSuperadminPassword(env2 = process.env) {
|
|
58
|
+
const inherited = env2.OM_INIT_SUPERADMIN_PASSWORD;
|
|
59
|
+
if (inherited !== void 0 && inherited.trim().length > 0) {
|
|
60
|
+
try {
|
|
61
|
+
resolveInitSuperadminCredentials([], { ...env2, NODE_ENV: "production", OM_INIT_SUPERADMIN_PASSWORD: inherited });
|
|
62
|
+
return inherited;
|
|
63
|
+
} catch {
|
|
64
|
+
console.warn(
|
|
65
|
+
"[integration] Ignoring the inherited OM_INIT_SUPERADMIN_PASSWORD: it is a placeholder or fails the password policy and the ephemeral app initializes with NODE_ENV=production. Using the ephemeral default instead."
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return DEFAULT_EPHEMERAL_SUPERADMIN_PASSWORD;
|
|
70
|
+
}
|
|
55
71
|
const PLAYWRIGHT_ENV_UNAVAILABLE_PATTERNS = [
|
|
56
72
|
/net::ERR_CONNECTION_REFUSED/i,
|
|
57
73
|
/Failed to connect to .* (localhost|127\.0\.0\.1)/i,
|
|
@@ -1597,6 +1613,7 @@ function buildReusableEnvironment(baseUrl, databaseUrl, queueBaseDir, captureScr
|
|
|
1597
1613
|
// OM_INIT_*_PASSWORD to fix the seeded credential.
|
|
1598
1614
|
OM_INIT_ADMIN_PASSWORD: process.env.OM_INIT_ADMIN_PASSWORD ?? "secret",
|
|
1599
1615
|
OM_INIT_EMPLOYEE_PASSWORD: process.env.OM_INIT_EMPLOYEE_PASSWORD ?? "secret",
|
|
1616
|
+
OM_INIT_SUPERADMIN_PASSWORD: resolveEphemeralSuperadminPassword(),
|
|
1600
1617
|
OM_INTEGRATION_TEST: "true",
|
|
1601
1618
|
OM_ENABLE_ENTERPRISE_MODULES: enterpriseModulesFlag,
|
|
1602
1619
|
OM_ENABLE_ENTERPRISE_MODULES_SSO: process.env.OM_ENABLE_ENTERPRISE_MODULES_SSO ?? enterpriseModulesFlag,
|
|
@@ -2806,6 +2823,10 @@ async function startEphemeralEnvironment(options) {
|
|
|
2806
2823
|
// NODE_ENV=production.
|
|
2807
2824
|
OM_INIT_ADMIN_PASSWORD: process.env.OM_INIT_ADMIN_PASSWORD ?? "secret",
|
|
2808
2825
|
OM_INIT_EMPLOYEE_PASSWORD: process.env.OM_INIT_EMPLOYEE_PASSWORD ?? "secret",
|
|
2826
|
+
// Production init refuses a missing or placeholder superadmin password
|
|
2827
|
+
// (#8896). Integration auth helpers read OM_INIT_SUPERADMIN_PASSWORD, so
|
|
2828
|
+
// the Playwright runner logs in with the same value.
|
|
2829
|
+
OM_INIT_SUPERADMIN_PASSWORD: resolveEphemeralSuperadminPassword(),
|
|
2809
2830
|
// Pool sizing for the ephemeral integration runtime. Defaults were once
|
|
2810
2831
|
// very aggressive (max=5, idle=1000) which exposed flaky 'timeout exceeded
|
|
2811
2832
|
// when trying to connect' errors on `progressService.createJob`-backed
|
|
@@ -3298,6 +3319,7 @@ export {
|
|
|
3298
3319
|
resolveBuildCacheTtlSeconds,
|
|
3299
3320
|
resolveEphemeralJwtSecret,
|
|
3300
3321
|
resolveEphemeralPostgresImage,
|
|
3322
|
+
resolveEphemeralSuperadminPassword,
|
|
3301
3323
|
runEphemeralAppForQa,
|
|
3302
3324
|
runIntegrationCoverageReport,
|
|
3303
3325
|
runIntegrationSpecCoverageReport,
|