@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
@@ -60,6 +60,7 @@ import type { Plugin } from 'vite';
60
60
  import type { PluginContext } from '../plugin-context.js';
61
61
  import {
62
62
  CACHE_MODULE_SOURCES,
63
+ collectShadowRanges,
63
64
  findCallSites,
64
65
  findImportBindings,
65
66
  generateStableCallsiteId,
@@ -71,6 +72,7 @@ import type {
71
72
  MemberExpressionNode,
72
73
  PositionedNode,
73
74
  ProgramNode,
75
+ ShadowRange,
74
76
  } from './callsite-ast.js';
75
77
 
76
78
  /**
@@ -96,6 +98,25 @@ function isCacheCallee(callee: PositionedNode, bindings: ImportBindings): boolea
96
98
  return false;
97
99
  }
98
100
 
101
+ /**
102
+ * Extract the local binding name that a cache callee resolves to — the
103
+ * identifier for named imports, or the namespace object for `ns.cache()`.
104
+ */
105
+ function calleeBindingName(callee: PositionedNode, bindings: ImportBindings): string | null {
106
+ if (callee.type === 'Identifier') {
107
+ const name = (callee as IdentifierNode).name;
108
+ return bindings.named.has(name) ? name : null;
109
+ }
110
+ if (callee.type === 'MemberExpression') {
111
+ const member = callee as unknown as MemberExpressionNode;
112
+ if (member.object.type === 'Identifier') {
113
+ const name = (member.object as IdentifierNode).name;
114
+ return bindings.namespaces.has(name) ? name : null;
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+
99
120
  /**
100
121
  * Generate a stable callsite ID for a cache() call. See callsite-ast.ts for
101
122
  * the derivation properties.
@@ -134,6 +155,25 @@ export function timberCacheTransform(ctx: PluginContext): Plugin {
134
155
  const cacheBindings = findImportBindings(program, CACHE_MODULE_SOURCES, 'cache');
135
156
  if (cacheBindings.named.size === 0 && cacheBindings.namespaces.size === 0) return;
136
157
 
158
+ // Collect byte ranges where tracked binding names are shadowed by
159
+ // function/arrow parameters or catch bindings (TIM-1141). Calls inside
160
+ // these ranges are NOT rewritten — they fall back to the runtime fnId.
161
+ const trackedNames = new Set([...cacheBindings.named, ...cacheBindings.namespaces]);
162
+ const shadowRanges: ShadowRange[] = [];
163
+ collectShadowRanges(program, trackedNames, shadowRanges);
164
+
165
+ // Check whether the specific binding name a callee resolves to is
166
+ // shadowed at the call position. Only that name is blocked — other
167
+ // tracked imports from the same module remain valid (codex review
168
+ // on PR #876).
169
+ const isCalleeShadowed = (call: CallExpressionNode): boolean => {
170
+ const name = calleeBindingName(call.callee, cacheBindings);
171
+ if (!name) return false;
172
+ return shadowRanges.some(
173
+ (r) => call.start >= r.start && call.start < r.end && r.names.has(name)
174
+ );
175
+ };
176
+
137
177
  // Collect call sites per top-level statement so each call carries its
138
178
  // enclosing statement's source text into the ID hash.
139
179
  const callSites: Array<{ call: CallExpressionNode; stmt: PositionedNode }> = [];
@@ -141,7 +181,10 @@ export function timberCacheTransform(ctx: PluginContext): Plugin {
141
181
  const calls: CallExpressionNode[] = [];
142
182
  findCallSites(
143
183
  stmt,
144
- (call) => call.arguments.length === 2 && isCacheCallee(call.callee, cacheBindings),
184
+ (call) =>
185
+ call.arguments.length === 2 &&
186
+ isCacheCallee(call.callee, cacheBindings) &&
187
+ !isCalleeShadowed(call),
145
188
  calls
146
189
  );
147
190
  for (const call of calls) callSites.push({ call, stmt });
@@ -95,16 +95,321 @@ export function findImportBindings(
95
95
  return { named, namespaces };
96
96
  }
97
97
 
98
+ /** Extract all bound identifier names from a parameter/binding pattern. */
99
+ function collectPatternNames(node: PositionedNode, out: Set<string>): void {
100
+ switch (node.type) {
101
+ case 'Identifier':
102
+ out.add((node as IdentifierNode).name);
103
+ break;
104
+ case 'AssignmentPattern':
105
+ collectPatternNames(node.left as PositionedNode, out);
106
+ break;
107
+ case 'RestElement':
108
+ collectPatternNames(node.argument as PositionedNode, out);
109
+ break;
110
+ case 'ArrayPattern':
111
+ for (const el of (node.elements as (PositionedNode | null)[] | undefined) ?? []) {
112
+ if (el) collectPatternNames(el, out);
113
+ }
114
+ break;
115
+ case 'ObjectPattern':
116
+ for (const prop of (node.properties as PositionedNode[] | undefined) ?? []) {
117
+ if (prop.type === 'RestElement') {
118
+ collectPatternNames(prop, out);
119
+ } else {
120
+ collectPatternNames(prop.value as PositionedNode, out);
121
+ }
122
+ }
123
+ break;
124
+ case 'TSParameterProperty':
125
+ // TS constructor parameter properties: `constructor(private cache)`
126
+ if (node.parameter) collectPatternNames(node.parameter as PositionedNode, out);
127
+ break;
128
+ }
129
+ }
130
+
131
+ /** Return the intersection of param names with `tracked`, or null if none. */
132
+ function shadowedParamNames(
133
+ params: PositionedNode[] | undefined,
134
+ tracked: ReadonlySet<string>
135
+ ): Set<string> | null {
136
+ if (!params) return null;
137
+ const names = new Set<string>();
138
+ for (const p of params) collectPatternNames(p, names);
139
+ const hit = new Set<string>();
140
+ for (const n of names) {
141
+ if (tracked.has(n)) hit.add(n);
142
+ }
143
+ return hit.size > 0 ? hit : null;
144
+ }
145
+
146
+ /**
147
+ * Collect names declared by `VariableDeclaration`, `FunctionDeclaration`,
148
+ * and `ClassDeclaration` in a statement list. Only scans direct children
149
+ * (not nested blocks). For-loop bindings are NOT included — they are
150
+ * scoped to the loop statement and handled separately in
151
+ * `collectShadowRanges` (codex review round 4 on PR #876).
152
+ */
153
+ function collectLexicalDeclNames(stmts: PositionedNode[], out: Set<string>): void {
154
+ for (const stmt of stmts) {
155
+ if (stmt.type === 'VariableDeclaration') {
156
+ for (const decl of (stmt.declarations as PositionedNode[] | undefined) ?? []) {
157
+ if (decl.id) collectPatternNames(decl.id as PositionedNode, out);
158
+ }
159
+ } else if (stmt.type === 'FunctionDeclaration' || stmt.type === 'ClassDeclaration') {
160
+ if (stmt.id && (stmt.id as PositionedNode).type === 'Identifier') {
161
+ out.add((stmt.id as IdentifierNode).name);
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Extract binding names from a for-loop's init/left clause, but ONLY for
169
+ * `const`/`let` (block-scoped). `var` bindings hoist to the enclosing
170
+ * function and are handled by `collectVarDeclNames` instead (codex review
171
+ * round 5 on PR #876).
172
+ */
173
+ function collectForLoopBlockBindingNames(loop: PositionedNode, out: Set<string>): void {
174
+ const init = (loop.init ?? loop.left) as PositionedNode | undefined;
175
+ if (init?.type === 'VariableDeclaration' && init.kind !== 'var') {
176
+ for (const decl of (init.declarations as PositionedNode[] | undefined) ?? []) {
177
+ if (decl.id) collectPatternNames(decl.id as PositionedNode, out);
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Recursively collect `var` declaration names from a statement tree.
184
+ * `var` hoists to the enclosing function regardless of block nesting,
185
+ * so we must scan nested blocks, loops, switch cases, etc.
186
+ */
187
+ function collectVarDeclNames(stmts: PositionedNode[], out: Set<string>): void {
188
+ for (const stmt of stmts) {
189
+ if (stmt.type === 'VariableDeclaration' && stmt.kind === 'var') {
190
+ for (const decl of (stmt.declarations as PositionedNode[] | undefined) ?? []) {
191
+ if (decl.id) collectPatternNames(decl.id as PositionedNode, out);
192
+ }
193
+ }
194
+ // Recurse into blocks, loops, if/else, switch, etc.
195
+ if (stmt.type === 'BlockStatement' || stmt.type === 'StaticBlock') {
196
+ collectVarDeclNames((stmt.body as PositionedNode[] | undefined) ?? [], out);
197
+ } else if (stmt.type === 'IfStatement') {
198
+ if (stmt.consequent) collectVarDeclNames([stmt.consequent as PositionedNode], out);
199
+ if (stmt.alternate) collectVarDeclNames([stmt.alternate as PositionedNode], out);
200
+ } else if (stmt.type === 'SwitchStatement') {
201
+ for (const c of (stmt.cases as PositionedNode[] | undefined) ?? []) {
202
+ collectVarDeclNames((c.consequent as PositionedNode[] | undefined) ?? [], out);
203
+ }
204
+ } else if (
205
+ stmt.type === 'ForStatement' ||
206
+ stmt.type === 'ForInStatement' ||
207
+ stmt.type === 'ForOfStatement' ||
208
+ stmt.type === 'WhileStatement' ||
209
+ stmt.type === 'DoWhileStatement'
210
+ ) {
211
+ const init = (stmt.init ?? stmt.left) as PositionedNode | undefined;
212
+ if (init?.type === 'VariableDeclaration' && init.kind === 'var') {
213
+ for (const decl of (init.declarations as PositionedNode[] | undefined) ?? []) {
214
+ if (decl.id) collectPatternNames(decl.id as PositionedNode, out);
215
+ }
216
+ }
217
+ if (stmt.body) collectVarDeclNames([stmt.body as PositionedNode], out);
218
+ } else if (stmt.type === 'WithStatement' || stmt.type === 'LabeledStatement') {
219
+ if (stmt.body) collectVarDeclNames([stmt.body as PositionedNode], out);
220
+ } else if (stmt.type === 'TryStatement') {
221
+ if (stmt.block) collectVarDeclNames([stmt.block as PositionedNode], out);
222
+ const handler = stmt.handler as PositionedNode | undefined;
223
+ if (handler?.body) collectVarDeclNames([handler.body as PositionedNode], out);
224
+ if (stmt.finalizer) collectVarDeclNames([stmt.finalizer as PositionedNode], out);
225
+ }
226
+ }
227
+ }
228
+
229
+ export interface ShadowRange {
230
+ start: number;
231
+ end: number;
232
+ /** Which tracked binding names are shadowed in this range. */
233
+ names: ReadonlySet<string>;
234
+ }
235
+
236
+ /**
237
+ * Collect byte ranges where any name in `tracked` is shadowed by a
238
+ * function/arrow parameter, catch clause binding, or lexical declaration
239
+ * (`const`/`let`/`var`, `function`, `class`, for-loop binding). Calls
240
+ * inside these ranges must NOT be rewritten — the name resolves to the
241
+ * local binding, not the import (TIM-1141).
242
+ *
243
+ * Each range records WHICH names are shadowed so the consumer can skip
244
+ * only calls to the shadowed name, not unrelated imports (codex review
245
+ * on PR #876).
246
+ *
247
+ * When a scope shadows some-but-not-all tracked names, the walker
248
+ * continues descending to find inner scopes that shadow remaining names
249
+ * (codex review round 2 on PR #876).
250
+ */
251
+ export function collectShadowRanges(
252
+ node: PositionedNode,
253
+ tracked: ReadonlySet<string>,
254
+ ranges: ShadowRange[]
255
+ ): void {
256
+ if (tracked.size === 0) return;
257
+
258
+ const isFn =
259
+ node.type === 'FunctionDeclaration' ||
260
+ node.type === 'FunctionExpression' ||
261
+ node.type === 'ArrowFunctionExpression';
262
+
263
+ let remainingTracked = tracked;
264
+
265
+ // ── Function/arrow parameters + named function expression id ───────
266
+ if (isFn) {
267
+ const paramNames = new Set<string>();
268
+ for (const p of (node.params as PositionedNode[] | undefined) ?? []) {
269
+ collectPatternNames(p, paramNames);
270
+ }
271
+ // Named function expressions: `const w = function cache() { ... }`
272
+ // — the name is in scope inside the body (codex review round 5).
273
+ if (node.type === 'FunctionExpression' && node.id) {
274
+ const fnId = node.id as PositionedNode;
275
+ if (fnId.type === 'Identifier') paramNames.add((fnId as IdentifierNode).name);
276
+ }
277
+ const shadowed = new Set<string>();
278
+ for (const n of paramNames) {
279
+ if (tracked.has(n)) shadowed.add(n);
280
+ }
281
+ if (shadowed.size > 0) {
282
+ ranges.push({ start: node.start, end: node.end, names: shadowed });
283
+ if (shadowed.size === tracked.size) return;
284
+ remainingTracked = new Set([...tracked].filter((n) => !shadowed.has(n)));
285
+ }
286
+ }
287
+
288
+ // ── Catch clause binding ───────────────────────────────────────────
289
+ if (node.type === 'CatchClause') {
290
+ const param = node.param as PositionedNode | undefined;
291
+ if (param) {
292
+ const shadowed = shadowedParamNames([param], tracked);
293
+ if (shadowed) {
294
+ ranges.push({ start: node.start, end: node.end, names: shadowed });
295
+ if (shadowed.size === tracked.size) return;
296
+ remainingTracked = new Set([...tracked].filter((n) => !shadowed.has(n)));
297
+ }
298
+ }
299
+ }
300
+
301
+ // ── Named class expressions ───────────────────────────────────────
302
+ // `const C = class cache { ... }` — the class name is in scope inside
303
+ // the class body (codex review round 6 on PR #876).
304
+ if (node.type === 'ClassExpression' && node.id) {
305
+ const clsId = node.id as PositionedNode;
306
+ if (clsId.type === 'Identifier' && remainingTracked.has((clsId as IdentifierNode).name)) {
307
+ const names = new Set([(clsId as IdentifierNode).name]);
308
+ ranges.push({ start: node.start, end: node.end, names });
309
+ if (names.size === remainingTracked.size) return;
310
+ remainingTracked = new Set([...remainingTracked].filter((n) => !names.has(n)));
311
+ }
312
+ }
313
+
314
+ // ── For-loop bindings (scoped to the loop, not the enclosing body) ─
315
+ // `for (const cache of items) { ... }` shadows `cache` only inside
316
+ // the loop statement, not in the rest of the function. Codex review
317
+ // round 4 on PR #876.
318
+ if (
319
+ node.type === 'ForStatement' ||
320
+ node.type === 'ForInStatement' ||
321
+ node.type === 'ForOfStatement'
322
+ ) {
323
+ const loopNames = new Set<string>();
324
+ collectForLoopBlockBindingNames(node, loopNames);
325
+ const loopShadowed = new Set<string>();
326
+ for (const n of loopNames) {
327
+ if (remainingTracked.has(n)) loopShadowed.add(n);
328
+ }
329
+ if (loopShadowed.size > 0) {
330
+ ranges.push({ start: node.start, end: node.end, names: loopShadowed });
331
+ if (loopShadowed.size === remainingTracked.size) return;
332
+ remainingTracked = new Set([...remainingTracked].filter((n) => !loopShadowed.has(n)));
333
+ }
334
+ }
335
+
336
+ // ── Lexical declarations in function/block/switch bodies ────────────
337
+ // `const cache = ...`, `function cache() {}`, `class cache {}` inside
338
+ // a function body, block, or switch shadow the tracked name for the
339
+ // enclosing scope. `var` hoists to the enclosing function — scanned
340
+ // recursively. Codex review rounds 3 & 5 on PR #876.
341
+ const isScope =
342
+ isFn ||
343
+ node.type === 'BlockStatement' ||
344
+ node.type === 'SwitchStatement' ||
345
+ node.type === 'StaticBlock';
346
+ if (isScope) {
347
+ let bodyStmts: PositionedNode[] | null = null;
348
+ if (isFn) {
349
+ const body = node.body as PositionedNode | undefined;
350
+ if (body?.type === 'BlockStatement') {
351
+ bodyStmts = (body.body as PositionedNode[] | undefined) ?? null;
352
+ }
353
+ } else if (node.type === 'SwitchStatement') {
354
+ // Switch cases: declarations in unbraced cases are scoped to the switch block
355
+ const caseStmts: PositionedNode[] = [];
356
+ for (const c of (node.cases as PositionedNode[] | undefined) ?? []) {
357
+ for (const s of (c.consequent as PositionedNode[] | undefined) ?? []) {
358
+ caseStmts.push(s);
359
+ }
360
+ }
361
+ bodyStmts = caseStmts;
362
+ } else {
363
+ bodyStmts = (node.body as PositionedNode[] | undefined) ?? null;
364
+ }
365
+ if (bodyStmts) {
366
+ const declNames = new Set<string>();
367
+ collectLexicalDeclNames(bodyStmts, declNames);
368
+ // For function scopes, also collect hoisted `var` declarations
369
+ // from the entire body tree (var hoists past blocks).
370
+ if (isFn) {
371
+ collectVarDeclNames(bodyStmts, declNames);
372
+ }
373
+ const lexShadowed = new Set<string>();
374
+ for (const n of declNames) {
375
+ if (remainingTracked.has(n)) lexShadowed.add(n);
376
+ }
377
+ if (lexShadowed.size > 0) {
378
+ ranges.push({ start: node.start, end: node.end, names: lexShadowed });
379
+ if (lexShadowed.size === remainingTracked.size) return;
380
+ remainingTracked = new Set([...remainingTracked].filter((n) => !lexShadowed.has(n)));
381
+ }
382
+ }
383
+ }
384
+
385
+ // ── Recurse into children ──────────────────────────────────────────
386
+ for (const key of Object.keys(node)) {
387
+ if (key === 'type' || key === 'start' || key === 'end') continue;
388
+ const val = node[key];
389
+ if (val && typeof val === 'object') {
390
+ if (Array.isArray(val)) {
391
+ for (const item of val) {
392
+ if (item && typeof item === 'object' && typeof item.type === 'string') {
393
+ collectShadowRanges(item as PositionedNode, remainingTracked, ranges);
394
+ }
395
+ }
396
+ } else if (typeof (val as PositionedNode).type === 'string') {
397
+ collectShadowRanges(val as PositionedNode, remainingTracked, ranges);
398
+ }
399
+ }
400
+ }
401
+ }
402
+
98
403
  /**
99
404
  * Recursively walk AST nodes collecting call expressions accepted by
100
405
  * `matches`. Calls are typically at module scope, but calls inside
101
406
  * functions/blocks are handled for completeness.
102
407
  *
103
- * NOTE: matching is by binding NAME, so nested scopes that shadow a tracked
104
- * import can produce false positives for consumers of this walker (see
105
- * TIM-1141 for the cache transform). The prebuilt transform avoids the
106
- * class entirely by not using this walker — it allowlists top-level
107
- * declarator positions instead (plugins/prebuilt.ts).
408
+ * Consumers that match by binding NAME should pair this with
409
+ * `collectShadowRanges` to filter out calls in scopes where the tracked
410
+ * name is shadowed (TIM-1141). The prebuilt transform avoids the class
411
+ * entirely by allowlisting top-level declarator positions
412
+ * (plugins/prebuilt.ts).
108
413
  */
109
414
  export function findCallSites(
110
415
  node: PositionedNode,
@@ -74,6 +74,14 @@ export function timberFonts(ctx: PluginContext): Plugin {
74
74
  configResolved(config: ResolvedConfig) {
75
75
  resolvedBase = config.base;
76
76
  pipeline.setBase(config.base);
77
+
78
+ // Font binaries are emitted in generateBundle, after Vite's CSS
79
+ // url() resolution runs. The warnings are benign — suppress them.
80
+ const originalWarnOnce = config.logger.warnOnce.bind(config.logger);
81
+ config.logger.warnOnce = (msg, options) => {
82
+ if (msg.includes('_timber/fonts/') && msg.includes("didn't resolve at build time")) return;
83
+ originalWarnOnce(msg, options);
84
+ };
77
85
  },
78
86
 
79
87
  /**
@@ -158,6 +158,13 @@ export async function runPrebuiltCapture(ctx: PluginContext): Promise<CaptureSum
158
158
 
159
159
  console.log('[timber] cache.component: capturing prebuilt flight payloads…');
160
160
 
161
+ // Tell the RSC entry to skip createRequestHandler — the capture pass
162
+ // only needs __timber_capturePrebuilt + the route manifest. Running the
163
+ // full production startup (instrumentation, cache handler, CDN purge,
164
+ // OTEL tracing, route matchers) is wasted work that can take 100+ seconds
165
+ // on large apps.
166
+ g.__TIMBER_CAPTURE_MODE__ = true;
167
+
161
168
  let mod: CaptureModule;
162
169
  try {
163
170
  // Cache-busting query: in a long-lived build process (vite build
@@ -240,6 +240,14 @@ export function timberShims(_ctx: PluginContext): Plugin {
240
240
  //
241
241
  // In Vite, we intercept them here to resolve to src/ for dev/build
242
242
  // correctness (same rationale as the @timber-js/app/* remap above).
243
+ if (cleanId === '#server-only-guard') {
244
+ const env = envName(this);
245
+ if (env === 'client') {
246
+ return SERVER_ONLY_VIRTUAL;
247
+ }
248
+ return resolve(PKG_ROOT, 'src', 'server', 'server-only-guard-noop.js');
249
+ }
250
+
243
251
  if (cleanId === '#server-internal') {
244
252
  const env = envName(this);
245
253
  if (env === 'client') {
@@ -288,6 +296,22 @@ export function timberShims(_ctx: PluginContext): Plugin {
288
296
  return resolve(PKG_ROOT, 'src', 'server', 'index.ts');
289
297
  }
290
298
 
299
+ // Alias hardening: monorepo dev configs (tests/shared-vite-config.ts,
300
+ // packages/website) alias @timber-js/app/server to the absolute
301
+ // src/server path for vitest compatibility. Vite's resolve.alias runs
302
+ // before every plugin, so the bare-specifier branch above never sees
303
+ // the import in those apps — the real server barrel (and its
304
+ // AsyncLocalStorage) would be bundled into the browser. Catch the
305
+ // already-aliased absolute path here and return the same stub.
306
+ if (
307
+ cleanId === resolve(PKG_ROOT, 'src', 'server', 'index.ts') ||
308
+ cleanId === resolve(PKG_ROOT, 'src', 'server', 'internal.ts')
309
+ ) {
310
+ if (envName(this) === 'client') {
311
+ return '\0timber:server-empty';
312
+ }
313
+ }
314
+
291
315
  if (cleanId === '@timber-js/app/client') {
292
316
  const env = envName(this);
293
317
  // RSC: return null — needs dist/ for 'use client' boundary detection.
@@ -167,7 +167,16 @@ export async function executeAction(
167
167
  // cache.invalidate handles both data-cache clearing AND component-seed
168
168
  // tombstone writes (design/45 §ISR mechanics) — no separate tombstone
169
169
  // step needed here.
170
- await Promise.all(state.tags.map((tag) => cache.invalidate({ tag })));
170
+ // Best-effort: the action body already executed and its mutation committed.
171
+ // Use allSettled so all tags settle before proceeding — Promise.all would
172
+ // short-circuit on the first rejection, leaving other invalidations (including
173
+ // component-seed tombstone writes) in-flight during revalidatePath rendering.
174
+ const results = await Promise.allSettled(state.tags.map((tag) => cache.invalidate({ tag })));
175
+ for (const r of results) {
176
+ if (r.status === 'rejected') {
177
+ console.error('[timber] revalidateTag invalidation failed:', r.reason);
178
+ }
179
+ }
171
180
  }
172
181
 
173
182
  // Process path revalidation — build element tree (not yet serialized)
@@ -31,6 +31,7 @@
31
31
  * `tests/als-registry-singleton.test.ts`.
32
32
  */
33
33
 
34
+ import '#server-only-guard';
34
35
  import { AsyncLocalStorage } from 'node:async_hooks';
35
36
  import type { DebugComponentEntry } from './rsc-entry/helpers.js';
36
37
 
@@ -33,7 +33,7 @@ export {
33
33
  // RenderError removed from public exports per TIM-724.
34
34
  // Use deny({ status: 500, data: ... }) instead. RenderError stays in
35
35
  // #server-internal for pipeline catch logic.
36
- export type { RenderErrorDigest, WaitUntilAdapter } from './primitives';
36
+ export type { RenderErrorDigest } from './primitives';
37
37
  // JsonSerializable moved to @timber-js/app/codec as the single canonical path.
38
38
  // Re-export removed per TIM-721.
39
39