feat(app): bindAll() now checks NODE_ENV in addition to USE_DEV_SEED

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.
This commit is contained in:
2026-05-06 19:32:00 +02:00
parent 61dde18b53
commit 6bf19f35c5
3 changed files with 136 additions and 12 deletions

View File

@@ -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();
});
});

View File

@@ -46,16 +46,34 @@ export async function bindAllDevSeed(): Promise<void> {
}
/**
* 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<void> {
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();
}