mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
refactor(build): single-source sidecar list + drop redundant Dockerfile COPYs (#3187)
Integrated into release/v3.8.11
This commit is contained in:
committed by
GitHub
parent
a7494e415e
commit
ee62c4c38b
@@ -9,6 +9,7 @@
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **mcp:** move `enforceScopes` guard before `MCP_TOOL_MAP` lookup, add inline `scopes` parameter to `withScopeEnforcement()`, and declare scopes on all 24 dynamic tool definitions (memory, skills, plugins, gamification, compression) to fix scope enforcement for dynamic MCP tool groups (#2958)
|
||||
- **build:** finish the build-output-isolation cleanup — `assembleStandalone.mjs` now derives both its async (`syncStandalone*`) and sync copy paths from a single `NATIVE_ASSET_ENTRIES`/`EXTRA_MODULE_ENTRIES` source of truth (previously two hand-maintained lists that could silently drift), guarded by a new parity test; and the `Dockerfile` drops 5 redundant per-module `COPY` overrides (`@swc/helpers`, `pino-abstract-transport`, `pino-pretty`, `split2`, `migrations`) now that `assembleStandalone` bundles them into the standalone regardless of NFT/Turbopack tracing (validated with a real Turbopack `docker build` + boot → `/api/monitoring/health` 200; `better-sqlite3` stays explicit since only its native `build/` is synced)
|
||||
|
||||
---
|
||||
|
||||
|
||||
28
Dockerfile
28
Dockerfile
@@ -64,23 +64,21 @@ ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_MEMORY_MB}"
|
||||
ENV DATA_DIR=/app/data
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# The standalone build + syncStandaloneExtraModules bundles all runtime files
|
||||
# (.next, node_modules, migrations, scripts, docs, etc.) into .build/next/standalone/.
|
||||
# Explicit overrides below cover modules that NFT tracing may miss.
|
||||
# `npm run build` (build-next-isolated → assembleStandalone) bundles ALL runtime
|
||||
# files into .build/next/standalone/ — .next, node_modules, migrations, scripts,
|
||||
# docs, and the previously hand-COPY'd modules below (@swc/helpers, pino-*, split2,
|
||||
# migrations). assembleStandalone copies them straight from the builder's
|
||||
# node_modules, so they are present regardless of NFT/Turbopack trace behaviour.
|
||||
# The old per-module overrides were therefore pure duplication and were removed
|
||||
# (build-output-isolation cleanup). See scripts/build/assembleStandalone.mjs
|
||||
# (EXTRA_MODULE_ENTRIES) for the single source of truth.
|
||||
COPY --from=builder /app/.build/next/standalone ./
|
||||
# Explicitly copy @swc/helpers — not always traced by standalone output but needed at runtime
|
||||
COPY --from=builder /app/node_modules/@swc/helpers ./node_modules/@swc/helpers
|
||||
# Explicitly copy better-sqlite3 — native bindings are not reliably traced by
|
||||
# Next.js standalone output, but bootstrap-env requires SQLite before startup.
|
||||
# better-sqlite3 is the one exception still copied explicitly: assembleStandalone
|
||||
# only syncs its native build/ dir; the JS wrapper (lib/, package.json) is left to
|
||||
# Next.js tracing. bootstrap-env requires SQLite BEFORE the standalone server
|
||||
# starts, so guarantee the complete package independent of trace behaviour.
|
||||
COPY --from=builder /app/node_modules/better-sqlite3 ./node_modules/better-sqlite3
|
||||
# Explicitly copy pino transport dependencies — pino spawns a worker that requires
|
||||
# pino-abstract-transport at runtime; Next.js standalone trace does not capture it (#449)
|
||||
COPY --from=builder /app/node_modules/pino-abstract-transport ./node_modules/pino-abstract-transport
|
||||
COPY --from=builder /app/node_modules/pino-pretty ./node_modules/pino-pretty
|
||||
COPY --from=builder /app/node_modules/split2 ./node_modules/split2
|
||||
# Migration SQL files are read via fs.readFileSync at runtime and are NOT
|
||||
# traced by Next.js standalone output — copy them explicitly.
|
||||
COPY --from=builder /app/src/lib/db/migrations ./migrations
|
||||
# migrations land at <standalone>/migrations via assembleStandalone; point the runtime at them.
|
||||
ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations
|
||||
|
||||
# Hand /app over to the baked-in `node` non-root user (UID/GID 1000) so the
|
||||
|
||||
@@ -61,6 +61,64 @@ async function exists(targetPath) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SINGLE SOURCE OF TRUTH for the standalone bundle's native assets and extra
|
||||
* modules/sidecars. Both the async path (syncStandaloneNativeAssets /
|
||||
* syncStandaloneExtraModules, used by build-next-isolated + tests) and the sync
|
||||
* path (copyNativeAssetsAndExtraModules, used by assembleStandalone) derive their
|
||||
* copy lists from these arrays. Add a sidecar in ONE place — never two.
|
||||
*
|
||||
* Each entry uses path SEGMENT arrays (not pre-joined strings) so the source
|
||||
* (relative to projectRoot) and destination (relative to outDir) can be joined
|
||||
* for either path/platform. @type {{label:string, src:string[], dest:string[]}[]}
|
||||
*/
|
||||
const NATIVE_ASSET_ENTRIES = [
|
||||
{ label: "wreq-js native runtime", src: ["node_modules", "wreq-js", "rust"], dest: ["node_modules", "wreq-js", "rust"] },
|
||||
{ label: "better-sqlite3 native binary", src: ["node_modules", "better-sqlite3", "build"], dest: ["node_modules", "better-sqlite3", "build"] },
|
||||
];
|
||||
|
||||
/** @type {{label:string, src:string[], dest:string[]}[]} */
|
||||
const EXTRA_MODULE_ENTRIES = [
|
||||
{ label: "@swc/helpers", src: ["node_modules", "@swc", "helpers"], dest: ["node_modules", "@swc", "helpers"] },
|
||||
{ label: "pino-abstract-transport", src: ["node_modules", "pino-abstract-transport"], dest: ["node_modules", "pino-abstract-transport"] },
|
||||
{ label: "pino-pretty", src: ["node_modules", "pino-pretty"], dest: ["node_modules", "pino-pretty"] },
|
||||
{ label: "split2", src: ["node_modules", "split2"], dest: ["node_modules", "split2"] },
|
||||
{ label: "migrations", src: ["src", "lib", "db", "migrations"], dest: ["migrations"] },
|
||||
{ label: "MITM server", src: ["src", "mitm", "server.cjs"], dest: ["src", "mitm", "server.cjs"] },
|
||||
{ label: "run-standalone script", src: ["scripts", "dev", "run-standalone.mjs"], dest: ["dev", "run-standalone.mjs"] },
|
||||
{
|
||||
// WS-aware wrapper that run-standalone.mjs prefers over bare server.js.
|
||||
// It installs the trusted peer-IP stamp the authz middleware needs to allow
|
||||
// loopback/LAN access to LOCAL_ONLY routes; without it the Docker container
|
||||
// fails closed (every LOCAL_ONLY request 403s). Imports peer-stamp.mjs +
|
||||
// responses-ws-proxy.mjs, so all three are co-located.
|
||||
label: "WS/peer-stamp standalone server wrapper",
|
||||
src: ["scripts", "dev", "standalone-server-ws.mjs"],
|
||||
dest: ["server-ws.mjs"],
|
||||
},
|
||||
{ label: "peer-stamp helper (server-ws.mjs dependency)", src: ["scripts", "dev", "peer-stamp.mjs"], dest: ["peer-stamp.mjs"] },
|
||||
{ label: "responses-ws-proxy (server-ws.mjs dependency)", src: ["scripts", "dev", "responses-ws-proxy.mjs"], dest: ["responses-ws-proxy.mjs"] },
|
||||
{ label: "runtime-env script", src: ["scripts", "build", "runtime-env.mjs"], dest: ["build", "runtime-env.mjs"] },
|
||||
{ label: "bootstrap-env script", src: ["scripts", "build", "bootstrap-env.mjs"], dest: ["build", "bootstrap-env.mjs"] },
|
||||
{ label: "healthcheck script", src: ["scripts", "dev", "healthcheck.mjs"], dest: ["healthcheck.mjs"] },
|
||||
{ label: "public directory", src: ["public"], dest: ["public"] },
|
||||
{ label: "playwright-core (dynamic import by gemini-web executor)", src: ["node_modules", "playwright-core"], dest: ["node_modules", "playwright-core"] },
|
||||
{ label: "sqlite-vec wrapper (vector memory - loaded at runtime via createRequire)", src: ["node_modules", "sqlite-vec"], dest: ["node_modules", "sqlite-vec"] },
|
||||
// sqlite-vec's native vec0.so lives in a platform-specific package resolved at
|
||||
// runtime via require.resolve(). Next.js does NOT trace it into the standalone
|
||||
// (the externalized wrapper is copied, but its optional platform dep is missed -
|
||||
// Next.js #88844), so without this the bundled/Docker build silently degrades
|
||||
// vector search to FTS5: the wrapper loads but getLoadablePath() throws
|
||||
// MODULE_NOT_FOUND. Copy whichever platform package npm actually installed. See #3066.
|
||||
...[
|
||||
"sqlite-vec-linux-x64",
|
||||
"sqlite-vec-linux-arm64",
|
||||
"sqlite-vec-darwin-x64",
|
||||
"sqlite-vec-darwin-arm64",
|
||||
"sqlite-vec-windows-x64",
|
||||
].map((pkg) => ({ label: pkg, src: ["node_modules", pkg], dest: ["node_modules", pkg] })),
|
||||
];
|
||||
|
||||
/**
|
||||
* Copy native standalone assets (wreq-js rust/, better-sqlite3 build/).
|
||||
*
|
||||
@@ -72,9 +130,9 @@ async function exists(targetPath) {
|
||||
* @param {Console|{log:Function}} [log] - logger
|
||||
* @returns {Promise<boolean>} true if any asset was copied
|
||||
*/
|
||||
export async function syncStandaloneNativeAssets(rootDir, fsImpl = fs, log = console) {
|
||||
const nextDistDir = process.env.NEXT_DIST_DIR || ".build/next";
|
||||
const standaloneRoot = path.join(rootDir, nextDistDir, "standalone");
|
||||
export async function syncStandaloneNativeAssets(rootDir, fsImpl = fs, log = console, outDir) {
|
||||
const standaloneRoot =
|
||||
outDir || path.join(rootDir, process.env.NEXT_DIST_DIR || ".build/next", "standalone");
|
||||
return syncNativeAssetsToDir(rootDir, standaloneRoot, fsImpl, log);
|
||||
}
|
||||
|
||||
@@ -89,9 +147,9 @@ export async function syncStandaloneNativeAssets(rootDir, fsImpl = fs, log = con
|
||||
* @param {Console|{log:Function}} [log] - logger
|
||||
* @returns {Promise<boolean>} true if any module was copied
|
||||
*/
|
||||
export async function syncStandaloneExtraModules(rootDir, fsImpl = fs, log = console) {
|
||||
const nextDistDir = process.env.NEXT_DIST_DIR || ".build/next";
|
||||
const standaloneRoot = path.join(rootDir, nextDistDir, "standalone");
|
||||
export async function syncStandaloneExtraModules(rootDir, fsImpl = fs, log = console, outDir) {
|
||||
const standaloneRoot =
|
||||
outDir || path.join(rootDir, process.env.NEXT_DIST_DIR || ".build/next", "standalone");
|
||||
return syncExtraModulesToDir(rootDir, standaloneRoot, fsImpl, log);
|
||||
}
|
||||
|
||||
@@ -105,35 +163,24 @@ export async function syncStandaloneExtraModules(rootDir, fsImpl = fs, log = con
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function syncNativeAssetsToDir(projectRoot, outDir, fsImpl, log) {
|
||||
const nativeAssetDirs = [
|
||||
{
|
||||
label: "wreq-js native runtime",
|
||||
sourcePath: path.join(projectRoot, "node_modules", "wreq-js", "rust"),
|
||||
destinationPath: path.join(outDir, "node_modules", "wreq-js", "rust"),
|
||||
},
|
||||
{
|
||||
label: "better-sqlite3 native binary",
|
||||
sourcePath: path.join(projectRoot, "node_modules", "better-sqlite3", "build"),
|
||||
destinationPath: path.join(outDir, "node_modules", "better-sqlite3", "build"),
|
||||
},
|
||||
];
|
||||
|
||||
let changed = false;
|
||||
|
||||
for (const entry of nativeAssetDirs) {
|
||||
if (!(await exists(entry.sourcePath))) continue;
|
||||
for (const entry of NATIVE_ASSET_ENTRIES) {
|
||||
const sourcePath = path.join(projectRoot, ...entry.src);
|
||||
if (!(await exists(sourcePath))) continue;
|
||||
|
||||
const destinationPath = path.join(outDir, ...entry.dest);
|
||||
const mkdir =
|
||||
typeof fsImpl.mkdir === "function" ? fsImpl.mkdir.bind(fsImpl) : fs.mkdir.bind(fs);
|
||||
await mkdir(path.dirname(entry.destinationPath), { recursive: true });
|
||||
await fsImpl.cp(entry.sourcePath, entry.destinationPath, {
|
||||
await mkdir(path.dirname(destinationPath), { recursive: true });
|
||||
await fsImpl.cp(sourcePath, destinationPath, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
log.log(
|
||||
`[assembleStandalone] Copied native standalone asset: ${path.relative(
|
||||
projectRoot,
|
||||
entry.destinationPath
|
||||
destinationPath
|
||||
)}`
|
||||
);
|
||||
changed = true;
|
||||
@@ -152,121 +199,17 @@ async function syncNativeAssetsToDir(projectRoot, outDir, fsImpl, log) {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function syncExtraModulesToDir(projectRoot, outDir, fsImpl, log) {
|
||||
const entries = [
|
||||
{
|
||||
label: "@swc/helpers",
|
||||
sourcePath: path.join(projectRoot, "node_modules", "@swc", "helpers"),
|
||||
destRelative: path.join("node_modules", "@swc", "helpers"),
|
||||
},
|
||||
{
|
||||
label: "pino-abstract-transport",
|
||||
sourcePath: path.join(projectRoot, "node_modules", "pino-abstract-transport"),
|
||||
destRelative: path.join("node_modules", "pino-abstract-transport"),
|
||||
},
|
||||
{
|
||||
label: "pino-pretty",
|
||||
sourcePath: path.join(projectRoot, "node_modules", "pino-pretty"),
|
||||
destRelative: path.join("node_modules", "pino-pretty"),
|
||||
},
|
||||
{
|
||||
label: "split2",
|
||||
sourcePath: path.join(projectRoot, "node_modules", "split2"),
|
||||
destRelative: path.join("node_modules", "split2"),
|
||||
},
|
||||
{
|
||||
label: "migrations",
|
||||
sourcePath: path.join(projectRoot, "src", "lib", "db", "migrations"),
|
||||
destRelative: "migrations",
|
||||
},
|
||||
{
|
||||
label: "MITM server",
|
||||
sourcePath: path.join(projectRoot, "src", "mitm", "server.cjs"),
|
||||
destRelative: path.join("src", "mitm", "server.cjs"),
|
||||
},
|
||||
{
|
||||
label: "run-standalone script",
|
||||
sourcePath: path.join(projectRoot, "scripts", "dev", "run-standalone.mjs"),
|
||||
destRelative: path.join("dev", "run-standalone.mjs"),
|
||||
},
|
||||
{
|
||||
// WS-aware wrapper that run-standalone.mjs prefers over bare server.js.
|
||||
// It installs the trusted peer-IP stamp the authz middleware needs to
|
||||
// allow loopback/LAN access to LOCAL_ONLY routes; without it the Docker
|
||||
// container fails closed (every LOCAL_ONLY request 403s). Imports
|
||||
// peer-stamp.mjs + responses-ws-proxy.mjs, so all three are co-located.
|
||||
label: "WS/peer-stamp standalone server wrapper",
|
||||
sourcePath: path.join(projectRoot, "scripts", "dev", "standalone-server-ws.mjs"),
|
||||
destRelative: "server-ws.mjs",
|
||||
},
|
||||
{
|
||||
label: "peer-stamp helper (server-ws.mjs dependency)",
|
||||
sourcePath: path.join(projectRoot, "scripts", "dev", "peer-stamp.mjs"),
|
||||
destRelative: "peer-stamp.mjs",
|
||||
},
|
||||
{
|
||||
label: "responses-ws-proxy (server-ws.mjs dependency)",
|
||||
sourcePath: path.join(projectRoot, "scripts", "dev", "responses-ws-proxy.mjs"),
|
||||
destRelative: "responses-ws-proxy.mjs",
|
||||
},
|
||||
{
|
||||
label: "runtime-env script",
|
||||
sourcePath: path.join(projectRoot, "scripts", "build", "runtime-env.mjs"),
|
||||
destRelative: path.join("build", "runtime-env.mjs"),
|
||||
},
|
||||
{
|
||||
label: "bootstrap-env script",
|
||||
sourcePath: path.join(projectRoot, "scripts", "build", "bootstrap-env.mjs"),
|
||||
destRelative: path.join("build", "bootstrap-env.mjs"),
|
||||
},
|
||||
{
|
||||
label: "healthcheck script",
|
||||
sourcePath: path.join(projectRoot, "scripts", "dev", "healthcheck.mjs"),
|
||||
destRelative: "healthcheck.mjs",
|
||||
},
|
||||
{
|
||||
label: "public directory",
|
||||
sourcePath: path.join(projectRoot, "public"),
|
||||
destRelative: "public",
|
||||
},
|
||||
{
|
||||
label: "playwright-core (dynamic import by gemini-web executor)",
|
||||
sourcePath: path.join(projectRoot, "node_modules", "playwright-core"),
|
||||
destRelative: path.join("node_modules", "playwright-core"),
|
||||
},
|
||||
{
|
||||
label: "sqlite-vec wrapper (vector memory - loaded at runtime via createRequire)",
|
||||
sourcePath: path.join(projectRoot, "node_modules", "sqlite-vec"),
|
||||
destRelative: path.join("node_modules", "sqlite-vec"),
|
||||
},
|
||||
// sqlite-vec's native vec0.so lives in a platform-specific package resolved at
|
||||
// runtime via require.resolve(). Next.js does NOT trace it into the standalone
|
||||
// (the externalized wrapper is copied, but its optional platform dep is missed -
|
||||
// Next.js #88844), so without this the bundled/Docker build silently degrades
|
||||
// vector search to FTS5: the wrapper loads but getLoadablePath() throws
|
||||
// MODULE_NOT_FOUND. Copy whichever platform package npm actually installed. See #3066.
|
||||
...[
|
||||
"sqlite-vec-linux-x64",
|
||||
"sqlite-vec-linux-arm64",
|
||||
"sqlite-vec-darwin-x64",
|
||||
"sqlite-vec-darwin-arm64",
|
||||
"sqlite-vec-windows-x64",
|
||||
].map((pkg) => ({
|
||||
label: pkg,
|
||||
sourcePath: path.join(projectRoot, "node_modules", pkg),
|
||||
destRelative: path.join("node_modules", pkg),
|
||||
})),
|
||||
];
|
||||
|
||||
let changed = false;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!(await exists(entry.sourcePath))) continue;
|
||||
for (const entry of EXTRA_MODULE_ENTRIES) {
|
||||
const sourcePath = path.join(projectRoot, ...entry.src);
|
||||
if (!(await exists(sourcePath))) continue;
|
||||
|
||||
const destPath = path.join(outDir, entry.destRelative);
|
||||
const destPath = path.join(outDir, ...entry.dest);
|
||||
const mkdir =
|
||||
typeof fsImpl.mkdir === "function" ? fsImpl.mkdir.bind(fsImpl) : fs.mkdir.bind(fs);
|
||||
await mkdir(path.dirname(destPath), { recursive: true });
|
||||
await fsImpl.cp(entry.sourcePath, destPath, { recursive: true, force: true });
|
||||
await fsImpl.cp(sourcePath, destPath, { recursive: true, force: true });
|
||||
log.log(`[assembleStandalone] Synced standalone module: ${entry.label}`);
|
||||
changed = true;
|
||||
}
|
||||
@@ -419,124 +362,21 @@ function copyStaticAndPublic({ distDir, relDistDir, projectRoot, resolvedOutDir
|
||||
* @param {string} resolvedOutDir
|
||||
*/
|
||||
function copyNativeAssetsAndExtraModules(projectRoot, resolvedOutDir) {
|
||||
const nativeAssets = [
|
||||
{
|
||||
label: "wreq-js native runtime",
|
||||
src: path.join(projectRoot, "node_modules", "wreq-js", "rust"),
|
||||
dest: path.join(resolvedOutDir, "node_modules", "wreq-js", "rust"),
|
||||
},
|
||||
{
|
||||
label: "better-sqlite3 native binary",
|
||||
src: path.join(projectRoot, "node_modules", "better-sqlite3", "build"),
|
||||
dest: path.join(resolvedOutDir, "node_modules", "better-sqlite3", "build"),
|
||||
},
|
||||
];
|
||||
|
||||
for (const asset of nativeAssets) {
|
||||
if (!fsSync.existsSync(asset.src)) continue;
|
||||
fsSync.mkdirSync(path.dirname(asset.dest), { recursive: true });
|
||||
fsSync.cpSync(asset.src, asset.dest, { recursive: true, force: true });
|
||||
for (const asset of NATIVE_ASSET_ENTRIES) {
|
||||
const src = path.join(projectRoot, ...asset.src);
|
||||
if (!fsSync.existsSync(src)) continue;
|
||||
const dest = path.join(resolvedOutDir, ...asset.dest);
|
||||
fsSync.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fsSync.cpSync(src, dest, { recursive: true, force: true });
|
||||
console.log(`[assembleStandalone] Copied native asset: ${asset.label}`);
|
||||
}
|
||||
|
||||
const extraModules = [
|
||||
{
|
||||
label: "@swc/helpers",
|
||||
src: path.join(projectRoot, "node_modules", "@swc", "helpers"),
|
||||
dest: path.join(resolvedOutDir, "node_modules", "@swc", "helpers"),
|
||||
},
|
||||
{
|
||||
label: "pino-abstract-transport",
|
||||
src: path.join(projectRoot, "node_modules", "pino-abstract-transport"),
|
||||
dest: path.join(resolvedOutDir, "node_modules", "pino-abstract-transport"),
|
||||
},
|
||||
{
|
||||
label: "pino-pretty",
|
||||
src: path.join(projectRoot, "node_modules", "pino-pretty"),
|
||||
dest: path.join(resolvedOutDir, "node_modules", "pino-pretty"),
|
||||
},
|
||||
{
|
||||
label: "split2",
|
||||
src: path.join(projectRoot, "node_modules", "split2"),
|
||||
dest: path.join(resolvedOutDir, "node_modules", "split2"),
|
||||
},
|
||||
{
|
||||
label: "migrations",
|
||||
src: path.join(projectRoot, "src", "lib", "db", "migrations"),
|
||||
dest: path.join(resolvedOutDir, "migrations"),
|
||||
},
|
||||
{
|
||||
label: "MITM server",
|
||||
src: path.join(projectRoot, "src", "mitm", "server.cjs"),
|
||||
dest: path.join(resolvedOutDir, "src", "mitm", "server.cjs"),
|
||||
},
|
||||
{
|
||||
label: "run-standalone script",
|
||||
src: path.join(projectRoot, "scripts", "dev", "run-standalone.mjs"),
|
||||
dest: path.join(resolvedOutDir, "dev", "run-standalone.mjs"),
|
||||
},
|
||||
{
|
||||
label: "WS/peer-stamp standalone server wrapper",
|
||||
src: path.join(projectRoot, "scripts", "dev", "standalone-server-ws.mjs"),
|
||||
dest: path.join(resolvedOutDir, "server-ws.mjs"),
|
||||
},
|
||||
{
|
||||
label: "peer-stamp helper",
|
||||
src: path.join(projectRoot, "scripts", "dev", "peer-stamp.mjs"),
|
||||
dest: path.join(resolvedOutDir, "peer-stamp.mjs"),
|
||||
},
|
||||
{
|
||||
label: "responses-ws-proxy",
|
||||
src: path.join(projectRoot, "scripts", "dev", "responses-ws-proxy.mjs"),
|
||||
dest: path.join(resolvedOutDir, "responses-ws-proxy.mjs"),
|
||||
},
|
||||
{
|
||||
label: "runtime-env script",
|
||||
src: path.join(projectRoot, "scripts", "build", "runtime-env.mjs"),
|
||||
dest: path.join(resolvedOutDir, "build", "runtime-env.mjs"),
|
||||
},
|
||||
{
|
||||
label: "bootstrap-env script",
|
||||
src: path.join(projectRoot, "scripts", "build", "bootstrap-env.mjs"),
|
||||
dest: path.join(resolvedOutDir, "build", "bootstrap-env.mjs"),
|
||||
},
|
||||
{
|
||||
label: "healthcheck script",
|
||||
src: path.join(projectRoot, "scripts", "dev", "healthcheck.mjs"),
|
||||
dest: path.join(resolvedOutDir, "healthcheck.mjs"),
|
||||
},
|
||||
{
|
||||
label: "public directory",
|
||||
src: path.join(projectRoot, "public"),
|
||||
dest: path.join(resolvedOutDir, "public"),
|
||||
},
|
||||
{
|
||||
label: "playwright-core",
|
||||
src: path.join(projectRoot, "node_modules", "playwright-core"),
|
||||
dest: path.join(resolvedOutDir, "node_modules", "playwright-core"),
|
||||
},
|
||||
{
|
||||
label: "sqlite-vec wrapper",
|
||||
src: path.join(projectRoot, "node_modules", "sqlite-vec"),
|
||||
dest: path.join(resolvedOutDir, "node_modules", "sqlite-vec"),
|
||||
},
|
||||
...[
|
||||
"sqlite-vec-linux-x64",
|
||||
"sqlite-vec-linux-arm64",
|
||||
"sqlite-vec-darwin-x64",
|
||||
"sqlite-vec-darwin-arm64",
|
||||
"sqlite-vec-windows-x64",
|
||||
].map((pkg) => ({
|
||||
label: pkg,
|
||||
src: path.join(projectRoot, "node_modules", pkg),
|
||||
dest: path.join(resolvedOutDir, "node_modules", pkg),
|
||||
})),
|
||||
];
|
||||
|
||||
for (const mod of extraModules) {
|
||||
if (!fsSync.existsSync(mod.src)) continue;
|
||||
fsSync.mkdirSync(path.dirname(mod.dest), { recursive: true });
|
||||
fsSync.cpSync(mod.src, mod.dest, { recursive: true, force: true });
|
||||
for (const mod of EXTRA_MODULE_ENTRIES) {
|
||||
const src = path.join(projectRoot, ...mod.src);
|
||||
if (!fsSync.existsSync(src)) continue;
|
||||
const dest = path.join(resolvedOutDir, ...mod.dest);
|
||||
fsSync.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fsSync.cpSync(src, dest, { recursive: true, force: true });
|
||||
console.log(`[assembleStandalone] Synced module: ${mod.label}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,60 @@ import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { assembleStandalone } from "../../../scripts/build/assembleStandalone.mjs";
|
||||
import {
|
||||
assembleStandalone,
|
||||
syncStandaloneNativeAssets,
|
||||
syncStandaloneExtraModules,
|
||||
} from "../../../scripts/build/assembleStandalone.mjs";
|
||||
|
||||
/** Recursively list relative file paths under dir (forward-slash normalised). */
|
||||
function listFiles(dir: string, rootDir: string = dir, out: string[] = []): string[] {
|
||||
let entries: string[] = [];
|
||||
try {
|
||||
entries = fs.readdirSync(dir);
|
||||
} catch {
|
||||
return out;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry);
|
||||
if (fs.statSync(full).isDirectory()) {
|
||||
listFiles(full, rootDir, out);
|
||||
} else {
|
||||
out.push(path.relative(rootDir, full).replace(/\\/g, "/"));
|
||||
}
|
||||
}
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
/** Build a synthetic projectRoot containing every sidecar source the assembler copies. */
|
||||
function seedSidecarSources(root: string) {
|
||||
const files = [
|
||||
"node_modules/wreq-js/rust/lib.so",
|
||||
"node_modules/better-sqlite3/build/Release/better_sqlite3.node",
|
||||
"node_modules/@swc/helpers/package.json",
|
||||
"node_modules/pino-abstract-transport/index.js",
|
||||
"node_modules/pino-pretty/index.js",
|
||||
"node_modules/split2/index.js",
|
||||
"node_modules/playwright-core/index.js",
|
||||
"node_modules/sqlite-vec/index.js",
|
||||
"node_modules/sqlite-vec-linux-x64/vec0.so",
|
||||
"src/lib/db/migrations/001_init.sql",
|
||||
"src/mitm/server.cjs",
|
||||
"scripts/dev/run-standalone.mjs",
|
||||
"scripts/dev/standalone-server-ws.mjs",
|
||||
"scripts/dev/peer-stamp.mjs",
|
||||
"scripts/dev/responses-ws-proxy.mjs",
|
||||
"scripts/build/runtime-env.mjs",
|
||||
"scripts/build/bootstrap-env.mjs",
|
||||
"scripts/dev/healthcheck.mjs",
|
||||
"public/logo.svg",
|
||||
];
|
||||
for (const rel of files) {
|
||||
const full = path.join(root, rel);
|
||||
fs.mkdirSync(path.dirname(full), { recursive: true });
|
||||
fs.writeFileSync(full, `// ${rel}`);
|
||||
}
|
||||
}
|
||||
|
||||
test("assembleStandalone copies standalone + static + public + sidecars into outDir", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "assemble-"));
|
||||
@@ -33,3 +86,47 @@ test("assembleStandalone copies standalone + static + public + sidecars into out
|
||||
assert.ok(fs.existsSync(path.join(outDir, "public/logo.svg")), "public copied");
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// Drift guard: the async path (syncStandaloneNativeAssets / syncStandaloneExtraModules,
|
||||
// used by build-next-isolated) and the sync path (assembleStandalone copyNatives, used by
|
||||
// prepublish/electron) must copy the SAME sidecar tree. After the single-source refactor
|
||||
// both derive from NATIVE_ASSET_ENTRIES/EXTRA_MODULE_ENTRIES — this test fails if a future
|
||||
// edit reintroduces two divergent lists.
|
||||
test("async and sync sidecar copy paths produce identical bundle trees", async () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "assemble-parity-"));
|
||||
const projectRoot = path.join(tmp, "src-root");
|
||||
seedSidecarSources(projectRoot);
|
||||
|
||||
// Async path: copy into outAsync via the exported sync* helpers.
|
||||
const outAsync = path.join(tmp, "out-async");
|
||||
fs.mkdirSync(outAsync, { recursive: true });
|
||||
const silent = { log() {} };
|
||||
// The exported helpers derive the standalone root from NEXT_DIST_DIR; point them at outAsync.
|
||||
await syncStandaloneNativeAssets(projectRoot, fs.promises, silent, outAsync);
|
||||
await syncStandaloneExtraModules(projectRoot, fs.promises, silent, outAsync);
|
||||
|
||||
// Sync path: assembleStandalone(copyNatives) needs a standalone dir to copy first.
|
||||
const distDir = path.join(projectRoot, ".build/next");
|
||||
fs.mkdirSync(path.join(distDir, "standalone"), { recursive: true });
|
||||
fs.writeFileSync(path.join(distDir, "standalone", "server.js"), "// server");
|
||||
const outSync = path.join(tmp, "out-sync");
|
||||
assembleStandalone({
|
||||
distDir,
|
||||
outDir: outSync,
|
||||
projectRoot,
|
||||
sanitizePaths: false,
|
||||
copyNatives: true,
|
||||
});
|
||||
|
||||
const asyncTree = listFiles(outAsync);
|
||||
// The sync path also copies the standalone server.js + patches package.json; compare only
|
||||
// the sidecar files the two paths share (drop server.js which is unique to assembleStandalone).
|
||||
const syncTree = listFiles(outSync).filter((f) => f !== "server.js");
|
||||
|
||||
assert.deepEqual(
|
||||
syncTree,
|
||||
asyncTree,
|
||||
"sync (assembleStandalone) and async (sync*ToDir) must copy the same sidecar tree"
|
||||
);
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user