@timber-js/app 0.1.11 → 0.1.12

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA2EA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;AA8qBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;8BAxjBpD,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AA0jBhD,wBAAiE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA2EA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;AAqrBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;8BA/jBpD,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AAikBhD,wBAAiE"}
@@ -46,6 +46,13 @@ export interface NavContext {
46
46
  /** Request cookies as name→value pairs. Used by useCookie() during SSR
47
47
  * to return correct cookie values before hydration. */
48
48
  cookies?: Map<string, string>;
49
+ /**
50
+ * Mutable flag: set by TimberErrorBoundary during SSR when it catches
51
+ * a DenySignal (via digest). This tells the RSC entry that the denial
52
+ * was contained by a slot error boundary and should NOT be promoted
53
+ * to a page-level deny. See LOCAL-298.
54
+ */
55
+ _denyHandledByBoundary?: boolean;
49
56
  }
50
57
  /**
51
58
  * Handle SSR: decode an RSC stream and render it to hydration-ready HTML.
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA6BH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CA2EnB;AAED,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA6BH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CA4EnB;AAED,eAAe,SAAS,CAAC"}
@@ -40,6 +40,16 @@ export interface TreeBuilderConfig {
40
40
  loadModule: ModuleLoader;
41
41
  /** React.createElement or equivalent. */
42
42
  createElement: CreateElement;
43
+ /**
44
+ * Error boundary component for wrapping segments.
45
+ *
46
+ * This is injected by the caller rather than imported directly to avoid
47
+ * pulling 'use client' code into the server barrel (@timber-js/app/server).
48
+ * In the RSC environment, the RSC plugin transforms this import to a
49
+ * client reference proxy — the caller handles the import so the server
50
+ * barrel stays free of client dependencies.
51
+ */
52
+ errorBoundaryComponent?: unknown;
43
53
  }
44
54
  /**
45
55
  * Framework-injected access gate component.
@@ -1 +1 @@
1
- {"version":3,"file":"tree-builder.d.ts","sourceRoot":"","sources":["../../src/server/tree-builder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tree-builder.d.ts","sourceRoot":"","sources":["../../src/server/tree-builder.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Vite-native React framework for Cloudflare Workers — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { Component, createElement, type ReactNode } from 'react';
22
+ import { getSsrData } from './ssr-data.js';
22
23
 
23
24
  // ─── Page Unload Detection ───────────────────────────────────────────────────
24
25
  // Track whether the page is being unloaded (user refreshed or navigated away).
@@ -97,6 +98,27 @@ export class TimberErrorBoundary extends Component<
97
98
  if (_isUnloading) {
98
99
  return { hasError: false, error: null };
99
100
  }
101
+
102
+ // Report DenySignal handling to prevent page-level promotion.
103
+ // When a slot's error boundary catches a DenySignal, the RSC onError
104
+ // callback has already tracked it globally. Setting this flag tells
105
+ // the RSC entry not to promote the denial to page-level (which would
106
+ // replace the entire SSR response). See LOCAL-298.
107
+ const digest = (error as { digest?: string }).digest;
108
+ if (typeof digest === 'string') {
109
+ try {
110
+ const parsed = JSON.parse(digest);
111
+ if (parsed?.type === 'deny') {
112
+ const ssrData = getSsrData();
113
+ if (ssrData?._navContext) {
114
+ ssrData._navContext._denyHandledByBoundary = true;
115
+ }
116
+ }
117
+ } catch {
118
+ // Not a JSON digest — ignore
119
+ }
120
+ }
121
+
100
122
  return { hasError: true, error };
101
123
  }
102
124
 
@@ -30,6 +30,13 @@ export interface SsrData {
30
30
  cookies: Map<string, string>;
31
31
  /** The request's route params (e.g. { id: '123' }) */
32
32
  params: Record<string, string | string[]>;
33
+ /**
34
+ * Mutable reference to NavContext for error boundary → RSC communication.
35
+ * When TimberErrorBoundary catches a DenySignal, it sets
36
+ * `_navContext._denyHandledByBoundary = true` to prevent the RSC entry
37
+ * from promoting the denial to page-level. See LOCAL-298.
38
+ */
39
+ _navContext?: { _denyHandledByBoundary?: boolean };
33
40
  }
34
41
 
35
42
  // ─── ALS-Backed Provider ─────────────────────────────────────────
@@ -173,21 +173,15 @@ export function timberShims(_ctx: PluginContext): Plugin {
173
173
  return 'export {};';
174
174
  }
175
175
 
