@timber-js/app 0.2.0-alpha.175 → 0.2.0-alpha.176

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 (51) hide show
  1. package/dist/_chunks/{actions-pN8r5Vnh.js → actions-O_LsyCE4.js} +3 -3
  2. package/dist/_chunks/{actions-pN8r5Vnh.js.map → actions-O_LsyCE4.js.map} +1 -1
  3. package/dist/_chunks/{cache-api-2hT5kfsr.js → cache-api-B-lhk9p4.js} +2 -2
  4. package/dist/_chunks/{cache-api-2hT5kfsr.js.map → cache-api-B-lhk9p4.js.map} +1 -1
  5. package/dist/_chunks/{cli-schema-sync-DvdvFwwE.js → cli-schema-sync-D3naS3eH.js} +2 -2
  6. package/dist/_chunks/{cli-schema-sync-DvdvFwwE.js.map → cli-schema-sync-D3naS3eH.js.map} +1 -1
  7. package/dist/_chunks/{logger-kUT0QH0K.js → logger-AWfuX-KJ.js} +1 -23
  8. package/dist/_chunks/logger-AWfuX-KJ.js.map +1 -0
  9. package/dist/_chunks/{walkers-Bv63zfAC.js → walkers-BtTlKviE.js} +2 -2
  10. package/dist/_chunks/{walkers-Bv63zfAC.js.map → walkers-BtTlKviE.js.map} +1 -1
  11. package/dist/cache/index.js +1 -1
  12. package/dist/cli.js +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +227 -4
  15. package/dist/index.js.map +1 -1
  16. package/dist/plugins/build-manifest.d.ts.map +1 -1
  17. package/dist/plugins/request-dep.d.ts +21 -0
  18. package/dist/plugins/request-dep.d.ts.map +1 -0
  19. package/dist/routing/index.js +2 -2
  20. package/dist/server/als-registry.d.ts +0 -26
  21. package/dist/server/als-registry.d.ts.map +1 -1
  22. package/dist/server/build-manifest.d.ts +6 -0
  23. package/dist/server/build-manifest.d.ts.map +1 -1
  24. package/dist/server/cookie-context.d.ts.map +1 -1
  25. package/dist/server/index.js +2 -2
  26. package/dist/server/internal.js +3 -3
  27. package/dist/server/internal.js.map +1 -1
  28. package/dist/server/request-context.d.ts +0 -15
  29. package/dist/server/request-context.d.ts.map +1 -1
  30. package/dist/server/request-dep.d.ts +15 -0
  31. package/dist/server/request-dep.d.ts.map +1 -0
  32. package/dist/server/route-element-builder.d.ts.map +1 -1
  33. package/dist/server/rsc-entry/helpers.d.ts +6 -6
  34. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  35. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  36. package/dist/server/slot-resolver.d.ts +1 -1
  37. package/dist/server/slot-resolver.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/index.ts +2 -0
  40. package/src/plugins/build-manifest.ts +10 -1
  41. package/src/plugins/request-dep.ts +458 -0
  42. package/src/server/als-registry.ts +0 -44
  43. package/src/server/build-manifest.ts +6 -0
  44. package/src/server/cookie-context.ts +1 -7
  45. package/src/server/request-context.ts +3 -53
  46. package/src/server/request-dep.ts +37 -0
  47. package/src/server/route-element-builder.ts +2 -6
  48. package/src/server/rsc-entry/helpers.ts +9 -26
  49. package/src/server/rsc-entry/rsc-payload.ts +4 -13
  50. package/src/server/slot-resolver.ts +52 -46
  51. package/dist/_chunks/logger-kUT0QH0K.js.map +0 -1
@@ -83,21 +83,6 @@ export declare function setMatchedSegmentPath(segmentPath: string): void;
83
83
  * @internal — used by redirect() for preserveSearchParams support.
84
84
  */
85
85
  export declare function getRequestSearchString(): string;
