fix(build): copy ioredis and bcryptjs into the standalone bundle (#13352)

Both packages are only reachable through code paths the standalone
tracer never follows, so they get silently dropped from the built
node_modules/:

- ioredis is a deliberately lazy dependency (#6559 in
  rateLimiter.ts) — reached only via a runtime `await
  import("ioredis")` in rateLimiter.ts,
  warmupScheduler/circuitBreakerFactory.ts and
  quota/redisQuotaStore.ts, never through a static top-level import.
  Any self-hosted deployment that sets REDIS_URL crashes on first use
  with "Cannot find module 'ioredis'".

- bcryptjs is statically imported by
  src/lib/auth/managementPassword.ts, so the main server bundle is
  fine (Next inlines the small pure-JS package into the compiled
  chunk). bin/cli/settings-store.mjs (the `omniroute reset-password`
  CLI) is a separate, unbundled entrypoint that needs the real
  package physically present in node_modules/ — nothing else
  requires it as a loose runtime dependency, so it was never copied.
  `node bin/reset-password.mjs --password-stdin` failed with
  "Cannot find package 'bcryptjs'" (ERR_MODULE_NOT_FOUND) on an
  otherwise healthy production deployment.

Both reproduced on a real self-hosted Docker deployment (v3.8.49/51).
Adds the two entries to EXTRA_MODULE_ENTRIES (the single source of
truth cited in the Dockerfile) and extends the sync/async parity
test with fixtures + assertions for both.
This commit is contained in:
Felipe Britto
2026-09-18 11:31:10 -03:00
committed by GitHub
parent 53a147c7ca
commit 138ccf2d04
2 changed files with 54 additions and 0 deletions

View File

@@ -163,6 +163,43 @@ const EXTRA_MODULE_ENTRIES = [
dest: ["node_modules", "pino-pretty"],
},
{ label: "split2", src: ["node_modules", "split2"], dest: ["node_modules", "split2"] },
{
// ioredis is a deliberately LAZY dependency (Redis is optional — see the
// #6559 comment in src/shared/utils/rateLimiter.ts) — reached only via a
// runtime `await import("ioredis")` in rateLimiter.ts,
// warmupScheduler/circuitBreakerFactory.ts and quota/redisQuotaStore.ts,
// never through a static top-level import. The standalone tracer only
// follows statically-analyzable imports, so it never sees these call
// sites and drops ioredis from node_modules/ entirely. Any self-hosted
// deployment that actually sets REDIS_URL crashes the first time it
// reaches one of those call sites with "Cannot find module 'ioredis'" —
// reproduced on a production Docker deployment (REDIS_URL configured,
// v3.8.49) where the standalone image shipped ioredis/package.json but
// none of its own dependencies or built/ output.
label: "ioredis (dynamic import — #6559)",
src: ["node_modules", "ioredis"],
dest: ["node_modules", "ioredis"],
},
{
// bcryptjs IS statically imported by src/lib/auth/managementPassword.ts,
// so the main server bundle is fine — Next's server compiler inlines the
// small pure-JS package directly into the compiled route chunk instead of
// leaving it as an external node_modules dependency. bin/cli/settings-
// store.mjs (the `omniroute reset-password` / bin/reset-password.mjs
// CLI, used to recover a lost dashboard password) is a separate,
// unbundled entrypoint that does a plain runtime `import bcrypt from
// "bcryptjs"` and needs the real package physically present in
// node_modules/ — which nothing else requires as a loose runtime
// dependency, so it is never copied. Reproduced on a production
// deployment: `node bin/reset-password.mjs --password-stdin` failed with
// "Cannot find package 'bcryptjs' imported from
// /app/bin/cli/settings-store.mjs" (ERR_MODULE_NOT_FOUND) even though the
// same container's dashboard login (which also depends on bcryptjs) was
// working normally.
label: "bcryptjs (bin/cli/settings-store.mjs — reset-password CLI)",
src: ["node_modules", "bcryptjs"],
dest: ["node_modules", "bcryptjs"],
},
{ label: "migrations", src: ["src", "lib", "db", "migrations"], dest: ["migrations"] },
{ label: "MITM server", src: ["src", "mitm", "server.cjs"], dest: ["src", "mitm", "server.cjs"] },
{

View File

@@ -40,6 +40,10 @@ function seedSidecarSources(root: string) {
"node_modules/pino-abstract-transport/index.js",
"node_modules/pino-pretty/index.js",
"node_modules/split2/index.js",
"node_modules/ioredis/package.json",
"node_modules/ioredis/built/index.js",
"node_modules/bcryptjs/package.json",
"node_modules/bcryptjs/index.js",
"node_modules/playwright-core/index.js",
"node_modules/sql.js/package.json",
"node_modules/sql.js/dist/sql-wasm.js",
@@ -173,6 +177,19 @@ test("async and sync sidecar copy paths produce identical bundle trees", async (
]) {
assert.ok(asyncTree.includes(sqlJsFile), `sql.js runtime file copied: ${sqlJsFile}`);
}
// #6559 / reset-password CLI: both are only reachable at runtime (a dynamic
// `import("ioredis")`, or a separate unbundled bin/cli/ entrypoint for
// bcryptjs), so the standalone tracer never picks them up on its own —
// regression guard for the two "Cannot find module/package" crashes
// reproduced on a real self-hosted deployment.
for (const runtimeOnlyFile of [
"node_modules/ioredis/package.json",
"node_modules/ioredis/built/index.js",
"node_modules/bcryptjs/package.json",
"node_modules/bcryptjs/index.js",
]) {
assert.ok(asyncTree.includes(runtimeOnlyFile), `runtime-only dep copied: ${runtimeOnlyFile}`);
}
fs.rmSync(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});