@timber-js/app 0.1.42 → 0.1.44

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
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",
@@ -100,7 +100,7 @@
100
100
  "nuqs": "^2.0.0",
101
101
  "react": "^19.2.4",
102
102
  "react-dom": "^19.2.4",
103
- "vite": "^8.0.0",
103
+ "vite": "^8.0.1",
104
104
  "zod": "^3.22.0 || ^4.0.0"
105
105
  },
106
106
  "peerDependenciesMeta": {
@@ -47,16 +47,35 @@ export interface NavigationState {
47
47
  * The context is created lazily to avoid calling createContext at module
48
48
  * level. In the RSC environment, React.createContext doesn't exist —
49
49
  * calling it at import time would crash the server.
50
+ *
51
+ * IMPORTANT: Context instances are stored on globalThis, NOT in module-
52
+ * level variables. The RSC client bundler duplicates this module across
53
+ * the browser-entry chunk (index) and client-reference chunk (shared-app)
54
+ * because both entry graphs import it. Module-level variables would create
55
+ * separate singleton instances per chunk — the provider in TransitionRoot
56
+ * (index chunk) would use context A while the consumer in LinkStatusProvider
57
+ * (shared-app chunk) reads from context B. globalThis guarantees a single
58
+ * instance regardless of how many times the module is duplicated.
59
+ *
60
+ * See design/19-client-navigation.md §"Singleton Context Guarantee"
50
61
  */
51
- let _context: React.Context<NavigationState | null> | undefined;
62
+
63
+ // Symbol keys for globalThis storage — prevents collisions with user code
64
+ const NAV_CTX_KEY = Symbol.for('__timber_nav_ctx');
65
+ const PENDING_CTX_KEY = Symbol.for('__timber_pending_nav_ctx');
52
66
 
53
67
  function getOrCreateContext(): React.Context<NavigationState | null> | undefined {
54
- if (_context !== undefined) return _context;
68
+ const existing = (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] as
69
+ | React.Context<NavigationState | null>
70
+ | undefined;
71
+ if (existing !== undefined) return existing;
55
72
  // createContext may not exist in the RSC environment
56
73
  if (typeof React.createContext === 'function') {
57
- _context = React.createContext<NavigationState | null>(null);
74
+ const ctx = React.createContext<NavigationState | null>(null);
75
+ (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] = ctx;
76
+ return ctx;
58
77
  }
59
- return _context;
78
+ return undefined;
60
79
  }
61
80
 
62
81
  /**
@@ -101,22 +120,34 @@ export function NavigationProvider({ value, children }: NavigationProviderProps)
101
120
  // ---------------------------------------------------------------------------
102
121
 
103
122
  /**
104
- * Module-level navigation state. Updated by the router before calling
105
- * renderRoot(). The renderRoot callback reads this to create the
106
- * NavigationProvider with the correct values.
123
+ * Navigation state communicated between the router and renderRoot.
124
+ *
125
+ * The router calls setNavigationState() before renderRoot(). The
126
+ * renderRoot callback reads via getNavigationState() to create the
127
+ * NavigationProvider with the correct params/pathname.
107
128
  *
108
129
  * This is NOT used by hooks directly — hooks read from React context.
109
- * This exists only as a communication channel between the router
110
- * (which knows the new nav state) and renderRoot (which wraps the element).
130
+ *
131
+ * Stored on globalThis (like the context instances above) because the
132
+ * router lives in the shared-app chunk while renderRoot lives in the
133
+ * index chunk. Module-level variables would be separate per chunk.
111
134
  */
112
- let _currentNavState: NavigationState = { params: {}, pathname: '/' };
135
+ const NAV_STATE_KEY = Symbol.for('__timber_nav_state');
136
+
137
+ function _getNavStateStore(): { current: NavigationState } {
138
+ const g = globalThis as Record<symbol, unknown>;
139
+ if (!g[NAV_STATE_KEY]) {
140
+ g[NAV_STATE_KEY] = { current: { params: {}, pathname: '/' } };
141
+ }
142
+ return g[NAV_STATE_KEY] as { current: NavigationState };
143
+ }
113
144
 
114
145
  export function setNavigationState(state: NavigationState): void {
115
- _currentNavState = state;
146
+ _getNavStateStore().current = state;
116
147
  }
117
148
 
118
149
  export function getNavigationState(): NavigationState {
119
- return _currentNavState;
150
+ return _getNavStateStore().current;
120
151
  }
121
152
 
122
153
  // ---------------------------------------------------------------------------
@@ -125,23 +156,25 @@ export function getNavigationState(): NavigationState {
125
156
 
126
157
  /**
127
158
  * Separate context for the in-flight navigation URL. Provided by
128
- * TransitionRoot (useOptimistic state), consumed by LinkStatusProvider
159
+ * TransitionRoot (urgent useState), consumed by LinkStatusProvider
129
160
  * and useNavigationPending.
130
161
  *
131
- * Lives in this module (not a separate file) to guarantee singleton
132
- * identity across chunks. The `'use client'` LinkStatusProvider and
133
- * the non-directive TransitionRoot both import from this module —
134
- * if they were in separate files, the bundler could duplicate the
135
- * module-level context variable across chunks.
162
+ * Uses globalThis via Symbol.for for the same reason as NavigationContext
163
+ * above the bundler duplicates this module across chunks, and module-
164
+ * level variables would create separate context instances.
136
165
  */
137
- let _pendingContext: React.Context<string | null> | undefined;
138
166
 
139
167
  function getOrCreatePendingContext(): React.Context<string | null> | undefined {
140
- if (_pendingContext !== undefined) return _pendingContext;
168
+ const existing = (globalThis as Record<symbol, unknown>)[PENDING_CTX_KEY] as
169
+ | React.Context<string | null>
170
+ | undefined;
171
+ if (existing !== undefined) return existing;
141
172
  if (typeof React.createContext === 'function') {
142
- _pendingContext = React.createContext<string | null>(null);
173
+ const ctx = React.createContext<string | null>(null);
174
+ (globalThis as Record<symbol, unknown>)[PENDING_CTX_KEY] = ctx;
175
+ return ctx;
143
176
  }
144
- return _pendingContext;
177
+ return undefined;
145
178
  }
146
179
 
147
180
  /**