Files
copilotkit__copilotkit/showcase/shell-dashboard/next.config.ts
Jordan Ritter 28f33ecc8a fix(showcase): stop SSR 500 + hook-order regressions in shell runtime-config; tolerate env-name variants
Six fixes addressing CR findings on the Option-B runtime URL-injection migration:

1. SSR_PLACEHOLDER must be parseable URL sentinels — `new URL("")` throws on
   SSR causing 500s for any consumer that constructs URLs from runtime-config
   fields. Use `.invalid`-TLD sentinels (RFC 2606) for URL fields; analytics
   keys stay empty string. Add `suppressHydrationWarning` on consumers that
   render the placeholder server-side and the real value post-hydration
   (integration-grid, page-actions popover).

2. Hook-order: move `usePathname()`/`useEffect` ABOVE the early-return in
   use-google-analytics. Gate the effect bodies on `GA_ID` instead so React
   sees a stable hook order across renders.

3. `readUrl`/`readKey` accept either bare or `NEXT_PUBLIC_*`-prefixed env
   names via a fallback chain — covers both server-only and inlined-public
   variable conventions without forcing a rename across deploy targets.

4. Extract `serializeRuntimeConfig` to `lib/runtime-config-serialize.ts` so
   the OWASP-escape behavior (XSS via </script>, U+2028/U+2029 line-terminator
   injection) can be unit-tested without importing the layout into vitest.

5. Reclassify `intelligenceSignupUrl`/`posthogHost` from FATAL-CONFIG to
   info-level in shell-docs — these are optional integrations, not hard
   wiring failures, so absence should not poison the error stream.

6. Comment-rot cleanup: drop "Option B", B12, "the bug we are fixing", fix
   "four substrings"→"three substrings" miscounts, and refresh shell-docs
   .env.example to describe the runtime-injection contract instead of a
   stale next.config throw claim.

V1: shell + shell-docs `next build` succeeds (no Edge-runtime crash on
`unstable_noStore`).
V2: `OPS_BASE_URL=` shell-dashboard `next build` no longer throws —
`next.config.ts` is now a phase-aware function that emits a sentinel
destination at build time and throws only at start (PHASE_PRODUCTION_BUILD
from next/constants).

Tests: shell-docs 72/72, shell 12/12, shell-dashboard runtime-config 16/16
(pre-existing baseline-partner-count failure unchanged).
2026-05-29 11:45:15 -07:00

65 lines
2.8 KiB
TypeScript

import type { NextConfig } from "next";
import { PHASE_PRODUCTION_BUILD } from "next/constants";
/**
* Next.js config for the dashboard shell.
*
* The Status tab calls the showcase-harness HTTP API at the relative path
* `/api/ops/*`. There is no /api/ops route handler in this app — the path
* is a deterministic same-origin proxy that this rewrite forwards to the
* real showcase-harness service. Going same-origin sidesteps two production
* blockers:
* 1. showcase-harness has no CORS allowlist for cross-origin browser calls.
* 2. We don't want the ops base URL inlined into the client bundle (it
* would also force `NEXT_PUBLIC_*` exposure semantics).
*
* `OPS_BASE_URL` is required at START — not at build. `next build`
* evaluates `rewrites()` so route metadata can be persisted into the
* artifact, AND `next start` evaluates it again at process boot.
* Throwing at build time would prevent the runtime-injection deploy
* pattern (single artifact, env supplied at start). So when invoked
* under the build phase we accept the absence with a sentinel
* destination — the rewrite is reconstructed with the real env value
* at start. At start time, missing OPS_BASE_URL still throws loudly to
* surface the wiring bug.
*
* The config is exported as a function `(phase) => NextConfig` so the
* build-vs-start distinction is reliable without relying on the
* `NEXT_PHASE` env (which Next.js does not always export to user code).
*/
const SENTINEL_OPS_BASE = "http://ops.invalid";
export default function nextConfig(phase: string): NextConfig {
const isBuildPhase = phase === PHASE_PRODUCTION_BUILD;
return {
async rewrites() {
const opsBase = process.env.OPS_BASE_URL;
if (!opsBase) {
if (isBuildPhase) {
// Build phase: emit a parseable sentinel destination so
// `next build` succeeds. `rewrites()` runs again at
// `next start` with the real env value (or throws below).
return [
{
source: "/api/ops/:path*",
destination: `${SENTINEL_OPS_BASE}/api/:path*`,
},
];
}
throw new Error(
"OPS_BASE_URL must be set on this Railway service — see showcase/RAILWAY.md " +
"(without it, /api/ops/* requests cannot proxy to showcase-harness)",
);
}
// Strip trailing slashes so we never produce `https://host//api/...`
// (some servers reject the double slash). Mirrors the same
// normalization in `src/lib/ops-api.ts:resolveBaseUrl` so the
// server-side rewrite and client-side fetch agree on the URL shape.
const normalized = opsBase.replace(/\/+$/, "");
return [
{ source: "/api/ops/:path*", destination: `${normalized}/api/:path*` },
];
},
};
}