Skip to content

Commit 29fc282

Browse files
committed
feat(showcase): add shell-dojo runtime-config server+client
Mirror of the runtime-config pattern from shell-dashboard for shell-dojo (B7). shell-dojo has no URL consumers today (B0 audit reported zero process.env.NEXT_PUBLIC_* reads), so RuntimeConfig is an empty object literal. Module exists to keep the runtime-config / layout-injection pattern symmetric across all four shells; adding a URL later is a single field addition. - src/lib/runtime-config.ts: server reader with unstable_noStore() opt-out (Node) and noStore-skip option (Edge wrapper not needed yet). - src/lib/runtime-config.client.ts: client reader from window.__SHOWCASE_CONFIG__ injected by root layout. No tests included for shell-dojo: matches B7 file list (no test files listed for shell-dojo) and reflects that there is no behavior to assert on an empty config beyond the type contract.
1 parent 8b02ebb commit 29fc282

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Client-side runtime config reader. Reads from
2+
// window.__SHOWCASE_CONFIG__ which the root layout injects via an
3+
// inline <script> tag BEFORE React hydrates (see app/layout.tsx). This
4+
// is the ONLY public API for these URLs in client code — never read
5+
// process.env.NEXT_PUBLIC_* directly (the ESLint rule in B12 enforces
6+
// this).
7+
//
8+
// shell-dojo's `RuntimeConfig` is intentionally empty today (no URL
9+
// consumers); the module exists to keep the pattern symmetric across
10+
// shells. When a URL is added on the server side, this reader picks it
11+
// up via the shared interface.
12+
13+
import type { RuntimeConfig } from "./runtime-config";
14+
15+
export type { RuntimeConfig };
16+
17+
declare global {
18+
interface Window {
19+
__SHOWCASE_CONFIG__?: RuntimeConfig;
20+
}
21+
}
22+
23+
/**
24+
* Returns the runtime config injected by the root server layout.
25+
*
26+
* Throws when called during SSR (no window). The intended call sites
27+
* are client components and hooks ("use client"), which only execute
28+
* after hydration — by which point the inline <script> has populated
29+
* window.__SHOWCASE_CONFIG__. If a server component needs the same
30+
* values, it MUST import runtime-config.ts (the server variant) — not
31+
* this file.
32+
*/
33+
export function getRuntimeConfig(): RuntimeConfig {
34+
if (typeof window === "undefined") {
35+
throw new Error(
36+
"[runtime-config.client] getRuntimeConfig() called on the server. " +
37+
"Server code must import from './runtime-config' instead.",
38+
);
39+
}
40+
const cfg = window.__SHOWCASE_CONFIG__;
41+
if (!cfg) {
42+
// The root layout always emits the <script> tag, so a missing
43+
// value here is a wiring bug (e.g. a route bypassed the layout,
44+
// or the injection script ran with empty inputs). Surface it
45+
// loudly rather than silently returning empty strings.
46+
throw new Error(
47+
"[runtime-config.client] window.__SHOWCASE_CONFIG__ is missing. " +
48+
"The root layout must inject runtime config before client mount.",
49+
);
50+
}
51+
return cfg;
52+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Server-only runtime config reader. Reads from process.env at REQUEST
2+
// time (not at module load) so a single built artifact can serve
3+
// different URL values across staging vs prod by changing the Railway
4+
// service's env vars — no rebuild required.
5+
//
6+
// `unstable_noStore()` opts the calling segment out of Next.js's static
7+
// cache so reads always reflect the live env. Without it, a server
8+
// component that uses this could be statically rendered at build time
9+
// and freeze the URLs back into the artifact (the exact bug we are
10+
// fixing). See Next.js App Router docs on Dynamic Rendering.
11+
//
12+
// This module MUST NOT be imported from client components. The matching
13+
// client-side reader lives in runtime-config.client.ts and reads from
14+
// window.__SHOWCASE_CONFIG__ which the root layout injects.
15+
//
16+
// shell-dojo today has no URL consumers (audit B0 reported zero
17+
// process.env.NEXT_PUBLIC_* reads). The `RuntimeConfig` interface is
18+
// intentionally an empty object literal — it exists to keep the
19+
// runtime-config / layout-injection pattern symmetric across all four
20+
// shells. Adding a URL later means adding a field here, reading it
21+
// from process.env via `readUrl`, and the client-side reader picks it
22+
// up automatically through the shared interface.
23+
24+
import { unstable_noStore as noStore } from "next/cache";
25+
26+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
27+
export interface RuntimeConfig {}
28+
29+
/**
30+
* Resolve the runtime config for shell-dojo. Called once per request
31+
* by the root layout. Currently returns an empty object since
32+
* shell-dojo has no URL-dependent consumers; the module exists to keep
33+
* the pattern symmetric across shells (see shell-dashboard's
34+
* runtime-config.ts for the full template).
35+
*
36+
* `opts.noStore` (default `true`) controls whether to call
37+
* `unstable_noStore()`. The Node.js server runtime needs the opt-out
38+
* so Next.js does not statically prerender callers and freeze any
39+
* future URL values into the build artifact.
40+
*/
41+
export function getRuntimeConfig(
42+
opts: { noStore?: boolean } = {},
43+
): RuntimeConfig {
44+
if (opts.noStore !== false) noStore();
45+
return {};
46+
}

0 commit comments

Comments
 (0)