fix(cli): include complete API artifacts in CLI package
Merge the complete generated .next-cli-build/server tree into the packaged CLI after the standalone copy, since Next's standalone output is trace-pruned and can omit route modules (e.g. /api/v1/messages) or chunks loaded dynamically. Add a post-copy integrity check for the required API route artifacts so an incomplete package fails during pack:cli instead of at runtime. Fixes #2945
This commit is contained in:
@@ -81,205 +81,272 @@ function copyRecursive(src, dest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("📦 Building 9Router CLI package with Next.js...\n");
|
function resolveStandaloneBuild(appDir, buildDistDir) {
|
||||||
|
const legacyStandaloneRoot = path.join(appDir, ".next", "standalone");
|
||||||
|
const resolvedStandaloneRoot = path.join(buildDistDir, "standalone");
|
||||||
|
let standaloneRoot = fs.existsSync(resolvedStandaloneRoot)
|
||||||
|
? resolvedStandaloneRoot
|
||||||
|
: legacyStandaloneRoot;
|
||||||
|
|
||||||
fs.mkdirSync(buildHomeDir, { recursive: true });
|
// Next.js 16 nests standalone output under the project name when
|
||||||
fs.mkdirSync(path.join(buildHomeDir, "AppData", "Roaming"), { recursive: true });
|
// NEXT_TRACING_ROOT_MODE=workspace, e.g. standalone/9router/server.js.
|
||||||
fs.mkdirSync(path.join(buildHomeDir, "AppData", "Local"), { recursive: true });
|
const pkgName = path.basename(appDir);
|
||||||
|
const nestedRoot = path.join(standaloneRoot, pkgName);
|
||||||
// Step 0: Sync version from app/cli/package.json to app/package.json
|
if (fs.existsSync(path.join(nestedRoot, "server.js")) && !fs.existsSync(path.join(standaloneRoot, "server.js"))) {
|
||||||
console.log("0️⃣ Syncing version to app/package.json...");
|
console.log(`ℹ️ Detected nested standalone output: ${pkgName}/`);
|
||||||
const cliPkg = JSON.parse(fs.readFileSync(path.join(cliDir, "package.json"), "utf8"));
|
standaloneRoot = nestedRoot;
|
||||||
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),
|
const standaloneApp = fs.existsSync(path.join(standaloneRoot, "server.js"))
|
||||||
path.join(rootDir, "node_modules", pkg),
|
? standaloneRoot
|
||||||
|
: path.join(standaloneRoot, "app");
|
||||||
|
if (!fs.existsSync(standaloneApp)) {
|
||||||
|
throw new Error(
|
||||||
|
"Next.js standalone build not found under .next/standalone; " +
|
||||||
|
"expected either .next/standalone/server.js or .next/standalone/app/",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { standaloneApp, standaloneRoot };
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyStandaloneBuild(appDir, buildDistDir, cliAppDir) {
|
||||||
|
const { standaloneApp, standaloneRoot } = resolveStandaloneBuild(appDir, buildDistDir);
|
||||||
|
copyRecursive(standaloneApp, cliAppDir);
|
||||||
|
|
||||||
|
// Older nested-app layout stores traced node_modules at standalone root.
|
||||||
|
const standaloneNodeModules = path.join(standaloneRoot, "node_modules");
|
||||||
|
if (standaloneApp !== standaloneRoot && fs.existsSync(standaloneNodeModules)) {
|
||||||
|
copyRecursive(standaloneNodeModules, path.join(cliAppDir, "node_modules"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeServerArtifacts(buildDistDir, cliAppDir) {
|
||||||
|
const serverSrc = path.join(buildDistDir, "server");
|
||||||
|
const serverDest = path.join(cliAppDir, buildDistDirName, "server");
|
||||||
|
if (!fs.existsSync(serverSrc)) {
|
||||||
|
throw new Error(`Complete Next.js server build not found: ${serverSrc}`);
|
||||||
|
}
|
||||||
|
copyRecursive(serverSrc, serverDest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertRequiredApiArtifacts(cliAppDir) {
|
||||||
|
const requiredArtifacts = [
|
||||||
|
"app/api/v1/chat/completions/route.js",
|
||||||
|
"app/api/v1/messages/route.js",
|
||||||
];
|
];
|
||||||
const src = candidates.find((p) => fs.existsSync(p));
|
const serverDir = path.join(cliAppDir, buildDistDirName, "server");
|
||||||
if (!src) {
|
const missingArtifacts = requiredArtifacts
|
||||||
console.warn(`⚠️ ${pkg} not found locally — bundle will rely on node:sqlite or runtime install`);
|
.map((artifact) => path.join(serverDir, artifact))
|
||||||
return;
|
.filter((artifact) => !fs.existsSync(artifact));
|
||||||
|
|
||||||
|
if (missingArtifacts.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Required CLI API route artifact${missingArtifacts.length === 1 ? " is" : "s are"} missing:\n` +
|
||||||
|
missingArtifacts.join("\n"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
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
|
function buildCliPackage() {
|
||||||
console.log("5️⃣ Copying public folder...");
|
console.log("📦 Building 9Router CLI package with Next.js...\n");
|
||||||
const publicSrc = path.join(appDir, "public");
|
|
||||||
const publicDest = path.join(cliAppDir, "public");
|
fs.mkdirSync(buildHomeDir, { recursive: true });
|
||||||
if (fs.existsSync(publicSrc)) {
|
fs.mkdirSync(path.join(buildHomeDir, "AppData", "Roaming"), { recursive: true });
|
||||||
copyRecursive(publicSrc, publicDest);
|
fs.mkdirSync(path.join(buildHomeDir, "AppData", "Local"), { recursive: true });
|
||||||
console.log("✅ Copied public folder\n");
|
|
||||||
} else {
|
// Step 0: Sync version from app/cli/package.json to app/package.json
|
||||||
console.log("⏭️ No public folder found\n");
|
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...");
|
||||||
|
try {
|
||||||
|
copyStandaloneBuild(appDir, buildDistDir, cliAppDir);
|
||||||
|
} catch (error) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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 6b: Merge the complete generated server tree. Next.js standalone output
|
||||||
|
// is trace-pruned and can omit route modules or chunks loaded dynamically.
|
||||||
|
console.log("6️⃣ b Copying complete server artifacts...");
|
||||||
|
mergeServerArtifacts(buildDistDir, cliAppDir);
|
||||||
|
assertRequiredApiArtifacts(cliAppDir);
|
||||||
|
console.log("✅ Copied complete server artifacts\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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: Copy vendor-chunks (required for production)
|
module.exports = {
|
||||||
console.log("6️⃣ Copying vendor-chunks...");
|
assertRequiredApiArtifacts,
|
||||||
const vendorChunksSrc = path.join(appDir, ".next", "server", "vendor-chunks");
|
copyStandaloneBuild,
|
||||||
const vendorChunksSrcResolved = path.join(buildDistDir, "server", "vendor-chunks");
|
mergeServerArtifacts,
|
||||||
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)
|
if (require.main === module) {
|
||||||
console.log("7️⃣ Copying MITM server files...");
|
buildCliPackage();
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
141
tests/unit/cli-build-artifacts.test.js
Normal file
141
tests/unit/cli-build-artifacts.test.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
let testApi;
|
||||||
|
try {
|
||||||
|
testApi = await import("vitest");
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "ERR_MODULE_NOT_FOUND") throw error;
|
||||||
|
testApi = await import("node:test");
|
||||||
|
}
|
||||||
|
const { afterEach, describe, it } = testApi;
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const {
|
||||||
|
assertRequiredApiArtifacts,
|
||||||
|
copyStandaloneBuild,
|
||||||
|
mergeServerArtifacts,
|
||||||
|
} = require("../../cli/scripts/build-cli.js");
|
||||||
|
|
||||||
|
const tempDirs = [];
|
||||||
|
|
||||||
|
function createTempDir() {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "9router-cli-build-"));
|
||||||
|
tempDirs.push(tempDir);
|
||||||
|
return tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFixture(root, relativePath, contents = relativePath) {
|
||||||
|
const filePath = path.join(root, relativePath);
|
||||||
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
fs.writeFileSync(filePath, contents);
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCompleteServer(buildDistDir) {
|
||||||
|
const serverDir = path.join(buildDistDir, "server");
|
||||||
|
writeFixture(serverDir, "app/api/v1/chat/completions/route.js", "chat route");
|
||||||
|
writeFixture(serverDir, "app/api/v1/messages/route.js", "messages route");
|
||||||
|
writeFixture(serverDir, "chunks/openai-provider.js", "openai chunk");
|
||||||
|
writeFixture(serverDir, "chunks/anthropic-provider.js", "anthropic chunk");
|
||||||
|
return serverDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const tempDir of tempDirs.splice(0)) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("CLI build server artifacts", () => {
|
||||||
|
for (const { name, standalonePath } of [
|
||||||
|
{
|
||||||
|
name: "legacy nested app",
|
||||||
|
standalonePath: (appDir, buildDistDir) => path.join(appDir, ".next", "standalone", "app"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Next 16 workspace",
|
||||||
|
standalonePath: (appDir, buildDistDir) => path.join(buildDistDir, "standalone", path.basename(appDir)),
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
it(`merges complete API routes and provider chunks for the ${name} layout`, () => {
|
||||||
|
const root = createTempDir();
|
||||||
|
const appDir = path.join(root, "9router");
|
||||||
|
const buildDistDir = path.join(appDir, ".next-cli-build");
|
||||||
|
const cliAppDir = path.join(root, "cli-app");
|
||||||
|
const standaloneDir = standalonePath(appDir, buildDistDir);
|
||||||
|
|
||||||
|
writeFixture(standaloneDir, "server.js", "standalone server");
|
||||||
|
writeFixture(
|
||||||
|
standaloneDir,
|
||||||
|
".next-cli-build/server/app/api/v1/chat/completions/route.js",
|
||||||
|
"standalone chat route",
|
||||||
|
);
|
||||||
|
createCompleteServer(buildDistDir);
|
||||||
|
|
||||||
|
copyStandaloneBuild(appDir, buildDistDir, cliAppDir);
|
||||||
|
mergeServerArtifacts(buildDistDir, cliAppDir);
|
||||||
|
assertRequiredApiArtifacts(cliAppDir);
|
||||||
|
|
||||||
|
const packagedServer = path.join(cliAppDir, ".next-cli-build", "server");
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(path.join(packagedServer, "app/api/v1/messages/route.js"), "utf8"),
|
||||||
|
"messages route",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(path.join(packagedServer, "chunks/openai-provider.js"), "utf8"),
|
||||||
|
"openai chunk",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(path.join(packagedServer, "chunks/anthropic-provider.js"), "utf8"),
|
||||||
|
"anthropic chunk",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("merges idempotently without removing standalone-generated files", () => {
|
||||||
|
const root = createTempDir();
|
||||||
|
const buildDistDir = path.join(root, ".next-cli-build");
|
||||||
|
const cliAppDir = path.join(root, "cli-app");
|
||||||
|
const packagedServer = path.join(cliAppDir, ".next-cli-build", "server");
|
||||||
|
|
||||||
|
createCompleteServer(buildDistDir);
|
||||||
|
writeFixture(packagedServer, "standalone-only.js", "keep me");
|
||||||
|
|
||||||
|
mergeServerArtifacts(buildDistDir, cliAppDir);
|
||||||
|
mergeServerArtifacts(buildDistDir, cliAppDir);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(path.join(packagedServer, "standalone-only.js"), "utf8"),
|
||||||
|
"keep me",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(path.join(packagedServer, "app/api/v1/messages/route.js"), "utf8"),
|
||||||
|
"messages route",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports the missing required API route artifact path", () => {
|
||||||
|
const root = createTempDir();
|
||||||
|
const buildDistDir = path.join(root, ".next-cli-build");
|
||||||
|
const cliAppDir = path.join(root, "cli-app");
|
||||||
|
|
||||||
|
writeFixture(
|
||||||
|
path.join(buildDistDir, "server"),
|
||||||
|
"app/api/v1/chat/completions/route.js",
|
||||||
|
"chat route",
|
||||||
|
);
|
||||||
|
mergeServerArtifacts(buildDistDir, cliAppDir);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => assertRequiredApiArtifacts(cliAppDir),
|
||||||
|
(error) => error.message.includes(path.join(
|
||||||
|
cliAppDir,
|
||||||
|
".next-cli-build/server/app/api/v1/messages/route.js",
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user