@framework-cwf/core
v0.2.6
Published
App-shell utilities: published-config loader, <Providers> wrapper, layout primitives, error boundaries, 404/500 pages.
Readme
@framework-cwf/core
The app shell — composes every foundation package (auth / tokens / seo /
ui / contracts) into the primitives a per-business Next.js site needs to
boot. Consumers import <Layout> once at app/layout.tsx and the
framework wires brand theming, auth providers, and SSR-safe Server /
Client boundary defaults from there.
Static-export only: the framework targets output: 'export' (locked
decision #7). The published-config loader is a build-time fetch; the
deploy controller (PROJECT_PLAN §5) handles rebuilds via GitHub Actions
workflow_dispatch. No revalidateTag, no /api/revalidate — those
patterns require an SSR/ISR runtime the framework deliberately doesn't
ship.
Installation
Published to GitHub Packages under the @framework-cwf scope. Consumers need an
.npmrc pointing the scope at the GitHub Packages registry plus an auth token:
@framework-cwf:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}pnpm add @framework-cwf/corePublic surface
| Export | Where it runs | Purpose |
| ------------------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| loadWebsiteConfig | Node / build | Fetches the published config from booking-api at build time, validates via WebsiteConfigSchema. |
| <Layout> | Server Component | Drop into app/layout.tsx. Wires <html lang>, favicon, providers, optional Nav / Footer slots. |
| <Providers> | Server Component | Composes <AuthProvider> + <ThemeProvider> + <AuthBootstrap />. |
| <ThemeProvider> | Server Component | Emits the brand @theme { ... } + :root { ... } CSS variable block into the document. |
| <AuthBootstrap /> | 'use client' | Calls auth configure() on first mount. T1.C.2 deferred follow-up #2 — lives here, not in auth. |
| <CallbackRoute /> | 'use client' | Drop into app/auth/callback/page.tsx. Wraps auth's <AuthCallbackHandler />. |
| <ErrorBoundary> | 'use client' | Class-based section-level error boundary with a brand-aware default fallback. |
| <NotFoundPage> | Server Component | Drop-in for app/not-found.tsx. Brand-aware via CSS variables. |
| <ErrorPage> | 'use client' | Drop-in for app/error.tsx. Receives { error, reset } from Next.js. |
| WebsiteConfigLoadError | — | Typed error thrown by loadWebsiteConfig on unrecoverable failure (http-4xx / http-5xx / network / parse-json / parse-schema). |
Providers composition
<Layout> (Server Component — <html><body>)
└── <Providers> (Server Component — composer)
└── <AuthProvider> (Server Component — auth context)
├── <ThemeProvider /> (Server Component — <style> emitter)
├── <AuthBootstrap /> ('use client' — calls configure())
└── {children} (consumer pages)The only 'use client' boundary the shell introduces by itself is
<AuthBootstrap /> (and the <AuthHydrator /> inside <AuthProvider>).
Marketing pages that never call useAuth() ship effectively zero
auth-related client JS.
Brand CSS — two outputs, one provider
<ThemeProvider> emits both a Tailwind @theme { ... } block AND a
:root { ... } block with the same variables. Reasoning:
- Tailwind v4 reads
@themeat build time to generate utility classes. - Browsers ignore
@themeas an unknown at-rule, so variables under it don't apply at runtime without Tailwind processing. - The mirror in
:root { ... }makesvar(--color-primary-500)work in every consumer, Tailwind-pipelined or not.
The duplication is ~1 KB and the static export gzips it.
Usage
// app/layout.tsx — Server Component
import { Layout, loadWebsiteConfig } from "@framework-cwf/core";
const BUSINESS_GUID = "a1b20001-0000-4000-8000-000000000001";
export default async function RootLayout({ children }) {
// Build-time fetch — no runtime calls during static export.
const config = await loadWebsiteConfig(BUSINESS_GUID, {
apiBaseUrl: process.env.BOOKING_API_BASE_URL,
});
return (
<Layout
business={config.operational}
marketing={config.marketing!}
authConfig={{
cognitoClientId: process.env.COGNITO_CLIENT_ID!,
cognitoHostedUiDomain: process.env.COGNITO_HOSTED_UI_DOMAIN!,
redirectUri: process.env.SITE_ORIGIN! + "/auth/callback/",
}}
>
{children}
</Layout>
);
}// app/auth/callback/page.tsx
import { CallbackRoute } from "@framework-cwf/core";
export default CallbackRoute;// app/not-found.tsx — Server Component
import { NotFoundPage } from "@framework-cwf/core";
export default function NotFound() {
return <NotFoundPage businessName="Acme Salon" />;
}// app/error.tsx — 'use client' is mandatory per Next.js App Router
"use client";
import { ErrorPage } from "@framework-cwf/core";
export default function Error({ error, reset }) {
return <ErrorPage businessName="Acme Salon" error={error} reset={reset} />;
}Example app
packages/core/example/ is a complete Next.js 16 app that consumes the
shell against a fixture config. It is the acceptance harness for the
"brand visibly applied" criterion:
pnpm --filter @framework-cwf/core-example build
# → produces packages/core/example/out/ (1.5 MB)
# → out/index.html contains @theme + :root blocks with --color-primary-500The example never hits the network — the fixture config is imported
directly. Real per-business sites call loadWebsiteConfig(...) at build
time against booking-api.
Tests
pnpm --filter @framework-cwf/core test36 tests across 7 files:
load-website-config.test.ts(12) — happy path, 4xx/5xx/network/parse failure modes, retry-with-backoff, header injection, anti-Next-options guard (nonext: { tags }ever attached to the fetch).ThemeProvider.test.tsx(3) — emits@theme+:rootblocks with the brand colours + font stacks.Layout.test.tsx(7) —<html lang>default + override, favicon fallback + override, brand block emission, children + nav + footer slot composition.hydration.test.tsx(1) —renderToString+hydrateRootround-trip through<Providers>with no React mismatch warnings (same pattern as T1.C.2's hydration test).ErrorBoundary.test.tsx(6) — happy path, default + ReactNode + function fallbacks,onErrorcallback,reset()lifecycle.not-found-and-error-page.test.tsx(5) —<NotFoundPage>+<ErrorPage>rendering, default + override props, reset callback wiring.ssr-import.test.ts(2) — locks in the Node-import path.
What's not here
/api/revalidate— TASKS.md T1.G.1 listed it; PROJECT_PLAN §3.7 explicitly forbids it. Static export has no runtime to revalidate.- Tagged
fetch—next: { tags: ... }only works under SSR/ISR. The loader uses plainfetchwith retries. - Rich page-level UI —
apps/template(T1.G.2) composes ui's primitives into actual marketing pages; this shell is the wiring.
SSR safety
The package imports cleanly from Node 24 with no browser globals
available — verified by ssr-import.test.ts. Every browser-touching
behaviour is gated behind a 'use client' boundary (the hydrator
inside <AuthProvider>, the <AuthBootstrap />, <ErrorBoundary>,
and the <ErrorPage> / <CallbackRoute> route components).
