@timber-js/app 0.2.0-alpha.163 → 0.2.0-alpha.165

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 (102) hide show
  1. package/dist/_chunks/{actions-cjklt63G.js → actions-CSDD6x7U.js} +2 -2
  2. package/dist/_chunks/{actions-cjklt63G.js.map → actions-CSDD6x7U.js.map} +1 -1
  3. package/dist/_chunks/{cache-api-CzYUlgXA.js → cache-api-eb1gydM7.js} +41 -13
  4. package/dist/_chunks/cache-api-eb1gydM7.js.map +1 -0
  5. package/dist/_chunks/{cli-schema-sync-NfLbLnDw.js → cli-schema-sync-mGfRbjh2.js} +2 -2
  6. package/dist/_chunks/{cli-schema-sync-NfLbLnDw.js.map → cli-schema-sync-mGfRbjh2.js.map} +1 -1
  7. package/dist/_chunks/{plugin-context-DeAxFRMq.js → plugin-context-BnaiU_cF.js} +37 -2
  8. package/dist/_chunks/plugin-context-BnaiU_cF.js.map +1 -0
  9. package/dist/_chunks/{walkers-9mz9T7mb.js → walkers-BL3MCMgO.js} +2 -2
  10. package/dist/_chunks/{walkers-9mz9T7mb.js.map → walkers-BL3MCMgO.js.map} +1 -1
  11. package/dist/adapters/cloudflare-kv-cache.d.ts +1 -0
  12. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
  13. package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
  14. package/dist/adapters/nitro.d.ts +40 -0
  15. package/dist/adapters/nitro.d.ts.map +1 -1
  16. package/dist/adapters/nitro.js +133 -73
  17. package/dist/adapters/nitro.js.map +1 -1
  18. package/dist/cache/index.d.ts +3 -0
  19. package/dist/cache/index.d.ts.map +1 -1
  20. package/dist/cache/index.js +1 -1
  21. package/dist/cache/redis-handler.d.ts +1 -0
  22. package/dist/cache/redis-handler.d.ts.map +1 -1
  23. package/dist/cache/tag-aware-handler.d.ts +1 -0
  24. package/dist/cache/tag-aware-handler.d.ts.map +1 -1
  25. package/dist/cache/timber-cache.d.ts.map +1 -1
  26. package/dist/cli.js +2 -2
  27. package/dist/client/slot-context.d.ts +29 -0
  28. package/dist/client/slot-context.d.ts.map +1 -0
  29. package/dist/client/slot-outlet.d.ts +16 -0
  30. package/dist/client/slot-outlet.d.ts.map +1 -0
  31. package/dist/client/slot-provider.d.ts +20 -0
  32. package/dist/client/slot-provider.d.ts.map +1 -0
  33. package/dist/config-types.d.ts +2 -1
  34. package/dist/config-types.d.ts.map +1 -1
  35. package/dist/dev-tools/logs.d.ts.map +1 -1
  36. package/dist/index.js +293 -124
  37. package/dist/index.js.map +1 -1
  38. package/dist/plugin-context.d.ts +27 -0
  39. package/dist/plugin-context.d.ts.map +1 -1
  40. package/dist/plugins/cache.d.ts.map +1 -1
  41. package/dist/plugins/client-chunks.d.ts.map +1 -1
  42. package/dist/plugins/dev-server.d.ts.map +1 -1
  43. package/dist/plugins/prebuilt-options-analysis.d.ts +40 -0
  44. package/dist/plugins/prebuilt-options-analysis.d.ts.map +1 -0
  45. package/dist/plugins/prebuilt.d.ts.map +1 -1
  46. package/dist/plugins/prerender-sugar.d.ts.map +1 -1
  47. package/dist/routing/index.js +2 -2
  48. package/dist/server/html-injector-core.d.ts +30 -9
  49. package/dist/server/html-injector-core.d.ts.map +1 -1
  50. package/dist/server/html-injectors.d.ts.map +1 -1
  51. package/dist/server/index.js +1 -1
  52. package/dist/server/internal.js +346 -51
  53. package/dist/server/internal.js.map +1 -1
  54. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  55. package/dist/server/pipeline-phases.d.ts.map +1 -1
  56. package/dist/server/prebuilt/cache-key.d.ts +4 -0
  57. package/dist/server/prebuilt/cache-key.d.ts.map +1 -1
  58. package/dist/server/prebuilt/key-discipline.d.ts +23 -0
  59. package/dist/server/prebuilt/key-discipline.d.ts.map +1 -0
  60. package/dist/server/prebuilt/slots.d.ts +74 -0
  61. package/dist/server/prebuilt/slots.d.ts.map +1 -0
  62. package/dist/server/prebuilt-builder.d.ts.map +1 -1
  63. package/dist/server/prebuilt-runtime.d.ts +12 -2
  64. package/dist/server/prebuilt-runtime.d.ts.map +1 -1
  65. package/dist/server/rsc-entry/deny-fallback.d.ts +29 -0
  66. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -0
  67. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  68. package/docs/api/34-api-config.mdx +165 -3
  69. package/docs/learn/00-introduction.mdx +78 -44
  70. package/docs/learn/13-configuration.mdx +27 -8
  71. package/package.json +3 -2
  72. package/src/adapters/cloudflare-kv-cache.ts +5 -1
  73. package/src/adapters/nitro.ts +188 -68
  74. package/src/cache/index.ts +25 -2
  75. package/src/cache/redis-handler.ts +27 -7
  76. package/src/cache/tag-aware-handler.ts +5 -1
  77. package/src/cache/timber-cache.ts +21 -10
  78. package/src/client/slot-context.ts +48 -0
  79. package/src/client/slot-outlet.tsx +22 -0
  80. package/src/client/slot-provider.tsx +25 -0
  81. package/src/config-types.ts +2 -1
  82. package/src/dev-tools/logs.ts +7 -0
  83. package/src/plugin-context.ts +54 -0
  84. package/src/plugins/cache.ts +1 -2
  85. package/src/plugins/client-chunks.ts +42 -1
  86. package/src/plugins/dev-server.ts +12 -69
  87. package/src/plugins/prebuilt-options-analysis.ts +175 -0
  88. package/src/plugins/prebuilt.ts +82 -127
  89. package/src/plugins/prerender-sugar.ts +1 -2
  90. package/src/server/html-injector-core.ts +85 -27
  91. package/src/server/html-injectors.ts +5 -1
  92. package/src/server/node-stream-transforms.ts +6 -1
  93. package/src/server/pipeline-phases.ts +4 -1
  94. package/src/server/prebuilt/cache-key.ts +74 -0
  95. package/src/server/prebuilt/key-discipline.ts +53 -0
  96. package/src/server/prebuilt/slots.ts +167 -0
  97. package/src/server/prebuilt-builder.ts +57 -23
  98. package/src/server/prebuilt-runtime.ts +144 -60
  99. package/src/server/rsc-entry/deny-fallback.ts +92 -0
  100. package/src/server/rsc-entry/index.ts +16 -70
  101. package/dist/_chunks/cache-api-CzYUlgXA.js.map +0 -1
  102. package/dist/_chunks/plugin-context-DeAxFRMq.js.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import { a as classifyMetadataRoute, i as METADATA_ROUTE_CONVENTIONS, o as getMetadataRouteAutoLink, r as canonicalize, s as getMetadataRouteServePath } from "../_chunks/canonicalize-P41GR6tY.js";
