@timber-js/app 0.2.0-alpha.175 → 0.2.0-alpha.177
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/dist/_chunks/{actions-pN8r5Vnh.js → actions-O_LsyCE4.js} +3 -3
- package/dist/_chunks/{actions-pN8r5Vnh.js.map → actions-O_LsyCE4.js.map} +1 -1
- package/dist/_chunks/{cache-api-2hT5kfsr.js → cache-api-B-lhk9p4.js} +2 -2
- package/dist/_chunks/{cache-api-2hT5kfsr.js.map → cache-api-B-lhk9p4.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-DvdvFwwE.js → cli-schema-sync-D3naS3eH.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-DvdvFwwE.js.map → cli-schema-sync-D3naS3eH.js.map} +1 -1
- package/dist/_chunks/{logger-kUT0QH0K.js → logger-AWfuX-KJ.js} +1 -23
- package/dist/_chunks/logger-AWfuX-KJ.js.map +1 -0
- package/dist/_chunks/{walkers-Bv63zfAC.js → walkers-BtTlKviE.js} +2 -2
- package/dist/_chunks/{walkers-Bv63zfAC.js.map → walkers-BtTlKviE.js.map} +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +227 -4
- package/dist/index.js.map +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/request-dep.d.ts +21 -0
- package/dist/plugins/request-dep.d.ts.map +1 -0
- package/dist/routing/index.js +2 -2
- package/dist/server/als-registry.d.ts +0 -26
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +6 -0
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +3 -3
- package/dist/server/internal.js.map +1 -1
- package/dist/server/request-context.d.ts +0 -15
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-dep.d.ts +15 -0
- package/dist/server/request-dep.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +6 -6
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/plugins/build-manifest.ts +10 -1
- package/src/plugins/request-dep.ts +458 -0
- package/src/server/als-registry.ts +0 -44
- package/src/server/build-manifest.ts +6 -0
- package/src/server/cookie-context.ts +1 -7
- package/src/server/request-context.ts +3 -53
- package/src/server/request-dep.ts +37 -0
- package/src/server/route-element-builder.ts +2 -6
- package/src/server/rsc-entry/helpers.ts +9 -26
- package/src/server/rsc-entry/rsc-payload.ts +4 -13
- package/src/server/slot-resolver.ts +52 -46
- package/src/server/state-tree-diff.ts +5 -6
- 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,
|
|
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;
|
|
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
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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[],
|
|
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;
|
|
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,
|
|
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[]
|
|
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;
|
|
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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-tree-diff.d.ts","sourceRoot":"","sources":["../../src/server/state-tree-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAI9D;;;;GAIG;AACH,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,EAAE,CAiBxE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIhF;AAID,2EAA2E;AAC3E,MAAM,WAAW,eAAgB,SAAQ,GAAG,CAAC,MAAM,CAAC;IAClD,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"state-tree-diff.d.ts","sourceRoot":"","sources":["../../src/server/state-tree-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAI9D;;;;GAIG;AACH,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,EAAE,CAiBxE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIhF;AAID,2EAA2E;AAC3E,MAAM,WAAW,eAAgB,SAAQ,GAAG,CAAC,MAAM,CAAC;IAClD,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAkBzE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,SAAS,EAC9D,MAAM,EAAE,OAAO,EACf,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GACjC,OAAO,CAaT;AAID,UAAU,kBAAkB;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAChC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,mBAAmB,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,WAAW,EACX,QAAQ,EACR,iBAAiB,EACjB,mBAAmB,GACpB,EAAE,kBAAkB,GAAG,OAAO,CAuB9B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.177",
|
|
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
|
+
}
|