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:
@@ -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/core-cms", () => ({ default: Promise.resolve({}) }));
|
||||||
vi.mock("@repo/blog/di/bind-production", () => ({ bindProductionBlog: vi.fn() }));
|
vi.mock("@repo/blog/di/bind-production", () => ({ bindProductionBlog: vi.fn() }));
|
||||||
vi.mock("@repo/auth/di/bind-production", () => ({ bindProductionAuth: 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/marketing-pages/di/bind-production", () => ({ bindProductionMarketingPages: vi.fn() }));
|
||||||
vi.mock("@repo/navigation/di/bind-production", () => ({ bindProductionNavigation: 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", () => {
|
describe("bindAllProduction", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -12,12 +21,13 @@ describe("bindAllProduction", () => {
|
|||||||
vi.clearAllMocks();
|
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 { bindAllProduction } = await import("./bind-production");
|
||||||
const { bindProductionBlog } = await import("@repo/blog/di/bind-production");
|
const { bindProductionBlog } = await import("@repo/blog/di/bind-production");
|
||||||
const { bindProductionAuth } = await import("@repo/auth/di/bind-production");
|
const { bindProductionAuth } = await import("@repo/auth/di/bind-production");
|
||||||
const { bindProductionMarketingPages } = await import("@repo/marketing-pages/di/bind-production");
|
const { bindProductionMarketingPages } = await import("@repo/marketing-pages/di/bind-production");
|
||||||
const { bindProductionNavigation } = await import("@repo/navigation/di/bind-production");
|
const { bindProductionNavigation } = await import("@repo/navigation/di/bind-production");
|
||||||
|
const { bindProductionMedia } = await import("@repo/media/di/bind-production");
|
||||||
|
|
||||||
await bindAllProduction();
|
await bindAllProduction();
|
||||||
|
|
||||||
@@ -25,6 +35,7 @@ describe("bindAllProduction", () => {
|
|||||||
expect(bindProductionAuth).toHaveBeenCalledOnce();
|
expect(bindProductionAuth).toHaveBeenCalledOnce();
|
||||||
expect(bindProductionMarketingPages).toHaveBeenCalledOnce();
|
expect(bindProductionMarketingPages).toHaveBeenCalledOnce();
|
||||||
expect(bindProductionNavigation).toHaveBeenCalledOnce();
|
expect(bindProductionNavigation).toHaveBeenCalledOnce();
|
||||||
|
expect(bindProductionMedia).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is idempotent — second call does not re-bind", async () => {
|
it("is idempotent — second call does not re-bind", async () => {
|
||||||
@@ -35,3 +46,86 @@ describe("bindAllProduction", () => {
|
|||||||
expect(bindProductionBlog).toHaveBeenCalledOnce();
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -46,16 +46,34 @@ export async function bindAllDevSeed(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boot dispatcher: pick the binder based on `process.env.USE_DEV_SEED`.
|
* Boot dispatcher: pick the binder based on the environment.
|
||||||
* Idempotent — guarded by the same `bound` flag as `bindAllProduction`.
|
|
||||||
*
|
*
|
||||||
* Call this from server entry points (route handlers, page server components)
|
* Resolution order (first match wins):
|
||||||
* before resolving any feature controller.
|
*
|
||||||
|
* 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> {
|
export async function bindAll(): Promise<void> {
|
||||||
if (process.env.USE_DEV_SEED === "true") {
|
if (process.env.USE_DEV_SEED === "true") {
|
||||||
await bindAllDevSeed();
|
await bindAllDevSeed();
|
||||||
return;
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -867,11 +867,18 @@ footer .colophon {
|
|||||||
<p>The web app's server entry point runs <code>bindAll()</code>, which checks <code>process.env.USE_DEV_SEED</code> and dispatches to either <code>bindAllProduction()</code> or <code>bindAllDevSeed()</code>. Each calls every feature's binder.</p>
|
<p>The web app's server entry point runs <code>bindAll()</code>, which checks <code>process.env.USE_DEV_SEED</code> and dispatches to either <code>bindAllProduction()</code> or <code>bindAllDevSeed()</code>. Each calls every feature's binder.</p>
|
||||||
</div>
|
</div>
|
||||||
<pre class="code" data-lang="typescript // apps/web-next/src/server/bind-production.ts"><span class="k">export async function</span> <span class="n">bindAll</span>(): <span class="t">Promise</span><<span class="t">void</span>> {
|
<pre class="code" data-lang="typescript // apps/web-next/src/server/bind-production.ts"><span class="k">export async function</span> <span class="n">bindAll</span>(): <span class="t">Promise</span><<span class="t">void</span>> {
|
||||||
|
<span class="c">// 1. Explicit override wins, regardless of NODE_ENV.</span>
|
||||||
<span class="k">if</span> (<span class="n">process</span>.<span class="n">env</span>.<span class="n">USE_DEV_SEED</span> === <span class="s">"true"</span>) {
|
<span class="k">if</span> (<span class="n">process</span>.<span class="n">env</span>.<span class="n">USE_DEV_SEED</span> === <span class="s">"true"</span>) {
|
||||||
<span class="k">await</span> <span class="n">bindAllDevSeed</span>();
|
<span class="k">await</span> <span class="n">bindAllDevSeed</span>();
|
||||||
<span class="k">return</span>;
|
<span class="k">return</span>;
|
||||||
}
|
}
|
||||||
<span class="k">await</span> <span class="n">bindAllProduction</span>();
|
<span class="c">// 2. Production env → real Payload.</span>
|
||||||
|
<span class="k">if</span> (<span class="n">process</span>.<span class="n">env</span>.<span class="n">NODE_ENV</span> === <span class="s">"production"</span>) {
|
||||||
|
<span class="k">await</span> <span class="n">bindAllProduction</span>();
|
||||||
|
<span class="k">return</span>;
|
||||||
|
}
|
||||||
|
<span class="c">// 3. Default: dev seed, so `pnpm dev` boots without Payload.</span>
|
||||||
|
<span class="k">await</span> <span class="n">bindAllDevSeed</span>();
|
||||||
}</pre>
|
}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1032,15 +1039,20 @@ footer .colophon {
|
|||||||
<div class="outcome">default · empty mock</div>
|
<div class="outcome">default · empty mock</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="conditions-row">
|
<div class="conditions-row">
|
||||||
<div class="scenario">Server (any env) with USE_DEV_SEED=true</div>
|
<div class="scenario">Server (any env) with USE_DEV_SEED=true · explicit override wins</div>
|
||||||
<div class="trigger">bindAll() → bindAllDevSeed()</div>
|
<div class="trigger">bindAll() → bindAllDevSeed()</div>
|
||||||
<div class="outcome">dev seed · populated mock</div>
|
<div class="outcome">dev seed · populated mock</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="conditions-row">
|
<div class="conditions-row">
|
||||||
<div class="scenario">Server (any env) with USE_DEV_SEED unset / not "true"</div>
|
<div class="scenario">NODE_ENV=production, USE_DEV_SEED unset</div>
|
||||||
<div class="trigger">bindAll() → bindAllProduction(config)</div>
|
<div class="trigger">bindAll() → bindAllProduction(config)</div>
|
||||||
<div class="outcome">production · real Payload</div>
|
<div class="outcome">production · real Payload</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="conditions-row">
|
||||||
|
<div class="scenario">pnpm dev (NODE_ENV=development or unset), no env flag</div>
|
||||||
|
<div class="trigger">bindAll() → bindAllDevSeed() (default)</div>
|
||||||
|
<div class="outcome">dev seed · populated mock</div>
|
||||||
|
</div>
|
||||||
<div class="conditions-row">
|
<div class="conditions-row">
|
||||||
<div class="scenario">Storybook story that wants populated data</div>
|
<div class="scenario">Storybook story that wants populated data</div>
|
||||||
<div class="trigger">await bindDevSeedBlog()</div>
|
<div class="trigger">await bindDevSeedBlog()</div>
|
||||||
@@ -1172,7 +1184,7 @@ const MODES = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
seed: {
|
seed: {
|
||||||
flag: 'USE_DEV_SEED === "true" · bindAllDevSeed() ran',
|
flag: 'USE_DEV_SEED="true" OR NODE_ENV ≠ "production" (default) · bindAllDevSeed() ran',
|
||||||
scenarioTag: 'when this happens',
|
scenarioTag: 'when this happens',
|
||||||
title: 'Dev seed binder ran at app boot',
|
title: 'Dev seed binder ran at app boot',
|
||||||
narrative: [
|
narrative: [
|
||||||
@@ -1191,7 +1203,7 @@ const MODES = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
prod: {
|
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',
|
scenarioTag: 'when this happens',
|
||||||
title: 'Production binder ran at app boot',
|
title: 'Production binder ran at app boot',
|
||||||
narrative: [
|
narrative: [
|
||||||
|
|||||||
Reference in New Issue
Block a user