@timber-js/app 0.2.0-alpha.133 → 0.2.0-alpha.135
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-BNhpxrnV.js → actions-CHFblsdT.js} +62 -19
- package/dist/_chunks/actions-CHFblsdT.js.map +1 -0
- package/dist/_chunks/{build-output-helper-K7oUCRqU.js → build-output-helper-CfBfOU9m.js} +1 -1
- package/dist/_chunks/{build-output-helper-K7oUCRqU.js.map → build-output-helper-CfBfOU9m.js.map} +1 -1
- package/dist/_chunks/cli-schema-sync-q-Ce6-iG.js.map +1 -1
- package/dist/_chunks/{codec-registry-DqkeAydt.js → codec-registry-G9hD5LOq.js} +1 -1
- package/dist/_chunks/{codec-registry-DqkeAydt.js.map → codec-registry-G9hD5LOq.js.map} +1 -1
- package/dist/_chunks/{define-jeHDXZGq.js → define-DXWUJb-K.js} +3 -3
- package/dist/_chunks/{define-jeHDXZGq.js.map → define-DXWUJb-K.js.map} +1 -1
- package/dist/_chunks/{define-DMq-Yjp7.js → define-Dp-qu3ql.js} +3 -3
- package/dist/_chunks/{define-DMq-Yjp7.js.map → define-Dp-qu3ql.js.map} +1 -1
- package/dist/_chunks/fast-hash-DXZ7S2D5.js +36 -0
- package/dist/_chunks/fast-hash-DXZ7S2D5.js.map +1 -0
- package/dist/_chunks/{format-BjN9HLu9.js → format-CR6v1pWE.js} +1 -1
- package/dist/_chunks/{format-BjN9HLu9.js.map → format-CR6v1pWE.js.map} +1 -1
- package/dist/_chunks/{handler-store-bIAstrxQ.js → handler-store-aQMpqPe7.js} +1 -1
- package/dist/_chunks/{handler-store-bIAstrxQ.js.map → handler-store-aQMpqPe7.js.map} +1 -1
- package/dist/_chunks/href-validation-2G-HZaQ8.js +124 -0
- package/dist/_chunks/href-validation-2G-HZaQ8.js.map +1 -0
- package/dist/_chunks/logger-CMd9P1xQ.js.map +1 -1
- package/dist/_chunks/metadata-routes-DESG8uk5.js.map +1 -1
- package/dist/_chunks/{navigation-root-B5-RmsPi.js → navigation-root-BQqFQI-n.js} +3 -3
- package/dist/_chunks/{navigation-root-B5-RmsPi.js.map → navigation-root-BQqFQI-n.js.map} +1 -1
- package/dist/_chunks/{registry-BI9cgCt7.js → registry-Dp_hJL4s.js} +1 -1
- package/dist/_chunks/{registry-BI9cgCt7.js.map → registry-Dp_hJL4s.js.map} +1 -1
- package/dist/_chunks/{resolve-schema-DU2nbVOi.js → resolve-schema-D3CbcdCW.js} +2 -2
- package/dist/_chunks/{resolve-schema-DU2nbVOi.js.map → resolve-schema-D3CbcdCW.js.map} +1 -1
- package/dist/_chunks/{router-ref-B2-2kIIC.js → router-ref-B50j42zB.js} +2 -2
- package/dist/_chunks/{router-ref-B2-2kIIC.js.map → router-ref-B50j42zB.js.map} +1 -1
- package/dist/_chunks/{schema-bridge-o0SwFtXW.js → schema-bridge-Cuy8BaYu.js} +2 -2
- package/dist/_chunks/{schema-bridge-o0SwFtXW.js.map → schema-bridge-Cuy8BaYu.js.map} +1 -1
- package/dist/_chunks/segment-classify-BjfuctV2.js.map +1 -1
- package/dist/_chunks/{segment-context-Bg0pFbLT.js → segment-context-BnxoPSFQ.js} +1 -1
- package/dist/_chunks/{segment-context-Bg0pFbLT.js.map → segment-context-BnxoPSFQ.js.map} +1 -1
- package/dist/_chunks/{ssr-data-DVX8S7vj.js → ssr-data-DxXyoRrz.js} +1 -1
- package/dist/_chunks/{ssr-data-DVX8S7vj.js.map → ssr-data-DxXyoRrz.js.map} +1 -1
- package/dist/_chunks/{stale-reload-CCqb62Z3.js → stale-reload-DIIIVGh_.js} +1 -1
- package/dist/_chunks/{stale-reload-CCqb62Z3.js.map → stale-reload-DIIIVGh_.js.map} +1 -1
- package/dist/_chunks/tracing-DP5djRq5.js.map +1 -1
- package/dist/_chunks/{use-query-states-C9G6LP0F.js → use-query-states-D9SBlAfp.js} +2 -2
- package/dist/_chunks/{use-query-states-C9G6LP0F.js.map → use-query-states-D9SBlAfp.js.map} +1 -1
- package/dist/_chunks/{use-segment-params-CIeinIx2.js → use-segment-params-x2poKmk8.js} +2 -2
- package/dist/_chunks/use-segment-params-x2poKmk8.js.map +1 -0
- package/dist/_chunks/{validation-AfEFM5ze.js → validation-BoWM_qI1.js} +31 -2
- package/dist/_chunks/validation-BoWM_qI1.js.map +1 -0
- package/dist/_chunks/walkers-BbEu2_Pp.js.map +1 -1
- package/dist/_chunks/warnings-CAzunaGo.js.map +1 -1
- package/dist/adapters/cloudflare-dev.js.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.js +1 -1
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/cache-api.d.ts +1 -1
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.js +17 -39
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/timber-cache.d.ts +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/client/error-boundary.js +2 -2
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +14 -26
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +8 -8
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts +2 -4
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/use-pathname.d.ts.map +1 -1
- package/dist/client/use-router.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/codec.js.map +1 -1
- package/dist/cookies/index.js +3 -3
- package/dist/cookies/index.js.map +1 -1
- package/dist/cookies/validation.d.ts +11 -23
- package/dist/cookies/validation.d.ts.map +1 -1
- package/dist/dev-tools/error-page.d.ts.map +1 -1
- package/dist/fonts/virtual-modules.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +259 -3
- package/dist/index.js.map +1 -1
- package/dist/params/define-schema.d.ts.map +1 -1
- package/dist/params/index.js +2 -2
- package/dist/params/index.js.map +1 -1
- package/dist/plugins/cache.d.ts +31 -0
- package/dist/plugins/cache.d.ts.map +1 -0
- package/dist/routing/link-codegen.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/index.js +3 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/body-limits.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts +2 -2
- package/dist/server/cookie-context.d.ts.map +1 -1
- package/dist/server/csrf.d.ts +3 -2
- package/dist/server/csrf.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +17 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +69 -21
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +5 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +5 -2
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/revalidate-renderer.d.ts +108 -0
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -0
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +15 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/sitemap-handler.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/shared/href-validation.d.ts +38 -0
- package/dist/shared/href-validation.d.ts.map +1 -0
- package/dist/shims/font-google.js.map +1 -1
- package/dist/shims/font-local.js.map +1 -1
- package/package.json +5 -5
- package/src/cache/cache-api.ts +3 -2
- package/src/cache/timber-cache.ts +27 -4
- package/src/client/index.ts +1 -0
- package/src/client/link.tsx +13 -37
- package/src/client/navigation-context.ts +2 -0
- package/src/client/use-pathname.ts +1 -0
- package/src/client/use-router.ts +3 -0
- package/src/client/use-search-params.ts +1 -0
- package/src/client/use-segment-params.ts +1 -0
- package/src/config-validation.ts +1 -1
- package/src/cookies/validation.ts +58 -0
- package/src/dev-tools/error-page.ts +1 -0
- package/src/fonts/virtual-modules.ts +2 -0
- package/src/index.ts +5 -1
- package/src/params/define-schema.ts +1 -0
- package/src/plugins/cache.ts +180 -0
- package/src/routing/link-codegen.ts +5 -3
- package/src/schema-bridge.ts +3 -2
- package/src/server/action-client.ts +31 -13
- package/src/server/actions.ts +20 -3
- package/src/server/body-limits.ts +10 -2
- package/src/server/cookie-context.ts +7 -2
- package/src/server/cookie-parsing.ts +1 -1
- package/src/server/csrf.ts +39 -6
- package/src/server/deny-renderer.ts +0 -1
- package/src/server/html-injector-core.ts +6 -5
- package/src/server/html-injectors.ts +8 -5
- package/src/server/pipeline-interception.ts +1 -0
- package/src/server/pipeline-phases.ts +30 -19
- package/src/server/pipeline.ts +27 -1
- package/src/server/primitives.ts +19 -18
- package/src/server/route-element-builder.ts +2 -0
- package/src/server/rsc-entry/error-renderer.ts +4 -3
- package/src/server/rsc-entry/index.ts +16 -36
- package/src/server/rsc-entry/revalidate-renderer.ts +344 -0
- package/src/server/rsc-entry/rsc-payload.ts +0 -4
- package/src/server/rsc-entry/ssr-renderer.ts +76 -0
- package/src/server/sitemap-handler.ts +24 -2
- package/src/server/slot-resolver.ts +8 -2
- package/src/server/ssr-wrappers.tsx +2 -0
- package/src/server/tree-builder.ts +2 -0
- package/src/server/types.ts +2 -2
- package/src/shared/href-validation.ts +127 -0
- package/dist/_chunks/actions-BNhpxrnV.js.map +0 -1
- package/dist/_chunks/merge-search-params-Cm1j99HC.js +0 -41
- package/dist/_chunks/merge-search-params-Cm1j99HC.js.map +0 -1
- package/dist/_chunks/use-segment-params-CIeinIx2.js.map +0 -1
- package/dist/_chunks/validation-AfEFM5ze.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/client/use-cookie.ts","../../src/cookies/define-cookie.ts","../../src/cookies/json-cookie.ts"],"sourcesContent":["/**\n * useCookie — reactive client-side cookie hook.\n *\n * Uses useSyncExternalStore for SSR-safe, reactive cookie access.\n * All components reading the same cookie name re-render on change.\n * No cross-tab sync (intentional — see design/29-cookies.md).\n *\n * See design/29-cookies.md §\"useCookie(name) Hook\"\n */\n\nimport { useSyncExternalStore } from 'react';\nimport { getSsrData } from './ssr-data.js';\nimport { assertValidCookieName } from '../cookies/validation.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nexport interface ClientCookieOptions {\n /** URL path scope. Default: '/'. */\n path?: string;\n /** Domain scope. Default: omitted (current domain). */\n domain?: string;\n /** Max age in seconds. */\n maxAge?: number;\n /** Expiration date. */\n expires?: Date;\n /** Cross-site policy. Default: 'lax'. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Only send over HTTPS. Default: true in production. */\n secure?: boolean;\n}\n\nexport type CookieSetter = (value: string, options?: ClientCookieOptions) => void;\n\n// ─── Module-Level Cookie Store ────────────────────────────────────────────\n\ntype Listener = () => void;\n\n/** Per-name subscriber sets. */\nconst listeners = new Map<string, Set<Listener>>();\n\n/** Parse a cookie name from document.cookie. */\nexport function getCookieValue(name: string): string | undefined {\n if (typeof document === 'undefined') return undefined;\n const match = document.cookie.match(\n new RegExp('(?:^|;\\\\s*)' + name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\s*=\\\\s*([^;]*)')\n );\n return match ? decodeURIComponent(match[1]) : undefined;\n}\n\n/** Serialize options into a cookie string suffix. */\nexport function serializeOptions(options?: ClientCookieOptions): string {\n if (!options) return '; Path=/; SameSite=Lax';\n const parts: string[] = [];\n parts.push(`Path=${options.path ?? '/'}`);\n if (options.domain) parts.push(`Domain=${options.domain}`);\n if (options.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);\n if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);\n const sameSite = options.sameSite ?? 'lax';\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n if (options.secure) parts.push('Secure');\n return '; ' + parts.join('; ');\n}\n\n/** Notify all subscribers for a given cookie name. */\nfunction notify(name: string): void {\n const subs = listeners.get(name);\n if (subs) {\n for (const fn of subs) fn();\n }\n}\n\n/**\n * Notify useCookie subscribers that a cookie value changed.\n * Called by defineCookie's imperative client .set()/.delete() methods\n * so mounted useCookie() consumers re-render.\n *\n * @internal — framework use only\n */\nexport function notifyCookieChange(name: string): void {\n notify(name);\n}\n\n// ─── Hook ─────────────────────────────────────────────────────────────────\n\n/**\n * Reactive hook for reading/writing a client-side cookie.\n *\n * Returns `[value, setCookie, deleteCookie]`:\n * - `value`: current cookie value (string | undefined)\n * - `setCookie`: sets the cookie and triggers re-renders\n * - `deleteCookie`: deletes the cookie and triggers re-renders\n *\n * @param name - Cookie name.\n * @param defaultOptions - Default options for setCookie calls.\n */\nexport function useCookie(\n name: string,\n defaultOptions?: ClientCookieOptions\n): [value: string | undefined, setCookie: CookieSetter, deleteCookie: () => void] {\n const subscribe = (callback: Listener): (() => void) => {\n let subs = listeners.get(name);\n if (!subs) {\n subs = new Set();\n listeners.set(name, subs);\n }\n subs.add(callback);\n return () => {\n subs!.delete(callback);\n if (subs!.size === 0) listeners.delete(name);\n };\n };\n\n const getSnapshot = (): string | undefined => getCookieValue(name);\n const getServerSnapshot = (): string | undefined => getSsrData()?.cookies.get(name);\n\n const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n\n // Validate the name once per hook instance — names are typically\n // stable string literals, so this catches the bug at first render\n // rather than on every write. Throws via `assertValidCookieName` with\n // a precise error if the name violates RFC 7230 token rules.\n assertValidCookieName(name);\n\n const setCookie: CookieSetter = (newValue: string, options?: ClientCookieOptions) => {\n if (typeof newValue !== 'string') {\n throw new Error(\n `[timber] useCookie(${JSON.stringify(name)}): value must be a string, got ${typeof newValue}.\\n` +\n ` To store a JSON-serializable value, use defineCookie + jsonCookieCodec from\\n` +\n ` '@timber-js/app/cookies' and call its useCookie() hook instead.`\n );\n }\n const merged = { ...defaultOptions, ...options };\n // Auto-encode mirrors the server's `cookies().set()` contract:\n // values are URL-encoded on write, URL-decoded on read, so the\n // logical bytes round-trip losslessly. See design/29-cookies.md\n // §\"Encoding Contract\".\n document.cookie = `${name}=${encodeURIComponent(newValue)}${serializeOptions(merged)}`;\n notify(name);\n };\n\n const deleteCookie = (): void => {\n const path = defaultOptions?.path ?? '/';\n const domain = defaultOptions?.domain;\n let cookieStr = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=${path}`;\n if (domain) cookieStr += `; Domain=${domain}`;\n document.cookie = cookieStr;\n notify(name);\n };\n\n return [value, setCookie, deleteCookie];\n}\n","/**\n * defineCookie — typed cookie definitions.\n *\n * Bundles name + codec + options into a reusable CookieDefinition<T>\n * with sync .get(), .set(), .delete() isomorphic methods (server + client)\n * and a .useCookie() client hook.\n *\n * Environment detection is call-time: on the server, getCookieJar() is\n * used directly (ALS-backed). On the client, document.cookie helpers\n * and the useCookie hook are used. No registration pattern — imports\n * resolve via the shims plugin (client bundle gets a stub for\n * @timber-js/app/server).\n *\n * Standard Schema objects (Zod, Valibot, ArkType) are auto-detected in\n * the codec option — no explicit fromSchema() wrapper needed.\n *\n * See design/29-cookies.md §\"Typed Cookies with Schema Validation\"\n */\n\nimport type { CookieOptions } from '../server/cookie-context.js';\nimport type { ClientCookieOptions } from '../client/use-cookie.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nimport type { Codec } from '../codec.js';\nimport type { StandardSchemaV1 } from '../schema-bridge.js';\n\n/**\n * A codec that converts between string cookie values and typed values.\n * Type alias for the shared Codec<T> protocol.\n */\nexport type CookieCodec<T> = Codec<T>;\n\n/** Options for defineCookie: codec + CookieOptions merged. */\nexport interface DefineCookieOptions<T, HttpOnly extends boolean = boolean> extends CookieOptions {\n /**\n * Codec for parsing/serializing the cookie value.\n * Accepts a Codec<T> or a Standard Schema object (Zod, Valibot, ArkType)\n * which is auto-wrapped via fromSchema.\n */\n codec: CookieCodec<T> | StandardSchemaV1<T>;\n /**\n * Prevent client-side JS access. Default: true.\n * When true (or omitted), client methods (useCookie, get/set/delete on\n * client) are omitted from the return type and throw at runtime.\n */\n httpOnly?: HttpOnly;\n}\n\n/**\n * Server-only cookie definition. Returned when httpOnly is true or omitted.\n * Client methods are absent from the type — accessing them is a TS error.\n */\nexport interface ServerOnlyCookieDefinition<T> {\n readonly name: string;\n readonly options: CookieOptions;\n readonly codec: CookieCodec<T>;\n\n /** Read the typed value. Sync, isomorphic (server + client). */\n get(): T;\n /** Set the typed value. Sync, isomorphic (server + client). */\n set(value: T): void;\n /** Delete the cookie. Sync, isomorphic (server + client). */\n delete(): void;\n}\n\n/**\n * Full cookie definition with client methods. Returned when httpOnly: false.\n */\nexport interface CookieDefinition<T> extends ServerOnlyCookieDefinition<T> {\n /** Client: React hook for reading/writing this cookie. Returns [value, setter, deleter]. */\n useCookie(): [T, (value: T) => void, () => void];\n}\n\n// ─── Direct Imports (no registration) ─────────────────────────────────────\n//\n// Server: getCookieJar is imported from @timber-js/app/server. In the client\n// bundle, the shims plugin replaces @timber-js/app/server with a stub module\n// where getCookieJar throws on call. We detect the environment at call time\n// by trying getCookieJar() — if it throws the stub error, we fall through\n// to the client path.\n//\n// Client: useCookie, getCookieValue, notifyCookieChange\n// are imported from use-cookie.ts. These are safe in all environments (no\n// node:async_hooks dependency).\n//\n// Schema: fromSchema is a pure function — safe in all environments.\n\nimport { getCookieJar } from '@timber-js/app/server';\nimport { fromSchema } from '../schema-bridge.js';\nimport {\n useCookie as useRawCookie,\n notifyCookieChange,\n getCookieValue,\n} from '../client/use-cookie.js';\n\n// ─── Environment Detection ────────────────────────────────────────────────\n\n/**\n * Try to get the server cookie jar. Returns null if we're in the client\n * environment (stub throws) or if there's no request context.\n *\n * On the server, getCookieJar() throws \"outside of a request context\" if\n * called without an ALS store — this is a real error and is re-thrown.\n * The client stub throws a different message — that's caught and returns null.\n */\nfunction tryGetServerCookieJar(): ReturnType<typeof getCookieJar> | null {\n try {\n return getCookieJar();\n } catch (e) {\n // Server-side \"no request context\" error — re-throw as a real bug.\n if (e instanceof Error && e.message.includes('outside of a request context')) {\n throw e;\n }\n // Client stub error or other — fall through to client path.\n return null;\n }\n}\n\n// ─── Client Cookie Helpers ────────────────────────────────────────────────\n\n/** Write a cookie via document.cookie and notify useCookie subscribers. */\nfunction setClientCookie(name: string, value: string, options?: ClientCookieOptions): void {\n const parts: string[] = [`${name}=${encodeURIComponent(value)}`];\n const path = options?.path ?? '/';\n parts.push(`Path=${path}`);\n if (options?.domain) parts.push(`Domain=${options.domain}`);\n if (options?.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);\n if (options?.expires) parts.push(`Expires=${options.expires.toUTCString()}`);\n const sameSite = options?.sameSite ?? 'lax';\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n if (options?.secure) parts.push('Secure');\n document.cookie = parts.join('; ');\n notifyCookieChange(name);\n}\n\n/** Delete a cookie via document.cookie and notify useCookie subscribers. */\nfunction deleteClientCookie(name: string, options?: ClientCookieOptions): void {\n const path = options?.path ?? '/';\n const domain = options?.domain;\n let cookieStr = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=${path}`;\n if (domain) cookieStr += `; Domain=${domain}`;\n document.cookie = cookieStr;\n notifyCookieChange(name);\n}\n\n// ─── Standard Schema Auto-Detection ───────────────────────────────────────\n\nfunction resolveCodec<T>(codecOrSchema: CookieCodec<T> | StandardSchemaV1<T>): CookieCodec<T> {\n // If it has parse + serialize, it's already a Codec\n if (\n typeof (codecOrSchema as Codec<T>).parse === 'function' &&\n typeof (codecOrSchema as Codec<T>).serialize === 'function'\n ) {\n return codecOrSchema as CookieCodec<T>;\n }\n\n // Auto-detect Standard Schema\n if (typeof codecOrSchema === 'object' && codecOrSchema !== null && '~standard' in codecOrSchema) {\n return fromSchema(codecOrSchema as StandardSchemaV1<unknown>) as CookieCodec<T>;\n }\n\n throw new Error(\n '[timber] defineCookie: codec must be a Codec<T> (with parse/serialize methods) ' +\n 'or a Standard Schema object (Zod, Valibot, ArkType).'\n );\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────\n\n/**\n * Define a typed cookie.\n *\n * ```ts\n * import { defineCookie } from '@timber-js/app/cookies';\n * import { z } from 'zod/v4';\n *\n * // httpOnly: false — client methods available\n * export const themeCookie = defineCookie('theme', {\n * codec: z.enum(['light', 'dark', 'system']).default('system'),\n * httpOnly: false,\n * maxAge: 60 * 60 * 24 * 365,\n * });\n *\n * // Server or client\n * const theme = themeCookie.get();\n * themeCookie.set('dark');\n *\n * // Client hook\n * const [theme, setTheme] = themeCookie.useCookie();\n *\n * // httpOnly: true (default) — server-only, no client methods\n * export const sessionCookie = defineCookie('session', {\n * codec: z.string(),\n * });\n * sessionCookie.get(); // works on server\n * sessionCookie.useCookie(); // TS error — httpOnly cookie\n * ```\n */\nexport function defineCookie<T, HttpOnly extends boolean = true>(\n name: string,\n options: DefineCookieOptions<T, HttpOnly>\n): HttpOnly extends false ? CookieDefinition<T> : ServerOnlyCookieDefinition<T> {\n const { codec: codecOrSchema, ...cookieOpts } = options;\n const codec = resolveCodec(codecOrSchema);\n const resolvedOptions: CookieOptions = { ...cookieOpts };\n const isHttpOnly = options.httpOnly !== false; // default true\n\n function getClientOpts(): ClientCookieOptions {\n return {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n maxAge: resolvedOptions.maxAge,\n expires: resolvedOptions.expires,\n sameSite: resolvedOptions.sameSite,\n secure: resolvedOptions.secure,\n };\n }\n\n const base: ServerOnlyCookieDefinition<T> = {\n name,\n options: resolvedOptions,\n codec,\n\n get(): T {\n const serverJar = tryGetServerCookieJar();\n if (serverJar) {\n return codec.parse(serverJar.get(name));\n }\n // Client path\n if (isHttpOnly) {\n throw new Error(\n `[timber] defineCookie('${name}').get() cannot be used with httpOnly cookies — ` +\n `the browser cannot access them. Set httpOnly: false to enable client access.`\n );\n }\n const raw = getCookieValue(name);\n return codec.parse(raw);\n },\n\n set(value: T): void {\n const serverJar = tryGetServerCookieJar();\n if (serverJar) {\n const serialized = codec.serialize(value);\n if (serialized === null) {\n serverJar.delete(name, {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n });\n } else {\n serverJar.set(name, serialized, resolvedOptions);\n }\n return;\n }\n // Client path\n if (isHttpOnly) {\n throw new Error(\n `[timber] defineCookie('${name}').set() cannot be used with httpOnly cookies — ` +\n `the browser cannot access them. Set httpOnly: false to enable client access.`\n );\n }\n const serialized = codec.serialize(value);\n if (serialized === null) {\n deleteClientCookie(name, getClientOpts());\n } else {\n setClientCookie(name, serialized, getClientOpts());\n }\n },\n\n delete(): void {\n const serverJar = tryGetServerCookieJar();\n if (serverJar) {\n serverJar.delete(name, {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n });\n return;\n }\n // Client path\n if (isHttpOnly) {\n throw new Error(\n `[timber] defineCookie('${name}').delete() cannot be used with httpOnly cookies — ` +\n `the browser cannot access them. Set httpOnly: false to enable client access.`\n );\n }\n deleteClientCookie(name, getClientOpts());\n },\n };\n\n if (isHttpOnly) {\n return base as HttpOnly extends false ? CookieDefinition<T> : ServerOnlyCookieDefinition<T>;\n }\n\n // httpOnly: false — add useCookie client hook\n const full: CookieDefinition<T> = {\n ...base,\n\n useCookie(): [T, (value: T) => void, () => void] {\n const clientOpts = getClientOpts();\n const [raw, setRaw, deleteRaw] = useRawCookie(name, clientOpts);\n const parsed = codec.parse(raw);\n\n const setTyped = (value: T): void => {\n const serialized = codec.serialize(value);\n if (serialized === null) {\n deleteRaw();\n } else {\n setRaw(serialized);\n }\n };\n\n return [parsed, setTyped, deleteRaw];\n },\n };\n\n return full as HttpOnly extends false ? CookieDefinition<T> : ServerOnlyCookieDefinition<T>;\n}\n","/**\n * jsonCookieCodec — Codec helper for storing JSON-serializable values in\n * cookies.\n *\n * The codec is intentionally minimal: `JSON.stringify` on serialize,\n * `JSON.parse` on parse. The framework handles the URL encoding /\n * decoding around it (see design/29-cookies.md §\"Encoding Contract\"):\n *\n * defineCookie.setCookie(value)\n * → codec.serialize: JSON.stringify → '{\"a\":1}'\n * → cookies().set: encodeURIComponent → '%7B%22a%22%3A1%7D'\n * → wire: Set-Cookie: prefs=%7B%22a%22%3A1%7D\n *\n * defineCookie.getCookie()\n * → wire: Cookie: prefs=%7B%22a%22%3A1%7D\n * → parseCookieHeader: decodeURIComponent → '{\"a\":1}'\n * → codec.parse: JSON.parse → { a: 1 }\n *\n * The same chain works on the client: `useCookie` auto-encodes on writes\n * and auto-decodes on reads, so `jsonCookieCodec` composes with both the\n * server `defineCookie` methods and the client `useCookie` hook with no\n * environment-specific branches.\n *\n * Parsing is total: any non-JSON value (or `undefined`) returns the\n * supplied default. This matches the \"never crash on user-controlled\n * cookie input\" semantics that `fromSchema` uses for typed search params.\n *\n * ```ts\n * import { defineCookie, jsonCookieCodec } from '@timber-js/app/cookies';\n *\n * interface Prefs { lang: string; fontSize: number }\n *\n * export const prefsCookie = defineCookie('prefs', {\n * codec: jsonCookieCodec<Prefs>({ lang: 'en', fontSize: 16 }),\n * httpOnly: false,\n * maxAge: 60 * 60 * 24 * 365,\n * });\n *\n * await prefsCookie.setCookie({ lang: 'fr', fontSize: 18 });\n * ```\n */\n\nimport type { Codec } from '../codec.js';\n\n/**\n * Build a CookieCodec that JSON-encodes the value. The framework's\n * cookie write path then URL-encodes the JSON string into a valid\n * `cookie-octet` form, and the read path reverses both transforms.\n *\n * @param defaultValue - Returned by `parse` when the cookie is missing\n * or the stored value cannot be JSON-parsed. Optional — if omitted,\n * `parse` returns `undefined` on missing/malformed input.\n *\n * The default is **deep-cloned** on every fallback return via\n * `structuredClone`, so mutating the parsed result in one request\n * cannot leak into later requests that fall back to the same\n * default. This matters for long-lived server processes where a\n * codec is defined once at module load and shared across all\n * requests — without the clone, a user doing\n * `const prefs = prefsCookie.get(); prefs.lang = 'fr'` on a missing\n * cookie would mutate the module-level default and poison every\n * later fallback. See the \"shared mutable default\" regression test\n * in `tests/define-cookie.test.ts`.\n */\nexport function jsonCookieCodec<T>(defaultValue?: T): Codec<T> {\n // Resolve the fallback at call time, not capture time, so every\n // fallback return produces a fresh deep copy. structuredClone handles\n // nested objects, arrays, dates, maps, sets — everything\n // JSON-serializable plus more. For primitives and undefined,\n // structuredClone is effectively a no-op.\n const cloneDefault = (): T => {\n if (defaultValue === undefined || defaultValue === null) {\n return defaultValue as T;\n }\n // structuredClone is a Node built-in since Node 17, available in\n // all supported runtimes.\n return structuredClone(defaultValue);\n };\n\n return {\n parse(value: string | string[] | undefined): T {\n if (value === undefined || value === '') {\n return cloneDefault();\n }\n // Cookies are single-valued by name; defensively pick the last\n // entry if a Codec consumer somehow passes an array.\n const raw = Array.isArray(value) ? value[value.length - 1] : value;\n if (raw === undefined || raw === '') {\n return cloneDefault();\n }\n try {\n return JSON.parse(raw) as T;\n } catch {\n return cloneDefault();\n }\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n return JSON.stringify(value);\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/client/use-cookie.ts","../../src/cookies/define-cookie.ts","../../src/cookies/json-cookie.ts"],"sourcesContent":["/**\n * useCookie — reactive client-side cookie hook.\n *\n * Uses useSyncExternalStore for SSR-safe, reactive cookie access.\n * All components reading the same cookie name re-render on change.\n * No cross-tab sync (intentional — see design/29-cookies.md).\n *\n * See design/29-cookies.md §\"useCookie(name) Hook\"\n */\n\nimport { useSyncExternalStore } from 'react';\nimport { getSsrData } from './ssr-data.js';\nimport { assertValidCookieName } from '../cookies/validation.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nexport interface ClientCookieOptions {\n /** URL path scope. Default: '/'. */\n path?: string;\n /** Domain scope. Default: omitted (current domain). */\n domain?: string;\n /** Max age in seconds. */\n maxAge?: number;\n /** Expiration date. */\n expires?: Date;\n /** Cross-site policy. Default: 'lax'. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Only send over HTTPS. Default: true in production. */\n secure?: boolean;\n}\n\nexport type CookieSetter = (value: string, options?: ClientCookieOptions) => void;\n\n// ─── Module-Level Cookie Store ────────────────────────────────────────────\n\ntype Listener = () => void;\n\n/** Per-name subscriber sets. */\nconst listeners = new Map<string, Set<Listener>>();\n\n/** Parse a cookie name from document.cookie. */\nexport function getCookieValue(name: string): string | undefined {\n if (typeof document === 'undefined') return undefined;\n const match = document.cookie.match(\n new RegExp('(?:^|;\\\\s*)' + name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\s*=\\\\s*([^;]*)')\n );\n return match ? decodeURIComponent(match[1]) : undefined;\n}\n\n/** Serialize options into a cookie string suffix. */\nexport function serializeOptions(options?: ClientCookieOptions): string {\n if (!options) return '; Path=/; SameSite=Lax';\n const parts: string[] = [];\n parts.push(`Path=${options.path ?? '/'}`);\n if (options.domain) parts.push(`Domain=${options.domain}`);\n if (options.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);\n if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);\n const sameSite = options.sameSite ?? 'lax';\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n if (options.secure) parts.push('Secure');\n return '; ' + parts.join('; ');\n}\n\n/** Notify all subscribers for a given cookie name. */\nfunction notify(name: string): void {\n const subs = listeners.get(name);\n if (subs) {\n for (const fn of subs) fn();\n }\n}\n\n/**\n * Notify useCookie subscribers that a cookie value changed.\n * Called by defineCookie's imperative client .set()/.delete() methods\n * so mounted useCookie() consumers re-render.\n *\n * @internal — framework use only\n */\nexport function notifyCookieChange(name: string): void {\n notify(name);\n}\n\n// ─── Hook ─────────────────────────────────────────────────────────────────\n\n/**\n * Reactive hook for reading/writing a client-side cookie.\n *\n * Returns `[value, setCookie, deleteCookie]`:\n * - `value`: current cookie value (string | undefined)\n * - `setCookie`: sets the cookie and triggers re-renders\n * - `deleteCookie`: deletes the cookie and triggers re-renders\n *\n * @param name - Cookie name.\n * @param defaultOptions - Default options for setCookie calls.\n */\nexport function useCookie(\n name: string,\n defaultOptions?: ClientCookieOptions\n): [value: string | undefined, setCookie: CookieSetter, deleteCookie: () => void] {\n const subscribe = (callback: Listener): (() => void) => {\n let subs = listeners.get(name);\n if (!subs) {\n subs = new Set();\n listeners.set(name, subs);\n }\n subs.add(callback);\n return () => {\n subs!.delete(callback);\n if (subs!.size === 0) listeners.delete(name);\n };\n };\n\n const getSnapshot = (): string | undefined => getCookieValue(name);\n const getServerSnapshot = (): string | undefined => getSsrData()?.cookies.get(name);\n\n const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n\n // Validate the name once per hook instance — names are typically\n // stable string literals, so this catches the bug at first render\n // rather than on every write. Throws via `assertValidCookieName` with\n // a precise error if the name violates RFC 7230 token rules.\n assertValidCookieName(name);\n\n const setCookie: CookieSetter = (newValue: string, options?: ClientCookieOptions) => {\n if (typeof newValue !== 'string') {\n throw new Error(\n `[timber] useCookie(${JSON.stringify(name)}): value must be a string, got ${typeof newValue}.\\n` +\n ` To store a JSON-serializable value, use defineCookie + jsonCookieCodec from\\n` +\n ` '@timber-js/app/cookies' and call its useCookie() hook instead.`\n );\n }\n const merged = { ...defaultOptions, ...options };\n // Auto-encode mirrors the server's `cookies().set()` contract:\n // values are URL-encoded on write, URL-decoded on read, so the\n // logical bytes round-trip losslessly. See design/29-cookies.md\n // §\"Encoding Contract\".\n document.cookie = `${name}=${encodeURIComponent(newValue)}${serializeOptions(merged)}`;\n notify(name);\n };\n\n const deleteCookie = (): void => {\n const path = defaultOptions?.path ?? '/';\n const domain = defaultOptions?.domain;\n let cookieStr = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=${path}`;\n if (domain) cookieStr += `; Domain=${domain}`;\n document.cookie = cookieStr;\n notify(name);\n };\n\n return [value, setCookie, deleteCookie];\n}\n","/**\n * defineCookie — typed cookie definitions.\n *\n * Bundles name + codec + options into a reusable CookieDefinition<T>\n * with sync .get(), .set(), .delete() isomorphic methods (server + client)\n * and a .useCookie() client hook.\n *\n * Environment detection is call-time: on the server, getCookieJar() is\n * used directly (ALS-backed). On the client, document.cookie helpers\n * and the useCookie hook are used. No registration pattern — imports\n * resolve via the shims plugin (client bundle gets a stub for\n * @timber-js/app/server).\n *\n * Standard Schema objects (Zod, Valibot, ArkType) are auto-detected in\n * the codec option — no explicit fromSchema() wrapper needed.\n *\n * See design/29-cookies.md §\"Typed Cookies with Schema Validation\"\n */\n\nimport type { CookieOptions } from '../server/cookie-context.js';\nimport type { ClientCookieOptions } from '../client/use-cookie.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nimport type { Codec } from '../codec.js';\nimport type { StandardSchemaV1 } from '../schema-bridge.js';\n\n/**\n * A codec that converts between string cookie values and typed values.\n * Type alias for the shared Codec<T> protocol.\n */\nexport type CookieCodec<T> = Codec<T>;\n\n/** Options for defineCookie: codec + CookieOptions merged. */\nexport interface DefineCookieOptions<T, HttpOnly extends boolean = boolean> extends CookieOptions {\n /**\n * Codec for parsing/serializing the cookie value.\n * Accepts a Codec<T> or a Standard Schema object (Zod, Valibot, ArkType)\n * which is auto-wrapped via fromSchema.\n */\n codec: CookieCodec<T> | StandardSchemaV1<T>;\n /**\n * Prevent client-side JS access. Default: true.\n * When true (or omitted), client methods (useCookie, get/set/delete on\n * client) are omitted from the return type and throw at runtime.\n */\n httpOnly?: HttpOnly;\n}\n\n/**\n * Server-only cookie definition. Returned when httpOnly is true or omitted.\n * Client methods are absent from the type — accessing them is a TS error.\n */\nexport interface ServerOnlyCookieDefinition<T> {\n readonly name: string;\n readonly options: CookieOptions;\n readonly codec: CookieCodec<T>;\n\n /** Read the typed value. Sync, isomorphic (server + client). */\n get(): T;\n /** Set the typed value. Sync, isomorphic (server + client). */\n set(value: T): void;\n /** Delete the cookie. Sync, isomorphic (server + client). */\n delete(): void;\n}\n\n/**\n * Full cookie definition with client methods. Returned when httpOnly: false.\n */\nexport interface CookieDefinition<T> extends ServerOnlyCookieDefinition<T> {\n /** Client: React hook for reading/writing this cookie. Returns [value, setter, deleter]. */\n useCookie(): [T, (value: T) => void, () => void];\n}\n\n// ─── Direct Imports (no registration) ─────────────────────────────────────\n//\n// Server: getCookieJar is imported from @timber-js/app/server. In the client\n// bundle, the shims plugin replaces @timber-js/app/server with a stub module\n// where getCookieJar throws on call. We detect the environment at call time\n// by trying getCookieJar() — if it throws the stub error, we fall through\n// to the client path.\n//\n// Client: useCookie, getCookieValue, notifyCookieChange\n// are imported from use-cookie.ts. These are safe in all environments (no\n// node:async_hooks dependency).\n//\n// Schema: fromSchema is a pure function — safe in all environments.\n\nimport { getCookieJar } from '@timber-js/app/server';\nimport { fromSchema } from '../schema-bridge.js';\nimport {\n useCookie as useRawCookie,\n notifyCookieChange,\n getCookieValue,\n} from '../client/use-cookie.js';\n\n// ─── Environment Detection ────────────────────────────────────────────────\n\n/**\n * Try to get the server cookie jar. Returns null if we're in the client\n * environment (stub throws) or if there's no request context.\n *\n * On the server, getCookieJar() throws \"outside of a request context\" if\n * called without an ALS store — this is a real error and is re-thrown.\n * The client stub throws a different message — that's caught and returns null.\n */\nfunction tryGetServerCookieJar(): ReturnType<typeof getCookieJar> | null {\n try {\n return getCookieJar();\n } catch (e) {\n // Server-side \"no request context\" error — re-throw as a real bug.\n if (e instanceof Error && e.message.includes('outside of a request context')) {\n throw e;\n }\n // Client stub error or other — fall through to client path.\n return null;\n }\n}\n\n// ─── Client Cookie Helpers ────────────────────────────────────────────────\n\n/** Write a cookie via document.cookie and notify useCookie subscribers. */\nfunction setClientCookie(name: string, value: string, options?: ClientCookieOptions): void {\n const parts: string[] = [`${name}=${encodeURIComponent(value)}`];\n const path = options?.path ?? '/';\n parts.push(`Path=${path}`);\n if (options?.domain) parts.push(`Domain=${options.domain}`);\n if (options?.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);\n if (options?.expires) parts.push(`Expires=${options.expires.toUTCString()}`);\n const sameSite = options?.sameSite ?? 'lax';\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n if (options?.secure) parts.push('Secure');\n document.cookie = parts.join('; ');\n notifyCookieChange(name);\n}\n\n/** Delete a cookie via document.cookie and notify useCookie subscribers. */\nfunction deleteClientCookie(name: string, options?: ClientCookieOptions): void {\n const path = options?.path ?? '/';\n const domain = options?.domain;\n let cookieStr = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=${path}`;\n if (domain) cookieStr += `; Domain=${domain}`;\n document.cookie = cookieStr;\n notifyCookieChange(name);\n}\n\n// ─── Standard Schema Auto-Detection ───────────────────────────────────────\n\nfunction resolveCodec<T>(codecOrSchema: CookieCodec<T> | StandardSchemaV1<T>): CookieCodec<T> {\n // If it has parse + serialize, it's already a Codec\n if (\n typeof (codecOrSchema as Codec<T>).parse === 'function' &&\n typeof (codecOrSchema as Codec<T>).serialize === 'function'\n ) {\n return codecOrSchema as CookieCodec<T>;\n }\n\n // Auto-detect Standard Schema\n if (typeof codecOrSchema === 'object' && codecOrSchema !== null && '~standard' in codecOrSchema) {\n return fromSchema(codecOrSchema as StandardSchemaV1<unknown>) as CookieCodec<T>;\n }\n\n throw new Error(\n '[timber] defineCookie: codec must be a Codec<T> (with parse/serialize methods) ' +\n 'or a Standard Schema object (Zod, Valibot, ArkType).'\n );\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────\n\n/**\n * Define a typed cookie.\n *\n * ```ts\n * import { defineCookie } from '@timber-js/app/cookies';\n * import { z } from 'zod/v4';\n *\n * // httpOnly: false — client methods available\n * export const themeCookie = defineCookie('theme', {\n * codec: z.enum(['light', 'dark', 'system']).default('system'),\n * httpOnly: false,\n * maxAge: 60 * 60 * 24 * 365,\n * });\n *\n * // Server or client\n * const theme = themeCookie.get();\n * themeCookie.set('dark');\n *\n * // Client hook\n * const [theme, setTheme] = themeCookie.useCookie();\n *\n * // httpOnly: true (default) — server-only, no client methods\n * export const sessionCookie = defineCookie('session', {\n * codec: z.string(),\n * });\n * sessionCookie.get(); // works on server\n * sessionCookie.useCookie(); // TS error — httpOnly cookie\n * ```\n */\nexport function defineCookie<T, HttpOnly extends boolean = true>(\n name: string,\n options: DefineCookieOptions<T, HttpOnly>\n): HttpOnly extends false ? CookieDefinition<T> : ServerOnlyCookieDefinition<T> {\n const { codec: codecOrSchema, ...cookieOpts } = options;\n const codec = resolveCodec(codecOrSchema);\n const resolvedOptions: CookieOptions = { ...cookieOpts };\n const isHttpOnly = options.httpOnly !== false; // default true\n\n function getClientOpts(): ClientCookieOptions {\n return {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n maxAge: resolvedOptions.maxAge,\n expires: resolvedOptions.expires,\n sameSite: resolvedOptions.sameSite,\n secure: resolvedOptions.secure,\n };\n }\n\n const base: ServerOnlyCookieDefinition<T> = {\n name,\n options: resolvedOptions,\n codec,\n\n get(): T {\n const serverJar = tryGetServerCookieJar();\n if (serverJar) {\n return codec.parse(serverJar.get(name));\n }\n // Client path\n if (isHttpOnly) {\n throw new Error(\n `[timber] defineCookie('${name}').get() cannot be used with httpOnly cookies — ` +\n `the browser cannot access them. Set httpOnly: false to enable client access.`\n );\n }\n const raw = getCookieValue(name);\n return codec.parse(raw);\n },\n\n set(value: T): void {\n const serverJar = tryGetServerCookieJar();\n if (serverJar) {\n const serialized = codec.serialize(value);\n if (serialized === null) {\n serverJar.delete(name, {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n });\n } else {\n serverJar.set(name, serialized, resolvedOptions);\n }\n return;\n }\n // Client path\n if (isHttpOnly) {\n throw new Error(\n `[timber] defineCookie('${name}').set() cannot be used with httpOnly cookies — ` +\n `the browser cannot access them. Set httpOnly: false to enable client access.`\n );\n }\n const serialized = codec.serialize(value);\n if (serialized === null) {\n deleteClientCookie(name, getClientOpts());\n } else {\n setClientCookie(name, serialized, getClientOpts());\n }\n },\n\n delete(): void {\n const serverJar = tryGetServerCookieJar();\n if (serverJar) {\n serverJar.delete(name, {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n });\n return;\n }\n // Client path\n if (isHttpOnly) {\n throw new Error(\n `[timber] defineCookie('${name}').delete() cannot be used with httpOnly cookies — ` +\n `the browser cannot access them. Set httpOnly: false to enable client access.`\n );\n }\n deleteClientCookie(name, getClientOpts());\n },\n };\n\n if (isHttpOnly) {\n return base as HttpOnly extends false ? CookieDefinition<T> : ServerOnlyCookieDefinition<T>;\n }\n\n // httpOnly: false — add useCookie client hook\n const full: CookieDefinition<T> = {\n ...base,\n\n useCookie(): [T, (value: T) => void, () => void] {\n const clientOpts = getClientOpts();\n const [raw, setRaw, deleteRaw] = useRawCookie(name, clientOpts);\n const parsed = codec.parse(raw);\n\n const setTyped = (value: T): void => {\n const serialized = codec.serialize(value);\n if (serialized === null) {\n deleteRaw();\n } else {\n setRaw(serialized);\n }\n };\n\n return [parsed, setTyped, deleteRaw];\n },\n };\n\n return full as HttpOnly extends false ? CookieDefinition<T> : ServerOnlyCookieDefinition<T>;\n}\n","/**\n * jsonCookieCodec — Codec helper for storing JSON-serializable values in\n * cookies.\n *\n * The codec is intentionally minimal: `JSON.stringify` on serialize,\n * `JSON.parse` on parse. The framework handles the URL encoding /\n * decoding around it (see design/29-cookies.md §\"Encoding Contract\"):\n *\n * defineCookie.setCookie(value)\n * → codec.serialize: JSON.stringify → '{\"a\":1}'\n * → cookies().set: encodeURIComponent → '%7B%22a%22%3A1%7D'\n * → wire: Set-Cookie: prefs=%7B%22a%22%3A1%7D\n *\n * defineCookie.getCookie()\n * → wire: Cookie: prefs=%7B%22a%22%3A1%7D\n * → parseCookieHeader: decodeURIComponent → '{\"a\":1}'\n * → codec.parse: JSON.parse → { a: 1 }\n *\n * The same chain works on the client: `useCookie` auto-encodes on writes\n * and auto-decodes on reads, so `jsonCookieCodec` composes with both the\n * server `defineCookie` methods and the client `useCookie` hook with no\n * environment-specific branches.\n *\n * Parsing is total: any non-JSON value (or `undefined`) returns the\n * supplied default. This matches the \"never crash on user-controlled\n * cookie input\" semantics that `fromSchema` uses for typed search params.\n *\n * ```ts\n * import { defineCookie, jsonCookieCodec } from '@timber-js/app/cookies';\n *\n * interface Prefs { lang: string; fontSize: number }\n *\n * export const prefsCookie = defineCookie('prefs', {\n * codec: jsonCookieCodec<Prefs>({ lang: 'en', fontSize: 16 }),\n * httpOnly: false,\n * maxAge: 60 * 60 * 24 * 365,\n * });\n *\n * await prefsCookie.setCookie({ lang: 'fr', fontSize: 18 });\n * ```\n */\n\nimport type { Codec } from '../codec.js';\n\n/**\n * Build a CookieCodec that JSON-encodes the value. The framework's\n * cookie write path then URL-encodes the JSON string into a valid\n * `cookie-octet` form, and the read path reverses both transforms.\n *\n * @param defaultValue - Returned by `parse` when the cookie is missing\n * or the stored value cannot be JSON-parsed. Optional — if omitted,\n * `parse` returns `undefined` on missing/malformed input.\n *\n * The default is **deep-cloned** on every fallback return via\n * `structuredClone`, so mutating the parsed result in one request\n * cannot leak into later requests that fall back to the same\n * default. This matters for long-lived server processes where a\n * codec is defined once at module load and shared across all\n * requests — without the clone, a user doing\n * `const prefs = prefsCookie.get(); prefs.lang = 'fr'` on a missing\n * cookie would mutate the module-level default and poison every\n * later fallback. See the \"shared mutable default\" regression test\n * in `tests/define-cookie.test.ts`.\n */\nexport function jsonCookieCodec<T>(defaultValue?: T): Codec<T> {\n // Resolve the fallback at call time, not capture time, so every\n // fallback return produces a fresh deep copy. structuredClone handles\n // nested objects, arrays, dates, maps, sets — everything\n // JSON-serializable plus more. For primitives and undefined,\n // structuredClone is effectively a no-op.\n const cloneDefault = (): T => {\n if (defaultValue === undefined || defaultValue === null) {\n return defaultValue as T;\n }\n // structuredClone is a Node built-in since Node 17, available in\n // all supported runtimes.\n return structuredClone(defaultValue);\n };\n\n return {\n parse(value: string | string[] | undefined): T {\n if (value === undefined || value === '') {\n return cloneDefault();\n }\n // Cookies are single-valued by name; defensively pick the last\n // entry if a Codec consumer somehow passes an array.\n const raw = Array.isArray(value) ? value[value.length - 1] : value;\n if (raw === undefined || raw === '') {\n return cloneDefault();\n }\n try {\n return JSON.parse(raw) as T;\n } catch {\n return cloneDefault();\n }\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n return JSON.stringify(value);\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -17,30 +17,18 @@
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function assertValidCookieName(name: string): void;
|
|
19
19
|
/**
|
|
20
|
-
* Validate
|
|
21
|
-
*
|
|
20
|
+
* Validate cookie option attributes at runtime to prevent Set-Cookie
|
|
21
|
+
* header injection through unsanitized option values.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Both the bare `*cookie-octet` form and the DQUOTE-wrapped form are
|
|
27
|
-
* valid. The wrapped form is common in upstream `Set-Cookie` headers
|
|
28
|
-
* (e.g. `sid="abc"; Path=/`) and must be forwardable through
|
|
29
|
-
* `setFromHeaders` verbatim — stripping or rejecting the surrounding
|
|
30
|
-
* quotes would corrupt the on-the-wire bytes and break value semantics
|
|
31
|
-
* for the upstream service.
|
|
32
|
-
*
|
|
33
|
-
* Excluded bytes (in either form): CTL, whitespace (CR/LF/TAB/SP),
|
|
34
|
-
* interior DQUOTE, comma, semicolon, backslash, DEL, and anything
|
|
35
|
-
* non-ASCII. The smuggling primitive (`;`) is rejected regardless of
|
|
36
|
-
* wrapping.
|
|
37
|
-
*
|
|
38
|
-
* Throws a developer-facing error naming the offending byte and its
|
|
39
|
-
* index. Callers passing user-controlled data through the default
|
|
40
|
-
* `set()` path don't need to call this — `set()` auto-encodes via
|
|
41
|
-
* `encodeURIComponent`, whose output is always valid cookie-octet.
|
|
42
|
-
* This validator is for the `{ raw: true }` opt-out and for
|
|
43
|
-
* `setFromHeaders` forwarding.
|
|
23
|
+
* TypeScript types enforce correct shapes at compile time, but at runtime
|
|
24
|
+
* (e.g. from untyped libraries or user-controlled config) invalid values
|
|
25
|
+
* could inject additional Set-Cookie attributes.
|
|
44
26
|
*/
|
|
27
|
+
export declare function assertValidCookieOptions(options: {
|
|
28
|
+
sameSite?: string;
|
|
29
|
+
path?: string;
|
|
30
|
+
domain?: string;
|
|
31
|
+
maxAge?: number;
|
|
32
|
+
}): void;
|
|
45
33
|
export declare function assertValidCookieValue(name: string, value: string): void;
|
|
46
34
|
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/cookies/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA+BH;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAoBxD;
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/cookies/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA+BH;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAoBxD;AAmDD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,IAAI,CA+BP;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CA2BxE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-page.d.ts","sourceRoot":"","sources":["../../src/dev-tools/error-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAA6C,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"error-page.d.ts","sourceRoot":"","sources":["../../src/dev-tools/error-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAA6C,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAgF1F;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAeD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAA;KAAE,CAAC;IAChG,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,kBAAkB,GAAG,SAAS,CAkBjC;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,UAAU,EACjB,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,kBAAkB,GAC9B,MAAM,CAoIR"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"virtual-modules.d.ts","sourceRoot":"","sources":["../../src/fonts/virtual-modules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAKpE,eAAO,MAAM,cAAc,yBAAyB,CAAC;AACrD,eAAO,MAAM,aAAa,wBAAwB,CAAC;AACnD,eAAO,MAAM,eAAe,2BAA2B,CAAC;AACxD,eAAO,MAAM,cAAc,0BAA0B,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAClE,eAAO,MAAM,uBAAuB,SAAS,CAAC;AAC9C,eAAO,MAAM,wBAAwB,QAAiC,CAAC;AAiBvE,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IAEjD;;;;OAIG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAM/B,0EAA0E;IAC1E,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAW3C,+DAA+D;IAC/D,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAK5B,oDAAoD;IACpD,KAAK,IAAI,IAAI;CAGd;AAUD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"virtual-modules.d.ts","sourceRoot":"","sources":["../../src/fonts/virtual-modules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAKpE,eAAO,MAAM,cAAc,yBAAyB,CAAC;AACrD,eAAO,MAAM,aAAa,wBAAwB,CAAC;AACnD,eAAO,MAAM,eAAe,2BAA2B,CAAC;AACxD,eAAO,MAAM,cAAc,0BAA0B,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAClE,eAAO,MAAM,uBAAuB,SAAS,CAAC;AAC9C,eAAO,MAAM,wBAAwB,QAAiC,CAAC;AAiBvE,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IAEjD;;;;OAIG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAM/B,0EAA0E;IAC1E,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAW3C,+DAA+D;IAC/D,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAK5B,oDAAoD;IACpD,KAAK,IAAI,IAAI;CAGd;AAUD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAqClF;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAcnD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,EAAE,WAAW,CAAC,EAAE,kBAAkB,EAAE,GAAG,MAAM,CAwB/F;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,EACpC,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,EAAE,CAAC,GACrD,MAAM,CAOR"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,MAAM,CAAC;AA2BjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAc1D,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAQ1D,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAElG;;;;;;;;;GASG;AAEH,MAAM,WAAW,MAAM;CAAG;AAE1B;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAEvE;AAgGD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,MAAM,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,YAAY,EAAE,CAmVhE;AAED,eAAe,MAAM,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,9 @@ import { a as isDynamicMetadataExtension, o as isMetadataRouteServePath } from "
|
|
|
3
3
|
import { h as swallow } from "./_chunks/logger-CMd9P1xQ.js";
|
|
4
4
|
import { c as loadTimberConfigFile, d as resolveBuildDir, f as resolveClientJavascript, l as mergeFileConfig, m as scanRoutes, o as DEFAULT_BUILD_DIR, p as createNoopTimer, s as createPluginContext, u as resolveAppDir } from "./_chunks/cli-schema-sync-q-Ce6-iG.js";
|
|
5
5
|
import { a as generateRouteMap, i as collectInterceptionRewrites, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-BbEu2_Pp.js";
|
|
6
|
-
import { t as
|
|
7
|
-
import {
|
|
6
|
+
import { t as fnv1aHash } from "./_chunks/fast-hash-DXZ7S2D5.js";
|
|
7
|
+
import { t as formatSize } from "./_chunks/format-CR6v1pWE.js";
|
|
8
|
+
import { basename, dirname, extname, join, normalize, relative, resolve } from "node:path";
|
|
8
9
|
import { createRequire } from "node:module";
|
|
9
10
|
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
|
10
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -1062,7 +1063,7 @@ function validateConfig(config) {
|
|
|
1062
1063
|
field: "renderTimeoutMs",
|
|
1063
1064
|
message: `renderTimeoutMs must be a non-negative number (got ${JSON.stringify(config.renderTimeoutMs)}).`,
|
|
1064
1065
|
value: config.renderTimeoutMs,
|
|
1065
|
-
suggestion: "Use renderTimeoutMs: 30000 (default)
|
|
1066
|
+
suggestion: "Use renderTimeoutMs: 30000 (default). Setting 0 uses a 120s safety ceiling."
|
|
1066
1067
|
});
|
|
1067
1068
|
}
|
|
1068
1069
|
if (config.serverTiming !== void 0) {
|
|
@@ -1368,6 +1369,15 @@ function timberDevServer(ctx) {
|
|
|
1368
1369
|
return {
|
|
1369
1370
|
name: "timber-dev-server",
|
|
1370
1371
|
apply: "serve",
|
|
1372
|
+
/**
|
|
1373
|
+
* Register the dev server middleware and config file watcher.
|
|
1374
|
+
*
|
|
1375
|
+
* Registers as a pre-hook (no return value) so our middleware runs
|
|
1376
|
+
* before Vite's built-in SPA fallback / historyApiFallback. This
|
|
1377
|
+
* ensures we see the original URL (e.g. /blog) rather than a
|
|
1378
|
+
* rewritten /index.html. Vite-internal and asset requests are
|
|
1379
|
+
* filtered out explicitly and passed through to Vite.
|
|
1380
|
+
*/
|
|
1371
1381
|
configureServer(server) {
|
|
1372
1382
|
const configPaths = CONFIG_FILE_NAMES.map((name) => join(ctx.root, name));
|
|
1373
1383
|
server.watcher.add(configPaths);
|
|
@@ -1825,6 +1835,17 @@ function generateCacheHandlerModule(configPath, hasCacheHandler) {
|
|
|
1825
1835
|
function timberEntries(ctx) {
|
|
1826
1836
|
return {
|
|
1827
1837
|
name: "timber-entries",
|
|
1838
|
+
/**
|
|
1839
|
+
* Resolve virtual entry and config module IDs.
|
|
1840
|
+
*
|
|
1841
|
+
* Handles:
|
|
1842
|
+
* - virtual:timber-rsc-entry → real file path
|
|
1843
|
+
* - virtual:timber-ssr-entry → real file path
|
|
1844
|
+
* - virtual:timber-browser-entry → real file path
|
|
1845
|
+
* - virtual:timber-config → \0-prefixed virtual ID
|
|
1846
|
+
* - \0 prefix stripping (RSC plugin re-imports)
|
|
1847
|
+
* - Root prefix stripping (SSR build entries)
|
|
1848
|
+
*/
|
|
1828
1849
|
resolveId(id) {
|
|
1829
1850
|
let cleanId = stripNullPrefix(id);
|
|
1830
1851
|
cleanId = stripRootPrefix(cleanId, ctx.root);
|
|
@@ -1834,6 +1855,12 @@ function timberEntries(ctx) {
|
|
|
1834
1855
|
if (cleanId === VIRTUAL_IDS.cacheHandler) return RESOLVED_CACHE_HANDLER_ID;
|
|
1835
1856
|
return null;
|
|
1836
1857
|
},
|
|
1858
|
+
/**
|
|
1859
|
+
* Load virtual modules: virtual:timber-config and virtual:timber-instrumentation.
|
|
1860
|
+
*
|
|
1861
|
+
* Entry files (rsc/ssr/browser) are real TypeScript files that Vite
|
|
1862
|
+
* processes normally. Only config and instrumentation need generated code.
|
|
1863
|
+
*/
|
|
1837
1864
|
load(id) {
|
|
1838
1865
|
if (id === RESOLVED_CONFIG_ID) return generateConfigModule(ctx);
|
|
1839
1866
|
if (id === RESOLVED_INSTRUMENTATION_ID) return generateInstrumentationModule(detectInstrumentationFile(ctx.root));
|
|
@@ -2372,6 +2399,14 @@ function timberRouting(ctx) {
|
|
|
2372
2399
|
}
|
|
2373
2400
|
return {
|
|
2374
2401
|
name: "timber-routing",
|
|
2402
|
+
/**
|
|
2403
|
+
* Resolve the virtual module ID.
|
|
2404
|
+
*
|
|
2405
|
+
* Handles:
|
|
2406
|
+
* - virtual:timber-route-manifest
|
|
2407
|
+
* - <root>/virtual:timber-route-manifest (Vite SSR build root prefix)
|
|
2408
|
+
* - \0virtual:timber-route-manifest (RSC plugin re-imports)
|
|
2409
|
+
*/
|
|
2375
2410
|
resolveId(id, importer) {
|
|
2376
2411
|
const cleanId = id.startsWith("\0") ? id.slice(1) : id;
|
|
2377
2412
|
if (cleanId === VIRTUAL_MODULE_ID$1) return RESOLVED_VIRTUAL_ID$1;
|
|
@@ -2386,6 +2421,12 @@ function timberRouting(ctx) {
|
|
|
2386
2421
|
}
|
|
2387
2422
|
return null;
|
|
2388
2423
|
},
|
|
2424
|
+
/**
|
|
2425
|
+
* Generate the route manifest module.
|
|
2426
|
+
*
|
|
2427
|
+
* The manifest exports the route tree as a serialized data structure
|
|
2428
|
+
* with absolute import paths for all file references.
|
|
2429
|
+
*/
|
|
2389
2430
|
load(id) {
|
|
2390
2431
|
if (id === RESOLVED_VIRTUAL_ID$1) {
|
|
2391
2432
|
if (!ctx.routeTree) rescan();
|
|
@@ -2406,10 +2447,23 @@ function timberRouting(ctx) {
|
|
|
2406
2447
|
}
|
|
2407
2448
|
return null;
|
|
2408
2449
|
},
|
|
2450
|
+
/**
|
|
2451
|
+
* Scan routes at build start.
|
|
2452
|
+
*
|
|
2453
|
+
* In dev mode, skip — configureServer runs after buildStart and will
|
|
2454
|
+
* do its own scan. Avoids a redundant FS traversal on cold start.
|
|
2455
|
+
*/
|
|
2409
2456
|
buildStart() {
|
|
2410
2457
|
if (ctx.dev) return;
|
|
2411
2458
|
rescan();
|
|
2412
2459
|
},
|
|
2460
|
+
/**
|
|
2461
|
+
* Set up file watcher in dev mode.
|
|
2462
|
+
*
|
|
2463
|
+
* Watches app/ for file additions/deletions/renames that affect
|
|
2464
|
+
* the route tree, regenerates the manifest, and invalidates
|
|
2465
|
+
* dependent modules via HMR.
|
|
2466
|
+
*/
|
|
2413
2467
|
configureServer(devServer) {
|
|
2414
2468
|
rescan();
|
|
2415
2469
|
devServer.watcher.add(ctx.appDir);
|
|
@@ -2802,6 +2856,24 @@ function timberShims(_ctx) {
|
|
|
2802
2856
|
return {
|
|
2803
2857
|
name: "timber-shims",
|
|
2804
2858
|
enforce: "pre",
|
|
2859
|
+
/**
|
|
2860
|
+
* Resolve next/* imports to shim files.
|
|
2861
|
+
*
|
|
2862
|
+
* Resolution order:
|
|
2863
|
+
* 1. Check server-only / client-only poison pill packages
|
|
2864
|
+
* 2. Strip .js extension from the import specifier
|
|
2865
|
+
* 3. Check next/* shim map
|
|
2866
|
+
* 4. Return null (pass through) for everything else
|
|
2867
|
+
*
|
|
2868
|
+
* All @timber-js/app/* subpath imports resolve to dist/ via package.json
|
|
2869
|
+
* exports in all environments. This ensures a single module instance
|
|
2870
|
+
* for each module — the dist/ chunks share code via rollup's code
|
|
2871
|
+
* splitting. No src/ remapping is needed (see TIM-568).
|
|
2872
|
+
*
|
|
2873
|
+
* The only exception is @timber-js/app/server in the client (browser)
|
|
2874
|
+
* environment, which returns a virtual stub — server code must never
|
|
2875
|
+
* be bundled into the browser.
|
|
2876
|
+
*/
|
|
2805
2877
|
resolveId(id) {
|
|
2806
2878
|
if (id === "server-only") return SERVER_ONLY_VIRTUAL;
|
|
2807
2879
|
if (id === "client-only") return CLIENT_ONLY_VIRTUAL;
|
|
@@ -2838,6 +2910,13 @@ function timberShims(_ctx) {
|
|
|
2838
2910
|
}
|
|
2839
2911
|
return null;
|
|
2840
2912
|
},
|
|
2913
|
+
/**
|
|
2914
|
+
* Serve virtual modules for server-only / client-only poison pills.
|
|
2915
|
+
*
|
|
2916
|
+
* In the correct environment, the module is a no-op (empty export).
|
|
2917
|
+
* In the wrong environment, it throws a build-time error message that
|
|
2918
|
+
* clearly identifies the boundary violation.
|
|
2919
|
+
*/
|
|
2841
2920
|
load(id) {
|
|
2842
2921
|
const isClient = this.environment?.name === "client";
|
|
2843
2922
|
if (id === SERVER_ONLY_VIRTUAL) {
|
|
@@ -5521,7 +5600,35 @@ function timberFonts(ctx) {
|
|
|
5521
5600
|
const pipeline = new FontPipeline();
|
|
5522
5601
|
return {
|
|
5523
5602
|
name: "timber-fonts",
|
|
5603
|
+
/**
|
|
5604
|
+
* `enforce: 'pre'` is load-bearing for the font CSS pipeline (TIM-828).
|
|
5605
|
+
*
|
|
5606
|
+
* The transform hook injects a side-effect
|
|
5607
|
+
* `import 'virtual:timber-font-css/<hash>.css'` into every file that
|
|
5608
|
+
* registers a font. @vitejs/plugin-rsc's `rsc:rsc-css-export-transform`
|
|
5609
|
+
* later inspects each server component's source for CSS imports and
|
|
5610
|
+
* wraps the default export in `import.meta.viteRsc.loadCss(...)` so
|
|
5611
|
+
* the matched virtual CSS module ends up `preinit`'d as
|
|
5612
|
+
* `<link rel="stylesheet" data-rsc-css-href=...>`.
|
|
5613
|
+
*
|
|
5614
|
+
* If timber-fonts ran in normal order it would inject the CSS import
|
|
5615
|
+
* *after* plugin-rsc had already scanned the source, and the font CSS
|
|
5616
|
+
* would be silently dropped from the rendered HTML. Running in the
|
|
5617
|
+
* `pre` slot guarantees our injected import is visible to plugin-rsc's
|
|
5618
|
+
* later CSS-export scan.
|
|
5619
|
+
*/
|
|
5524
5620
|
enforce: "pre",
|
|
5621
|
+
/**
|
|
5622
|
+
* Resolve `@timber/fonts/google`, `@timber/fonts/local`, and the
|
|
5623
|
+
* per-importer `virtual:timber-font-css/<hash>.css` modules injected
|
|
5624
|
+
* by the transform hook.
|
|
5625
|
+
*
|
|
5626
|
+
* Strips the `\0` prefix added by the RSC plugin's re-imports and the
|
|
5627
|
+
* absolute root prefix added by SSR build entries before comparing
|
|
5628
|
+
* against the public IDs. The `.css` suffix on font-css ids is
|
|
5629
|
+
* preserved so Vite's CSS pipeline (`isCSSRequest`) recognises the
|
|
5630
|
+
* resolved id as CSS.
|
|
5631
|
+
*/
|
|
5525
5632
|
resolveId(id) {
|
|
5526
5633
|
let cleanId = id.startsWith("\0") ? id.slice(1) : id;
|
|
5527
5634
|
const qIdx = cleanId.indexOf("?");
|
|
@@ -5537,6 +5644,19 @@ function timberFonts(ctx) {
|
|
|
5537
5644
|
if (pipeline.cssIdMap.matches(cleanId)) return "\0" + cleanId + query;
|
|
5538
5645
|
return null;
|
|
5539
5646
|
},
|
|
5647
|
+
/**
|
|
5648
|
+
* Return generated source for font virtual modules.
|
|
5649
|
+
*
|
|
5650
|
+
* The per-importer `virtual:timber-font-css/<hash>.css` modules
|
|
5651
|
+
* return the combined font CSS for the specific importer that
|
|
5652
|
+
* registered them. Vite's CSS pipeline handles injection from here:
|
|
5653
|
+
* in dev the module rides the CSS HMR channel; in production it is
|
|
5654
|
+
* emitted as a real CSS asset and @vitejs/plugin-rsc's `collectCss`
|
|
5655
|
+
* walks the RSC module graph to find it.
|
|
5656
|
+
*
|
|
5657
|
+
* In dev mode we also lazily resolve Google `@font-face` descriptors
|
|
5658
|
+
* from the CDN here, since `buildStart` is a no-op in dev.
|
|
5659
|
+
*/
|
|
5540
5660
|
async load(id) {
|
|
5541
5661
|
if (id === "\0@timber/fonts/google") return generateGoogleVirtualModule(pipeline.fonts());
|
|
5542
5662
|
if (id === "\0@timber/fonts/local") return generateLocalVirtualModule();
|
|
@@ -5558,9 +5678,22 @@ function timberFonts(ctx) {
|
|
|
5558
5678
|
}
|
|
5559
5679
|
return null;
|
|
5560
5680
|
},
|
|
5681
|
+
/**
|
|
5682
|
+
* Serve local font files in dev mode under `/_timber/fonts/<basename>`.
|
|
5683
|
+
* Font CSS goes through Vite's CSS pipeline (virtual modules), not
|
|
5684
|
+
* this middleware.
|
|
5685
|
+
*/
|
|
5561
5686
|
configureServer(server) {
|
|
5562
5687
|
installFontDevMiddleware(server, pipeline);
|
|
5563
5688
|
},
|
|
5689
|
+
/**
|
|
5690
|
+
* Download and cache Google Fonts during production builds.
|
|
5691
|
+
*
|
|
5692
|
+
* In dev mode this is a no-op — fonts point to the Google CDN and are
|
|
5693
|
+
* resolved lazily inside `load()` when the side-effect module runs.
|
|
5694
|
+
* The registry is populated by the transform hook which runs before
|
|
5695
|
+
* `buildStart` in the build pipeline, so all fonts are known here.
|
|
5696
|
+
*/
|
|
5564
5697
|
async buildStart() {
|
|
5565
5698
|
if (ctx.dev) return;
|
|
5566
5699
|
const googleFonts = [...pipeline.googleFonts()];
|
|
@@ -5572,9 +5705,23 @@ function timberFonts(ctx) {
|
|
|
5572
5705
|
pipeline.attachFaces(font.id, generateProductionFontFaces(familyCached, font.display));
|
|
5573
5706
|
}
|
|
5574
5707
|
},
|
|
5708
|
+
/**
|
|
5709
|
+
* Scan source files for font function calls and extract static config.
|
|
5710
|
+
*
|
|
5711
|
+
* Delegates to `runFontsTransform`. Errors raised through the
|
|
5712
|
+
* `this.error` callback are surfaced as Vite plugin errors.
|
|
5713
|
+
*/
|
|
5575
5714
|
transform(code, id) {
|
|
5576
5715
|
return runFontsTransform(code, id, pipeline, (msg) => this.error(msg));
|
|
5577
5716
|
},
|
|
5717
|
+
/**
|
|
5718
|
+
* Emit font binaries and build-manifest entries.
|
|
5719
|
+
*
|
|
5720
|
+
* Google fonts use the content-hashed filenames produced during
|
|
5721
|
+
* `buildStart`. Local fonts are emitted by basename. Font CSS is
|
|
5722
|
+
* handled by Vite's CSS pipeline via virtual modules — we only emit
|
|
5723
|
+
* binaries here.
|
|
5724
|
+
*/
|
|
5578
5725
|
generateBundle() {
|
|
5579
5726
|
emitFontAssets(pipeline, (asset) => this.emitFile(asset), (msg) => this.warn(msg));
|
|
5580
5727
|
if (!ctx.buildManifest) return;
|
|
@@ -5643,6 +5790,13 @@ function detectDynamicApis(code, fileId) {
|
|
|
5643
5790
|
function timberStaticBuild(ctx) {
|
|
5644
5791
|
return {
|
|
5645
5792
|
name: "timber-static-build",
|
|
5793
|
+
/**
|
|
5794
|
+
* Validate source files during transform.
|
|
5795
|
+
*
|
|
5796
|
+
* In static mode, we check every app/ file for:
|
|
5797
|
+
* - Dynamic API usage (getCookies(), getHeaders()) → build error
|
|
5798
|
+
* - When client JS disabled: 'use client' / 'use server' directives → build error
|
|
5799
|
+
*/
|
|
5646
5800
|
transform(code, id) {
|
|
5647
5801
|
if (!(ctx.config.output === "static")) return null;
|
|
5648
5802
|
if (id.includes("node_modules")) return null;
|
|
@@ -6349,6 +6503,107 @@ function timberServerBundle() {
|
|
|
6349
6503
|
];
|
|
6350
6504
|
}
|
|
6351
6505
|
//#endregion
|
|
6506
|
+
//#region src/plugins/cache.ts
|
|
6507
|
+
/**
|
|
6508
|
+
* timber-cache-transform — Vite plugin that injects stable callsite IDs
|
|
6509
|
+
* into `cache()` calls imported from `@timber-js/app/cache`.
|
|
6510
|
+
*
|
|
6511
|
+
* Without this plugin, cache keys use a process-local counter (fnIdCounter)
|
|
6512
|
+
* which is unstable across cold starts in serverless/multi-instance
|
|
6513
|
+
* deployments. With a shared CacheHandler (Redis/KV), colliding counters
|
|
6514
|
+
* cause cache key poisoning — serving one route's cached data to another.
|
|
6515
|
+
*
|
|
6516
|
+
* The plugin transforms:
|
|
6517
|
+
* cache(fn, opts) → cache(fn, opts, "stable-id")
|
|
6518
|
+
*
|
|
6519
|
+
* where stable-id = fnv1a(relativeModulePath + ":cache:" + callIndex).
|
|
6520
|
+
*
|
|
6521
|
+
* See TIM-997, design/06-caching.md §"Cache Key Security".
|
|
6522
|
+
*/
|
|
6523
|
+
var CACHE_MODULE_SOURCES = new Set(["@timber-js/app/cache", "timber/cache"]);
|
|
6524
|
+
/**
|
|
6525
|
+
* Find the local binding name(s) for `cache` imported from the cache module.
|
|
6526
|
+
*/
|
|
6527
|
+
function findCacheBindings(program) {
|
|
6528
|
+
const bindings = /* @__PURE__ */ new Set();
|
|
6529
|
+
for (const stmt of program.body) {
|
|
6530
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
6531
|
+
const decl = stmt;
|
|
6532
|
+
if (!CACHE_MODULE_SOURCES.has(decl.source.value)) continue;
|
|
6533
|
+
for (const spec of decl.specifiers) {
|
|
6534
|
+
if (spec.type !== "ImportSpecifier") continue;
|
|
6535
|
+
if ((spec.imported?.name ?? spec.local.name) === "cache") bindings.add(spec.local.name);
|
|
6536
|
+
}
|
|
6537
|
+
}
|
|
6538
|
+
return bindings;
|
|
6539
|
+
}
|
|
6540
|
+
/**
|
|
6541
|
+
* Recursively walk AST nodes to find cache() call expressions.
|
|
6542
|
+
*
|
|
6543
|
+
* cache() is typically called at module scope, but we also handle calls
|
|
6544
|
+
* inside functions/blocks for completeness.
|
|
6545
|
+
*/
|
|
6546
|
+
function findCacheCallSites(node, cacheBindings, results) {
|
|
6547
|
+
if (node.type === "CallExpression") {
|
|
6548
|
+
const call = node;
|
|
6549
|
+
if (call.callee.type === "Identifier" && cacheBindings.has(call.callee.name) && call.arguments.length === 2) results.push(call);
|
|
6550
|
+
}
|
|
6551
|
+
for (const key of Object.keys(node)) {
|
|
6552
|
+
if (key === "type" || key === "start" || key === "end") continue;
|
|
6553
|
+
const val = node[key];
|
|
6554
|
+
if (val && typeof val === "object") {
|
|
6555
|
+
if (Array.isArray(val)) {
|
|
6556
|
+
for (const item of val) if (item && typeof item === "object" && typeof item.type === "string") findCacheCallSites(item, cacheBindings, results);
|
|
6557
|
+
} else if (typeof val.type === "string") findCacheCallSites(val, cacheBindings, results);
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
}
|
|
6561
|
+
/**
|
|
6562
|
+
* Generate a stable callsite ID from a module path and call index.
|
|
6563
|
+
*
|
|
6564
|
+
* The ID is an FNV-1a hash of the relative module path + call index,
|
|
6565
|
+
* providing a deterministic identifier that is:
|
|
6566
|
+
* - Stable across cold starts of the same build
|
|
6567
|
+
* - Unique across different modules
|
|
6568
|
+
* - Unique across multiple cache() calls within the same module
|
|
6569
|
+
*/
|
|
6570
|
+
function generateStableId(relativeModulePath, callIndex) {
|
|
6571
|
+
return fnv1aHash(`${relativeModulePath}:cache:${callIndex}`);
|
|
6572
|
+
}
|
|
6573
|
+
function timberCacheTransform(ctx) {
|
|
6574
|
+
return {
|
|
6575
|
+
name: "timber-cache-transform",
|
|
6576
|
+
transform(code, id) {
|
|
6577
|
+
if (!code.includes("@timber-js/app/cache") && !code.includes("timber/cache")) return;
|
|
6578
|
+
if (id.includes("node_modules")) return;
|
|
6579
|
+
let program;
|
|
6580
|
+
try {
|
|
6581
|
+
program = parseAst(code, { lang: "tsx" });
|
|
6582
|
+
} catch {
|
|
6583
|
+
return;
|
|
6584
|
+
}
|
|
6585
|
+
const cacheBindings = findCacheBindings(program);
|
|
6586
|
+
if (cacheBindings.size === 0) return;
|
|
6587
|
+
const callSites = [];
|
|
6588
|
+
findCacheCallSites(program, cacheBindings, callSites);
|
|
6589
|
+
if (callSites.length === 0) return;
|
|
6590
|
+
callSites.sort((a, b) => a.start - b.start);
|
|
6591
|
+
const relPath = relative(ctx.root || process.cwd(), id);
|
|
6592
|
+
const s = new MagicString(code);
|
|
6593
|
+
for (let i = 0; i < callSites.length; i++) {
|
|
6594
|
+
const call = callSites[i];
|
|
6595
|
+
const stableId = generateStableId(relPath, i);
|
|
6596
|
+
const insertPos = call.end - 1;
|
|
6597
|
+
s.appendLeft(insertPos, `, "${stableId}"`);
|
|
6598
|
+
}
|
|
6599
|
+
return {
|
|
6600
|
+
code: s.toString(),
|
|
6601
|
+
map: s.generateMap({ hires: "boundary" })
|
|
6602
|
+
};
|
|
6603
|
+
}
|
|
6604
|
+
};
|
|
6605
|
+
}
|
|
6606
|
+
//#endregion
|
|
6352
6607
|
//#region src/plugins/cloudflare-wasm.ts
|
|
6353
6608
|
/**
|
|
6354
6609
|
* timber-cloudflare-wasm — Handle WASM modules for Cloudflare Workers builds.
|
|
@@ -7255,6 +7510,7 @@ function timber(config) {
|
|
|
7255
7510
|
timberStaticBuild(ctx),
|
|
7256
7511
|
timberFonts(ctx),
|
|
7257
7512
|
timberMdx(ctx),
|
|
7513
|
+
timberCacheTransform(ctx),
|
|
7258
7514
|
timberContent(ctx),
|
|
7259
7515
|
timberServerBundle(),
|
|
7260
7516
|
timberCloudflareWasm(ctx),
|