@timber-js/app 0.2.0-alpha.160 → 0.2.0-alpha.163

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.
Files changed (90) hide show
  1. package/dist/_chunks/{actions-C9yAuoX3.js → actions-cjklt63G.js} +7 -4
  2. package/dist/_chunks/{actions-C9yAuoX3.js.map → actions-cjklt63G.js.map} +1 -1
  3. package/dist/_chunks/{cache-api-swbefQ6F.js → cache-api-CzYUlgXA.js} +2 -2
  4. package/dist/_chunks/{cache-api-swbefQ6F.js.map → cache-api-CzYUlgXA.js.map} +1 -1
  5. package/dist/_chunks/{cli-schema-sync-BhYDz_-x.js → cli-schema-sync-NfLbLnDw.js} +2 -2
  6. package/dist/_chunks/{cli-schema-sync-BhYDz_-x.js.map → cli-schema-sync-NfLbLnDw.js.map} +1 -1
  7. package/dist/_chunks/{logger-CPoQkGK6.js → logger-B_O6-mdJ.js} +7 -45
  8. package/dist/_chunks/logger-B_O6-mdJ.js.map +1 -0
  9. package/dist/_chunks/{walkers-c8aReVgo.js → walkers-9mz9T7mb.js} +2 -2
  10. package/dist/_chunks/{walkers-c8aReVgo.js.map → walkers-9mz9T7mb.js.map} +1 -1
  11. package/dist/adapters/cloudflare.d.ts +13 -13
  12. package/dist/adapters/cloudflare.d.ts.map +1 -1
  13. package/dist/adapters/cloudflare.js +44 -46
  14. package/dist/adapters/cloudflare.js.map +1 -1
  15. package/dist/adapters/nitro.d.ts.map +1 -1
  16. package/dist/adapters/nitro.js +1 -8
  17. package/dist/adapters/nitro.js.map +1 -1
  18. package/dist/adapters/types.d.ts +0 -6
  19. package/dist/adapters/types.d.ts.map +1 -1
  20. package/dist/cache/index.js +1 -1
  21. package/dist/cli.js +1 -1
  22. package/dist/client/child-segment-context.d.ts +22 -0
  23. package/dist/client/child-segment-context.d.ts.map +1 -0
  24. package/dist/client/child-segment-outlet.d.ts +18 -0
  25. package/dist/client/child-segment-outlet.d.ts.map +1 -0
  26. package/dist/client/child-segment-provider.d.ts +21 -0
  27. package/dist/client/child-segment-provider.d.ts.map +1 -0
  28. package/dist/client/internal.js +1 -2
  29. package/dist/client/internal.js.map +1 -1
  30. package/dist/client/segment-cache.d.ts.map +1 -1
  31. package/dist/client/use-cookie.d.ts.map +1 -1
  32. package/dist/cookies/index.js +5 -1
  33. package/dist/cookies/index.js.map +1 -1
  34. package/dist/index.js +256 -9
  35. package/dist/index.js.map +1 -1
  36. package/dist/plugins/cache.d.ts.map +1 -1
  37. package/dist/plugins/callsite-ast.d.ts +27 -5
  38. package/dist/plugins/callsite-ast.d.ts.map +1 -1
  39. package/dist/plugins/fonts.d.ts.map +1 -1
  40. package/dist/plugins/prebuilt-capture.d.ts.map +1 -1
  41. package/dist/plugins/shims.d.ts.map +1 -1
  42. package/dist/routing/index.js +2 -2
  43. package/dist/server/actions.d.ts.map +1 -1
  44. package/dist/server/als-registry.d.ts +1 -0
  45. package/dist/server/als-registry.d.ts.map +1 -1
  46. package/dist/server/index.d.ts +1 -1
  47. package/dist/server/index.d.ts.map +1 -1
  48. package/dist/server/index.js +2 -2
  49. package/dist/server/internal.js +85 -10
  50. package/dist/server/internal.js.map +1 -1
  51. package/dist/server/prebuilt-builder.d.ts.map +1 -1
  52. package/dist/server/prebuilt-runtime.d.ts.map +1 -1
  53. package/dist/server/primitives.d.ts +5 -12
  54. package/dist/server/primitives.d.ts.map +1 -1
  55. package/dist/server/route-element-builder.d.ts +7 -0
  56. package/dist/server/route-element-builder.d.ts.map +1 -1
  57. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  58. package/dist/server/rsc-entry/index.d.ts +2 -2
  59. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  60. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  61. package/dist/server/state-tree-diff.d.ts +26 -3
  62. package/dist/server/state-tree-diff.d.ts.map +1 -1
  63. package/package.json +3 -2
  64. package/src/adapters/cloudflare.ts +41 -63
  65. package/src/adapters/nitro.ts +0 -11
  66. package/src/adapters/types.ts +0 -7
  67. package/src/client/child-segment-context.ts +40 -0
  68. package/src/client/child-segment-outlet.tsx +25 -0
  69. package/src/client/child-segment-provider.tsx +27 -0
  70. package/src/client/segment-cache.ts +4 -5
  71. package/src/client/use-cookie.ts +11 -1
  72. package/src/plugins/cache.ts +44 -1
  73. package/src/plugins/callsite-ast.ts +310 -5
  74. package/src/plugins/fonts.ts +8 -0
  75. package/src/plugins/prebuilt-capture.ts +7 -0
  76. package/src/plugins/shims.ts +24 -0
  77. package/src/server/actions.ts +10 -1
  78. package/src/server/als-registry.ts +1 -0
  79. package/src/server/index.ts +1 -1
  80. package/src/server/prebuilt-builder.ts +120 -42
  81. package/src/server/prebuilt-runtime.ts +24 -1
  82. package/src/server/primitives.ts +5 -20
  83. package/src/server/route-element-builder.ts +113 -79
  84. package/src/server/rsc-entry/helpers.ts +12 -10
  85. package/src/server/rsc-entry/index.ts +40 -40
  86. package/src/server/rsc-entry/render-route.ts +6 -2
  87. package/src/server/rsc-entry/rsc-payload.ts +2 -2
  88. package/src/server/server-only-guard-noop.js +3 -0
  89. package/src/server/state-tree-diff.ts +49 -4
  90. package/dist/_chunks/logger-CPoQkGK6.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 { parseCookie, stringifySetCookie } from 'cookie';\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 cookies = parseCookie(document.cookie);\n return cookies[name];\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 sameSite = options.sameSite ?? 'lax';\n // Build a Set-Cookie string via the cookie package, then strip the\n // `__placeholder__=` name=value prefix to get just the attributes.\n const full = stringifySetCookie({\n name: '__placeholder__',\n value: '',\n path: options.path ?? '/',\n domain: options.domain,\n maxAge: options.maxAge,\n expires: options.expires,\n sameSite,\n secure: options.secure,\n });\n // Strip everything up to and including the first \"; \"\n const idx = full.indexOf('; ');\n return idx >= 0 ? full.slice(idx) : '';\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';\nimport { resolveCodecOrSchema } 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 {\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 return resolveCodecOrSchema('codec', codecOrSchema, 'search') as CookieCodec<T>;\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 { parseCookie, stringifySetCookie } from 'cookie';\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 cookies = parseCookie(document.cookie);\n return cookies[name];\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 sameSite = options.sameSite ?? 'lax';\n // Build a Set-Cookie string via the cookie package, then strip the\n // `__placeholder__=` name=value prefix to get just the attributes.\n const full = stringifySetCookie({\n name: '__placeholder__',\n value: '',\n path: options.path ?? '/',\n domain: options.domain,\n maxAge: options.maxAge,\n expires: options.expires,\n sameSite,\n secure: options.secure,\n });\n // Strip everything up to and including the first \"; \"\n const idx = full.indexOf('; ');\n return idx >= 0 ? full.slice(idx) : '';\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 => {\n // During SSR, read the ALS-backed request cookies.\n const ssrData = getSsrData();\n if (ssrData) return ssrData.cookies.get(name);\n // During client hydration there is no SSR data (it lives in the server's\n // module graph), but React still calls getServerSnapshot. document.cookie\n // holds the same cookies the server rendered with, so reading it here\n // keeps the hydration render consistent with the server HTML.\n // See design/29-cookies.md §\"SSR Hydration\".\n return getCookieValue(name);\n };\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';\nimport { resolveCodecOrSchema } 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 {\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 return resolveCodecOrSchema('codec', codecOrSchema, 'search') as CookieCodec<T>;\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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"}
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { a as __esmMin, o as __exportAll, s as __toCommonJS } from "./_chunks/dist-BA3u1z3W.js";
2
2
  import { c as isDynamicMetadataExtension, l as isMetadataRouteServePath, r as canonicalize } from "./_chunks/canonicalize-P41GR6tY.js";