2
2
  import { n as classifyUrlSegment } from "../_chunks/segment-classify-CDDRVKs7.js";
3
3
  import { A as runWithRequestContext, D as getSearchParams, G as getTraceId, H as generateTraceId, I as getSetCookieHeaders, J as setSpanAttribute, K as replaceTraceId, L as getWaitUntil, M as setMutableCookieContext, N as setSegmentParams, O as getSegmentParams, P as getCookie, Q as requestContextAls, R as isDebug, T as getHeader, U as getOtelTraceId, X as earlyHintsSenderAls, Y as withSpan, _ as RedirectSignal, a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, et as timingAls, f as logWaitUntilRejected, g as DenySignal, h as swallow, i as logMiddlewareShortCircuit, j as setMatchedSegmentPath, k as markResponseFlushed, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, q as runWithTraceId, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest, v as RenderError, w as applyRequestHeaderOverlay } from "../_chunks/logger-B_O6-mdJ.js";
4
- import { i as coerce, t as executeAction } from "../_chunks/actions-cjklt63G.js";
5
- import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-CzYUlgXA.js";
4
+ import { i as coerce, t as executeAction } from "../_chunks/actions-CSDD6x7U.js";
5
+ import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-eb1gydM7.js";
6
6
  import "../client/error-boundary.js";
7
7
  import { n as toBracketKey } from "../_chunks/resolve-schema-3iUvBV5T.js";
8
8
  import "../_chunks/segment-context-ZDnXDkbz.js";
