@timber-js/app 0.2.0-alpha.181 → 0.2.0-alpha.182

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.
@@ -17,5 +17,16 @@
17
17
  */
18
18
  import type { Plugin } from 'vite';
19
19
  import type { PluginContext } from '../plugin-context.js';
20
+ import { type ProgramNode } from './callsite-ast.js';
21
+ export declare function isFrameworkModule(id: string): boolean;
22
+ export interface ModuleFacts {
23
+ /** Module directly calls a request accessor. */
24
+ callsAccessor: boolean;
25
+ /** Module imports a definition factory (defineSearchParams/defineCookie). */
26
+ importsDefinitionFactory: boolean;
27
+ /** Import specifiers where `.get()` was called on the binding. */
28
+ getterCallSources: string[];
29
+ }
30
+ export declare function analyzeModule(code: string, program: ProgramNode): ModuleFacts;
20
31
  export declare function timberRequestDep(ctx: PluginContext): Plugin;
21
32
  //# sourceMappingURL=request-dep.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-dep.d.ts","sourceRoot":"","sources":["../../src/plugins/request-dep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAoT1D,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CA0G3D"}
1
+ {"version":3,"file":"request-dep.d.ts","sourceRoot":"","sources":["../../src/plugins/request-dep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAEL,KAAK,WAAW,EAIjB,MAAM,mBAAmB,CAAC;AA0B3B,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAKrD;AA2BD,MAAM,WAAW,WAAW;IAC1B,gDAAgD;IAChD,aAAa,EAAE,OAAO,CAAC;IACvB,6EAA6E;IAC7E,wBAAwB,EAAE,OAAO,CAAC;IAClC,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAID,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,WAAW,CAiF7E;AA0LD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAgH3D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.181",
3
+ "version": "0.2.0-alpha.182",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -158,7 +158,7 @@
158
158
  "jsonc-parser": "^3.3.1",
159
159
  "magic-string": "^0.30.21",
160
160
  "nitro": "3.0.260610-beta",
161
- "srvx": "^0.11.22"
161
+ "srvx": "^0.12.0"
162
162
  },
163
163
  "peerDependencies": {
164
164
  "@content-collections/core": "^0.14.0 || ^0.15.0",
@@ -166,7 +166,7 @@
166
166
  "@content-collections/vite": "^0.2.0 || ^0.3.0",
167
167
  "@mdx-js/rollup": "^3.0.0",
168
168
  "@vitejs/plugin-react": "^6.0.0",
169
- "@vitejs/plugin-rsc": ">=0.5.27",
169
+ "@vitejs/plugin-rsc": ">=0.5.28",
170
170
  "nuqs": "^2.0.0",
171
171
  "react": "19.2.7",
172
172
  "react-dom": "19.2.7",
@@ -63,13 +63,6 @@ function bootstrap(runtimeConfig: typeof config): void {
63
63
  setClientDeploymentId(deploymentId);
64
64
  }
65
65
 
66
- // Take manual control of scroll restoration. Even though segment tree
67
- // merging preserves shared layout DOM via cloneElement (so React doesn't
68
- // reset scroll on those elements), the root-level reactRoot.render() with
69
- // a new element tree can still cause scroll resets on the document during
70
- // reconciliation. Manual control ensures consistent behavior.
71
- window.history.scrollRestoration = 'manual';
72
-
73
66
  // Step 2: Decode inlined RSC payload (may be null for JS-only clients)
74
67
  const rscResult = createRscPayloadStream();
75
68
 
@@ -59,13 +59,24 @@ export function setupPostHydration({
59
59
  segmentInfo: initialSegmentInfo,
60
60
  });
61
61
 
62
- // Initialize scroll state for the initial entry.
63
- // When Navigation API is available, use per-entry state.
64
- // Otherwise fall back to history.state.
62
+ // Leave scrollRestoration = 'auto' so the browser natively restores
63
+ // scroll on reload, tab restore, and back-from-external-page — cases
64
+ // JS cannot reliably cover because 'manual' persists on the session
65
+ // history entry across reloads. For client navigations the router's
66
+ // restoreScrollAfterPaint() re-applies the saved position after React
67
+ // commits, writing last and winning over the browser's auto-restore.
68
+ // Explicitly set 'auto' to heal entries from previously-deployed builds
69
+ // that set 'manual'.
70
+ window.history.scrollRestoration = 'auto';
71
+
72
+ // Initialize scroll state for the initial entry. On reload the browser's
73
+ // native restoration may land before or after this runs; the debounced
74
+ // scroll listener below updates the entry state to the true position.
75
+ const initialScrollY = getScrollY();
65
76
  if (navApiController) {
66
- navApiController.saveScrollPosition(0);
77
+ navApiController.saveScrollPosition(initialScrollY);
67
78
  } else {
68
- window.history.replaceState({ timber: true, scrollY: 0 }, '');
79
+ window.history.replaceState({ timber: true, scrollY: initialScrollY }, '');
69
80
  }
70
81
 
71
82
  // Register popstate handler for back/forward navigation.
@@ -695,7 +695,9 @@ export function createRouter(deps: RouterDeps): RouterInstance {
695
695
  }
696
696
  if (error instanceof ServerErrorResponse) {
697
697
  setHardNavigating(true);
698
- window.location.href = error.url;
698
+ // Use the original `url` (with #fragment), not error.url which
699
+ // is hash-stripped. Preserves fragment on hard navigation.
700
+ window.location.href = url;
699
701
  await new Promise(() => {});
700
702
  }
701
703
  if (error instanceof NonRscResponse) {
@@ -432,6 +432,10 @@ export async function fetchRscPayload(
432
432
  throw new RedirectError(location);
433
433
  }
434
434
  }
435
+ // Server error guard (same as production path above).
436
+ if (response.headers.get('X-Timber-Error') === '1') {
437
+ throw new ServerErrorResponse(response.status, url);
438
+ }
435
439
  // Content-Type guard (same as production path above). See TIM-1231.
436
440
  const fallbackContentType = response.headers.get('content-type');
437
441
  if (
@@ -48,6 +48,15 @@ const DEFINITION_FACTORY_SOURCES = new Set([
48
48
 
49
49
  const DEFINITION_FACTORY_NAMES = new Set(['defineSearchParams', 'defineCookie']);
50
50
 
51
+ // ─── Framework module detection ─────────────────────────────────────────
52
+
53
+ export function isFrameworkModule(id: string): boolean {
54
+ return (
55
+ /[\\/]node_modules[\\/]@timber-js[\\/]app[\\/]/.test(id) ||
56
+ /[\\/]packages[\\/]timber-app[\\/]src[\\/]/.test(id)
57
+ );
58
+ }
59
+
51
60
  // ─── Fast token pre-filter ───────────────────────────────────────────────
52
61
 
53
62
  const ACCESSOR_TOKENS = [
@@ -73,7 +82,7 @@ function hasRelevantToken(code: string): boolean {
73
82
 
74
83
  // ─── Per-module facts ────────────────────────────────────────────────────
75
84
 
76
- interface ModuleFacts {
85
+ export interface ModuleFacts {
77
86
  /** Module directly calls a request accessor. */
78
87
  callsAccessor: boolean;
79
88
  /** Module imports a definition factory (defineSearchParams/defineCookie). */
@@ -84,7 +93,7 @@ interface ModuleFacts {
84
93
 
85
94
  // ─── AST analysis ────────────────────────────────────────────────────────
86
95
 
87
- function analyzeModule(code: string, program: ProgramNode): ModuleFacts {
96
+ export function analyzeModule(code: string, program: ProgramNode): ModuleFacts {
88
97
  const facts: ModuleFacts = {
89
98
  callsAccessor: false,
90
99
  importsDefinitionFactory: false,
@@ -224,7 +233,34 @@ function walkForCall(
224
233
  return false;
225
234
  }
226
235
 
227
- /** Check if `localName.get(...)` appears anywhere in the module. */
236
+ /**
237
+ * Check if the object chain of a `.get()` call contains `localName`.
238
+ * Walks through MemberExpression objects and CallExpression callees
239
+ * to handle chained patterns like `getFilterCookie(key).get()` or
240
+ * `cookies.theme.get()`.
241
+ */
242
+ function objectChainContains(node: Record<string, unknown>, name: string): boolean {
243
+ if (node.type === 'Identifier' && (node as unknown as IdentifierNode).name === name) return true;
244
+ if (node.type === 'MemberExpression') {
245
+ return objectChainContains(
246
+ (node as unknown as MemberExpressionNode).object as Record<string, unknown>,
247
+ name
248
+ );
249
+ }
250
+ if (node.type === 'CallExpression') {
251
+ return objectChainContains(
252
+ (node as unknown as CallExpressionNode).callee as Record<string, unknown>,
253
+ name
254
+ );
255
+ }
256
+ return false;
257
+ }
258
+
259
+ /**
260
+ * Check if `.get(...)` is called on `localName` anywhere in the module.
261
+ * Handles direct (`localName.get()`), chained call (`localName(args).get()`),
262
+ * and nested member (`localName.prop.get()`) patterns.
263
+ */
228
264
  function hasGetterCall(node: Record<string, unknown>, localName: string): boolean {
229
265
  if (node.type === 'CallExpression') {
230
266
  const call = node as unknown as CallExpressionNode;
@@ -232,10 +268,9 @@ function hasGetterCall(node: Record<string, unknown>, localName: string): boolea
232
268
  const member = call.callee as MemberExpressionNode;
233
269
  if (
234
270
  !member.computed &&
235
- member.object.type === 'Identifier' &&
236
- (member.object as IdentifierNode).name === localName &&
237
271
  member.property.type === 'Identifier' &&
238
- (member.property as IdentifierNode).name === 'get'
272
+ (member.property as IdentifierNode).name === 'get' &&
273
+ objectChainContains(member.object as Record<string, unknown>, localName)
239
274
  ) {
240
275
  return true;
241
276
  }
@@ -342,6 +377,12 @@ export function timberRequestDep(ctx: PluginContext): Plugin {
342
377
  if (!/\.[mc]?[jt]sx?$/.test(id) && !/\.mdx?$/.test(id)) return;
343
378
  // Skip virtual modules
344
379
  if (id.startsWith('\0') || id.startsWith('virtual:')) return;
380
+ // Skip framework modules — their internal accessor calls (e.g.,
381
+ // getCookieJar in define-cookie.ts) are implementation details.
382
+ // In dev they're externalized and never reach transform; in prod
383
+ // they would cause false positives (any module importing defineCookie
384
+ // would be tainted). Analysis relies on user-code .get() patterns.
385
+ if (isFrameworkModule(id)) return;
345
386
  // Fast token check
346
387
  if (!hasRelevantToken(code)) return;
347
388