3
3
  import { t as matchUrlParts } from "./_chunks/tree-match-D2l830j2.js";
4
- import { h as swallow } from "./_chunks/logger-CPoQkGK6.js";
5
- import { i as scanRoutes } from "./_chunks/cli-schema-sync-BhYDz_-x.js";
6
- import { a as generateRouteMap, c as fileHasDirective, d as getPrerenderExport, i as validateSchemaAgainstRoutes, l as fileHasExport, n as collectInterceptionRewrites, o as fileHasAnyExport, s as fileHasDefaultExport, t as collectLeafRoutes, u as fileHasStarExport } from "./_chunks/walkers-c8aReVgo.js";
4
+ import { h as swallow } from "./_chunks/logger-B_O6-mdJ.js";
5
+ import { i as scanRoutes } from "./_chunks/cli-schema-sync-NfLbLnDw.js";
6
+ import { a as generateRouteMap, c as fileHasDirective, d as getPrerenderExport, i as validateSchemaAgainstRoutes, l as fileHasExport, n as collectInterceptionRewrites, o as fileHasAnyExport, s as fileHasDefaultExport, t as collectLeafRoutes, u as fileHasStarExport } from "./_chunks/walkers-9mz9T7mb.js";
7
7
  import { a as resolveAppDir, c as createNoopTimer, n as loadTimberConfigFile, o as resolveBuildDir, r as mergeFileConfig, s as resolveClientJavascript, t as createPluginContext } from "./_chunks/plugin-context-DeAxFRMq.js";
