#!/usr/bin/env node /** * OmniRoute โ€” Prepublish Build Script * * Consumes the .build/next/standalone artifact produced by `npm run build` * (build-next-isolated.mjs) and assembles the npm staging `dist/` directory. * Does NOT run a second `next build` โ€” the caller must run `npm run build` first, * or this script will invoke it exactly once if the artifact is absent. * * Run with: node scripts/build/prepublish.ts */ import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, cpSync, rmSync, writeFileSync, readFileSync, readdirSync, statSync, chmodSync, } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { assembleStandalone } from "./assembleStandalone.mjs"; import { APP_STAGING_ALLOWED_EXACT_PATHS, APP_STAGING_ALLOWED_PATH_PREFIXES, APP_STAGING_REMOVAL_PATHS, findUnexpectedArtifactPaths, } from "./pack-artifact-policy.ts"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ROOT = join(__dirname, "..", ".."); const NPX_BIN = process.platform === "win32" ? "npx.cmd" : "npx"; const DIST_DIR = join(ROOT, "dist"); function walkFiles(dir: string, rootDir: string = dir, files: string[] = []): string[] { let entries: string[] = []; try { entries = readdirSync(dir); } catch { return files; } for (const entry of entries) { const fullPath = join(dir, entry); let stat; try { stat = statSync(fullPath); } catch { continue; } if (stat.isDirectory()) { walkFiles(fullPath, rootDir, files); continue; } files.push( fullPath .replace(rootDir, "") .replace(/^[/\\]/, "") .replace(/\\/g, "/") ); } return files; } function removeEmptyDirectories(dir: string): boolean { let entries: string[] = []; try { entries = readdirSync(dir); } catch { return false; } let hasFiles = false; for (const entry of entries) { const fullPath = join(dir, entry); let stat; try { stat = statSync(fullPath); } catch { continue; } if (stat.isDirectory()) { const childHasFiles = removeEmptyDirectories(fullPath); if (!childHasFiles) { rmSync(fullPath, { recursive: true, force: true }); } else { hasFiles = true; } continue; } hasFiles = true; } return hasFiles; } console.log("๐Ÿ”จ OmniRoute โ€” Building for npm publish...\n"); // โ”€โ”€ Step 1: Clean previous dist/ directory โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ if (existsSync(DIST_DIR)) { console.log(" ๐Ÿงน Cleaning previous dist/ directory..."); rmSync(DIST_DIR, { recursive: true, force: true }); } // โ”€โ”€ Step 2: Assert / trigger the Next.js standalone build โ”€โ”€ // prepublish no longer runs its own `next build`. It consumes the // .build/next/standalone artifact produced by `npm run build` (build-next-isolated.mjs). // If the artifact is absent we invoke it exactly once. const NEXT_DIST = process.env.NEXT_DIST_DIR || ".build/next"; const standaloneServerJs = join(ROOT, NEXT_DIST, "standalone", "server.js"); if (!existsSync(standaloneServerJs)) { console.log(" ๐Ÿ—๏ธ .build/next/standalone not found โ€” running `npm run build` once..."); execFileSync(process.execPath, ["scripts/build/build-next-isolated.mjs"], { cwd: ROOT, stdio: "inherit", }); if (!existsSync(standaloneServerJs)) { console.error("\n โŒ Standalone build not found after `npm run build` at:", standaloneServerJs); console.error(" Make sure next.config.mjs has: output: 'standalone'"); process.exit(1); } } console.log(" โœ… Standalone artifact present:", standaloneServerJs); // โ”€โ”€ Step 3โ€“7: Assemble standalone into dist/ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // All shared copy/sync/sanitize/chunk-patch operations are delegated to // assembleStandalone. npm-UNIQUE steps (MITM, MCP, CLI, sidecars) follow. console.log(" ๐Ÿ“‹ Assembling standalone bundle into dist/..."); assembleStandalone({ distDir: join(ROOT, NEXT_DIST), outDir: DIST_DIR, projectRoot: ROOT, sanitizePaths: true, patchTurbopackChunks: true, copyNatives: true, }); console.log(" โœ… Standalone bundle assembled to dist/"); // โ”€โ”€ Step 8: Compile + copy MITM cert utilities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const mitmSrc = join(ROOT, "src", "mitm"); const mitmDest = join(DIST_DIR, "src", "mitm"); if (existsSync(mitmSrc)) { console.log(" ๐Ÿ”จ Compiling MITM utilities (TypeScript โ†’ JavaScript)..."); mkdirSync(mitmDest, { recursive: true }); // Write a temporary tsconfig.json targeting the mitm directory const mitmTsconfig = { compilerOptions: { target: "ES2022", module: "NodeNext", moduleResolution: "NodeNext", outDir: mitmDest, rootDir: mitmSrc, strict: false, noImplicitAny: false, strictNullChecks: false, noEmitOnError: true, allowImportingTsExtensions: true, rewriteRelativeImportExtensions: true, ignoreDeprecations: "6.0", resolveJsonModule: true, esModuleInterop: true, skipLibCheck: true, types: ["node"], baseUrl: ".", paths: { "@/*": ["src/*"], }, }, include: [mitmSrc + "/**/*"], }; const tmpTsconfigPath = join(ROOT, "tsconfig.mitm.tmp.json"); writeFileSync(tmpTsconfigPath, JSON.stringify(mitmTsconfig, null, 2)); try { execFileSync(NPX_BIN, ["tsc", "-p", "tsconfig.mitm.tmp.json"], { cwd: ROOT, stdio: "inherit", }); const mitmServerSrc = join(mitmSrc, "server.cjs"); if (existsSync(mitmServerSrc)) { cpSync(mitmServerSrc, join(mitmDest, "server.cjs")); } console.log(" โœ… MITM utilities compiled to dist/src/mitm/"); } catch (err: any) { console.warn(" โš ๏ธ MITM compile warning (non-fatal):", err.message); // Fallback: copy source files so at least they are present cpSync(mitmSrc, mitmDest, { recursive: true }); } finally { // Cleanup temp tsconfig try { rmSync(tmpTsconfigPath); } catch {} } } // โ”€โ”€ Step 8.5: Bundle MCP server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const mcpSrcFile = join(ROOT, "open-sse", "mcp-server", "server.ts"); const mcpDestDir = join(DIST_DIR, "open-sse", "mcp-server"); const mcpDestFile = join(mcpDestDir, "server.js"); if (existsSync(mcpSrcFile)) { console.log(" ๐Ÿ”จ Bundling MCP Server (TypeScript โ†’ JavaScript)..."); mkdirSync(mcpDestDir, { recursive: true }); try { execFileSync( NPX_BIN, [ "esbuild", "open-sse/mcp-server/server.ts", "--bundle", "--platform=node", "--packages=external", "--format=esm", "--outfile=dist/open-sse/mcp-server/server.js", ], { cwd: ROOT, stdio: "inherit" } ); console.log(" โœ… MCP Server bundled to dist/open-sse/mcp-server/server.js"); } catch (err: any) { console.warn(" โš ๏ธ MCP Server bundle error:", err.message); } } // โ”€โ”€ Step 8.7: Bundle CLI Entrypoint โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const cliSrcFile = join(ROOT, "bin", "omniroute.ts"); const cliDestFile = join(ROOT, "bin", "omniroute.mjs"); if (existsSync(cliSrcFile)) { console.log(" ๐Ÿ”จ Bundling CLI Entrypoint (TypeScript โ†’ JavaScript)..."); try { execFileSync( NPX_BIN, [ "esbuild", "bin/omniroute.ts", "--bundle", "--platform=node", "--packages=external", "--format=esm", "--outfile=bin/omniroute.mjs", ], { cwd: ROOT, stdio: "inherit" } ); chmodSync(cliDestFile, 0o755); console.log(" โœ… CLI Entrypoint bundled to bin/omniroute.mjs"); } catch (err: any) { console.warn(" โš ๏ธ CLI bundle error:", err.message); } } // โ”€โ”€ Step 9: Copy shared utilities needed at runtime โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const sharedApiKey = join(ROOT, "src", "shared", "utils", "apiKey.js"); const sharedApiKeyDest = join(DIST_DIR, "src", "shared", "utils"); if (existsSync(sharedApiKey)) { console.log(" ๐Ÿ“‹ Copying shared utilities..."); mkdirSync(sharedApiKeyDest, { recursive: true }); cpSync(sharedApiKey, join(sharedApiKeyDest, "apiKey.js")); } // โ”€โ”€ Step 9.5: Copy minimal runtime sidecars required outside .next โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const envExampleSrc = join(ROOT, ".env.example"); if (existsSync(envExampleSrc)) { cpSync(envExampleSrc, join(DIST_DIR, ".env.example")); } const openapiSpecSrc = join(ROOT, "docs", "openapi.yaml"); if (existsSync(openapiSpecSrc)) { const docsDest = join(DIST_DIR, "docs"); mkdirSync(docsDest, { recursive: true }); cpSync(openapiSpecSrc, join(docsDest, "openapi.yaml")); } const docsMarkdownSrc = join(ROOT, "docs"); if (existsSync(docsMarkdownSrc)) { const docsDest = join(DIST_DIR, "docs"); mkdirSync(docsDest, { recursive: true }); const mdFiles = readdirSync(docsMarkdownSrc).filter( (f) => f.endsWith(".md") || f.endsWith(".mdx") ); for (const mdFile of mdFiles) { cpSync(join(docsMarkdownSrc, mdFile), join(docsDest, mdFile)); } if (mdFiles.length > 0) { console.log(`[prepublish] Copied ${mdFiles.length} docs markdown files to dist/docs/`); } } const syncEnvSrc = join(ROOT, "scripts", "sync-env.mjs"); if (existsSync(syncEnvSrc)) { const scriptsDest = join(DIST_DIR, "scripts"); mkdirSync(scriptsDest, { recursive: true }); cpSync(syncEnvSrc, join(scriptsDest, "sync-env.mjs")); } const migrationsSrc = join(ROOT, "src", "lib", "db", "migrations"); if (existsSync(migrationsSrc)) { const migrationsDest = join(DIST_DIR, "src", "lib", "db", "migrations"); mkdirSync(join(DIST_DIR, "src", "lib", "db"), { recursive: true }); cpSync(migrationsSrc, migrationsDest, { recursive: true, force: true }); } const runtimeAssetDirs = [ { source: join(ROOT, "open-sse", "services", "compression", "engines", "rtk", "filters"), destination: join(DIST_DIR, "open-sse", "services", "compression", "engines", "rtk", "filters"), }, { source: join(ROOT, "open-sse", "services", "compression", "rules"), destination: join(DIST_DIR, "open-sse", "services", "compression", "rules"), }, ]; for (const assetDir of runtimeAssetDirs) { if (existsSync(assetDir.source)) { mkdirSync(dirname(assetDir.destination), { recursive: true }); cpSync(assetDir.source, assetDir.destination, { recursive: true, force: true }); } } // โ”€โ”€ Step 10: Ensure data/ directory exists โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ mkdirSync(join(DIST_DIR, "data"), { recursive: true }); // โ”€โ”€ Step 10.5: Copy @swc/helpers into standalone โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Next.js standalone tracer sometimes omits @swc/helpers from dist/node_modules/, // causing MODULE_NOT_FOUND at runtime. Always copy it explicitly. const swcHelpersSrc = join(ROOT, "node_modules", "@swc", "helpers"); const swcHelpersDst = join(DIST_DIR, "node_modules", "@swc", "helpers"); if (existsSync(swcHelpersSrc) && !existsSync(swcHelpersDst)) { console.log(" ๐Ÿ“‹ Copying @swc/helpers to standalone dist/node_modules..."); mkdirSync(join(DIST_DIR, "node_modules", "@swc"), { recursive: true }); cpSync(swcHelpersSrc, swcHelpersDst, { recursive: true }); console.log(" โœ… @swc/helpers included in standalone build."); } // โ”€โ”€ Step 10.6: Remove development-only residue from staged dist/ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ for (const relativePath of APP_STAGING_REMOVAL_PATHS) { const targetPath = join(DIST_DIR, relativePath); if (existsSync(targetPath)) { console.log(` ๐Ÿงน Removing dist/${relativePath} (not needed in npm package)...`); rmSync(targetPath, { recursive: true, force: true }); console.log(` โœ… dist/${relativePath} removed.`); } } // โ”€โ”€ Step 10.7: Prune any staged dist/ file outside the allowed runtime set โ”€โ”€ const stagedFiles = walkFiles(DIST_DIR); const unexpectedStagedFiles = findUnexpectedArtifactPaths(stagedFiles, { exactPaths: APP_STAGING_ALLOWED_EXACT_PATHS, prefixPaths: APP_STAGING_ALLOWED_PATH_PREFIXES, }); if (unexpectedStagedFiles.length > 0) { console.log(" ๐Ÿงน Pruning unexpected files from staged dist/..."); unexpectedStagedFiles.forEach((unexpectedPath: string) => { rmSync(join(DIST_DIR, unexpectedPath), { force: true }); console.log(` โœ… Removed dist/${unexpectedPath}`); }); removeEmptyDirectories(DIST_DIR); } const remainingUnexpectedFiles = findUnexpectedArtifactPaths(walkFiles(DIST_DIR), { exactPaths: APP_STAGING_ALLOWED_EXACT_PATHS, prefixPaths: APP_STAGING_ALLOWED_PATH_PREFIXES, }); if (remainingUnexpectedFiles.length > 0) { console.error("\n โŒ Staged dist/ still contains unexpected publish artifacts:"); remainingUnexpectedFiles.forEach((violation: string) => console.error(` - dist/${violation}`)); process.exit(1); } // โ”€โ”€ Done โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const distPkg = join(DIST_DIR, "package.json"); if (existsSync(distPkg)) { JSON.parse(readFileSync(distPkg, "utf8")); console.log(`\n โœ… Build complete!`); console.log(` Dist directory: dist/`); console.log(` Server entry: dist/server.js`); } else { console.log(`\n โœ… Build complete! (dist/ ready for publish)`); } console.log("");