#!/usr/bin/env node /** * OmniRoute โ€” Prepublish Build Script * * Builds the Next.js app in standalone mode and copies output * into the `app/` directory that gets published to npm. * * Run with: node scripts/prepublish.mjs */ import { execSync } from "node:child_process"; import { existsSync, mkdirSync, cpSync, rmSync, writeFileSync, readFileSync, readdirSync, statSync, } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ROOT = join(__dirname, ".."); const APP_DIR = join(ROOT, "app"); console.log("๐Ÿ”จ OmniRoute โ€” Building for npm publish...\n"); // โ”€โ”€ Step 1: Clean previous app/ directory โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ if (existsSync(APP_DIR)) { console.log(" ๐Ÿงน Cleaning previous app/ directory..."); rmSync(APP_DIR, { recursive: true, force: true }); } // โ”€โ”€ Step 2: Install dependencies โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ console.log(" ๐Ÿ“ฆ Installing dependencies..."); execSync("npm install", { cwd: ROOT, stdio: "inherit" }); // โ”€โ”€ Step 3: Build Next.js โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ console.log(" ๐Ÿ—๏ธ Building Next.js (standalone)..."); execSync("npx next build", { cwd: ROOT, stdio: "inherit", env: { ...process.env, // Force webpack codegen โ€” Turbopack emits hashed require() calls for // server external packages that break npm global installs (#394, #396, #398). EXPERIMENTAL_TURBOPACK: "0", NEXT_PRIVATE_BUILD_WORKER: "0", }, }); // โ”€โ”€ Step 4: Verify standalone output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const standaloneDir = join(ROOT, ".next", "standalone"); const serverJs = join(standaloneDir, "server.js"); if (!existsSync(serverJs)) { console.error("\n โŒ Standalone build not found at:", standaloneDir); console.error(" Make sure next.config.mjs has: output: 'standalone'"); process.exit(1); } // โ”€โ”€ Step 5: Copy standalone output to app/ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // โ”€โ”€ Step 4.5: Check build for hashed external references โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Warn if Turbopack-style hash suffixes are found โ€” they will be resolved at // runtime by the externals patch in next.config.mjs, but log for visibility. { const HASH_RE = /require\(["']([\w@./-]+-[0-9a-f]{16})["']\)/; const scanDir = (dir, hits = []) => { let entries = []; try { entries = readdirSync(dir); } catch { return hits; } for (const e of entries) { const f = join(dir, e); try { if (statSync(f).isDirectory()) { scanDir(f, hits); continue; } if (!f.endsWith(".js")) continue; const m = readFileSync(f, "utf8").match(HASH_RE); if (m) hits.push({ file: f.replace(standaloneDir, "app"), mod: m[1] }); } catch { continue; } } return hits; }; const hits = scanDir(join(standaloneDir, ".next", "server")); if (hits.length > 0) { console.warn( " โš ๏ธ Hashed externals in build (will be auto-fixed at runtime by externals patch):" ); hits.slice(0, 5).forEach((h) => console.warn()); if (hits.length > 5) console.warn(); } else { console.log(" โœ… Build clean โ€” no hashed externals found."); } } console.log(" ๐Ÿ“‹ Copying standalone build to app/..."); mkdirSync(APP_DIR, { recursive: true }); cpSync(standaloneDir, APP_DIR, { recursive: true }); // โ”€โ”€ Step 5.5: Sanitize hardcoded build-machine paths โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Next.js standalone bakes absolute build-time paths into server.js and // required-server-files.json (outputFileTracingRoot, appDir, turbopack root). // Replace the build machine's absolute path with "." (current directory) // so paths resolve relative to wherever the standalone app/ is installed. console.log(" ๐Ÿงน Sanitizing build-machine paths..."); const buildRoot = ROOT.replace(/\\/g, "/"); // normalise for regex safety const sanitizeTargets = [ join(APP_DIR, "server.js"), join(APP_DIR, ".next", "required-server-files.json"), ]; let sanitisedCount = 0; for (const filePath of sanitizeTargets) { if (!existsSync(filePath)) continue; let content = readFileSync(filePath, "utf8"); // Escape special regex characters in the path const escaped = buildRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const re = new RegExp(escaped, "g"); const matches = content.match(re); if (matches) { // Replace with "." so Next.js resolves paths relative to the standalone dir content = content.replace(re, "."); writeFileSync(filePath, content); sanitisedCount += matches.length; } } if (sanitisedCount > 0) { console.log(` โœ… Sanitised ${sanitisedCount} hardcoded path references`); } else { console.log(" โ„น๏ธ No hardcoded paths found to sanitise"); } // โ”€โ”€ Step 5.6: Strip Turbopack hashed externals from compiled chunks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Even when Turbopack is disabled at build time, some instrumentation chunks // may still emit require('package-<16hexchars>') instead of require('package'). // These hashed names don't exist in node_modules and cause MODULE_NOT_FOUND at // runtime. We strip the hex suffix from all .js files in app/.next/server/ // to ensure all require() calls use the real package names. { const serverOutput = join(APP_DIR, ".next", "server"); const HASH_RE = /(['"\\])([a-z@][a-z0-9@./_-]+-[0-9a-f]{16})\1/g; let patchedFiles = 0; let patchedMatches = 0; const walkDir = (dir) => { let entries = []; try { entries = readdirSync(dir); } catch { return; } for (const entry of entries) { const full = join(dir, entry); try { const st = statSync(full); if (st.isDirectory()) { walkDir(full); continue; } if (!entry.endsWith(".js")) continue; const src = readFileSync(full, "utf8"); let count = 0; const patched = src.replace(HASH_RE, (_, q, name) => { const base = name.replace(/-[0-9a-f]{16}$/, ""); count++; return `${q}${base}${q}`; }); if (count > 0) { writeFileSync(full, patched); patchedFiles++; patchedMatches += count; } } catch { /* skip unreadable files */ } } }; if (existsSync(serverOutput)) { walkDir(serverOutput); if (patchedMatches > 0) { console.log( ` ๐Ÿ”ง Hash-strip: patched ${patchedMatches} hashed require() in ${patchedFiles} server chunk file(s)` ); } else { console.log(" โœ… Hash-strip: no hashed externals found in compiled chunks."); } } } // โ”€โ”€ Step 6: Copy static assets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const staticSrc = join(ROOT, ".next", "static"); const staticDest = join(APP_DIR, ".next", "static"); if (existsSync(staticSrc)) { console.log(" ๐Ÿ“‹ Copying static assets..."); mkdirSync(staticDest, { recursive: true }); cpSync(staticSrc, staticDest, { recursive: true }); } // โ”€โ”€ Step 7: Copy public/ assets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const publicSrc = join(ROOT, "public"); const publicDest = join(APP_DIR, "public"); if (existsSync(publicSrc)) { console.log(" ๐Ÿ“‹ Copying public/ assets..."); mkdirSync(publicDest, { recursive: true }); cpSync(publicSrc, publicDest, { recursive: true }); } // โ”€โ”€ Step 8: Compile + copy MITM cert utilities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const mitmSrc = join(ROOT, "src", "mitm"); const mitmDest = join(APP_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: "ES2020", module: "CommonJS", outDir: mitmDest, rootDir: mitmSrc, resolveJsonModule: true, esModuleInterop: true, skipLibCheck: true, }, include: [mitmSrc + "/**/*"], }; const tmpTsconfigPath = join(ROOT, "tsconfig.mitm.tmp.json"); writeFileSync(tmpTsconfigPath, JSON.stringify(mitmTsconfig, null, 2)); try { execSync("npx tsc -p " + JSON.stringify(tmpTsconfigPath), { cwd: ROOT, stdio: "inherit" }); console.log(" โœ… MITM utilities compiled to app/src/mitm/"); } catch (err) { 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 9: Copy shared utilities needed at runtime โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const sharedApiKey = join(ROOT, "src", "shared", "utils", "apiKey.js"); const sharedApiKeyDest = join(APP_DIR, "src", "shared", "utils"); if (existsSync(sharedApiKey)) { console.log(" ๐Ÿ“‹ Copying shared utilities..."); mkdirSync(sharedApiKeyDest, { recursive: true }); cpSync(sharedApiKey, join(sharedApiKeyDest, "apiKey.js")); } // โ”€โ”€ Step 10: Ensure data/ directory exists โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ mkdirSync(join(APP_DIR, "data"), { recursive: true }); // โ”€โ”€ Step 10.5: Copy @swc/helpers into standalone โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Next.js standalone tracer sometimes omits @swc/helpers from app/node_modules/, // causing MODULE_NOT_FOUND at runtime. Always copy it explicitly. const swcHelpersSrc = join(ROOT, "node_modules", "@swc", "helpers"); const swcHelpersDst = join(APP_DIR, "node_modules", "@swc", "helpers"); if (existsSync(swcHelpersSrc) && !existsSync(swcHelpersDst)) { console.log(" ๐Ÿ“‹ Copying @swc/helpers to standalone app/node_modules..."); mkdirSync(join(APP_DIR, "node_modules", "@swc"), { recursive: true }); cpSync(swcHelpersSrc, swcHelpersDst, { recursive: true }); console.log(" โœ… @swc/helpers included in standalone build."); } // โ”€โ”€ Step 10.6: Remove large binaries from standalone build โ”€โ”€ // These directories contain platform-native binaries (.node, .asar) that // trigger Z_DATA_ERROR during npm pack. They are not needed in the npm package. const binaryDirsToRemove = ["vscode-extension", "electron"]; for (const dir of binaryDirsToRemove) { const targetDir = join(APP_DIR, dir); if (existsSync(targetDir)) { console.log(` ๐Ÿงน Removing app/${dir}/ (not needed in npm package)...`); rmSync(targetDir, { recursive: true, force: true }); console.log(` โœ… app/${dir}/ removed.`); } } // โ”€โ”€ Done โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const appPkg = join(APP_DIR, "package.json"); if (existsSync(appPkg)) { const pkg = JSON.parse(readFileSync(appPkg, "utf8")); console.log(`\n โœ… Build complete!`); console.log(` App directory: app/`); console.log(` Server entry: app/server.js`); } else { console.log(`\n โœ… Build complete! (app/ ready for publish)`); } console.log("");