176
- // Stub for @timber-js/app/server in client environment.
177
- // Exports throw-on-call stubs so named imports resolve but
178
- // calling them gives a clear error instead of crashing the bundle.
176
+ // Error module for @timber-js/app/server in client environment.
177
+ // Server modules must never be bundled into the browser — if this
178
+ // module is reached, there is a broken import chain that needs fixing.
179
179
  if (id === '\0timber:server-empty') {
180
- return `
181
- const stub = (name) => () => { throw new Error(name + "() is a server-only function and cannot be called in client code."); };
182
- export const headers = stub("headers");
183
- export const cookies = stub("cookies");
184
- export const notFound = stub("notFound");
185
- export const redirect = stub("redirect");
186
- export const permanentRedirect = stub("permanentRedirect");
187
- export const deny = stub("deny");
188
- export const searchParams = stub("searchParams");
189
- export const RedirectType = { push: "push", replace: "replace" };
190
- `;
180
+ return `throw new Error(
181
+ "[timber] @timber-js/app/server was imported from client code. " +
182
+ "Server modules (headers, cookies, redirect, deny, etc.) cannot be used in client components. " +
183
+ "If you need these APIs, move the import to a server component or middleware."
184
+ );`;
191
185
  }
192
186
  },
193
187
  };
@@ -661,10 +661,17 @@ async function renderRoute(
661
661
  // Helper: check if render-phase signals were captured and return the
662
662
  // appropriate HTTP response. Used after both successful SSR (signal
663
663
  // promotion from Suspense) and failed SSR (signal outside Suspense).
664
- function checkCapturedSignals(): Response | Promise<Response> | null {
664
+ //
665
+ // When `skipHandledDeny` is true (SSR success path), skip DenySignal
666
+ // promotion if the denial was already handled by a TimberErrorBoundary
667
+ // (e.g., slot error boundary). The boundary sets navContext._denyHandledByBoundary
668
+ // during SSR rendering. See LOCAL-298.
669
+ function checkCapturedSignals(
670
+ skipHandledDeny = false
671
+ ): Response | Promise<Response> | null {
665
672
  const sig = redirectSignal as RedirectSignal | null;
666
673
  if (sig) return buildRedirectResponse(_req, sig, responseHeaders);
667
- if (denySignal) {
674
+ if (denySignal && !(skipHandledDeny && navContext._denyHandledByBoundary)) {
668
675
  return renderDenyPage(
669
676
  denySignal,
670
677
  segments,
@@ -703,7 +710,7 @@ async function renderRoute(
703
710
  // See design/05-streaming.md §"deferSuspenseFor and the Hold Window"
704
711
  await new Promise<void>((r) => setTimeout(r, 0));
705
712
 
706
- const promoted = checkCapturedSignals();
713
+ const promoted = checkCapturedSignals(/* skipHandledDeny */ true);
707
714
  if (promoted) {
708
715
  ssrResponse.body?.cancel();
709
716
  return promoted;
@@ -74,6 +74,13 @@ export interface NavContext {
74
74
  /** Request cookies as name→value pairs. Used by useCookie() during SSR
75
75
  * to return correct cookie values before hydration. */
76
76
  cookies?: Map<string, string>;
77
+ /**
78
+ * Mutable flag: set by TimberErrorBoundary during SSR when it catches
79
+ * a DenySignal (via digest). This tells the RSC entry that the denial
80
+ * was contained by a slot error boundary and should NOT be promoted
81
+ * to a page-level deny. See LOCAL-298.
82
+ */
83
+ _denyHandledByBoundary?: boolean;
77
84
  }
78
85
 
79
86
  /**
@@ -111,6 +118,7 @@ export async function handleSsr(
111
118
  searchParams: navContext.searchParams,
112
119
  cookies: navContext.cookies ?? new Map(),
113
120
  params: navContext.params,
121
+ _navContext: navContext,
114
122
  };
115
123
 
116
124
  // Run the entire render inside the SSR data ALS scope.
@@ -11,7 +11,6 @@
11
11
  */
12
12
 
13
13
  import type { SegmentNode, RouteFile } from '#/routing/types.js';
14
- import { TimberErrorBoundary } from '#/client/error-boundary.js';
15
14
 
16
15
  // ─── Types ───────────────────────────────────────────────────────────────────
17
16
 
@@ -55,6 +54,16 @@ export interface TreeBuilderConfig {
55
54
  loadModule: ModuleLoader;
56
55
  /** React.createElement or equivalent. */
57
56
  createElement: CreateElement;
57
+ /**
58
+ * Error boundary component for wrapping segments.
59
+ *
60
+ * This is injected by the caller rather than imported directly to avoid
61
+ * pulling 'use client' code into the server barrel (@timber-js/app/server).
62
+ * In the RSC environment, the RSC plugin transforms this import to a
63
+ * client reference proxy — the caller handles the import so the server
64
+ * barrel stays free of client dependencies.
65
+ */
66
+ errorBoundaryComponent?: unknown;
58
67
  }
59
68
 
60
69
  // ─── Component wrappers ──────────────────────────────────────────────────────
@@ -134,7 +143,8 @@ export interface TreeBuildResult {
134
143
  * Parallel slots are resolved at each layout level and composed as named props.
135
144
  */
136
145
  export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeBuildResult> {
137
- const { segments, params, searchParams, loadModule, createElement } = config;
146
+ const { segments, params, searchParams, loadModule, createElement, errorBoundaryComponent } =
147
+ config;
138
148
 
139
149
  if (segments.length === 0) {
140
150
  throw new Error('[timber] buildElementTree: empty segment chain');
@@ -166,7 +176,13 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
166
176
  const segment = segments[i];
167
177
 
168
178
  // Wrap in error boundaries (status-code files + error.tsx)
169
- element = await wrapWithErrorBoundaries(segment, element, loadModule, createElement);
179
+ element = await wrapWithErrorBoundaries(
180
+ segment,
181
+ element,
182
+ loadModule,
183
+ createElement,
184
+ errorBoundaryComponent
185
+ );
170
186
 
171
187
  // Wrap in AccessGate if segment has access.ts
172
188
  if (segment.access) {
@@ -198,7 +214,8 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
198
214
  params,
199
215
  searchParams,
200
216
  loadModule,
201
- createElement
217
+ createElement,
218
+ errorBoundaryComponent
202
219
  );
203
220
  }
204
221
  }
@@ -229,7 +246,8 @@ async function buildSlotElement(
229
246
  params: Record<string, string | string[]>,
230
247
  searchParams: unknown,
231
248
  loadModule: ModuleLoader,
232
- createElement: CreateElement
249
+ createElement: CreateElement,
250
+ errorBoundaryComponent: unknown
233
251
  ): Promise<ReactElement> {
234
252
  // Load slot page
235
253
  const pageModule = slotNode.page ? await loadModule(slotNode.page) : null;
@@ -249,7 +267,13 @@ async function buildSlotElement(
249
267
  let element: ReactElement = createElement(PageComponent, { params, searchParams });
250
268
 
251
269
  // Wrap in error boundaries
252
- element = await wrapWithErrorBoundaries(slotNode, element, loadModule, createElement);
270
+ element = await wrapWithErrorBoundaries(
271
+ slotNode,
272
+ element,
273
+ loadModule,
274
+ createElement,
275
+ errorBoundaryComponent
276
+ );
253
277
 
254
278
  // Wrap in SlotAccessGate if slot has access.ts
255
279
  if (slotNode.access) {
@@ -301,7 +325,8 @@ async function wrapWithErrorBoundaries(
301
325
  segment: SegmentNode,
302
326
  element: ReactElement,
303
327
  loadModule: ModuleLoader,
304
- createElement: CreateElement
328
+ createElement: CreateElement,
329
+ errorBoundaryComponent: unknown
305
330
  ): Promise<ReactElement> {
306
331
  // Wrapping is applied inside-out. The last wrap call produces the outermost boundary.
307
332
  // Order: specific status → category → error.tsx (outermost)
@@ -315,7 +340,7 @@ async function wrapWithErrorBoundaries(
315
340
  const mod = await loadModule(file);
316
341
  const Component = mod.default;
317
342
  if (Component) {
318
- element = createElement(TimberErrorBoundary, {
343
+ element = createElement(errorBoundaryComponent, {
319
344
  fallbackComponent: Component,
320
345
  status,
321
346
  children: element,
@@ -331,7 +356,7 @@ async function wrapWithErrorBoundaries(
331
356
  const mod = await loadModule(file);
332
357
  const Component = mod.default;
333
358
  if (Component) {
334
- element = createElement(TimberErrorBoundary, {
359
+ element = createElement(errorBoundaryComponent, {
335
360
  fallbackComponent: Component,
336
361
  status: key === '4xx' ? 400 : 500, // category marker
337
362
  children: element,
@@ -346,7 +371,7 @@ async function wrapWithErrorBoundaries(
346
371
  const errorModule = await loadModule(segment.error);
347
372
  const ErrorComponent = errorModule.default;
348
373
  if (ErrorComponent) {
349
- element = createElement(TimberErrorBoundary, {
374
+ element = createElement(errorBoundaryComponent, {
350
375
  fallbackComponent: ErrorComponent,
351
376
  children: element,
352
377
  } satisfies ErrorBoundaryProps);
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-cookie-HcvNlW4L.js","names":[],"sources":["../../src/client/ssr-data.ts","../../src/client/use-cookie.ts"],"sourcesContent":["/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\n */\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: Record<string, string | string[]>;\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n\nlet _ssrDataProvider: (() => SsrData | undefined) | undefined;\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _ssrDataProvider = provider;\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\nlet currentSsrData: SsrData | undefined;\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n currentSsrData = data;\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n currentSsrData = undefined;\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (_ssrDataProvider) {\n return _ssrDataProvider();\n }\n return currentSsrData;\n}\n","/**\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';\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. */\nfunction 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. */\nfunction 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// ─── 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 const setCookie: CookieSetter = (newValue: string, options?: ClientCookieOptions) => {\n const merged = { ...defaultOptions, ...options };\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"],"mappings":";;AAwCA,IAAI;AAkBJ,IAAI;;;;;;;AAQJ,SAAgB,WAAW,MAAqB;AAC9C,kBAAiB;;;;;;;;AASnB,SAAgB,eAAqB;AACnC,kBAAiB,KAAA;;;;;;;;;;;AAYnB,SAAgB,aAAkC;AAChD,KAAI,iBACF,QAAO,kBAAkB;AAE3B,QAAO;;;;;;;;;;;;;;ACxDT,IAAM,4BAAY,IAAI,KAA4B;;AAGlD,SAAS,eAAe,MAAkC;AACxD,KAAI,OAAO,aAAa,YAAa,QAAO,KAAA;CAC5C,MAAM,QAAQ,SAAS,OAAO,MAC5B,IAAI,OAAO,gBAAgB,KAAK,QAAQ,uBAAuB,OAAO,GAAG,mBAAmB,CAC7F;AACD,QAAO,QAAQ,mBAAmB,MAAM,GAAG,GAAG,KAAA;;;AAIhD,SAAS,iBAAiB,SAAuC;AAC/D,KAAI,CAAC,QAAS,QAAO;CACrB,MAAM,QAAkB,EAAE;AAC1B,OAAM,KAAK,QAAQ,QAAQ,QAAQ,MAAM;AACzC,KAAI,QAAQ,OAAQ,OAAM,KAAK,UAAU,QAAQ,SAAS;AAC1D,KAAI,QAAQ,WAAW,KAAA,EAAW,OAAM,KAAK,WAAW,QAAQ,SAAS;AACzE,KAAI,QAAQ,QAAS,OAAM,KAAK,WAAW,QAAQ,QAAQ,aAAa,GAAG;CAC3E,MAAM,WAAW,QAAQ,YAAY;AACrC,OAAM,KAAK,YAAY,SAAS,OAAO,EAAE,CAAC,aAAa,GAAG,SAAS,MAAM,EAAE,GAAG;AAC9E,KAAI,QAAQ,OAAQ,OAAM,KAAK,SAAS;AACxC,QAAO,OAAO,MAAM,KAAK,KAAK;;;AAIhC,SAAS,OAAO,MAAoB;CAClC,MAAM,OAAO,UAAU,IAAI,KAAK;AAChC,KAAI,KACF,MAAK,MAAM,MAAM,KAAM,KAAI;;;;;;;;;;;;;AAiB/B,SAAgB,UACd,MACA,gBACgF;CAChF,MAAM,aAAa,aAAqC;EACtD,IAAI,OAAO,UAAU,IAAI,KAAK;AAC9B,MAAI,CAAC,MAAM;AACT,0BAAO,IAAI,KAAK;AAChB,aAAU,IAAI,MAAM,KAAK;;AAE3B,OAAK,IAAI,SAAS;AAClB,eAAa;AACX,QAAM,OAAO,SAAS;AACtB,OAAI,KAAM,SAAS,EAAG,WAAU,OAAO,KAAK;;;CAIhD,MAAM,oBAAwC,eAAe,KAAK;CAClE,MAAM,0BAA8C,YAAY,EAAE,QAAQ,IAAI,KAAK;CAEnF,MAAM,QAAQ,qBAAqB,WAAW,aAAa,kBAAkB;CAE7E,MAAM,aAA2B,UAAkB,YAAkC;EACnF,MAAM,SAAS;GAAE,GAAG;GAAgB,GAAG;GAAS;AAChD,WAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,SAAS,GAAG,iBAAiB,OAAO;AACpF,SAAO,KAAK;;CAGd,MAAM,qBAA2B;EAC/B,MAAM,OAAO,gBAAgB,QAAQ;EACrC,MAAM,SAAS,gBAAgB;EAC/B,IAAI,YAAY,GAAG,KAAK,4DAA4D;AACpF,MAAI,OAAQ,cAAa,YAAY;AACrC,WAAS,SAAS;AAClB,SAAO,KAAK;;AAGd,QAAO;EAAC;EAAO;EAAW;EAAa"}