@@ -1633,17 +1633,17 @@ async function coerceSegmentParams(match) {
1633
1633
  * See design/19-client-navigation.md §"Singleton Guarantee via globalThis"
1634
1634
  */
1635
1635
  var EMPTY_SEGMENT_UPDATES = /* @__PURE__ */ new Map();
1636
- var CTX_KEY$1 = Symbol.for("__timber_segment_update_ctx");
1637
- function getOrCreateContext$1() {
1638
- const existing = globalThis[CTX_KEY$1];
1636
+ var CTX_KEY$2 = Symbol.for("__timber_segment_update_ctx");
1637
+ function getOrCreateContext$2() {
1638
+ const existing = globalThis[CTX_KEY$2];
1639
1639
  if (existing !== void 0) return existing;
1640
1640
  if (typeof React.createContext === "function") {
1641
1641
  const ctx = React.createContext(EMPTY_SEGMENT_UPDATES);
1642
- globalThis[CTX_KEY$1] = ctx;
1642
+ globalThis[CTX_KEY$2] = ctx;
1643
1643
  return ctx;
1644
1644
  }
1645
1645
  }
1646
- getOrCreateContext$1();
1646
+ getOrCreateContext$2();
1647
1647
  //#endregion
1648
1648
  //#region src/client/segment-outlet.tsx
1649
1649
  /**
@@ -1690,17 +1690,17 @@ getOrCreateContext$1();
1690
1690
  *
1691
1691
  * See design/45-cache-lifetimes.md §Layout Propagation.
1692
1692
  */
1693
- var CTX_KEY = Symbol.for("__timber_child_segment_ctx");
1694
- function getOrCreateContext() {
1695
- const existing = globalThis[CTX_KEY];
1693
+ var CTX_KEY$1 = Symbol.for("__timber_child_segment_ctx");
1694
+ function getOrCreateContext$1() {
1695
+ const existing = globalThis[CTX_KEY$1];
1696
1696
  if (existing !== void 0) return existing;
1697
1697
  if (typeof React.createContext === "function") {
1698
1698
  const ctx = React.createContext(null);
1699
- globalThis[CTX_KEY] = ctx;
1699
+ globalThis[CTX_KEY$1] = ctx;
1700
1700
  return ctx;
1701
1701
  }
1702
1702
  }
1703
- var ChildSegmentContext = getOrCreateContext();
1703
+ var ChildSegmentContext = getOrCreateContext$1();
1704
1704
  //#endregion
1705
1705
  //#region src/client/child-segment-outlet.tsx
1706
1706
  /**
@@ -2287,7 +2287,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2287
2287
  const responseHeaders = new Headers();
2288
2288
  const requestHeaderOverlay = new Headers();
2289
2289
  responseHeaders.set("Cache-Control", "private, no-cache, no-store, max-age=0, must-revalidate");
2290
- if (config.earlyHints) try {
2290
+ if (config.earlyHints && !isRscRequest) try {
2291
2291
  await config.earlyHints(match, req, responseHeaders);
2292
2292
  } catch (err) {
2293
2293
  swallow(err, "early hints hook threw");
@@ -3195,6 +3195,58 @@ function toUnkeyable(error) {
3195
3195
  return null;
3196
3196
  }
3197
3197
  /**
3198
+ * Split a props record into data props (participate in the cache key) and
3199
+ * slot values (React-element holes, excluded from the key) per the
3200
+ * component's declared `slots` option (TIM-1173).
3201
+ *
3202
+ * Every DECLARED slot lands in `slotValues` — present or not — because the
3203
+ * capture render always injects a SlotOutlet placeholder for each declared
3204
+ * slot: the cached shell is identical whether the caller passed the slot
3205
+ * or omitted it, so slot presence must not vary the key. `dataProps` is
3206
+ * everything else and is what gets hashed.
3207
+ *
3208
+ * Returns null when the props OBJECT itself is outside the identity
3209
+ * contract at the top level — accessors, symbol-keyed or non-enumerable
3210
+ * properties, or a non-`Object.prototype` prototype. Splitting such an
3211
+ * object would normalize away observable state (and reading an accessor
3212
+ * would invoke user code, which this walk must never do) BEFORE
3213
+ * `computeComponentCacheKey` gets the chance to reject it — a cached
3214
+ * shell could then be served for inputs a live render distinguishes.
3215
+ * Null means unkeyable: the caller renders live with the original props.
3216
+ * The walk is descriptor-reads only; getters are never invoked. Nested
3217
+ * data-prop values still face the full allowlist when hashed.
3218
+ */
3219
+ /** Create an own enumerable data property — safe for keys like `__proto__`. */
3220
+ function defineOwn(target, key, value) {
3221
+ Object.defineProperty(target, key, {
3222
+ value,
3223
+ enumerable: true,
3224
+ writable: true,
3225
+ configurable: true
3226
+ });
3227
+ }
3228
+ function splitSlotProps(props, slots) {
3229
+ try {
3230
+ if (Object.getPrototypeOf(props) !== Object.prototype) return null;
3231
+ if (Object.getOwnPropertySymbols(props).length > 0) return null;
3232
+ const slotSet = new Set(slots);
3233
+ const dataProps = {};
3234
+ const slotValues = {};
3235
+ for (const key of Object.getOwnPropertyNames(props)) {
3236
+ const desc = Object.getOwnPropertyDescriptor(props, key);
3237
+ if (desc === void 0 || desc.get !== void 0 || desc.set !== void 0 || !desc.enumerable) return null;
3238
+ defineOwn(slotSet.has(key) ? slotValues : dataProps, key, desc.value);
3239
+ }
3240
+ for (const slot of slotSet) if (!Object.hasOwn(slotValues, slot)) defineOwn(slotValues, slot, void 0);
3241
+ return {
3242
+ dataProps,
3243
+ slotValues
3244
+ };
3245
+ } catch (error) {
3246
+ return toUnkeyable(error);
3247
+ }
3248
+ }
3249
+ /**
3198
3250
  * Compute the cache-entry hash for a component render:
3199
3251
  * `sha256({props, params}).slice(0, 16)` over canonical JSON.
3200
3252
  *
@@ -3212,6 +3264,216 @@ function computeComponentCacheKey(props, params) {
3212
3264
  return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
3213
3265
  }
3214
3266
  //#endregion
3267
+ //#region src/client/slot-context.ts
3268
+ /**
3269
+ * SlotContext — delivers live slot content to SlotOutlet holes inside
3270
+ * cached component shells (TIM-1173).
3271
+ *
3272
+ * A `cache.component(Fn, { slots: [...] })` shell is captured with a
3273
+ * stable <SlotOutlet slot={name} /> client reference in place of each
3274
+ * declared slot prop. At request time the revived shell is wrapped in a
3275
+ * SlotsProvider carrying the live slot values; each outlet reads its
3276
+ * value from this context by name.
3277
+ *
3278
+ * The value is a per-instance record — the NEAREST provider wins, so two
3279
+ * instances of the same cached component on one page each resolve their
3280
+ * own children, and a slot component nested inside another slot
3281
+ * component's children reads its own provider, not the outer one.
3282
+ *
3283
+ * Generalizes the ChildSegmentContext pattern (TIM-1181) from a single
3284
+ * implicit `children` hole on layouts to explicitly declared, named slot
3285
+ * props on any cached component.
3286
+ *
3287
+ * SINGLETON GUARANTEE: globalThis + Symbol.for — the RSC client bundler
3288
+ * can duplicate this module across chunks; globalThis guarantees a single
3289
+ * context instance. Same pattern as ChildSegmentContext.
3290
+ *
3291
+ * See design/45-cache-lifetimes.md §Slot Components.
3292
+ */
3293
+ var CTX_KEY = Symbol.for("__timber_slot_ctx");
3294
+ function getOrCreateContext() {
3295
+ const existing = globalThis[CTX_KEY];
3296
+ if (existing !== void 0) return existing;
3297
+ if (typeof React.createContext === "function") {
3298
+ const ctx = React.createContext(null);
3299
+ globalThis[CTX_KEY] = ctx;
3300
+ return ctx;
3301
+ }
3302
+ }
3303
+ var SlotContext = getOrCreateContext();
3304
+ //#endregion
3305
+ //#region src/client/slot-outlet.tsx
3306
+ /**
3307
+ * SlotOutlet — the hole in a cached component shell (TIM-1173).
3308
+ *
3309
+ * At capture time the prebuilt runtime renders the wrapped component with
3310
+ * <SlotOutlet slot={name} /> in place of each declared slot prop. Being a
3311
+ * client component, it serializes into the flight payload as a stable,
3312
+ * deterministic client reference — the same bytes regardless of what live
3313
+ * content will fill the hole. At request time the revived shell sits under
3314
+ * a SlotsProvider and each outlet reads its live value from SlotContext.
3315
+ *
3316
+ * See design/45-cache-lifetimes.md §Slot Components.
3317
+ */
3318
+ function SlotOutlet({ slot }) {
3319
+ const values = useContext(SlotContext);
3320
+ return values ? values[slot] ?? null : null;
3321
+ }
3322
+ //#endregion
3323
+ //#region src/client/slot-provider.tsx
3324
+ /**
3325
+ * SlotsProvider — wraps a revived cached shell to supply live slot content
3326
+ * to the SlotOutlet holes inside it (TIM-1173).
3327
+ *
3328
+ * Tree structure per cached-component instance:
3329
+ * SlotsProvider(values={children: <Live />})
3330
+ * └── revived shell
3331
+ * └── SlotOutlet(slot="children") → reads context → <Live />
3332
+ *
3333
+ * See design/45-cache-lifetimes.md §Slot Components.
3334
+ */
3335
+ function SlotsProvider({ values, children }) {
3336
+ return createElement(SlotContext.Provider, { value: values }, children);
3337
+ }
3338
+ //#endregion
3339
+ //#region src/server/prebuilt/slots.ts
3340
+ /**
3341
+ * Slot-component helpers for cache.component (TIM-1173).
3342
+ *
3343
+ * A `slots: ['children', ...]` declaration caches the component's output
3344
+ * as a shell with a stable <SlotOutlet slot={name} /> client-reference
3345
+ * hole per declared slot prop; live slot content composes in per instance
3346
+ * at request time via SlotsProvider/SlotContext. This generalizes the
3347
+ * TIM-1181 layout mechanism (ChildSegmentOutlet) to explicitly declared,
3348
+ * named slots on any cached component.
3349
+ *
3350
+ * Both the production wrapper (cache path) and the dev wrapper (no cache,
3351
+ * same tree shape for dev/prod parity) build their render from these
3352
+ * helpers. See design/45-cache-lifetimes.md §Slot Components.
3353
+ */
3354
+ /**
3355
+ * Is this value the framework-injected `<ChildSegmentOutlet />` element?
3356
+ * Total: the `in` probe and `.type` read run against a user-controlled
3357
+ * value, and a Proxy trap can throw — that must classify as "not the
3358
+ * outlet" (the canonicalizer then rejects the value as unkeyable → live
3359
+ * render), not abort the request (codex P2 on PR #890).
3360
+ */
3361
+ function isChildSegmentOutletElement(value) {
3362
+ try {
3363
+ return value != null && typeof value === "object" && "type" in value && value.type === ChildSegmentOutlet;
3364
+ } catch {
3365
+ return false;
3366
+ }
3367
+ }
3368
+ /**
3369
+ * Dense array of non-empty strings — no holes, no non-string entries.
3370
+ * Total: the reads run against a user-controlled value at module load,
3371
+ * and a throwing Proxy trap must classify as invalid, not crash startup.
3372
+ */
3373
+ function isDenseStringArray(value) {
3374
+ try {
3375
+ if (!Array.isArray(value)) return false;
3376
+ for (let i = 0; i < value.length; i++) {
3377
+ if (!Object.hasOwn(value, i)) return false;
3378
+ const entry = value[i];
3379
+ if (typeof entry !== "string" || entry.length === 0) return false;
3380
+ }
3381
+ return true;
3382
+ } catch {
3383
+ return false;
3384
+ }
3385
+ }
3386
+ /**
3387
+ * Validate the `slots` option. Both the production and dev wrappers gate
3388
+ * on this — slot substitution is active in dev exactly when it would be
3389
+ * active in production, so the tree shape a component sees never
3390
+ * diverges between the two.
3391
+ */
3392
+ function resolveSlotNames(id, options) {
3393
+ const slots = options.slots;
3394
+ if (slots === void 0) return { kind: "none" };
3395
+ if (!isDenseStringArray(slots)) {
3396
+ console.warn(`[timber] cache.component "${id}": invalid slots declaration — expected an array of non-empty prop-name strings. Slot caching is disabled.`);
3397
+ return { kind: "disabled" };
3398
+ }
3399
+ if (slots.length === 0) return { kind: "none" };
3400
+ if (!(options.ttl !== void 0 || options.tags != null)) {
3401
+ console.warn(`[timber] cache.component "${id}": slots require a runtime lifetime (ttl and/or tags). Slot caching is disabled.`);
3402
+ return { kind: "disabled" };
3403
+ }
3404
+ return {
3405
+ kind: "active",
3406
+ slots
3407
+ };
3408
+ }
3409
+ /**
3410
+ * Split the live props into the cacheable shell render (data props plus a
3411
+ * deterministic SlotOutlet placeholder per declared slot — identical
3412
+ * whether or not the caller passed the slot) and the per-instance live
3413
+ * slot values.
3414
+ *
3415
+ * Null when the props object itself is unkeyable at the top level
3416
+ * (accessors, hidden properties, exotic prototype — see splitSlotProps):
3417
+ * the caller must render live with the original props.
3418
+ */
3419
+ function prepareSlotRender(props, slots) {
3420
+ const split = splitSlotProps(props ?? {}, slots);
3421
+ if (split === null) return null;
3422
+ const { dataProps, slotValues } = split;
3423
+ const shellProps = { ...dataProps };
3424
+ for (const slot of slots) Object.defineProperty(shellProps, slot, {
3425
+ value: createElement(SlotOutlet, { slot }),
3426
+ enumerable: true,
3427
+ writable: true,
3428
+ configurable: true
3429
+ });
3430
+ return {
3431
+ shellProps,
3432
+ dataProps,
3433
+ slotValues
3434
+ };
3435
+ }
3436
+ /**
3437
+ * Wrap a rendered shell (revived payload or live element) in a
3438
+ * per-instance SlotsProvider feeding the live values to its outlets.
3439
+ */
3440
+ function wrapInSlotsProvider(shell, slotValues) {
3441
+ return createElement(SlotsProvider, {
3442
+ values: slotValues,
3443
+ children: shell
3444
+ });
3445
+ }
3446
+ //#endregion
3447
+ //#region src/server/prebuilt/key-discipline.ts
3448
+ /** Components already warned — exported so tests can reset between cases. */
3449
+ var keyDisciplineWarned = /* @__PURE__ */ new Set();
3450
+ var REQUEST_CONTEXT_FIELDS = {
3451
+ headers: "headers()",
3452
+ parsedCookies: "cookies()",
3453
+ searchParams: "searchParams"
3454
+ };
3455
+ /**
3456
+ * Wrap a store with a proxy that records per-request field access.
3457
+ * The proxy forwards all reads/writes transparently — it only observes.
3458
+ */
3459
+ function createKeyDisciplineProxy(store) {
3460
+ const accessedApis = /* @__PURE__ */ new Set();
3461
+ return {
3462
+ proxy: new Proxy(store, { get(target, prop, receiver) {
3463
+ const api = REQUEST_CONTEXT_FIELDS[prop];
3464
+ if (api) accessedApis.add(api);
3465
+ return Reflect.get(target, prop, receiver);
3466
+ } }),
3467
+ accessedApis
3468
+ };
3469
+ }
3470
+ function emitKeyDisciplineWarning(id, apis) {
3471
+ if (keyDisciplineWarned.has(id)) return;
3472
+ keyDisciplineWarned.add(id);
3473
+ const apiList = Array.from(apis).join(", ");
3474
+ console.warn(`[timber] cache.component key-discipline warning: "${id}" reads ${apiList} but the cache key is computed from props and segment params only. If the output varies by ${apiList}, different users may see each other's cached content. Either pass request-derived values as props (included in the cache key) or remove the ${apiList} call.`);
3475
+ }
3476
+ //#endregion
3215
3477
  //#region src/server/prebuilt/capture-state.ts
3216
3478
  /** Thrown by the wrapper when a capture renders its own component. */
3217
3479
  var PrebuiltCaptureCycleError = class extends Error {
@@ -3343,6 +3605,26 @@ function resolveKeyParams(store) {
3343
3605
  }
3344
3606
  return main;
3345
3607
  }
3608
+ /**
3609
+ * Mirror the production cache-key decision for a slot render, with no
3610
+ * cache IO — used by the dev wrapper so slot substitution happens in dev
3611
+ * exactly when production would serve the cache path rather than the
3612
+ * live fallback (dev/prod parity, codex P2 on PR #890): requires an ALS
3613
+ * store, keyable params (slot-param divergence unkeys), and keyable data
3614
+ * props (after the ChildSegmentOutlet strip).
3615
+ */
3616
+ function isSlotRenderKeyable(dataProps) {
3617
+ const store = requestContextAls.getStore();
3618
+ if (!store) return false;
3619
+ const keyParams = resolveKeyParams(store);
3620
+ if (keyParams === null) return false;
3621
+ let keyProps = dataProps;
3622
+ if (isChildSegmentOutletElement(keyProps.children)) {
3623
+ const { children: _children, ...rest } = keyProps;
3624
+ keyProps = rest;
3625
+ }
3626
+ return computeComponentCacheKey(keyProps, keyParams) !== null;
3627
+ }
3346
3628
  /** Wrap payload bytes as a single-chunk ReadableStream for the decoder. */
3347
3629
  function bytesToStream(bytes) {
3348
3630
  return new ReadableStream({ start(controller) {
@@ -3354,11 +3636,34 @@ function bytesToStream(bytes) {
3354
3636
  function hasRuntimeLifetime(options) {
3355
3637
  return options.ttl !== void 0 || options.tags !== void 0 && options.tags !== null;
3356
3638
  }
3357
- /** Resolve tags from options (may be static array or function of props). */
3639
+ /**
3640
+ * Resolve tags from options (may be static array or function of props).
3641
+ * Function-form tags receive DATA props only — slot props are excluded so
3642
+ * the tags fn never sees SlotOutlet placeholders (capture paths pass
3643
+ * placeholder-substituted props) and tags stay deterministic per key.
3644
+ */
3358
3645
  function resolveTags(options, props) {
3359
3646
  if (!options.tags) return [];
3360
3647
  if (Array.isArray(options.tags)) return options.tags;
3361
- return options.tags(props);
3648
+ const slots = options.slots;
3649
+ const fnProps = slots && slots.length > 0 ? splitSlotProps(props, slots)?.dataProps ?? props : props;
3650
+ return options.tags(fnProps);
3651
+ }
3652
+ /**
3653
+ * Serve a slot component from the cache: split live slot values out of the
3654
+ * props, look up / capture the shell (data props + a deterministic
3655
+ * SlotOutlet placeholder per slot), and wrap the revived shell in a
3656
+ * per-instance SlotsProvider that feeds the live values to the outlets.
3657
+ *
3658
+ * Null on any miss — the caller renders live with the original props.
3659
+ */
3660
+ async function tryReviveSlotShell(id, options, slots, props) {
3661
+ const render = prepareSlotRender(props, slots);
3662
+ if (render === null) return null;
3663
+ const { shellProps, dataProps, slotValues } = render;
3664
+ const revived = await tryRevivePayload(id, options, shellProps, dataProps);
3665
+ if (revived === null) return null;
3666
+ return { element: wrapInSlotsProvider(revived.element, slotValues) };
3362
3667
  }
3363
3668
  var rscFns = null;
3364
3669
  function requireRscFunctions() {
@@ -3382,33 +3687,6 @@ async function revivePayload(id, payload, outcome) {
3382
3687
  function getCaptureTimeoutMs() {
3383
3688
  return componentTimeoutMs;
3384
3689
  }
3385
- var keyDisciplineWarned = /* @__PURE__ */ new Set();
3386
- var REQUEST_CONTEXT_FIELDS = {
3387
- headers: "headers()",
3388
- parsedCookies: "cookies()",
3389
- searchParams: "searchParams"
3390
- };
3391
- /**
3392
- * Wrap a store with a proxy that records per-request field access.
3393
- * The proxy forwards all reads/writes transparently — it only observes.
3394
- */
3395
- function createKeyDisciplineProxy(store) {
3396
- const accessedApis = /* @__PURE__ */ new Set();
3397
- return {
3398
- proxy: new Proxy(store, { get(target, prop, receiver) {
3399
- const api = REQUEST_CONTEXT_FIELDS[prop];
3400
- if (api) accessedApis.add(api);
3401
- return Reflect.get(target, prop, receiver);
3402
- } }),
3403
- accessedApis
3404
- };
3405
- }
3406
- function emitKeyDisciplineWarning(id, apis) {
3407
- if (keyDisciplineWarned.has(id)) return;
3408
- keyDisciplineWarned.add(id);
3409
- const apiList = Array.from(apis).join(", ");
3410
- console.warn(`[timber] cache.component key-discipline warning: "${id}" reads ${apiList} but the cache key is computed from props and segment params only. If the output varies by ${apiList}, different users may see each other's cached content. Either pass request-derived values as props (included in the cache key) or remove the ${apiList} call.`);
3411
- }
3412
3690
  /**
3413
3691
  * Render a component in isolation and capture its flight payload bytes.
3414
3692
  * Used for inline re-renders (tombstone/miss) and SWR background re-renders.
@@ -3539,7 +3817,7 @@ function backgroundRerender(id, component, props, cacheKey, options, hasSeed, sn
3539
3817
  *
3540
3818
  * Null on any miss — the caller renders live.
3541
3819
  */
3542
- async function tryRevivePayload(id, options, props) {
3820
+ async function tryRevivePayload(id, options, props, keyPropsOverride) {
3543
3821
  const hasSource = hasPrebuiltPayloadSource();
3544
3822
  const hasRuntime = hasRuntimeLifetime(options);
3545
3823
  if (!hasSource && !hasRuntime) return null;
@@ -3547,13 +3825,11 @@ async function tryRevivePayload(id, options, props) {
3547
3825
  if (!store) return null;
3548
3826
  const keyParams = (options.prerender ? await isParamIndependent(id) : false) ? {} : resolveKeyParams(store);
3549
3827
  if (keyParams === null) return null;
3550
- const allProps = props ?? {};
3551
- let keyProps;
3552
- const childrenValue = allProps.children;
3553
- if (childrenValue != null && typeof childrenValue === "object" && "type" in childrenValue && childrenValue.type === ChildSegmentOutlet) {
3554
- const { children: _children, ...rest } = allProps;
3828
+ let keyProps = keyPropsOverride ?? props ?? {};
3829
+ if (isChildSegmentOutletElement(keyProps.children)) {
3830
+ const { children: _children, ...rest } = keyProps;
3555
3831
  keyProps = rest;
3556
- } else keyProps = allProps;
3832
+ }
3557
3833
  const cacheKey = computeComponentCacheKey(keyProps, keyParams);
3558
3834
  if (cacheKey === null) return null;
3559
3835
  const outcome = options.fallbackOnMissingSegmentParams === false ? "failing the render (fallbackOnMissingSegmentParams: false)" : "rendering dynamically";
@@ -3605,6 +3881,19 @@ async function tryRevivePayload(id, options, props) {
3605
3881
  * component live (miss).
3606
3882
  */
3607
3883
  function __prebuilt(id, component, options = {}) {
3884
+ const resolution = resolveSlotNames(id, options);
3885
+ const slots = resolution.kind === "active" ? resolution.slots : null;
3886
+ if (resolution.kind === "active" && options.prerender) {
3887
+ console.warn(`[timber] cache.component "${id}": slots are runtime-tier only — prerender is ignored (the build cannot enumerate slot content). Use ttl/tags for the cache lifetime.`);
3888
+ options = {
3889
+ ...options,
3890
+ prerender: void 0
3891
+ };
3892
+ }
3893
+ if (resolution.kind === "disabled") options = {
3894
+ ...options,
3895
+ slots: void 0
3896
+ };
3608
3897
  registry.set(id, {
3609
3898
  id,
3610
3899
  component,
@@ -3613,7 +3902,7 @@ function __prebuilt(id, component, options = {}) {
3613
3902
  const wrapper = async (props) => {
3614
3903
  noteWrapperRender(id, options);
3615
3904
  if (isCaptureActive()) return createElement(component, props);
3616
- const revived = await tryRevivePayload(id, options, props);
3905
+ const revived = slots ? await tryReviveSlotShell(id, options, slots, props) : await tryRevivePayload(id, options, props);
3617
3906
  if (revived !== null) return revived.element;
3618
3907
  if (options.fallbackOnMissingSegmentParams === false) throw new Error(`[timber] cache.component "${id}": no prebuilt payload for the current params and fallbackOnMissingSegmentParams is false`);
3619
3908
  return createElement(component, props);
@@ -3635,13 +3924,19 @@ function getPrebuiltRegistry() {
3635
3924
  * component on first render so the production behavior difference is visible.
3636
3925
  * See design/44 §Dev Mode.
3637
3926
  */
3638
- function __prebuiltDev(name, component, _options) {
3927
+ function __prebuiltDev(name, component, options = {}) {
3928
+ const resolution = resolveSlotNames(name, options);
3929
+ const slots = resolution.kind === "active" ? resolution.slots : null;
3639
3930
  let logged = false;
3640
3931
  const wrapper = (props) => {
3641
3932
  if (!logged) {
3642
3933
  logged = true;
3643
3934
  console.log(`[timber] cache.component: "${name}" — rendering dynamically (dev mode)`);
3644
3935
  }
3936
+ if (slots) {
3937
+ const render = prepareSlotRender(props, slots);
3938
+ if (render !== null && isSlotRenderKeyable(render.dataProps)) return wrapInSlotsProvider(createElement(component, render.shellProps), render.slotValues);
3939
+ }
3645
3940
  return createElement(component, props);
3646
3941
  };
3647
3942
  return wrapper;