From 6bf19f35c5d78f75f57a607fdf72b3007d221861 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 6 May 2026 19:32:00 +0200 Subject: [PATCH] feat(app): bindAll() now checks NODE_ENV in addition to USE_DEV_SEED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-rule resolution order in bindAll() (first match wins): 1. USE_DEV_SEED === 'true' → bindAllDevSeed (explicit override) 2. NODE_ENV === 'production' → bindAllProduction 3. otherwise → bindAllDevSeed (developer default) Rationale: 'pnpm dev' should boot the app without requiring Payload to be running locally — dev seed is the more useful default for non- production environments. Production servers explicitly set NODE_ENV=production and get the real binding. The USE_DEV_SEED override remains the escape hatch (force seed in any NODE_ENV — e.g. staging preview, design review). bind-production.test.ts grows from 3 tests to 8 — covers the dispatcher matrix: - USE_DEV_SEED='true' wins even when NODE_ENV='production' - NODE_ENV='production' (no override) → production - NODE_ENV='development' → dev seed (default) - NODE_ENV unset → dev seed (default) - USE_DEV_SEED='false' treated as not-set (only the literal 'true' triggers) - Pre-existing 'binds all five repos' test now also asserts bindProductionMedia di-explainer.html conditions table + mode flag strings updated to match the new three-rule logic. --- .../src/server/bind-production.test.ts | 98 ++++++++++++++++++- apps/web-next/src/server/bind-production.ts | 28 +++++- docs/architecture/di-explainer.html | 22 ++++- 3 files changed, 136 insertions(+), 12 deletions(-) diff --git a/apps/web-next/src/server/bind-production.test.ts b/apps/web-next/src/server/bind-production.test.ts index 0722b49..1bf8e2c 100644 --- a/apps/web-next/src/server/bind-production.test.ts +++ b/apps/web-next/src/server/bind-production.test.ts @@ -1,10 +1,19 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; vi.mock("@repo/core-cms", () => ({ default: Promise.resolve({}) })); vi.mock("@repo/blog/di/bind-production", () => ({ bindProductionBlog: vi.fn() })); vi.mock("@repo/auth/di/bind-production", () => ({ bindProductionAuth: vi.fn() })); vi.mock("@repo/marketing-pages/di/bind-production", () => ({ bindProductionMarketingPages: vi.fn() })); vi.mock("@repo/navigation/di/bind-production", () => ({ bindProductionNavigation: vi.fn() })); +vi.mock("@repo/media/di/bind-production", () => ({ bindProductionMedia: vi.fn() })); +vi.mock("@repo/blog/di/bind-dev-seed", () => ({ bindDevSeedBlog: vi.fn() })); +vi.mock("@repo/auth/di/bind-dev-seed", () => ({ bindDevSeedAuth: vi.fn() })); +vi.mock("@repo/marketing-pages/di/bind-dev-seed", () => ({ bindDevSeedMarketingPages: vi.fn() })); +vi.mock("@repo/navigation/di/bind-dev-seed", () => ({ bindDevSeedNavigation: vi.fn() })); +vi.mock("@repo/media/di/bind-dev-seed", () => ({ bindDevSeedMedia: vi.fn() })); + +const ORIGINAL_USE_DEV_SEED = process.env.USE_DEV_SEED; +const ORIGINAL_NODE_ENV = process.env.NODE_ENV; describe("bindAllProduction", () => { beforeEach(() => { @@ -12,12 +21,13 @@ describe("bindAllProduction", () => { vi.clearAllMocks(); }); - it("binds all four feature production repos", async () => { + it("binds all five feature production repos", async () => { const { bindAllProduction } = await import("./bind-production"); const { bindProductionBlog } = await import("@repo/blog/di/bind-production"); const { bindProductionAuth } = await import("@repo/auth/di/bind-production"); const { bindProductionMarketingPages } = await import("@repo/marketing-pages/di/bind-production"); const { bindProductionNavigation } = await import("@repo/navigation/di/bind-production"); + const { bindProductionMedia } = await import("@repo/media/di/bind-production"); await bindAllProduction(); @@ -25,6 +35,7 @@ describe("bindAllProduction", () => { expect(bindProductionAuth).toHaveBeenCalledOnce(); expect(bindProductionMarketingPages).toHaveBeenCalledOnce(); expect(bindProductionNavigation).toHaveBeenCalledOnce(); + expect(bindProductionMedia).toHaveBeenCalledOnce(); }); it("is idempotent — second call does not re-bind", async () => { @@ -35,3 +46,86 @@ describe("bindAllProduction", () => { expect(bindProductionBlog).toHaveBeenCalledOnce(); }); }); + +describe("bindAll dispatcher", () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + delete process.env.USE_DEV_SEED; + delete process.env.NODE_ENV; + }); + + afterEach(() => { + if (ORIGINAL_USE_DEV_SEED === undefined) { + delete process.env.USE_DEV_SEED; + } else { + process.env.USE_DEV_SEED = ORIGINAL_USE_DEV_SEED; + } + if (ORIGINAL_NODE_ENV === undefined) { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = ORIGINAL_NODE_ENV; + } + }); + + it("USE_DEV_SEED='true' wins → dispatches to bindAllDevSeed", async () => { + process.env.USE_DEV_SEED = "true"; + process.env.NODE_ENV = "production"; // even in production, dev seed wins + const { bindAll } = await import("./bind-production"); + const { bindDevSeedBlog } = await import("@repo/blog/di/bind-dev-seed"); + const { bindProductionBlog } = await import("@repo/blog/di/bind-production"); + + await bindAll(); + + expect(bindDevSeedBlog).toHaveBeenCalledOnce(); + expect(bindProductionBlog).not.toHaveBeenCalled(); + }); + + it("NODE_ENV='production' (no override) → dispatches to bindAllProduction", async () => { + process.env.NODE_ENV = "production"; + const { bindAll } = await import("./bind-production"); + const { bindProductionBlog } = await import("@repo/blog/di/bind-production"); + const { bindDevSeedBlog } = await import("@repo/blog/di/bind-dev-seed"); + + await bindAll(); + + expect(bindProductionBlog).toHaveBeenCalledOnce(); + expect(bindDevSeedBlog).not.toHaveBeenCalled(); + }); + + it("NODE_ENV='development' → dispatches to bindAllDevSeed (developer default)", async () => { + process.env.NODE_ENV = "development"; + const { bindAll } = await import("./bind-production"); + const { bindDevSeedBlog } = await import("@repo/blog/di/bind-dev-seed"); + const { bindProductionBlog } = await import("@repo/blog/di/bind-production"); + + await bindAll(); + + expect(bindDevSeedBlog).toHaveBeenCalledOnce(); + expect(bindProductionBlog).not.toHaveBeenCalled(); + }); + + it("NODE_ENV unset → dispatches to bindAllDevSeed (developer default)", async () => { + const { bindAll } = await import("./bind-production"); + const { bindDevSeedBlog } = await import("@repo/blog/di/bind-dev-seed"); + const { bindProductionBlog } = await import("@repo/blog/di/bind-production"); + + await bindAll(); + + expect(bindDevSeedBlog).toHaveBeenCalledOnce(); + expect(bindProductionBlog).not.toHaveBeenCalled(); + }); + + it("USE_DEV_SEED='false' is treated as not-set (only 'true' triggers dev seed)", async () => { + process.env.USE_DEV_SEED = "false"; + process.env.NODE_ENV = "production"; + const { bindAll } = await import("./bind-production"); + const { bindProductionBlog } = await import("@repo/blog/di/bind-production"); + const { bindDevSeedBlog } = await import("@repo/blog/di/bind-dev-seed"); + + await bindAll(); + + expect(bindProductionBlog).toHaveBeenCalledOnce(); + expect(bindDevSeedBlog).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web-next/src/server/bind-production.ts b/apps/web-next/src/server/bind-production.ts index a946f26..3d3bc11 100644 --- a/apps/web-next/src/server/bind-production.ts +++ b/apps/web-next/src/server/bind-production.ts @@ -46,16 +46,34 @@ export async function bindAllDevSeed(): Promise { } /** - * Boot dispatcher: pick the binder based on `process.env.USE_DEV_SEED`. - * Idempotent — guarded by the same `bound` flag as `bindAllProduction`. + * Boot dispatcher: pick the binder based on the environment. * - * Call this from server entry points (route handlers, page server components) - * before resolving any feature controller. + * Resolution order (first match wins): + * + * 1. `USE_DEV_SEED === "true"` → dev seed (explicit override; works in + * any NODE_ENV — useful for staging, + * storybook, design review). + * 2. `NODE_ENV === "production"` → real Payload via bindAllProduction(config). + * 3. otherwise → dev seed (developer-friendly default; + * `pnpm dev` works without Payload booted). + * + * To force production locally without changing NODE_ENV, set + * `USE_DEV_SEED=false` is NOT enough — set `NODE_ENV=production` instead. + * + * Idempotent: guarded by the same `bound` flag the underlying binders share. + * Call from server entry points (route handlers, page server components, + * tRPC route handlers) before resolving any feature controller. */ export async function bindAll(): Promise { if (process.env.USE_DEV_SEED === "true") { await bindAllDevSeed(); return; } - await bindAllProduction(); + if (process.env.NODE_ENV === "production") { + await bindAllProduction(); + return; + } + // Default for dev / test / unset NODE_ENV: dev seed so the app boots + // without Payload running. + await bindAllDevSeed(); } diff --git a/docs/architecture/di-explainer.html b/docs/architecture/di-explainer.html index 8c602fe..2f8c80e 100644 --- a/docs/architecture/di-explainer.html +++ b/docs/architecture/di-explainer.html @@ -867,11 +867,18 @@ footer .colophon {

The web app's server entry point runs bindAll(), which checks process.env.USE_DEV_SEED and dispatches to either bindAllProduction() or bindAllDevSeed(). Each calls every feature's binder.

export async function bindAll(): Promise<void> {
+  // 1. Explicit override wins, regardless of NODE_ENV.
   if (process.env.USE_DEV_SEED === "true") {
     await bindAllDevSeed();
     return;
   }
-  await bindAllProduction();
+  // 2. Production env → real Payload.
+  if (process.env.NODE_ENV === "production") {
+    await bindAllProduction();
+    return;
+  }
+  // 3. Default: dev seed, so `pnpm dev` boots without Payload.
+  await bindAllDevSeed();
 }
@@ -1032,15 +1039,20 @@ footer .colophon {
default · empty mock
-
Server (any env) with USE_DEV_SEED=true
+
Server (any env) with USE_DEV_SEED=true · explicit override wins
bindAll() → bindAllDevSeed()
dev seed · populated mock
-
Server (any env) with USE_DEV_SEED unset / not "true"
+
NODE_ENV=production, USE_DEV_SEED unset
bindAll() → bindAllProduction(config)
production · real Payload
+
+
pnpm dev (NODE_ENV=development or unset), no env flag
+
bindAll() → bindAllDevSeed() (default)
+
dev seed · populated mock
+
Storybook story that wants populated data
await bindDevSeedBlog()
@@ -1172,7 +1184,7 @@ const MODES = { ], }, seed: { - flag: 'USE_DEV_SEED === "true" · bindAllDevSeed() ran', + flag: 'USE_DEV_SEED="true" OR NODE_ENV ≠ "production" (default) · bindAllDevSeed() ran', scenarioTag: 'when this happens', title: 'Dev seed binder ran at app boot', narrative: [ @@ -1191,7 +1203,7 @@ const MODES = { ], }, prod: { - flag: 'USE_DEV_SEED unset or not "true" · bindAllProduction(config) ran', + flag: 'NODE_ENV="production", USE_DEV_SEED unset · bindAllProduction(config) ran', scenarioTag: 'when this happens', title: 'Production binder ran at app boot', narrative: [