86
- /**
87
- * Run a callback inside a taint tracking scope. Returns whether any
88
- * request accessor (getHeaders, getSearchParams, cookies) was called
89
- * during the callback's execution.
90
- *
91
- * Uses a dedicated ALS so concurrent async components (Flight renders
92
- * siblings in parallel) each get their own isolated scope. The shared
93
- * push/pop stack model was unsound under concurrency.
94
- *
95
- * Results are stored in the request context's taintResults map, keyed
96
- * by segment/slot ID, for buildSegmentInfo() to consume.
97
- *
98
- * @internal — framework use only (route-element-builder.ts, slot-resolver.ts)
99
- */
100
- export declare function runInTaintScope<T>(segmentKey: string, fn: () => T | Promise<T>): Promise<T>;
101
86
  /**
102
87
  * Read-only Headers interface. The standard Headers class is mutable;
103
88
  * this type narrows it to read-only methods. The underlying object is
@@ -1 +1 @@
1
- {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/server/request-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACL,iBAAiB,EAKlB,MAAM,mBAAmB,CAAC;AAO3B,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAI7B;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,eAAe,CAU5C;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI1D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,eAAe,CAUjD;AAaD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CA+CxF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAMhF;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GACxC,IAAI,CAKN;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAI/D;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAG/C;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAuBjG;AAID;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,OAAO,EACP,KAAK,GAAG,KAAK,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,MAAM,CAAC,QAAQ,CACnF,CAAC;AAIF;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAmBrE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAK9D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmBhE"}
1
+ {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/server/request-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,iBAAiB,EAA4B,MAAM,mBAAmB,CAAC;AAOhF,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAI7B;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,eAAe,CAU5C;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG1D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,eAAe,CAUjD;AAaD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CA+CxF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAMhF;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GACxC,IAAI,CAKN;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAI/D;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAG/C;AAID;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,OAAO,EACP,KAAK,GAAG,KAAK,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,MAAM,CAAC,QAAQ,CACnF,CAAC;AAIF;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAmBrE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAK9D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmBhE"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Static request-dependency lookup.
3
+ *
4
+ * Queries the Vite plugin's precomputed module-graph analysis to determine
5
+ * whether a segment's entry file (layout/page/default) is request-dependent.
6
+ *
7
+ * In dev: the plugin registers a resolver function via Symbol.for bridge
8
+ * that lazily walks the RSC module graph.
9
+ * In prod: reads from the build manifest's requestDep map (set during build
10
+ * and serialized into the manifest init script by the adapter).
11
+ *
12
+ * Returns true (conservative) when no data is available.
13
+ */
14
+ export declare function isStaticRequestDependent(filePath: string): boolean;
15
+ //# sourceMappingURL=request-dep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-dep.d.ts","sourceRoot":"","sources":["../../src/server/request-dep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAkBlE"}
@@ -1 +1 @@
1
- {"version":3,"file":"route-element-builder.d.ts","sourceRoot":"","sources":["../../src/server/route-element-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAe9D,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAO1E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAyC,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AA0CnG;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAM7D;AAID;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAID,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC;IAC5B,wDAAwD;IACxD,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,qCAAqC;IACrC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B;;;;OAIG;IACH,YAAY,EAAE,aAAa,EAAE,CAAC;CAC/B;AA8DD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,YAAY,CAAC,EAAE,mBAAmB,EAClC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,EACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3C,OAAO,CAAC,kBAAkB,CAAC,CAygB7B"}
1
+ {"version":3,"file":"route-element-builder.d.ts","sourceRoot":"","sources":["../../src/server/route-element-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAe9D,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAO1E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAyC,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAyCnG;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAM7D;AAID;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAID,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC;IAC5B,wDAAwD;IACxD,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,qCAAqC;IACrC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B;;;;OAIG;IACH,YAAY,EAAE,aAAa,EAAE,CAAC;CAC/B;AA8DD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,YAAY,CAAC,EAAE,mBAAmB,EAClC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,EACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3C,OAAO,CAAC,kBAAkB,CAAC,CAsgB7B"}
@@ -73,11 +73,11 @@ export declare function parseDebugRows(text: string): DebugComponentEntry[];
73
73
  * Describes the rendered segment chain with request-dependency status,
74
74
  * enabling the client to populate its segment cache for state tree diffing.
75
75
  *
76
- * Request-dependency is determined by render-time taint tracking via
77
- * runInTaintScope(). If taint results are available (RSC payload path),
78
- * segments/slots that called getHeaders()/getSearchParams()/cookies()
79
- * are marked request-dependent. Missing taint data (SSR path) defaults
80
- * to true (safe fallback — never incorrectly cached).
76
+ * Request-dependency is determined by static module-graph analysis: the
77
+ * Vite plugin walks each segment's import graph and marks it as
78
+ * request-dependent if any reachable module calls a request accessor.
79
+ * This replaces the previous runtime ALS-based taint tracking which
80
+ * couldn't attribute accessor calls in Flight-deferred child components.
81
81
  *
82
82
  * See design/07-routing.md §"Server Diffing Rules".
83
83
  */
@@ -92,7 +92,7 @@ interface SegmentInfoEntry {
92
92
  export declare function buildSegmentInfo(segments: ManifestSegmentNode[], layoutComponents: Array<{
93
93
  component: (...args: unknown[]) => unknown;
94
94
  segment: ManifestSegmentNode;
95
- }>, slotSkipInfo?: SlotSkipEntry[], taintResults?: Map<string, boolean>, orphanTaint?: boolean, skippedSegmentKeys?: string[]): SegmentInfoEntry[];
95
+ }>, slotSkipInfo?: SlotSkipEntry[], skippedSegmentKeys?: string[]): SegmentInfoEntry[];
96
96
  /**
97
97
  * Check if a request is asking for an RSC payload (client navigation)
98
98
  * rather than full HTML. Client-side navigation sends Accept: text/x-component.
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,IAAI;IAAE,QAAQ,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,cAAc,CAAA;CAAE,CAQ/F;AAID;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,cAAc,CAAC;IACzB,yDAAyD;IACzD,gBAAgB,IAAI,MAAM,CAAC;IAC3B,yEAAyE;IACzE,aAAa,IAAI,mBAAmB,EAAE,CAAC;CACxC;AAED,wBAAgB,2BAA2B,IAAI,qBAAqB,CAkCnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,EAAE,CA8ClE;AAED;;;;;;;;;;;;GAYG;AACH,UAAU,gBAAgB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,KAAK,CAAC;IACtB,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B,CAAC,EACF,YAAY,CAAC,EAAE,aAAa,EAAE,EAC9B,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,WAAW,CAAC,EAAE,OAAO,EACrB,kBAAkB,CAAC,EAAE,MAAM,EAAE,GAC5B,gBAAgB,EAAE,CAmEpB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGzD;AAMD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,IAAI;IAAE,QAAQ,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,cAAc,CAAA;CAAE,CAQ/F;AAID;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,cAAc,CAAC;IACzB,yDAAyD;IACzD,gBAAgB,IAAI,MAAM,CAAC;IAC3B,yEAAyE;IACzE,aAAa,IAAI,mBAAmB,EAAE,CAAC;CACxC;AAED,wBAAgB,2BAA2B,IAAI,qBAAqB,CAkCnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,EAAE,CA8ClE;AAED;;;;;;;;;;;;GAYG;AACH,UAAU,gBAAgB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,KAAK,CAAC;IACtB,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B,CAAC,EACF,YAAY,CAAC,EAAE,aAAa,EAAE,EAC9B,kBAAkB,CAAC,EAAE,MAAM,EAAE,GAC5B,gBAAgB,EAAE,CAmDpB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGzD;AAMD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"rsc-payload.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,oBAAoB,EAAE,EACxC,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,CAAC,EAAE,MAAM,EAAE,EAC1B,YAAY,CAAC,EAAE,aAAa,EAAE,GAC7B,OAAO,CAAC,QAAQ,CAAC,CA8LnB"}
1
+ {"version":3,"file":"rsc-payload.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,oBAAoB,EAAE,EACxC,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,CAAC,EAAE,MAAM,EAAE,EAC1B,YAAY,CAAC,EAAE,aAAa,EAAE,GAC7B,OAAO,CAAC,QAAQ,CAAC,CAqLnB"}
@@ -33,7 +33,7 @@ type CreateElementFn = (...args: unknown[]) => React.ReactElement;
33
33
  * 3. SlotAccessGate if the segment has access.ts (gate OUTSIDE the layout,
34
34
  * so a denied layout never executes — see TIM-1074)
35
35
  */
36
- export declare function resolveSlotElement(slotNode: ManifestSegmentNode, match: RouteMatch, h: CreateElementFn, interception?: InterceptionContext, parentTreePath?: string, accessVerdicts?: SlotAccessVerdict[], taintKey?: string): Promise<React.ReactElement | null>;
36
+ export declare function resolveSlotElement(slotNode: ManifestSegmentNode, match: RouteMatch, h: CreateElementFn, interception?: InterceptionContext, parentTreePath?: string, accessVerdicts?: SlotAccessVerdict[]): Promise<React.ReactElement | null>;
37
37
  /** Result of eagerly evaluating one segment's access.ts. */
38
38
  export interface SlotAccessVerdict {
39
39
  node: ManifestSegmentNode;
@@ -1 +1 @@
1
- {"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAK9D,OAAO,EAAkC,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5F,KAAK,eAAe,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,KAAK,CAAC,YAAY,CAAC;AA4ClE;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,UAAU,EACjB,CAAC,EAAE,eAAe,EAClB,YAAY,CAAC,EAAE,mBAAmB,EAClC,cAAc,CAAC,EAAE,MAAM,EACvB,cAAc,CAAC,EAAE,iBAAiB,EAAE,EACpC,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,CAiIpC;AAmQD,4DAA4D;AAC5D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,mBAAmB,CAAC;IAC1B;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,cAAc,CAAC;CACzD;AAwDD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,kBAAkB,EAAE,OAAO,CAAC;IAC5B,0DAA0D;IAC1D,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,UAAU,oBAAoB;IAC5B,OAAO,EAAE,mBAAmB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;IAClB,CAAC,EAAE,eAAe,CAAC;IACnB,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,YAAY,EAAE,aAAa,EAAE,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,EACrC,OAAO,EACP,SAAS,EACT,KAAK,EACL,CAAC,EACD,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,cAAc,EACd,eAAe,EACf,YAAY,GACb,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAuJzD"}
1
+ {"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAK9D,OAAO,EAAkC,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5F,KAAK,eAAe,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,KAAK,CAAC,YAAY,CAAC;AAgClE;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,UAAU,EACjB,CAAC,EAAE,eAAe,EAClB,YAAY,CAAC,EAAE,mBAAmB,EAClC,cAAc,CAAC,EAAE,MAAM,EACvB,cAAc,CAAC,EAAE,iBAAiB,EAAE,GACnC,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,CAwHpC;AA8PD,4DAA4D;AAC5D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,mBAAmB,CAAC;IAC1B;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,cAAc,CAAC;CACzD;AAwDD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,kBAAkB,EAAE,OAAO,CAAC;IAC5B,0DAA0D;IAC1D,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,UAAU,oBAAoB;IAC5B,OAAO,EAAE,mBAAmB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;IAClB,CAAC,EAAE,eAAe,CAAC;IACnB,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,YAAY,EAAE,aAAa,EAAE,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,EACrC,OAAO,EACP,SAAS,EACT,KAAK,EACL,CAAC,EACD,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,cAAc,EACd,eAAe,EACf,YAAY,GACb,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAwLzD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.175",
3
+ "version": "0.2.0-alpha.176",
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",
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ import { timberChunks } from './plugins/chunks';
31
31
  import { clientChunkGroup } from './plugins/client-chunks';
32
32
  import { timberServerBundle } from './plugins/server-bundle';
33
33
  import { timberCacheTransform } from './plugins/cache';
34
+ import { timberRequestDep } from './plugins/request-dep';
34
35
  import { timberPrebuilt } from './plugins/prebuilt';
35
36
  import { timberPrerenderSugar } from './plugins/prerender-sugar';
36
37
  import { timberCloudflareWasm } from './plugins/cloudflare-wasm';
@@ -559,6 +560,7 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
559
560
  timberStaticBuild(ctx),
560
561
  timberFonts(ctx),
561
562
  timberMdx(ctx),
563
+ timberRequestDep(ctx),
562
564
  timberCacheTransform(ctx),
563
565
  timberPrerenderSugar(ctx), // export const prerender → cache.component() desugaring
564
566
  timberPrebuilt(ctx), // cache.component() → __prebuilt() callsite rewrite
@@ -393,7 +393,7 @@ export function timberBuildManifest(ctx: PluginContext): Plugin {
393
393
  '// Auto-generated build manifest — do not edit.',
394
394
  '// Production: reads manifest from globalThis (set by _timber-manifest-init.js).',
395
395
  '',
396
- 'const manifest = globalThis.__TIMBER_BUILD_MANIFEST__ ?? { css: {}, js: {}, modulepreload: {}, fonts: {}, metadataRouteHashes: {} };',
396
+ 'const manifest = globalThis.__TIMBER_BUILD_MANIFEST__ ?? { css: {}, js: {}, modulepreload: {}, fonts: {}, metadataRouteHashes: {}, requestDep: {} };',
397
397
  '',
398
398
  'export default manifest;',
399
399
  ].join('\n');
@@ -443,6 +443,15 @@ export function timberBuildManifest(ctx: PluginContext): Plugin {
443
443
  ctx.buildManifest.metadataRouteHashes = computeMetadataRouteHashes(ctx.routeTree.root);
444
444
  }
445
445
 
446
+ // Attach static request-dependency map from the request-dep plugin.
447
+ // Computed in the RSC buildEnd hook and stored on ctx.
448
+ const requestDepMap = (ctx as unknown as Record<string, unknown>).requestDepMap as
449
+ | Record<string, boolean>
450
+ | undefined;
451
+ if (requestDepMap) {
452
+ ctx.buildManifest.requestDep = requestDepMap;
453
+ }
454
+
446
455
  // When client JavaScript is disabled, strip JS chunks from the bundle
447
456
  // so Rollup never writes them to disk. CSS assets are preserved —
448
457
  // they're still needed for server-rendered HTML.
@@ -0,0 +1,458 @@
1
+ /**
2
+ * timber-request-dep — Static module-graph analysis for request-dependency.
3
+ *
4
+ * Replaces runtime ALS-based taint tracking with a build/dev-time computed
5
+ * map: segment entry file → statically request-dependent?
6
+ *
7
+ * A segment/slot is request-dependent iff any module reachable from its
8
+ * layout/page/default in the RSC module graph (stopping at client-reference
9
+ * boundaries) imports and calls a request accessor (getHeaders, cookies,
10
+ * getSearchParams, etc.) or calls .get() on a search-params/cookie
11
+ * definition object.
12
+ *
13
+ * This is deterministic, covers children/barrels/node_modules/Suspense-
14
+ * deferred code, and is computed before any byte is flushed.
15
+ *
16
+ * See design/19-client-navigation.md §"X-Timber-State-Tree Header"
17
+ */
18
+
19
+ import type { Plugin } from 'vite';
20
+ import type { PluginContext } from '../plugin-context.js';
21
+ import {
22
+ findImportBindings,
23
+ type ProgramNode,
24
+ type CallExpressionNode,
25
+ type MemberExpressionNode,
26
+ type IdentifierNode,
27
+ } from './callsite-ast.js';
28
+
29
+ // ─── Module specifiers whose bindings are request accessors ──────────────
30
+
31
+ const ACCESSOR_SOURCES = new Set(['@timber-js/app/server', 'next/headers', 'next/headers.js']);
32
+
33
+ const ACCESSOR_NAMES = new Set([
34
+ 'getHeaders',
35
+ 'getHeader',
36
+ 'getSearchParams',
37
+ 'getCookieJar',
38
+ 'getCookie',
39
+ // next/headers shim names
40
+ 'headers',
41
+ 'cookies',
42
+ ]);
43
+
44
+ const DEFINITION_FACTORY_SOURCES = new Set([
45
+ '@timber-js/app/search-params',
46
+ '@timber-js/app/cookies',
47
+ ]);
48
+
49
+ const DEFINITION_FACTORY_NAMES = new Set(['defineSearchParams', 'defineCookie']);
50
+
51
+ // ─── Fast token pre-filter ───────────────────────────────────────────────
52
+
53
+ const ACCESSOR_TOKENS = [
54
+ 'getHeaders',
55
+ 'getHeader',
56
+ 'getSearchParams',
57
+ 'getCookieJar',
58
+ 'getCookie',
59
+ 'headers',
60
+ 'cookies',
61
+ 'defineSearchParams',
62
+ 'defineCookie',
63
+ // .get() on imported/local definition bindings (searchParams.get())
64
+ '.get(',
65
+ ];
66
+
67
+ function hasRelevantToken(code: string): boolean {
68
+ for (const token of ACCESSOR_TOKENS) {
69
+ if (code.includes(token)) return true;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ // ─── Per-module facts ────────────────────────────────────────────────────
75
+
76
+ interface ModuleFacts {
77
+ /** Module directly calls a request accessor. */
78
+ callsAccessor: boolean;
79
+ /** Module imports a definition factory (defineSearchParams/defineCookie). */
80
+ importsDefinitionFactory: boolean;
81
+ /** Import specifiers where `.get()` was called on the binding. */
82
+ getterCallSources: string[];
83
+ }
84
+
85
+ // ─── AST analysis ────────────────────────────────────────────────────────
86
+
87
+ function analyzeModule(code: string, program: ProgramNode): ModuleFacts {
88
+ const facts: ModuleFacts = {
89
+ callsAccessor: false,
90
+ importsDefinitionFactory: false,
91
+ getterCallSources: [],
92
+ };
93
+
94
+ // Check for accessor imports
95
+ for (const name of ACCESSOR_NAMES) {
96
+ const bindings = findImportBindings(program, ACCESSOR_SOURCES, name);
97
+ if (bindings.named.size > 0 || bindings.namespaces.size > 0) {
98
+ // Check if any binding is actually called (not just imported)
99
+ if (hasCallSite(program, bindings.named, bindings.namespaces, name)) {
100
+ facts.callsAccessor = true;
101
+ }
102
+ }
103
+ }
104
+
105
+ // Check for definition factory imports
106
+ for (const name of DEFINITION_FACTORY_NAMES) {
107
+ const bindings = findImportBindings(program, DEFINITION_FACTORY_SOURCES, name);
108
+ if (bindings.named.size > 0 || bindings.namespaces.size > 0) {
109
+ facts.importsDefinitionFactory = true;
110
+ }
111
+ }
112
+
113
+ // Check for .get() calls on imported bindings (searchParams.get(), etc.)
114
+ // Collect which import sources have .get() called on their bindings.
115
+ for (const stmt of program.body) {
116
+ if (stmt.type !== 'ImportDeclaration') continue;
117
+ const decl = stmt as unknown as {
118
+ source: { value: string };
119
+ specifiers: Array<{ type: string; local: IdentifierNode; imported?: IdentifierNode }>;
120
+ };
121
+ for (const spec of decl.specifiers) {
122
+ if (spec.type === 'ImportDefaultSpecifier' || spec.type === 'ImportSpecifier') {
123
+ const localName = spec.local.name;
124
+ if (hasGetterCall(program, localName)) {
125
+ facts.getterCallSources.push(decl.source.value);
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ // Check for co-located definitions: const sp = defineSearchParams(...); sp.get()
132
+ // If a module both mints a definition AND calls .get() on any local binding
133
+ // whose init is a defineSearchParams/defineCookie call, it's a taint source.
134
+ if (facts.importsDefinitionFactory) {
135
+ for (const stmt of program.body) {
136
+ if (stmt.type !== 'VariableDeclaration') continue;
137
+ for (const decl of ((stmt as Record<string, unknown>).declarations as Array<
138
+ Record<string, unknown>
139
+ >) ?? []) {
140
+ if (!decl.init || !decl.id) continue;
141
+ const init = decl.init as Record<string, unknown>;
142
+ // Check if init is a call to a definition factory
143
+ if (init.type === 'CallExpression') {
144
+ const callee = init.callee as Record<string, unknown>;
145
+ if (callee.type === 'Identifier') {
146
+ const calleeName = (callee as unknown as IdentifierNode).name;
147
+ // Check if this name is a definition factory binding
148
+ for (const factoryName of DEFINITION_FACTORY_NAMES) {
149
+ const bindings = findImportBindings(program, DEFINITION_FACTORY_SOURCES, factoryName);
150
+ if (bindings.named.has(calleeName)) {
151
+ // This local variable is a definition instance
152
+ const localName =
153
+ (decl.id as Record<string, unknown>).type === 'Identifier'
154
+ ? (decl.id as unknown as IdentifierNode).name
155
+ : null;
156
+ if (localName && hasGetterCall(program, localName)) {
157
+ facts.callsAccessor = true;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ return facts;
168
+ }
169
+
170
+ /** Check if any named/namespace binding is called as a function. */
171
+ function hasCallSite(
172
+ program: ProgramNode,
173
+ named: Set<string>,
174
+ namespaces: Set<string>,
175
+ importedName: string
176
+ ): boolean {
177
+ return walkForCall(program, named, namespaces, importedName);
178
+ }
179
+
180
+ function walkForCall(
181
+ node: Record<string, unknown>,
182
+ named: Set<string>,
183
+ namespaces: Set<string>,
184
+ importedName: string
185
+ ): boolean {
186
+ if (node.type === 'CallExpression') {
187
+ const call = node as unknown as CallExpressionNode;
188
+ const callee = call.callee;
189
+ // Direct call: getHeaders()
190
+ if (callee.type === 'Identifier' && named.has((callee as IdentifierNode).name)) {
191
+ return true;
192
+ }
193
+ // Namespace call: ns.getHeaders()
194
+ if (callee.type === 'MemberExpression') {
195
+ const member = callee as MemberExpressionNode;
196
+ if (
197
+ !member.computed &&
198
+ member.object.type === 'Identifier' &&
199
+ namespaces.has((member.object as IdentifierNode).name) &&
200
+ member.property.type === 'Identifier' &&
201
+ (member.property as IdentifierNode).name === importedName
202
+ ) {
203
+ return true;
204
+ }
205
+ }
206
+ }
207
+ for (const key of Object.keys(node)) {
208
+ if (key === 'type' || key === 'start' || key === 'end') continue;
209
+ const val = node[key];
210
+ if (val && typeof val === 'object') {
211
+ if (Array.isArray(val)) {
212
+ for (const item of val) {
213
+ if (item && typeof item === 'object' && typeof item.type === 'string') {
214
+ if (walkForCall(item as Record<string, unknown>, named, namespaces, importedName))
215
+ return true;
216
+ }
217
+ }
218
+ } else if (typeof (val as Record<string, unknown>).type === 'string') {
219
+ if (walkForCall(val as Record<string, unknown>, named, namespaces, importedName))
220
+ return true;
221
+ }
222
+ }
223
+ }
224
+ return false;
225
+ }
226
+
227
+ /** Check if `localName.get(...)` appears anywhere in the module. */
228
+ function hasGetterCall(node: Record<string, unknown>, localName: string): boolean {
229
+ if (node.type === 'CallExpression') {
230
+ const call = node as unknown as CallExpressionNode;
231
+ if (call.callee.type === 'MemberExpression') {
232
+ const member = call.callee as MemberExpressionNode;
233
+ if (
234
+ !member.computed &&
235
+ member.object.type === 'Identifier' &&
236
+ (member.object as IdentifierNode).name === localName &&
237
+ member.property.type === 'Identifier' &&
238
+ (member.property as IdentifierNode).name === 'get'
239
+ ) {
240
+ return true;
241
+ }
242
+ }
243
+ }
244
+ for (const key of Object.keys(node)) {
245
+ if (key === 'type' || key === 'start' || key === 'end') continue;
246
+ const val = node[key];
247
+ if (val && typeof val === 'object') {
248
+ if (Array.isArray(val)) {
249
+ for (const item of val) {
250
+ if (item && typeof item === 'object' && typeof item.type === 'string') {
251
+ if (hasGetterCall(item as Record<string, unknown>, localName)) return true;
252
+ }
253
+ }
254
+ } else if (typeof (val as Record<string, unknown>).type === 'string') {
255
+ if (hasGetterCall(val as Record<string, unknown>, localName)) return true;
256
+ }
257
+ }
258
+ }
259
+ return false;
260
+ }
261
+
262
+ // ─── Module graph reachability ───────────────────────────────────────────
263
+
264
+ /** Walk the module graph from an entry, returning true if any taint source is reachable. */
265
+ function isReachableTaintSource(
266
+ entryId: string,
267
+ factsMap: Map<string, ModuleFacts>,
268
+ definitionModules: Set<string>,
269
+ getImports: (id: string) => string[]
270
+ ): boolean {
271
+ const visited = new Set<string>();
272
+ const stack = [entryId];
273
+
274
+ while (stack.length > 0) {
275
+ const id = stack.pop()!;
276
+ if (visited.has(id)) continue;
277
+ visited.add(id);
278
+
279
+ const facts = factsMap.get(id);
280
+ if (facts) {
281
+ if (facts.callsAccessor) return true;
282
+ // Check if any .get() call is on a definition module
283
+ for (const source of facts.getterCallSources) {
284
+ // Resolve relative to the module — for now just check if the
285
+ // resolved ID is a definition module.
286
+ // In dev, the getter source is unresolved; we check if any
287
+ // imported module is a definition module by looking at actual imports.
288
+ }
289
+ }
290
+
291
+ // Check if this module IS a definition module and has .get() callers
292
+ // (handled by the importer's facts)
293
+
294
+ const imports = getImports(id);
295
+ for (const imp of imports) {
296
+ // Stop at client boundaries — 'use client' modules in the RSC graph
297
+ // resolve to generated proxy stubs with no further imports.
298
+ // Also stop at virtual modules (they're framework internals).
299
+ if (!imp.startsWith('\0') && !visited.has(imp)) {
300
+ stack.push(imp);
301
+ }
302
+ }
303
+ }
304
+
305
+ // Second pass: check getter calls against definition modules
306
+ for (const id of visited) {
307
+ const facts = factsMap.get(id);
308
+ if (!facts) continue;
309
+ if (facts.getterCallSources.length === 0) continue;
310
+ // Check if any import of this module is a definition module
311
+ const imports = getImports(id);
312
+ for (const imp of imports) {
313
+ if (definitionModules.has(imp)) {
314
+ // This module imports a definition module and calls .get() on some binding.
315
+ // Conservative: treat it as a taint source.
316
+ return true;
317
+ }
318
+ }
319
+ }
320
+
321
+ return false;
322
+ }
323
+
324
+ // ─── Plugin ──────────────────────────────────────────────────────────────
325
+
326
+ const BRIDGE_KEY = Symbol.for('timber:request-dep');
327
+
328
+ export function timberRequestDep(ctx: PluginContext): Plugin {
329
+ const factsMap = new Map<string, ModuleFacts>();
330
+ const definitionModules = new Set<string>();
331
+ const cache = new Map<string, boolean>();
332
+
333
+ return {
334
+ name: 'timber-request-dep',
335
+ // Only run in RSC environment — that's where server components render.
336
+ applyToEnvironment(env) {
337
+ return env.name === 'rsc';
338
+ },
339
+
340
+ async transform(code, id) {
341
+ // Skip non-JS (include .mdx/.md — compiled to JS by timber-mdx before this runs)
342
+ if (!/\.[mc]?[jt]sx?$/.test(id) && !/\.mdx?$/.test(id)) return;
343
+ // Skip virtual modules
344
+ if (id.startsWith('\0') || id.startsWith('virtual:')) return;
345
+ // Fast token check
346
+ if (!hasRelevantToken(code)) return;
347
+
348
+ const program = (this as unknown as { parse(code: string): ProgramNode }).parse(code);
349
+ const facts = analyzeModule(code, program);
350
+
351
+ if (
352
+ facts.callsAccessor ||
353
+ facts.importsDefinitionFactory ||
354
+ facts.getterCallSources.length > 0
355
+ ) {
356
+ factsMap.set(id, facts);
357
+ if (facts.importsDefinitionFactory) {
358
+ definitionModules.add(id);
359
+ }
360
+ }
361
+
362
+ // No code transform needed — this is analysis only.
363
+ return undefined;
364
+ },
365
+
366
+ configureServer(server) {
367
+ // Register the bridge so runtime can query request-dep status.
368
+ const resolver = (filePath: string): boolean => {
369
+ if (cache.has(filePath)) return cache.get(filePath)!;
370
+
371
+ const rscEnv = server.environments?.rsc;
372
+ if (!rscEnv?.moduleGraph) {
373
+ return true; // Unknown → conservative
374
+ }
375
+
376
+ const getImports = (moduleId: string): string[] => {
377
+ const mod = rscEnv.moduleGraph.getModuleById(moduleId);
378
+ if (!mod) return [];
379
+ const result: string[] = [];
380
+ for (const imp of mod.importedModules) {
381
+ if (imp.id) result.push(imp.id);
382
+ }
383
+ return result;
384
+ };
385
+
386
+ const result = isReachableTaintSource(filePath, factsMap, definitionModules, getImports);
387
+ cache.set(filePath, result);
388
+ return result;
389
+ };
390
+
391
+ (globalThis as Record<symbol, unknown>)[BRIDGE_KEY] = resolver;
392
+
393
+ // Clear cache on file changes
394
+ const clearCache = () => cache.clear();
395
+ server.watcher.on('change', clearCache);
396
+ server.watcher.on('add', clearCache);
397
+ server.watcher.on('unlink', clearCache);
398
+ },
399
+
400
+ buildEnd() {
401
+ // Prod: compute the full map and register on globalThis for the
402
+ // build manifest plugin to pick up.
403
+ if (ctx.dev) return;
404
+
405
+ const getModuleInfo = (
406
+ this as unknown as {
407
+ getModuleInfo: (
408
+ id: string
409
+ ) => { importedIds?: string[]; dynamicallyImportedIds?: string[] } | null;
410
+ }
411
+ ).getModuleInfo;
412
+
413
+ const getImports = (moduleId: string): string[] => {
414
+ const info = getModuleInfo(moduleId);
415
+ if (!info) return [];
416
+ return [...(info.importedIds ?? []), ...(info.dynamicallyImportedIds ?? [])];
417
+ };
418
+
419
+ const requestDepMap: Record<string, boolean> = {};
420
+
421
+ // Walk the route tree to find all segment entry files
422
+ if (ctx.routeTree) {
423
+ walkSegmentTree(ctx.routeTree.root as unknown as WalkableNode, (filePath: string) => {
424
+ const result = isReachableTaintSource(filePath, factsMap, definitionModules, getImports);
425
+ requestDepMap[filePath] = result;
426
+ });
427
+ }
428
+
429
+ // Store on ctx for build-manifest plugin to serialize into the manifest.
430
+ // Prod runtime reads from __TIMBER_BUILD_MANIFEST__.requestDep.
431
+ (ctx as unknown as Record<string, unknown>).requestDepMap = requestDepMap;
432
+ },
433
+ };
434
+ }
435
+
436
+ interface WalkableNode {
437
+ layout?: { filePath: string };
438
+ page?: { filePath: string };
439
+ default?: { filePath: string };
440
+ children?: unknown[];
441
+ slots?: Record<string, unknown>;
442
+ }
443
+
444
+ function walkSegmentTree(node: WalkableNode, fn: (filePath: string) => void): void {
445
+ if (node.layout) fn(node.layout.filePath);
446
+ if (node.page) fn(node.page.filePath);
447
+ if (node.default) fn((node.default as { filePath: string }).filePath);
448
+ if (node.children) {
449
+ for (const child of node.children) {
450
+ walkSegmentTree(child as typeof node, fn);
451
+ }
452
+ }
453
+ if (node.slots) {
454
+ for (const slot of Object.values(node.slots)) {
455
+ walkSegmentTree(slot as typeof node, fn);
456
+ }
457
+ }
458
+ }