`open` derives its own directory from import.meta.url at module scope. Webpack replaces that with the build machine's absolute path as a string literal, so a release built on macOS ships a file:///Users/... URL that fileURLToPath rejects on Windows (no drive letter), throwing on import. refreshXaiToken dynamic-imports the xAI OAuth service, which imports open eagerly, so every Grok token refresh silently failed and was swallowed by a catch that only logs a warning. Add open to serverExternalPackages so it keeps its real import.meta.url at runtime, and bundle it into the CLI package via ensureModuleInBundle (same guard already used for sql.js) since externalizing it means webpack no longer traces/copies it automatically.
286 lines
11 KiB
JavaScript
286 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const { execSync } = require("child_process");
|
||
|
||
const cliDir = path.resolve(__dirname, "..");
|
||
const appDir = path.resolve(cliDir, "..");
|
||
const rootDir = path.resolve(appDir, "..");
|
||
const cliAppDir = process.env.NINEROUTER_CLI_APP_DIR || path.join(cliDir, "app");
|
||
const buildHomeDir = path.join(cliDir, ".build-home");
|
||
const buildDistDirName = ".next-cli-build";
|
||
const buildDistDir = path.join(appDir, buildDistDirName);
|
||
|
||
// Exclude patterns for files/folders we don't want to copy
|
||
const EXCLUDE_PATTERNS = [
|
||
"@img", // Sharp image processing (not needed with unoptimized images)
|
||
"sharp", // Sharp core lib (not needed with unoptimized images)
|
||
"detect-libc", // Sharp dependency
|
||
".env", // Environment files
|
||
".env.local",
|
||
".env.*.local",
|
||
"*.log", // Log files
|
||
"tmp", // Temp files
|
||
".DS_Store", // macOS files
|
||
];
|
||
|
||
function shouldExclude(name) {
|
||
return EXCLUDE_PATTERNS.some(pattern => {
|
||
if (pattern.includes("*")) {
|
||
const regex = new RegExp("^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
|
||
return regex.test(name);
|
||
}
|
||
return name === pattern;
|
||
});
|
||
}
|
||
|
||
function copyRecursive(src, dest) {
|
||
if (!fs.existsSync(src)) {
|
||
console.warn(`Warning: Source ${src} does not exist`);
|
||
return;
|
||
}
|
||
|
||
if (!fs.existsSync(dest)) {
|
||
fs.mkdirSync(dest, { recursive: true });
|
||
}
|
||
|
||
const entries = fs.readdirSync(src, { withFileTypes: true });
|
||
for (const entry of entries) {
|
||
if (shouldExclude(entry.name)) {
|
||
continue;
|
||
}
|
||
|
||
const srcPath = path.join(src, entry.name);
|
||
const destPath = path.join(dest, entry.name);
|
||
|
||
// Skip broken symlinks (common in workspace setups)
|
||
try {
|
||
fs.accessSync(srcPath);
|
||
} catch {
|
||
continue;
|
||
}
|
||
|
||
if (entry.isDirectory()) {
|
||
copyRecursive(srcPath, destPath);
|
||
} else if (entry.isSymbolicLink()) {
|
||
// Resolve and copy target (avoid linking outside bundle)
|
||
try {
|
||
const real = fs.realpathSync(srcPath);
|
||
if (fs.statSync(real).isDirectory()) {
|
||
copyRecursive(real, destPath);
|
||
} else {
|
||
fs.copyFileSync(real, destPath);
|
||
}
|
||
} catch {}
|
||
} else {
|
||
try {
|
||
fs.copyFileSync(srcPath, destPath);
|
||
} catch {}
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log("📦 Building 9Router CLI package with Next.js...\n");
|
||
|
||
fs.mkdirSync(buildHomeDir, { recursive: true });
|
||
fs.mkdirSync(path.join(buildHomeDir, "AppData", "Roaming"), { recursive: true });
|
||
fs.mkdirSync(path.join(buildHomeDir, "AppData", "Local"), { recursive: true });
|
||
|
||
// Step 0: Sync version from app/cli/package.json to app/package.json
|
||
console.log("0️⃣ Syncing version to app/package.json...");
|
||
const cliPkg = JSON.parse(fs.readFileSync(path.join(cliDir, "package.json"), "utf8"));
|
||
const appPkgPath = path.join(appDir, "package.json");
|
||
const appPkg = JSON.parse(fs.readFileSync(appPkgPath, "utf8"));
|
||
if (appPkg.version !== cliPkg.version) {
|
||
appPkg.version = cliPkg.version;
|
||
fs.writeFileSync(appPkgPath, JSON.stringify(appPkg, null, 2) + "\n");
|
||
console.log(`✅ Version synced: ${cliPkg.version}\n`);
|
||
} else {
|
||
console.log(`✅ Version already synced: ${cliPkg.version}\n`);
|
||
}
|
||
|
||
// Step 1: Build app with Next.js (workspace tracing root → traced node_modules in standalone).
|
||
console.log("1️⃣ Building Next.js app...");
|
||
try {
|
||
execSync("npm run build", {
|
||
stdio: "inherit",
|
||
cwd: appDir,
|
||
env: {
|
||
...process.env,
|
||
HOME: buildHomeDir,
|
||
USERPROFILE: buildHomeDir,
|
||
APPDATA: path.join(buildHomeDir, "AppData", "Roaming"),
|
||
LOCALAPPDATA: path.join(buildHomeDir, "AppData", "Local"),
|
||
NEXT_DIST_DIR: buildDistDirName,
|
||
NEXT_TRACING_ROOT_MODE: "workspace",
|
||
}
|
||
});
|
||
console.log("✅ Next.js build completed\n");
|
||
} catch (error) {
|
||
console.error("❌ Next.js build failed");
|
||
process.exit(1);
|
||
}
|
||
|
||
// Step 2: Clean old app/cli/app if exists
|
||
console.log("2️⃣ Cleaning old app/cli/app...");
|
||
if (fs.existsSync(cliAppDir)) {
|
||
fs.rmSync(cliAppDir, { recursive: true, force: true });
|
||
}
|
||
console.log("✅ Cleaned\n");
|
||
|
||
// Step 3: Copy Next.js standalone build to app/cli/app.
|
||
// Newer Next.js standalone output writes server.js/package.json plus .next/, src/, and
|
||
// node_modules/ directly under .next/standalone. Older builds may still use a nested app/.
|
||
console.log("3️⃣ Copying Next.js standalone build to app/cli/app...");
|
||
const standaloneRoot = path.join(appDir, ".next", "standalone");
|
||
const standaloneRootResolved = path.join(buildDistDir, "standalone");
|
||
let standaloneRootToUse = fs.existsSync(standaloneRootResolved) ? standaloneRootResolved : standaloneRoot;
|
||
// Next.js 16 nests standalone output under the project name when NEXT_TRACING_ROOT_MODE=workspace
|
||
// e.g. .next-cli-build/standalone/9router/server.js
|
||
const pkgName = path.basename(appDir);
|
||
const nestedRoot = path.join(standaloneRootToUse, pkgName);
|
||
if (fs.existsSync(path.join(nestedRoot, "server.js")) && !fs.existsSync(path.join(standaloneRootToUse, "server.js"))) {
|
||
console.log(`ℹ️ Detected nested standalone output: ${pkgName}/`);
|
||
standaloneRootToUse = nestedRoot;
|
||
}
|
||
const standaloneApp = fs.existsSync(path.join(standaloneRootToUse, "server.js"))
|
||
? standaloneRootToUse
|
||
: path.join(standaloneRootToUse, "app");
|
||
if (!fs.existsSync(standaloneApp)) {
|
||
console.error("❌ Next.js standalone build not found under .next/standalone");
|
||
console.error("Expected either .next/standalone/server.js or .next/standalone/app/");
|
||
process.exit(1);
|
||
}
|
||
copyRecursive(standaloneApp, cliAppDir);
|
||
|
||
// Older nested-app layout stores traced node_modules at standalone root.
|
||
const standaloneNodeModules = path.join(standaloneRootToUse, "node_modules");
|
||
if (standaloneApp !== standaloneRootToUse && fs.existsSync(standaloneNodeModules)) {
|
||
copyRecursive(standaloneNodeModules, path.join(cliAppDir, "node_modules"));
|
||
}
|
||
console.log("✅ Copied standalone build\n");
|
||
|
||
// Step 3a: Copy custom server (injects real socket IP, strips spoofable XFF).
|
||
const customServerSrc = path.join(appDir, "custom-server.js");
|
||
if (fs.existsSync(customServerSrc)) {
|
||
fs.copyFileSync(customServerSrc, path.join(cliAppDir, "custom-server.js"));
|
||
console.log("✅ Copied custom-server.js\n");
|
||
} else {
|
||
console.warn("⚠️ custom-server.js not found — server will run without real-IP injection\n");
|
||
}
|
||
|
||
// Step 3b: Ensure sql.js (pure JS fallback) bundled in app/cli/app/node_modules.
|
||
// Strip better-sqlite3 (native) — it lives in ~/.9router/runtime to avoid
|
||
// Windows EBUSY during global CLI updates. node:sqlite (Node ≥22.5) is also
|
||
// available as a no-install middle tier.
|
||
console.log("3️⃣ b Configuring SQLite drivers...");
|
||
function ensureModuleInBundle(pkg) {
|
||
const dest = path.join(cliAppDir, "node_modules", pkg);
|
||
if (fs.existsSync(dest)) {
|
||
console.log(`✅ ${pkg} already bundled`);
|
||
return;
|
||
}
|
||
const candidates = [
|
||
path.join(appDir, "node_modules", pkg),
|
||
path.join(rootDir, "node_modules", pkg),
|
||
];
|
||
const src = candidates.find((p) => fs.existsSync(p));
|
||
if (!src) {
|
||
console.warn(`⚠️ ${pkg} not found locally — bundle will rely on node:sqlite or runtime install`);
|
||
return;
|
||
}
|
||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||
copyRecursive(src, dest);
|
||
console.log(`✅ Bundled ${pkg}`);
|
||
}
|
||
ensureModuleInBundle("sql.js");
|
||
// `open` is external (see serverExternalPackages in next.config.mjs), so it must exist in
|
||
// the bundle's node_modules or every importer throws MODULE_NOT_FOUND at runtime. Output
|
||
// tracing normally copies it; this is the same belt-and-braces guard used for sql.js.
|
||
ensureModuleInBundle("open");
|
||
const betterDir = path.join(cliAppDir, "node_modules", "better-sqlite3");
|
||
if (fs.existsSync(betterDir)) {
|
||
fs.rmSync(betterDir, { recursive: true, force: true });
|
||
console.log("✅ Stripped better-sqlite3 (lives in ~/.9router/runtime)");
|
||
}
|
||
console.log("");
|
||
|
||
// Step 4: Copy static files
|
||
console.log("4️⃣ Copying static files...");
|
||
const staticSrc = path.join(appDir, ".next", "static");
|
||
const staticSrcResolved = path.join(buildDistDir, "static");
|
||
const staticDest = path.join(cliAppDir, buildDistDirName, "static");
|
||
if (fs.existsSync(staticSrcResolved) || fs.existsSync(staticSrc)) {
|
||
copyRecursive(fs.existsSync(staticSrcResolved) ? staticSrcResolved : staticSrc, staticDest);
|
||
console.log("✅ Copied static files\n");
|
||
} else {
|
||
console.log("⏭️ No static files found\n");
|
||
}
|
||
|
||
// Step 5: Copy public folder if exists
|
||
console.log("5️⃣ Copying public folder...");
|
||
const publicSrc = path.join(appDir, "public");
|
||
const publicDest = path.join(cliAppDir, "public");
|
||
if (fs.existsSync(publicSrc)) {
|
||
copyRecursive(publicSrc, publicDest);
|
||
console.log("✅ Copied public folder\n");
|
||
} else {
|
||
console.log("⏭️ No public folder found\n");
|
||
}
|
||
|
||
// Step 6: Copy vendor-chunks (required for production)
|
||
console.log("6️⃣ Copying vendor-chunks...");
|
||
const vendorChunksSrc = path.join(appDir, ".next", "server", "vendor-chunks");
|
||
const vendorChunksSrcResolved = path.join(buildDistDir, "server", "vendor-chunks");
|
||
const vendorChunksDest = path.join(cliAppDir, buildDistDirName, "server", "vendor-chunks");
|
||
if (fs.existsSync(vendorChunksSrcResolved) || fs.existsSync(vendorChunksSrc)) {
|
||
copyRecursive(fs.existsSync(vendorChunksSrcResolved) ? vendorChunksSrcResolved : vendorChunksSrc, vendorChunksDest);
|
||
console.log("✅ Copied vendor-chunks\n");
|
||
} else {
|
||
console.log("⏭️ No vendor-chunks found\n");
|
||
}
|
||
|
||
// Step 7: Copy MITM server files (not bundled by Next.js standalone)
|
||
console.log("7️⃣ Copying MITM server files...");
|
||
const mitmSrc = path.join(appDir, "src", "mitm");
|
||
const mitmDest = path.join(cliAppDir, "src", "mitm");
|
||
if (fs.existsSync(mitmSrc)) {
|
||
copyRecursive(mitmSrc, mitmDest);
|
||
console.log("✅ Copied MITM files\n");
|
||
} else {
|
||
console.log("⏭️ No MITM files found\n");
|
||
}
|
||
|
||
// Step 7b: Copy standalone updater (headless Node process for install progress)
|
||
console.log("7️⃣ b Copying updater files...");
|
||
const updaterSrc = path.join(appDir, "src", "lib", "updater");
|
||
const updaterDest = path.join(cliAppDir, "src", "lib", "updater");
|
||
if (fs.existsSync(updaterSrc)) {
|
||
copyRecursive(updaterSrc, updaterDest);
|
||
console.log("✅ Copied updater files\n");
|
||
} else {
|
||
console.log("⏭️ No updater files found\n");
|
||
}
|
||
|
||
// Step 8: Build MITM server (config driven - see app/cli/scripts/buildMitm.js)
|
||
console.log("8️⃣ Building MITM server...");
|
||
try {
|
||
execSync("node scripts/buildMitm.js", { stdio: "inherit", cwd: cliDir });
|
||
console.log("✅ MITM server build completed\n");
|
||
} catch (error) {
|
||
console.error("❌ MITM build failed");
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log("✨ CLI package build completed!");
|
||
console.log(`📁 Output: ${cliAppDir}`);
|
||
|
||
try {
|
||
const { execSync: exec } = require("child_process");
|
||
const size = exec(`du -sh "${cliAppDir}"`, { encoding: "utf8" }).trim();
|
||
console.log(`📊 Package size: ${size.split("\t")[0]}`);
|
||
} catch (e) {
|
||
// Silent fail on size check
|
||
}
|