8
8
  import { t as fnv1aHash } from "./_chunks/fast-hash-D6hIVt1Y.js";
9
9
  import { t as formatSize } from "./_chunks/format-CfwjgPz9.js";
@@ -3027,6 +3027,10 @@ function timberShims(_ctx) {
3027
3027
  if (envName(this) === "client" && cleanId === "next/navigation") return resolve(SHIMS_DIR, "navigation-client.ts");
3028
3028
  return SHIM_MAP[cleanId];
3029
3029
  }
3030
+ if (cleanId === "#server-only-guard") {
3031
+ if (envName(this) === "client") return SERVER_ONLY_VIRTUAL;
3032
+ return resolve(PKG_ROOT, "src", "server", "server-only-guard-noop.js");
3033
+ }
3030
3034
  if (cleanId === "#server-internal") {
3031
3035
  if (envName(this) === "client") return "\0timber:server-empty";
3032
3036
  return resolve(PKG_ROOT, "src", "server", "internal.ts");
@@ -3042,6 +3046,9 @@ function timberShims(_ctx) {
3042
3046
  if (envName(this) === "client") return "\0timber:server-empty";
3043
3047
  return resolve(PKG_ROOT, "src", "server", "index.ts");
3044
3048
  }
3049
+ if (cleanId === resolve(PKG_ROOT, "src", "server", "index.ts") || cleanId === resolve(PKG_ROOT, "src", "server", "internal.ts")) {
3050
+ if (envName(this) === "client") return "\0timber:server-empty";
3051
+ }
3045
3052
  if (cleanId === "@timber-js/app/client") {
3046
3053
  const env = envName(this);
3047
3054
  if (env === "ssr" || env === "client") return resolve(PKG_ROOT, "src", "client", "index.ts");
@@ -5934,6 +5941,11 @@ function timberFonts(ctx) {
5934
5941
  configResolved(config) {
5935
5942
  resolvedBase = config.base;
5936
5943
  pipeline.setBase(config.base);
5944
+ const originalWarnOnce = config.logger.warnOnce.bind(config.logger);
5945
+ config.logger.warnOnce = (msg, options) => {
5946
+ if (msg.includes("_timber/fonts/") && msg.includes("didn't resolve at build time")) return;
5947
+ originalWarnOnce(msg, options);
5948
+ };
5937
5949
  },
5938
5950
  /**
5939
5951
  * `enforce: 'pre'` is load-bearing for the font CSS pipeline (TIM-828).
@@ -6944,16 +6956,224 @@ function findImportBindings(program, sources, importedName) {
6944
6956
  namespaces
6945
6957
  };
6946
6958
  }
6959
+ /** Extract all bound identifier names from a parameter/binding pattern. */
6960
+ function collectPatternNames(node, out) {
6961
+ switch (node.type) {
6962
+ case "Identifier":
6963
+ out.add(node.name);
6964
+ break;
6965
+ case "AssignmentPattern":
6966
+ collectPatternNames(node.left, out);
6967
+ break;
6968
+ case "RestElement":
6969
+ collectPatternNames(node.argument, out);
6970
+ break;
6971
+ case "ArrayPattern":
6972
+ for (const el of node.elements ?? []) if (el) collectPatternNames(el, out);
6973
+ break;
6974
+ case "ObjectPattern":
6975
+ for (const prop of node.properties ?? []) if (prop.type === "RestElement") collectPatternNames(prop, out);
6976
+ else collectPatternNames(prop.value, out);
6977
+ break;
6978
+ case "TSParameterProperty":
6979
+ if (node.parameter) collectPatternNames(node.parameter, out);
6980
+ break;
6981
+ }
6982
+ }
6983
+ /** Return the intersection of param names with `tracked`, or null if none. */
6984
+ function shadowedParamNames(params, tracked) {
6985
+ if (!params) return null;
6986
+ const names = /* @__PURE__ */ new Set();
6987
+ for (const p of params) collectPatternNames(p, names);
6988
+ const hit = /* @__PURE__ */ new Set();
6989
+ for (const n of names) if (tracked.has(n)) hit.add(n);
6990
+ return hit.size > 0 ? hit : null;
6991
+ }
6992
+ /**
6993
+ * Collect names declared by `VariableDeclaration`, `FunctionDeclaration`,
6994
+ * and `ClassDeclaration` in a statement list. Only scans direct children
6995
+ * (not nested blocks). For-loop bindings are NOT included — they are
6996
+ * scoped to the loop statement and handled separately in
6997
+ * `collectShadowRanges` (codex review round 4 on PR #876).
6998
+ */
6999
+ function collectLexicalDeclNames(stmts, out) {
7000
+ for (const stmt of stmts) if (stmt.type === "VariableDeclaration") {
7001
+ for (const decl of stmt.declarations ?? []) if (decl.id) collectPatternNames(decl.id, out);
7002
+ } else if (stmt.type === "FunctionDeclaration" || stmt.type === "ClassDeclaration") {
7003
+ if (stmt.id && stmt.id.type === "Identifier") out.add(stmt.id.name);
7004
+ }
7005
+ }
7006
+ /**
7007
+ * Extract binding names from a for-loop's init/left clause, but ONLY for
7008
+ * `const`/`let` (block-scoped). `var` bindings hoist to the enclosing
7009
+ * function and are handled by `collectVarDeclNames` instead (codex review
7010
+ * round 5 on PR #876).
7011
+ */
7012
+ function collectForLoopBlockBindingNames(loop, out) {
7013
+ const init = loop.init ?? loop.left;
7014
+ if (init?.type === "VariableDeclaration" && init.kind !== "var") {
7015
+ for (const decl of init.declarations ?? []) if (decl.id) collectPatternNames(decl.id, out);
7016
+ }
7017
+ }
7018
+ /**
7019
+ * Recursively collect `var` declaration names from a statement tree.
7020
+ * `var` hoists to the enclosing function regardless of block nesting,
7021
+ * so we must scan nested blocks, loops, switch cases, etc.
7022
+ */
7023
+ function collectVarDeclNames(stmts, out) {
7024
+ for (const stmt of stmts) {
7025
+ if (stmt.type === "VariableDeclaration" && stmt.kind === "var") {
7026
+ for (const decl of stmt.declarations ?? []) if (decl.id) collectPatternNames(decl.id, out);
7027
+ }
7028
+ if (stmt.type === "BlockStatement" || stmt.type === "StaticBlock") collectVarDeclNames(stmt.body ?? [], out);
7029
+ else if (stmt.type === "IfStatement") {
7030
+ if (stmt.consequent) collectVarDeclNames([stmt.consequent], out);
7031
+ if (stmt.alternate) collectVarDeclNames([stmt.alternate], out);
7032
+ } else if (stmt.type === "SwitchStatement") for (const c of stmt.cases ?? []) collectVarDeclNames(c.consequent ?? [], out);
7033
+ else if (stmt.type === "ForStatement" || stmt.type === "ForInStatement" || stmt.type === "ForOfStatement" || stmt.type === "WhileStatement" || stmt.type === "DoWhileStatement") {
7034
+ const init = stmt.init ?? stmt.left;
7035
+ if (init?.type === "VariableDeclaration" && init.kind === "var") {
7036
+ for (const decl of init.declarations ?? []) if (decl.id) collectPatternNames(decl.id, out);
7037
+ }
7038
+ if (stmt.body) collectVarDeclNames([stmt.body], out);
7039
+ } else if (stmt.type === "WithStatement" || stmt.type === "LabeledStatement") {
7040
+ if (stmt.body) collectVarDeclNames([stmt.body], out);
7041
+ } else if (stmt.type === "TryStatement") {
7042
+ if (stmt.block) collectVarDeclNames([stmt.block], out);
7043
+ const handler = stmt.handler;
7044
+ if (handler?.body) collectVarDeclNames([handler.body], out);
7045
+ if (stmt.finalizer) collectVarDeclNames([stmt.finalizer], out);
7046
+ }
7047
+ }
7048
+ }
7049
+ /**
7050
+ * Collect byte ranges where any name in `tracked` is shadowed by a
7051
+ * function/arrow parameter, catch clause binding, or lexical declaration
7052
+ * (`const`/`let`/`var`, `function`, `class`, for-loop binding). Calls
7053
+ * inside these ranges must NOT be rewritten — the name resolves to the
7054
+ * local binding, not the import (TIM-1141).
7055
+ *
7056
+ * Each range records WHICH names are shadowed so the consumer can skip
7057
+ * only calls to the shadowed name, not unrelated imports (codex review
7058
+ * on PR #876).
7059
+ *
7060
+ * When a scope shadows some-but-not-all tracked names, the walker
7061
+ * continues descending to find inner scopes that shadow remaining names
7062
+ * (codex review round 2 on PR #876).
7063
+ */
7064
+ function collectShadowRanges(node, tracked, ranges) {
7065
+ if (tracked.size === 0) return;
7066
+ const isFn = node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
7067
+ let remainingTracked = tracked;
7068
+ if (isFn) {
7069
+ const paramNames = /* @__PURE__ */ new Set();
7070
+ for (const p of node.params ?? []) collectPatternNames(p, paramNames);
7071
+ if (node.type === "FunctionExpression" && node.id) {
7072
+ const fnId = node.id;
7073
+ if (fnId.type === "Identifier") paramNames.add(fnId.name);
7074
+ }
7075
+ const shadowed = /* @__PURE__ */ new Set();
7076
+ for (const n of paramNames) if (tracked.has(n)) shadowed.add(n);
7077
+ if (shadowed.size > 0) {
7078
+ ranges.push({
7079
+ start: node.start,
7080
+ end: node.end,
7081
+ names: shadowed
7082
+ });
7083
+ if (shadowed.size === tracked.size) return;
7084
+ remainingTracked = new Set([...tracked].filter((n) => !shadowed.has(n)));
7085
+ }
7086
+ }
7087
+ if (node.type === "CatchClause") {
7088
+ const param = node.param;
7089
+ if (param) {
7090
+ const shadowed = shadowedParamNames([param], tracked);
7091
+ if (shadowed) {
7092
+ ranges.push({
7093
+ start: node.start,
7094
+ end: node.end,
7095
+ names: shadowed
7096
+ });
7097
+ if (shadowed.size === tracked.size) return;
7098
+ remainingTracked = new Set([...tracked].filter((n) => !shadowed.has(n)));
7099
+ }
7100
+ }
7101
+ }
7102
+ if (node.type === "ClassExpression" && node.id) {
7103
+ const clsId = node.id;
7104
+ if (clsId.type === "Identifier" && remainingTracked.has(clsId.name)) {
7105
+ const names = /* @__PURE__ */ new Set([clsId.name]);
7106
+ ranges.push({
7107
+ start: node.start,
7108
+ end: node.end,
7109
+ names
7110
+ });
7111
+ if (names.size === remainingTracked.size) return;
7112
+ remainingTracked = new Set([...remainingTracked].filter((n) => !names.has(n)));
7113
+ }
7114
+ }
7115
+ if (node.type === "ForStatement" || node.type === "ForInStatement" || node.type === "ForOfStatement") {
7116
+ const loopNames = /* @__PURE__ */ new Set();
7117
+ collectForLoopBlockBindingNames(node, loopNames);
7118
+ const loopShadowed = /* @__PURE__ */ new Set();
7119
+ for (const n of loopNames) if (remainingTracked.has(n)) loopShadowed.add(n);
7120
+ if (loopShadowed.size > 0) {
7121
+ ranges.push({
7122
+ start: node.start,
7123
+ end: node.end,
7124
+ names: loopShadowed
7125
+ });
7126
+ if (loopShadowed.size === remainingTracked.size) return;
7127
+ remainingTracked = new Set([...remainingTracked].filter((n) => !loopShadowed.has(n)));
7128
+ }
7129
+ }
7130
+ if (isFn || node.type === "BlockStatement" || node.type === "SwitchStatement" || node.type === "StaticBlock") {
7131
+ let bodyStmts = null;
7132
+ if (isFn) {
7133
+ const body = node.body;
7134
+ if (body?.type === "BlockStatement") bodyStmts = body.body ?? null;
7135
+ } else if (node.type === "SwitchStatement") {
7136
+ const caseStmts = [];
7137
+ for (const c of node.cases ?? []) for (const s of c.consequent ?? []) caseStmts.push(s);
7138
+ bodyStmts = caseStmts;
7139
+ } else bodyStmts = node.body ?? null;
7140
+ if (bodyStmts) {
7141
+ const declNames = /* @__PURE__ */ new Set();
7142
+ collectLexicalDeclNames(bodyStmts, declNames);
7143
+ if (isFn) collectVarDeclNames(bodyStmts, declNames);
7144
+ const lexShadowed = /* @__PURE__ */ new Set();
7145
+ for (const n of declNames) if (remainingTracked.has(n)) lexShadowed.add(n);
7146
+ if (lexShadowed.size > 0) {
7147
+ ranges.push({
7148
+ start: node.start,
7149
+ end: node.end,
7150
+ names: lexShadowed
7151
+ });
7152
+ if (lexShadowed.size === remainingTracked.size) return;
7153
+ remainingTracked = new Set([...remainingTracked].filter((n) => !lexShadowed.has(n)));
7154
+ }
7155
+ }
7156
+ }
7157
+ for (const key of Object.keys(node)) {
7158
+ if (key === "type" || key === "start" || key === "end") continue;
7159
+ const val = node[key];
7160
+ if (val && typeof val === "object") {
7161
+ if (Array.isArray(val)) {
7162
+ for (const item of val) if (item && typeof item === "object" && typeof item.type === "string") collectShadowRanges(item, remainingTracked, ranges);
7163
+ } else if (typeof val.type === "string") collectShadowRanges(val, remainingTracked, ranges);
7164
+ }
7165
+ }
7166
+ }
6947
7167
  /**
6948
7168
  * Recursively walk AST nodes collecting call expressions accepted by
6949
7169
  * `matches`. Calls are typically at module scope, but calls inside
6950
7170
  * functions/blocks are handled for completeness.
6951
7171
  *
6952
- * NOTE: matching is by binding NAME, so nested scopes that shadow a tracked
6953
- * import can produce false positives for consumers of this walker (see
6954
- * TIM-1141 for the cache transform). The prebuilt transform avoids the
6955
- * class entirely by not using this walker — it allowlists top-level
6956
- * declarator positions instead (plugins/prebuilt.ts).
7172
+ * Consumers that match by binding NAME should pair this with
7173
+ * `collectShadowRanges` to filter out calls in scopes where the tracked
7174
+ * name is shadowed (TIM-1141). The prebuilt transform avoids the class
7175
+ * entirely by allowlisting top-level declarator positions
7176
+ * (plugins/prebuilt.ts).
6957
7177
  */
6958
7178
  function findCallSites(node, matches, results) {
6959
7179
  if (node.type === "CallExpression") {
@@ -7064,6 +7284,24 @@ function isCacheCallee(callee, bindings) {
7064
7284
  return false;
7065
7285
  }
7066
7286
  /**
7287
+ * Extract the local binding name that a cache callee resolves to — the
7288
+ * identifier for named imports, or the namespace object for `ns.cache()`.
7289
+ */
7290
+ function calleeBindingName(callee, bindings) {
7291
+ if (callee.type === "Identifier") {
7292
+ const name = callee.name;
7293
+ return bindings.named.has(name) ? name : null;
7294
+ }
7295
+ if (callee.type === "MemberExpression") {
7296
+ const member = callee;
7297
+ if (member.object.type === "Identifier") {
7298
+ const name = member.object.name;
7299
+ return bindings.namespaces.has(name) ? name : null;
7300
+ }
7301
+ }
7302
+ return null;
7303
+ }
7304
+ /**
7067
7305
  * Generate a stable callsite ID for a cache() call. See callsite-ast.ts for
7068
7306
  * the derivation properties.
7069
7307
  */
@@ -7084,10 +7322,18 @@ function timberCacheTransform(ctx) {
7084
7322
  }
7085
7323
  const cacheBindings = findImportBindings(program, CACHE_MODULE_SOURCES, "cache");
7086
7324
  if (cacheBindings.named.size === 0 && cacheBindings.namespaces.size === 0) return;
7325
+ const trackedNames = /* @__PURE__ */ new Set([...cacheBindings.named, ...cacheBindings.namespaces]);
7326
+ const shadowRanges = [];
7327
+ collectShadowRanges(program, trackedNames, shadowRanges);
7328
+ const isCalleeShadowed = (call) => {
7329
+ const name = calleeBindingName(call.callee, cacheBindings);
7330
+ if (!name) return false;
7331
+ return shadowRanges.some((r) => call.start >= r.start && call.start < r.end && r.names.has(name));
7332
+ };
7087
7333
  const callSites = [];
7088
7334
  for (const stmt of program.body) {
7089
7335
  const calls = [];
7090
- findCallSites(stmt, (call) => call.arguments.length === 2 && isCacheCallee(call.callee, cacheBindings), calls);
7336
+ findCallSites(stmt, (call) => call.arguments.length === 2 && isCacheCallee(call.callee, cacheBindings) && !isCalleeShadowed(call), calls);
7091
7337
  for (const call of calls) callSites.push({
7092
7338
  call,
7093
7339
  stmt
@@ -7376,6 +7622,7 @@ async function runPrebuiltCapture(ctx) {
7376
7622
  if (ctx.buildManifest) g.__TIMBER_BUILD_MANIFEST__ ??= ctx.buildManifest;
7377
7623
  if (ctx.deploymentId) g.__TIMBER_DEPLOYMENT_ID__ ??= ctx.deploymentId;
7378
7624
  console.log("[timber] cache.component: capturing prebuilt flight payloads…");
7625
+ g.__TIMBER_CAPTURE_MODE__ = true;
7379
7626
  let mod;
7380
7627
  try {
7381
7628
  mod = await import(