3432dfd280
Build Electron Desktop App / Validate version (push) Failing after 35s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
Build Electron Desktop App / Publish to npm (push) Has been skipped
* test: resolve typescript strictness complaints in unit tests * Update Claude Code obfuscation to version 2.1.114 (#1403) * fix(cloud-code): scope thinking stripping to executor boundaries (#1401) * fix(cloud-code): scope thinking stripping to executors * fix(cloud-code): guard antigravity normalized body * Update Claude Code obfuscation to version 2.1.114 - Update Claude Code version from 2.1.87 to 2.1.114 - Update X-Stainless-Package-Version from 0.80.0 to 0.81.0 - Add new beta flags: redact-thinking-2026-02-12, advisor-tool-2026-03-01, advanced-tool-use-2025-11-20 - Add missing headers: anthropic-version, anthropic-dangerous-direct-browser-access, x-app, X-Stainless-Timeout - Add all X-Stainless-* headers (Arch, Lang, OS, Runtime, Runtime-Version, Retry-Count) - Fix accept-encoding header: identity -> gzip, deflate, br, zstd - Add connection: keep-alive header - Update tool name mapping: add lsp, apply_patch, websearch These changes ensure that requests from OpenCode through Omniroute are indistinguishable from genuine Claude Code 2.1.114 requests, allowing proper authentication with Anthropic's API without triggering extra credits errors. * fix: resolve CodeQL password hash alert and TruffleHog CI failure --------- Co-authored-by: Randi <55005611+rdself@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Nikolay Popov <ekklesio.dev@gmail.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> * fix(claude-code): scope obfuscation to cli clients and fix tests * docs(workflows): enforce PR merge instead of manual close * docs(changelog): update 3.6.9 notes with missing PR 1403 and fixes * docs(workflows): update generate-release to use full changelog for PR body * fix(tsc): silence baseUrl deprecation warnings for TS 5.5+ * fix(chatcore): apply proactive compression before provider translation (#1406) Integrated into release/v3.6.9 * docs(changelog): add PR 1406 * Makes text visible in dark-mode (#1409) Integrated into release/v3.6.9 * docs(changelog): add PR 1409 * chore: save local work * chore(release): sync version references to 3.6.9 * fix(codex): prevent proactive token refresh consumption and strip background parameter * ci: shard long-running suites and relax timeouts * ci: allow manual CI dispatch for release branches * feat(skills): provider-aware marketplace UX, scored AUTO injection, and memory pipeline hardening (#1411) * fix/400 for GeminiCLI(add "ref" in GEMINI_UNSUPPORTED_SCHEMA_KEYS) * feat(cc-compatible): align request shape with Claude CLI * fix(cc-compatible): add Claude CLI system skeleton for OpenAI input * preserve reasoning when translating chat to responses (#1414) Integrated into release/v3.6.9 * fix(skills): optimize AUTO scoring and include Responses input context (#1418) Integrated into release/v3.6.9 * chore: fix TS errors and update review-prs workflow * fix(api): stop sending unsupported Gemini and Codex parameters Prevent Gemini request translation from injecting default thoughtSignature values that the upstream API strictly validates and rejects. Only preserve real signatures resolved from prior upstream responses, and strip additionalProperties from Gemini function schemas to avoid 400 "Unknown name" errors. Also remove fallback-injected session_id and conversation_id fields before sending Codex requests, and restore compatibility with the legacy OUTBOUND_SSRF_GUARD_ENABLED flag when determining whether private provider URLs are allowed. Updates the Gemini translator and regression tests for issue #1410 and related 400 error cases. * fix(core): stabilization fixes for token refresh, usage translation, and testing - Update Codex token refresh detection logic - Mark provider connections invalid on unrecoverable refresh error - Fix Claude usage translation under-reporting cached tokens - Update test expectations - Update CHANGELOG.md for v3.6.9 * fix(auth): reload fresh token state and unify expiry persistence Refresh checks now re-read the latest stored provider connection before attempting rotation so they do not use stale refresh tokens captured by an earlier sweep. Token updates also persist both expiresAt and tokenExpiresAt across the health check, usage-limit refresh path, and SSE refresh flow. This keeps known token expiry metadata in sync and avoids interval-based refreshes for connections whose tokens are still valid well into the future. * fix: resolve SSRF environment static evaluation bug (#1427) Fix import aliases and strict TS typings for tests and ACP agents. * test: resolve remaining strict type errors in test files * test: fix provider service assertion for anthropic-compatible header * fix(codex): respect openaiStoreEnabled setting during native passthrough (#1432) * fix(codex): fix token refresh unrecoverable detection for expired tokens * fix(ci): restore release v3.6.9 build and flaky tests * fix(cc-compatible): trim default OpenAI system skeleton (#1433) Integrated into release/v3.6.9 * fix: prevent masked API keys from being written to CLI tool configs (#1435) * feat: mark Qwen provider as deprecated and add deprecation warning to CLI tool (#1437) * docs(changelog): comprehensive v3.6.9 update with all 59 commits since v3.6.8 * test(ci): align qwen guide settings assertions * fix(security): resolve CodeQL alert 163 for incomplete URL sanitization in Qwen CLI settings --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: Nikolay Popov <74762779+nikolay-popov-ideogram@users.noreply.github.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com> Co-authored-by: Nikolay Popov <ekklesio.dev@gmail.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: Tim Massey <tim-massey@users.noreply.github.com> Co-authored-by: Paijo <oyi77@users.noreply.github.com> Co-authored-by: dail45 <dail45@yandex.ru> Co-authored-by: R.D. <rogerproself@gmail.com>
494 lines
17 KiB
JavaScript
494 lines
17 KiB
JavaScript
#!/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";
|
||
|
||
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 APP_DIR = join(ROOT, "app");
|
||
|
||
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 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 2.5: Remove app/ directory before build ───────────
|
||
// CRITICAL: The postinstall script may create app/node_modules/@swc/helpers/,
|
||
// which causes Next.js 16 to interpret app/ as an App Router directory
|
||
// (competing with src/app/). This makes the build silently skip all real
|
||
// routes, producing a standalone with only _global-error and _not-found.
|
||
// We MUST remove app/ before running `next build`.
|
||
if (existsSync(APP_DIR)) {
|
||
console.log(" 🧹 Removing app/ created by postinstall (App Router conflict fix)...");
|
||
rmSync(APP_DIR, { recursive: true, force: true });
|
||
}
|
||
|
||
// ── Step 3: Build Next.js ──────────────────────────────────
|
||
console.log(" 🏗️ Building Next.js (standalone)...");
|
||
execSync("npx next build", {
|
||
cwd: ROOT,
|
||
stdio: "inherit",
|
||
env: {
|
||
...process.env,
|
||
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: string,
|
||
hits: { file: string; mod: string }[] = []
|
||
): { file: string; mod: string }[] => {
|
||
let entries: string[] = [];
|
||
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 });
|
||
|
||
// ── Next.js Turbopack Standalone Tracer Fix ───────────────
|
||
// Workaround for Next.js 15+ standalone mode missing Turbopack chunks
|
||
const staticChunksSrc = join(ROOT, ".next", "server", "chunks");
|
||
const staticChunksDest = join(APP_DIR, ".next", "server", "chunks");
|
||
if (existsSync(staticChunksSrc)) {
|
||
console.log(" 📋 Patching standalone build with missing Turbopack chunks...");
|
||
cpSync(staticChunksSrc, staticChunksDest, { recursive: true, force: false });
|
||
}
|
||
|
||
// ── 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: string) => {
|
||
let entries: string[] = [];
|
||
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,
|
||
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 {
|
||
execSync("npx tsc -p tsconfig.mitm.tmp.json", { cwd: ROOT, stdio: "inherit" });
|
||
console.log(" ✅ MITM utilities compiled to app/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(APP_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 {
|
||
execSync(
|
||
`npx esbuild open-sse/mcp-server/server.ts --bundle --platform=node --packages=external --format=esm --outfile=app/open-sse/mcp-server/server.js`,
|
||
{ cwd: ROOT, stdio: "inherit" }
|
||
);
|
||
console.log(" ✅ MCP Server bundled to app/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 {
|
||
execSync(
|
||
`npx esbuild bin/omniroute.ts --bundle --platform=node --packages=external --format=esm --outfile=bin/omniroute.mjs`,
|
||
{ cwd: ROOT, stdio: "inherit" }
|
||
);
|
||
execSync(`chmod +x bin/omniroute.mjs`, { cwd: ROOT });
|
||
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(APP_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(APP_DIR, ".env.example"));
|
||
}
|
||
|
||
const openapiSpecSrc = join(ROOT, "docs", "openapi.yaml");
|
||
if (existsSync(openapiSpecSrc)) {
|
||
const docsDest = join(APP_DIR, "docs");
|
||
mkdirSync(docsDest, { recursive: true });
|
||
cpSync(openapiSpecSrc, join(docsDest, "openapi.yaml"));
|
||
}
|
||
|
||
const syncEnvSrc = join(ROOT, "scripts", "sync-env.mjs");
|
||
if (existsSync(syncEnvSrc)) {
|
||
const scriptsDest = join(APP_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(APP_DIR, "src", "lib", "db", "migrations");
|
||
mkdirSync(join(APP_DIR, "src", "lib", "db"), { recursive: true });
|
||
cpSync(migrationsSrc, migrationsDest, { recursive: true, force: true });
|
||
}
|
||
|
||
// ── 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 development-only residue from staged app/ ─────────────
|
||
for (const relativePath of APP_STAGING_REMOVAL_PATHS) {
|
||
const targetPath = join(APP_DIR, relativePath);
|
||
if (existsSync(targetPath)) {
|
||
console.log(` 🧹 Removing app/${relativePath} (not needed in npm package)...`);
|
||
rmSync(targetPath, { recursive: true, force: true });
|
||
console.log(` ✅ app/${relativePath} removed.`);
|
||
}
|
||
}
|
||
|
||
// ── Step 10.7: Prune any staged app/ file outside the allowed runtime set ───
|
||
const stagedFiles = walkFiles(APP_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 app/...");
|
||
unexpectedStagedFiles.forEach((unexpectedPath: string) => {
|
||
rmSync(join(APP_DIR, unexpectedPath), { force: true });
|
||
console.log(` ✅ Removed app/${unexpectedPath}`);
|
||
});
|
||
removeEmptyDirectories(APP_DIR);
|
||
}
|
||
|
||
const remainingUnexpectedFiles = findUnexpectedArtifactPaths(walkFiles(APP_DIR), {
|
||
exactPaths: APP_STAGING_ALLOWED_EXACT_PATHS,
|
||
prefixPaths: APP_STAGING_ALLOWED_PATH_PREFIXES,
|
||
});
|
||
|
||
if (remainingUnexpectedFiles.length > 0) {
|
||
console.error("\n ❌ Staged app/ still contains unexpected publish artifacts:");
|
||
remainingUnexpectedFiles.forEach((violation: string) => console.error(` - app/${violation}`));
|
||
process.exit(1);
|
||
}
|
||
|
||
// ── 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("");
|