@timber-js/app 0.2.0-alpha.157 → 0.2.0-alpha.159

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 (290) hide show
  1. package/dist/_chunks/{actions-CqE7SW7E.js → actions-CBbP1hHO.js} +22 -6
  2. package/dist/_chunks/actions-CBbP1hHO.js.map +1 -0
  3. package/dist/_chunks/{build-output-helper-C8q4Zpf5.js → build-output-helper-DF5uScqS.js} +4 -1
  4. package/dist/_chunks/{build-output-helper-C8q4Zpf5.js.map → build-output-helper-DF5uScqS.js.map} +1 -1
  5. package/dist/_chunks/cache-api-DsWEYwVZ.js +1120 -0
  6. package/dist/_chunks/cache-api-DsWEYwVZ.js.map +1 -0
  7. package/dist/_chunks/{cli-schema-sync-DcmQrzJG.js → cli-schema-sync-Bivx4Rn7.js} +4 -3
  8. package/dist/_chunks/{cli-schema-sync-DcmQrzJG.js.map → cli-schema-sync-Bivx4Rn7.js.map} +1 -1
  9. package/dist/_chunks/dist-BA3u1z3W.js +423 -0
  10. package/dist/_chunks/dist-BA3u1z3W.js.map +1 -0
  11. package/dist/_chunks/{logger-C5v1IMuh.js → logger-CrRmZMlo.js} +81 -56
  12. package/dist/_chunks/logger-CrRmZMlo.js.map +1 -0
  13. package/dist/_chunks/{plugin-context-DUl3dnCF.js → plugin-context-DeAxFRMq.js} +3 -36
  14. package/dist/_chunks/plugin-context-DeAxFRMq.js.map +1 -0
  15. package/dist/_chunks/sizeof-lXffQ1Vr.js +35 -0
  16. package/dist/_chunks/sizeof-lXffQ1Vr.js.map +1 -0
  17. package/dist/_chunks/{walkers-D5jCGPum.js → walkers-DXbJkeCY.js} +217 -19
  18. package/dist/_chunks/walkers-DXbJkeCY.js.map +1 -0
  19. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  20. package/dist/adapters/cloudflare.d.ts +28 -0
  21. package/dist/adapters/cloudflare.d.ts.map +1 -1
  22. package/dist/adapters/cloudflare.js +143 -9
  23. package/dist/adapters/cloudflare.js.map +1 -1
  24. package/dist/adapters/nitro.js +1 -1
  25. package/dist/cache/cache-api.d.ts +2 -0
  26. package/dist/cache/cache-api.d.ts.map +1 -1
  27. package/dist/cache/handler-store.d.ts +2 -0
  28. package/dist/cache/handler-store.d.ts.map +1 -1
  29. package/dist/cache/index.d.ts +2 -0
  30. package/dist/cache/index.d.ts.map +1 -1
  31. package/dist/cache/index.js +3 -186
  32. package/dist/cache/redis-handler.d.ts +17 -5
  33. package/dist/cache/redis-handler.d.ts.map +1 -1
  34. package/dist/cache/singleflight.d.ts +1 -1
  35. package/dist/cache/singleflight.d.ts.map +1 -1
  36. package/dist/cache/store.d.ts +53 -0
  37. package/dist/cache/store.d.ts.map +1 -0
  38. package/dist/cache/stores/cloudflare-kv.d.ts +38 -0
  39. package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -0
  40. package/dist/cache/stores/cloudflare-kv.js +29 -0
  41. package/dist/cache/stores/cloudflare-kv.js.map +1 -0
  42. package/dist/cache/stores/memory.d.ts +42 -0
  43. package/dist/cache/stores/memory.d.ts.map +1 -0
  44. package/dist/cache/stores/memory.js +90 -0
  45. package/dist/cache/stores/memory.js.map +1 -0
  46. package/dist/cache/stores/redis.d.ts +69 -0
  47. package/dist/cache/stores/redis.d.ts.map +1 -0
  48. package/dist/cache/stores/redis.js +35 -0
  49. package/dist/cache/stores/redis.js.map +1 -0
  50. package/dist/cache/stores/vercel.d.ts +36 -0
  51. package/dist/cache/stores/vercel.d.ts.map +1 -0
  52. package/dist/cache/stores/vercel.js +33 -0
  53. package/dist/cache/stores/vercel.js.map +1 -0
  54. package/dist/cache/tag-aware-handler.d.ts +51 -0
  55. package/dist/cache/tag-aware-handler.d.ts.map +1 -0
  56. package/dist/cache/timber-cache.d.ts.map +1 -1
  57. package/dist/cdn/cloudflare-purge.d.ts +23 -0
  58. package/dist/cdn/cloudflare-purge.d.ts.map +1 -0
  59. package/dist/cdn/fastly-purge.d.ts +25 -0
  60. package/dist/cdn/fastly-purge.d.ts.map +1 -0
  61. package/dist/cdn/headers.d.ts +82 -0
  62. package/dist/cdn/headers.d.ts.map +1 -0
  63. package/dist/cdn/index.d.ts +5 -0
  64. package/dist/cdn/index.d.ts.map +1 -0
  65. package/dist/cdn/purge-store.d.ts +23 -0
  66. package/dist/cdn/purge-store.d.ts.map +1 -0
  67. package/dist/cli.js +2 -2
  68. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  69. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  70. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  71. package/dist/client/history.d.ts +0 -3
  72. package/dist/client/history.d.ts.map +1 -1
  73. package/dist/client/index.js +0 -1
  74. package/dist/client/index.js.map +1 -1
  75. package/dist/client/internal.js +11 -86
  76. package/dist/client/internal.js.map +1 -1
  77. package/dist/client/navigation-api.d.ts +1 -3
  78. package/dist/client/navigation-api.d.ts.map +1 -1
  79. package/dist/client/router.d.ts +1 -4
  80. package/dist/client/router.d.ts.map +1 -1
  81. package/dist/client/rsc-fetch.d.ts +1 -11
  82. package/dist/client/rsc-fetch.d.ts.map +1 -1
  83. package/dist/client/segment-cache.d.ts +1 -3
  84. package/dist/client/segment-cache.d.ts.map +1 -1
  85. package/dist/client/use-cookie.d.ts.map +1 -1
  86. package/dist/config-types.d.ts +3 -1
  87. package/dist/config-types.d.ts.map +1 -1
  88. package/dist/config-validation.d.ts.map +1 -1
  89. package/dist/cookies/index.js +15 -11
  90. package/dist/cookies/index.js.map +1 -1
  91. package/dist/dev-tools/browser-logs.d.ts +28 -72
  92. package/dist/dev-tools/browser-logs.d.ts.map +1 -1
  93. package/dist/dev-tools/index.d.ts +1 -1
  94. package/dist/dev-tools/index.d.ts.map +1 -1
  95. package/dist/dev-tools/logger.d.ts.map +1 -1
  96. package/dist/dev-tools/terminal.d.ts.map +1 -1
  97. package/dist/fonts/transform.d.ts +4 -0
  98. package/dist/fonts/transform.d.ts.map +1 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +1269 -359
  101. package/dist/index.js.map +1 -1
  102. package/dist/plugin-context.d.ts +36 -0
  103. package/dist/plugin-context.d.ts.map +1 -1
  104. package/dist/plugins/build-report.d.ts +21 -0
  105. package/dist/plugins/build-report.d.ts.map +1 -1
  106. package/dist/plugins/cache.d.ts +5 -14
  107. package/dist/plugins/cache.d.ts.map +1 -1
  108. package/dist/plugins/callsite-ast.d.ts +95 -0
  109. package/dist/plugins/callsite-ast.d.ts.map +1 -0
  110. package/dist/plugins/callsite-jsx.d.ts +35 -0
  111. package/dist/plugins/callsite-jsx.d.ts.map +1 -0
  112. package/dist/plugins/entries.d.ts.map +1 -1
  113. package/dist/plugins/fonts.d.ts.map +1 -1
  114. package/dist/plugins/prebuilt-capture.d.ts +60 -0
  115. package/dist/plugins/prebuilt-capture.d.ts.map +1 -0
  116. package/dist/plugins/prebuilt.d.ts +82 -0
  117. package/dist/plugins/prebuilt.d.ts.map +1 -0
  118. package/dist/plugins/prerender-sugar.d.ts +42 -0
  119. package/dist/plugins/prerender-sugar.d.ts.map +1 -0
  120. package/dist/plugins/server-bundle.d.ts.map +1 -1
  121. package/dist/plugins/shims.d.ts.map +1 -1
  122. package/dist/plugins/static-build.d.ts.map +1 -1
  123. package/dist/routing/convention-lint.d.ts.map +1 -1
  124. package/dist/routing/export-detect.d.ts +56 -0
  125. package/dist/routing/export-detect.d.ts.map +1 -0
  126. package/dist/routing/index.js +2 -2
  127. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  128. package/dist/rsc-runtime/rsc.d.ts +1 -1
  129. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  130. package/dist/server/action-handler.d.ts.map +1 -1
  131. package/dist/server/actions.d.ts +0 -3
  132. package/dist/server/actions.d.ts.map +1 -1
  133. package/dist/server/als-registry.d.ts +7 -0
  134. package/dist/server/als-registry.d.ts.map +1 -1
  135. package/dist/server/cookie-parsing.d.ts +15 -1
  136. package/dist/server/cookie-parsing.d.ts.map +1 -1
  137. package/dist/server/deny-renderer.d.ts.map +1 -1
  138. package/dist/server/form-data.d.ts +12 -0
  139. package/dist/server/form-data.d.ts.map +1 -1
  140. package/dist/server/html-injectors.d.ts +1 -0
  141. package/dist/server/html-injectors.d.ts.map +1 -1
  142. package/dist/server/index.js +2 -2
  143. package/dist/server/internal.d.ts +6 -1
  144. package/dist/server/internal.d.ts.map +1 -1
  145. package/dist/server/internal.js +563 -74
  146. package/dist/server/internal.js.map +1 -1
  147. package/dist/server/prebuilt/cache-key.d.ts +58 -0
  148. package/dist/server/prebuilt/cache-key.d.ts.map +1 -0
  149. package/dist/server/prebuilt/capture-state.d.ts +57 -0
  150. package/dist/server/prebuilt/capture-state.d.ts.map +1 -0
  151. package/dist/server/prebuilt/manifest-tag-index.d.ts +27 -0
  152. package/dist/server/prebuilt/manifest-tag-index.d.ts.map +1 -0
  153. package/dist/server/prebuilt/overlay.d.ts +100 -0
  154. package/dist/server/prebuilt/overlay.d.ts.map +1 -0
  155. package/dist/server/prebuilt/payload-source.d.ts +75 -0
  156. package/dist/server/prebuilt/payload-source.d.ts.map +1 -0
  157. package/dist/server/prebuilt/synthetic-store.d.ts +64 -0
  158. package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -0
  159. package/dist/server/prebuilt-builder.d.ts +110 -0
  160. package/dist/server/prebuilt-builder.d.ts.map +1 -0
  161. package/dist/server/prebuilt-runtime.d.ts +101 -0
  162. package/dist/server/prebuilt-runtime.d.ts.map +1 -0
  163. package/dist/server/route-element-builder.d.ts +10 -8
  164. package/dist/server/route-element-builder.d.ts.map +1 -1
  165. package/dist/server/rsc-entry/helpers.d.ts +0 -5
  166. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  167. package/dist/server/rsc-entry/index.d.ts +10 -0
  168. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  169. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  170. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  171. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -4
  172. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  173. package/dist/server/sitemap-generator.d.ts +6 -6
  174. package/dist/server/sitemap-generator.d.ts.map +1 -1
  175. package/dist/server/tracing.d.ts +37 -3
  176. package/dist/server/tracing.d.ts.map +1 -1
  177. package/docs/api/32-api-cache.mdx +10 -4
  178. package/docs/learn/00-introduction.mdx +8 -13
  179. package/docs/learn/02-pages-and-layouts.mdx +9 -0
  180. package/docs/learn/06-forms-and-actions.mdx +0 -4
  181. package/docs/learn/07-typed-routes.mdx +127 -0
  182. package/docs/learn/09-caching.mdx +12 -1
  183. package/docs/learn/10-middleware.mdx +1 -0
  184. package/docs/learn/11-error-handling.mdx +9 -0
  185. package/docs/learn/13-configuration.mdx +28 -0
  186. package/docs/learn/14-deploying.mdx +52 -0
  187. package/docs/more/06-instrumentation.mdx +27 -1
  188. package/docs/more/07-security.mdx +8 -2
  189. package/docs/more/08-developer-experience.mdx +134 -0
  190. package/package.json +31 -2
  191. package/src/adapters/build-output-helper.ts +10 -0
  192. package/src/adapters/cloudflare.ts +178 -4
  193. package/src/cache/cache-api.ts +60 -0
  194. package/src/cache/handler-store.ts +8 -0
  195. package/src/cache/index.ts +4 -0
  196. package/src/cache/redis-handler.ts +25 -13
  197. package/src/cache/singleflight.ts +11 -11
  198. package/src/cache/store.ts +71 -0
  199. package/src/cache/stores/cloudflare-kv.ts +52 -0
  200. package/src/cache/stores/memory.ts +125 -0
  201. package/src/cache/stores/redis.ts +97 -0
  202. package/src/cache/stores/vercel.ts +59 -0
  203. package/src/cache/tag-aware-handler.ts +233 -0
  204. package/src/cache/timber-cache.ts +17 -7
  205. package/src/cdn/cloudflare-purge.ts +50 -0
  206. package/src/cdn/fastly-purge.ts +63 -0
  207. package/src/cdn/headers.ts +127 -0
  208. package/src/cdn/index.ts +16 -0
  209. package/src/cdn/purge-store.ts +34 -0
  210. package/src/client/browser-entry/action-dispatch.ts +1 -10
  211. package/src/client/browser-entry/post-hydration.ts +0 -1
  212. package/src/client/browser-entry/router-init.ts +0 -5
  213. package/src/client/history.ts +0 -4
  214. package/src/client/navigation-api.ts +5 -11
  215. package/src/client/router.ts +9 -43
  216. package/src/client/rsc-fetch.ts +5 -27
  217. package/src/client/segment-cache.ts +1 -4
  218. package/src/client/use-cookie.ts +18 -12
  219. package/src/config-types.ts +3 -1
  220. package/src/config-validation.ts +1 -0
  221. package/src/dev-tools/browser-logs.ts +37 -273
  222. package/src/dev-tools/index.ts +2 -2
  223. package/src/dev-tools/instrumentation.ts +1 -1
  224. package/src/dev-tools/logger.ts +33 -51
  225. package/src/dev-tools/terminal.ts +21 -22
  226. package/src/fonts/transform.ts +4 -43
  227. package/src/index.ts +16 -3
  228. package/src/plugin-context.ts +36 -0
  229. package/src/plugins/build-report.ts +133 -1
  230. package/src/plugins/cache.ts +32 -131
  231. package/src/plugins/callsite-ast.ts +167 -0
  232. package/src/plugins/callsite-jsx.ts +243 -0
  233. package/src/plugins/entries.ts +23 -4
  234. package/src/plugins/fonts.ts +16 -2
  235. package/src/plugins/prebuilt-capture.ts +208 -0
  236. package/src/plugins/prebuilt.ts +673 -0
  237. package/src/plugins/prerender-sugar.ts +329 -0
  238. package/src/plugins/server-bundle.ts +12 -0
  239. package/src/plugins/shims.ts +9 -0
  240. package/src/plugins/static-build.ts +0 -1
  241. package/src/routing/codegen.ts +1 -21
  242. package/src/routing/convention-lint.ts +85 -49
  243. package/src/routing/export-detect.ts +304 -0
  244. package/src/routing/manifest-codegen.ts +2 -11
  245. package/src/rsc-runtime/rsc.ts +7 -0
  246. package/src/server/action-handler.ts +11 -27
  247. package/src/server/actions.ts +3 -4
  248. package/src/server/als-registry.ts +7 -0
  249. package/src/server/cookie-parsing.ts +75 -59
  250. package/src/server/deny-renderer.ts +7 -15
  251. package/src/server/form-data.ts +23 -7
  252. package/src/server/html-injectors.ts +11 -3
  253. package/src/server/internal.ts +17 -1
  254. package/src/server/prebuilt/cache-key.ts +224 -0
  255. package/src/server/prebuilt/capture-state.ts +89 -0
  256. package/src/server/prebuilt/manifest-tag-index.ts +46 -0
  257. package/src/server/prebuilt/overlay.ts +256 -0
  258. package/src/server/prebuilt/payload-source.ts +192 -0
  259. package/src/server/prebuilt/synthetic-store.ts +108 -0
  260. package/src/server/prebuilt-builder.ts +640 -0
  261. package/src/server/prebuilt-runtime.ts +706 -0
  262. package/src/server/route-element-builder.ts +40 -12
  263. package/src/server/rsc-entry/helpers.ts +0 -17
  264. package/src/server/rsc-entry/index.ts +83 -0
  265. package/src/server/rsc-entry/render-route.ts +6 -33
  266. package/src/server/rsc-entry/revalidate-renderer.ts +16 -11
  267. package/src/server/rsc-entry/rsc-payload.ts +5 -11
  268. package/src/server/sitemap-generator.ts +16 -10
  269. package/src/server/tracing.ts +88 -3
  270. package/dist/_chunks/actions-CqE7SW7E.js.map +0 -1
  271. package/dist/_chunks/cache-api-Cbyh3RW4.js +0 -401
  272. package/dist/_chunks/cache-api-Cbyh3RW4.js.map +0 -1
  273. package/dist/_chunks/logger-C5v1IMuh.js.map +0 -1
  274. package/dist/_chunks/plugin-context-DUl3dnCF.js.map +0 -1
  275. package/dist/_chunks/validation-CnKhGJsp.js +0 -113
  276. package/dist/_chunks/validation-CnKhGJsp.js.map +0 -1
  277. package/dist/_chunks/walkers-D5jCGPum.js.map +0 -1
  278. package/dist/cache/index.js.map +0 -1
  279. package/dist/client/head.d.ts +0 -16
  280. package/dist/client/head.d.ts.map +0 -1
  281. package/dist/client/navigation-api-types.d.ts +0 -90
  282. package/dist/client/navigation-api-types.d.ts.map +0 -1
  283. package/dist/server/client-module-map.d.ts +0 -47
  284. package/dist/server/client-module-map.d.ts.map +0 -1
  285. package/dist/shared/head-header.d.ts +0 -54
  286. package/dist/shared/head-header.d.ts.map +0 -1
  287. package/src/client/head.ts +0 -61
  288. package/src/client/navigation-api-types.ts +0 -112
  289. package/src/server/client-module-map.ts +0 -58
  290. package/src/shared/head-header.ts +0 -69
@@ -41,8 +41,9 @@ export function parseFormData(formData: FormData): Record<string, unknown> {
41
41
  if (processed.length === 1) {
42
42
  flat[key] = processed[0];
43
43
  } else {
44
- // Filter out undefined entries from multi-value fields
45
- flat[key] = processed.filter((v) => v !== undefined);
44
+ // Filter out undefined (empty Files) and empty strings from multi-value fields.
45
+ // Multi-value fields (e.g. tags=js&tags=&tags=ts) should not include blanks.
46
+ flat[key] = processed.filter((v) => v !== undefined && v !== '');
46
47
  }
47
48
  }
48
49
 
@@ -52,15 +53,13 @@ export function parseFormData(formData: FormData): Record<string, unknown> {
52
53
 
53
54
  /**
54
55
  * Normalize a single FormData entry value.
55
- * - Empty strings → undefined (enables .optional() semantics)
56
56
  * - Empty File objects (no selection) → undefined
57
+ * - Strings pass through as-is (empty strings stay as empty strings so
58
+ * schema validators like `z.string().min(1, 'Required')` can produce
59
+ * their custom error messages instead of a generic type mismatch)
57
60
  * - Everything else passes through as-is
58
61
  */
59
62
  function normalizeValue(value: FormDataEntryValue): unknown {
60
- if (typeof value === 'string') {
61
- return value === '' ? undefined : value;
62
- }
63
-
64
63
  // File input with no selection: browsers submit a File with name="" and size=0
65
64
  if (value instanceof File && value.size === 0 && value.name === '') {
66
65
  return undefined;
@@ -149,6 +148,23 @@ const DANGEROUS_KEYS = new Set(['__proto__']);
149
148
  * ```
150
149
  */
151
150
  export const coerce = {
151
+ /**
152
+ * Coerce an empty string to undefined for optional text fields.
153
+ * Use with `.optional()` schemas where an empty input means "not provided".
154
+ *
155
+ * ```ts
156
+ * // Zod
157
+ * z.unknown().transform(coerce.text).pipe(z.string().optional())
158
+ * // Valibot
159
+ * v.pipe(v.unknown(), v.transform(coerce.text), v.optional(v.string()))
160
+ * ```
161
+ */
162
+ text(value: unknown): string | undefined {
163
+ if (value === undefined || value === null || value === '') return undefined;
164
+ if (typeof value === 'string') return value;
165
+ return undefined;
166
+ },
167
+
152
168
  /**
153
169
  * Coerce a string to a number.
154
170
  * - `"42"` → `42`
@@ -409,12 +409,20 @@ export function buildClientScripts(runtimeConfig: {
409
409
  output: string;
410
410
  clientJavascript: { disabled: boolean; enableHMRInDev: boolean };
411
411
  dev: boolean;
412
+ devBrowserLogs?: string;
412
413
  buildManifest?: import('./build-manifest.js').BuildManifest;
413
414
  }): ClientBootstrapConfig {
414
415
  if (runtimeConfig.clientJavascript.disabled) {
415
- // When client JS is disabled but enableHMRInDev is true in dev mode,
416
- // inject only the Vite HMR client for hot reloading CSS, etc.
417
- if (runtimeConfig.dev && runtimeConfig.clientJavascript.enableHMRInDev) {
416
+ // When client JS is disabled, import /@vite/client in dev mode if
417
+ // any dev feature needs it: HMR for CSS (enableHMRInDev) or
418
+ // forwardConsole for browser→terminal log forwarding (devBrowserLogs
419
+ // not 'none'). Without /@vite/client, server.forwardConsole's
420
+ // client-side console patching never activates. When both features
421
+ // are off, honor the zero-JS opt-out. See TIM-1152.
422
+ const needsViteClient =
423
+ runtimeConfig.clientJavascript.enableHMRInDev ||
424
+ (runtimeConfig.devBrowserLogs ?? 'warn') !== 'none';
425
+ if (runtimeConfig.dev && needsViteClient) {
418
426
  return {
419
427
  bootstrapScriptContent: 'import("/@vite/client")',
420
428
  preloadLinks: '',
@@ -101,7 +101,7 @@ export {
101
101
  resolveMetadataUrls,
102
102
  renderMetadataToElements,
103
103
  } from './metadata.js';
104
- export type { SegmentMetadataEntry, ResolveMetadataOptions, HeadElement } from './metadata.js';
104
+ export type { SegmentMetadataEntry, ResolveMetadataOptions } from './metadata.js';
105
105
 
106
106
  // ── Metadata Routes ──────────────────────────────────────────────────────
107
107
  export {
@@ -163,3 +163,19 @@ export type { RouteModule, RouteHandler, HttpMethod } from './route-handler.js';
163
163
  // ── Render Timeout ───────────────────────────────────────────────────────
164
164
  export { RenderTimeoutError } from './render-timeout.js';
165
165
  export type { RenderTimeout } from './render-timeout.js';
166
+
167
+ // ── Prebuilt Components (cache.component transform targets) ─────────────
168
+ export {
169
+ __prebuilt,
170
+ __prebuiltDev,
171
+ getPrebuiltRegistration,
172
+ getPrebuiltRegistry,
173
+ } from './prebuilt-runtime.js';
174
+ export type { PrebuiltComponentOptions, PrebuiltRegistration } from './prebuilt-runtime.js';
175
+ export { setPrebuiltPayloadSource, hasPrebuiltPayloadSource } from './prebuilt/payload-source.js';
176
+ export type {
177
+ PrebuiltManifest,
178
+ PrebuiltManifestEntry,
179
+ PrebuiltPayloadSource,
180
+ } from './prebuilt/payload-source.js';
181
+ export { computeComponentCacheKey } from './prebuilt/cache-key.js';
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Component cache-key hashing for prebuilt (cache.component) entries.
3
+ *
4
+ * Key: `sha256(canonicalJson({ props, params })).slice(0, 16)` — see
5
+ * design/44-render-at-build-time.md §Cache Key. Both the request-time
6
+ * lookup (prebuilt-runtime.ts) and the post-build capture pass (TIM-1119)
7
+ * MUST use this exact function, or build artifacts become unaddressable.
8
+ *
9
+ * THE IDENTITY CONTRACT (design/44 §Cache Key): cache identity is **JSON
10
+ * value semantics** — two inputs are the same entry iff they are the same
11
+ * JSON value. This is an ALLOWLIST, not a denylist: a value participates
12
+ * in caching iff it is plain JSON data — null, boolean, finite number,
13
+ * string, dense array, or a plain `Object.prototype` object with only
14
+ * enumerable string-keyed data properties — plus `undefined` and `-0`,
15
+ * which hash as distinct sentinels because flight preserves them. Every
16
+ * value outside that domain (functions, symbols, bigints, accessors,
17
+ * hidden or symbol-keyed properties, non-`Object.prototype` prototypes
18
+ * including null, aliased references, sparse arrays, cycles, non-finite
19
+ * numbers) is UNKEYABLE by definition: `computeComponentCacheKey` returns
20
+ * null and the caller renders live — slower, never wrong.
21
+ *
22
+ * The line between rejected and declared-out-of-domain: values are
23
+ * REJECTED when the JSON projection itself would be ambiguous or lossy
24
+ * (hidden/symbol/non-enumerable properties, accessors, exotic
25
+ * prototypes). Observable state that leaves the projection intact — key
26
+ * order, reference identity, descriptor metadata (writable/configurable,
27
+ * frozen/sealed/extensible), proxy behavior — is DECLARED outside
28
+ * cache.component's supported input domain rather than rejected:
29
+ * key-order insensitivity is required for determinism, and rejecting
30
+ * frozen objects would unkey ubiquitous legitimate inputs
31
+ * (Object.freeze'd config props) to defend against components that
32
+ * render differently based on property attributes.
33
+ *
34
+ * Two hard rules for this walk itself: it must be TOTAL (never throw
35
+ * anything but UnkeyableValueError) and SIDE-EFFECT-FREE (descriptor
36
+ * reads only; never invoke user code such as getters).
37
+ *
38
+ * Object keys sort recursively so `{a, b}` and `{b, a}` hash identically
39
+ * (design/13-security.md checklist #14 — cache key determinism; key order
40
+ * is deliberately NOT part of identity).
41
+ */
42
+
43
+ import { createHash } from 'node:crypto';
44
+
45
+ class UnkeyableValueError extends Error {}
46
+
47
+ function canonicalize(value: unknown, seen: Set<object>): string {
48
+ if (value === null) return 'null';
49
+ switch (typeof value) {
50
+ case 'string':
51
+ return JSON.stringify(value);
52
+ case 'boolean':
53
+ return value ? 'true' : 'false';
54
+ case 'number':
55
+ if (!Number.isFinite(value)) throw new UnkeyableValueError('non-finite number');
56
+ // -0 is distinct: flight preserves it ("$-0"), and String(-0) would
57
+ // collapse it to "0" (codex review round 4 on PR #835).
58
+ return Object.is(value, -0) ? '-0' : String(value);
59
+ case 'object':
60
+ break;
61
+ case 'undefined':
62
+ // Distinct sentinel, NOT collapsed to null or omitted: flight
63
+ // serialization preserves `undefined` ("$undefined"), so a component
64
+ // can render [undefined] differently from [null] and {a: undefined}
65
+ // differently from {}. Bare `undefined` cannot collide with any
66
+ // other canonical output (strings are always quoted). Codex review
67
+ // round 3 on PR #835.
68
+ return 'undefined';
69
+ default:
70
+ // function, symbol, bigint
71
+ throw new UnkeyableValueError(`unsupported type: ${typeof value}`);
72
+ }
73
+ const obj = value as object;
74
+ // Any REPEATED reference — cycle or alias — is unkeyable. Flight
75
+ // preserves reference identity (the second occurrence serializes as a
76
+ // row reference), so a component can observe `props.a === props.b`;
77
+ // hashing an aliased shape identically to a structurally-equal
78
+ // non-aliased one could splice the wrong subtree (codex review round 5
79
+ // on PR #835). Objects are never removed from `seen`: revisiting one
80
+ // anywhere in the walk throws.
81
+ if (seen.has(obj)) throw new UnkeyableValueError('repeated object reference (cycle or alias)');
82
+ seen.add(obj);
83
+ // Hidden own properties — symbol keys and non-enumerable props — are
84
+ // readable by a live-rendered component but invisible to this walk, so
85
+ // two observably different inputs would collide on one key (codex
86
+ // review on PR #835). Unkeyable.
87
+ if (Object.getOwnPropertySymbols(obj).length > 0) {
88
+ throw new UnkeyableValueError('symbol-keyed property');
89
+ }
90
+ {
91
+ if (Array.isArray(obj)) {
92
+ // Dense-array own names are exactly ['0'..'n-1', 'length'] — anything
93
+ // else is a custom expando property this walk would silently drop.
94
+ if (Object.getOwnPropertyNames(obj).length !== obj.length + 1) {
95
+ throw new UnkeyableValueError('non-index array property');
96
+ }
97
+ const parts: string[] = [];
98
+ for (let i = 0; i < obj.length; i++) {
99
+ // Sparse holes are unkeyable: a server component receiving the
100
+ // original array can observe `i in arr`, which no canonical form
101
+ // survives faithfully. (An explicit undefined element is fine —
102
+ // handled by the sentinel above.)
103
+ const desc = Object.getOwnPropertyDescriptor(obj, i);
104
+ if (desc === undefined) throw new UnkeyableValueError('sparse array hole');
105
+ // Accessors are rejected WITHOUT invoking them — a getter may
106
+ // throw or side-effect, and the key walk must never alter a
107
+ // render (codex review on PR #835).
108
+ if (desc.get !== undefined || desc.set !== undefined) {
109
+ throw new UnkeyableValueError('accessor property');
110
+ }
111
+ // Non-enumerable elements are invisible to Object.keys/spread/
112
+ // JSON.stringify — the JSON projection would be ambiguous, same
113
+ // rule as the object branch's keys-vs-names check.
114
+ if (!desc.enumerable) throw new UnkeyableValueError('non-enumerable array element');
115
+ parts.push(canonicalize(desc.value, seen));
116
+ }
117
+ return '[' + parts.join(',') + ']';
118
+ }
119
+ // Allowlist: exactly Object.prototype. Dates, Maps, class instances —
120
+ // and null-prototype objects, which a component can distinguish from
121
+ // literals via Object.getPrototypeOf — are all outside the JSON
122
+ // domain.
123
+ if (Object.getPrototypeOf(obj) !== Object.prototype) {
124
+ throw new UnkeyableValueError('non-plain object');
125
+ }
126
+ const keys = Object.keys(obj).sort();
127
+ if (keys.length !== Object.getOwnPropertyNames(obj).length) {
128
+ throw new UnkeyableValueError('non-enumerable property');
129
+ }
130
+ const parts: string[] = [];
131
+ for (const key of keys) {
132
+ // Descriptor read, not property read: accessors are rejected WITHOUT
133
+ // invoking them — a getter may throw or side-effect, and the key
134
+ // walk must never alter a render (codex review on PR #835).
135
+ const desc = Object.getOwnPropertyDescriptor(obj, key)!;
136
+ if (desc.get !== undefined || desc.set !== undefined) {
137
+ throw new UnkeyableValueError('accessor property');
138
+ }
139
+ parts.push(JSON.stringify(key) + ':' + canonicalize(desc.value, seen));
140
+ }
141
+ return '{' + parts.join(',') + '}';
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Canonicalize a single value, or null if it is unkeyable. The ONE
147
+ * definition of "faithfully comparable" — every prebuilt code path that
148
+ * needs value equality (cache keys, slot-param divergence) goes through
149
+ * this, so the unkeyable rules (accessors, hidden props, aliases,
150
+ * bigints, …) live in exactly one place.
151
+ */
152
+ export function tryCanonicalize(value: unknown): string | null {
153
+ try {
154
+ return canonicalize(value, new Set());
155
+ } catch (error) {
156
+ return toUnkeyable(error);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Totality by construction: ANY throw from the walk means the input is
162
+ * outside the keyable domain. The walk touches only user-controlled
163
+ * values through reflection ops — and a Proxy trap can throw arbitrary
164
+ * errors from any of them (codex review on PR #835) — so converting only
165
+ * UnkeyableValueError would let hostile/exotic inputs abort a render
166
+ * that plain live rendering would have served fine. Unexpected error
167
+ * types are logged for visibility (they would indicate either a proxy
168
+ * trap or a canonicalizer bug) but still degrade to unkeyable.
169
+ */
170
+ function toUnkeyable(error: unknown): null {
171
+ if (!(error instanceof UnkeyableValueError)) {
172
+ console.error(
173
+ '[timber] cache key canonicalization threw — treating input as unkeyable:',
174
+ error
175
+ );
176
+ }
177
+ return null;
178
+ }
179
+
180
+ /**
181
+ * Compute the cache-entry hash for a component render:
182
+ * `sha256({props, params}).slice(0, 16)` over canonical JSON.
183
+ *
184
+ * Returns null when props (or params) contain unkeyable values — the
185
+ * caller must fall back to dynamic rendering.
186
+ */
187
+ export function computeComponentCacheKey(
188
+ props: Record<string, unknown>,
189
+ params: Record<string, string | string[]>
190
+ ): string | null {
191
+ let canonical: string;
192
+ try {
193
+ // One shared `seen` set: an object aliased between params and props is
194
+ // just as observable as one aliased within either.
195
+ const seen = new Set<object>();
196
+ // The params RECORD is framework-produced: coerceSegmentParams builds
197
+ // it with Object.create(null) as a prototype-pollution defense
198
+ // (design/13 checklist #36b), uniformly on every request — its
199
+ // prototype is an implementation detail, not user-observable input
200
+ // variation, so the record is normalized to a plain object before the
201
+ // walk (codex P1 on PR #835: rejecting it made every real request
202
+ // unkeyable). Nested VALUES still face the full allowlist, and user
203
+ // props stay strict — a null-prototype prop object remains unkeyable.
204
+ canonical =
205
+ '{"params":' +
206
+ canonicalize({ ...params }, seen) +
207
+ ',"props":' +
208
+ canonicalize(props, seen) +
209
+ '}';
210
+ } catch (error) {
211
+ return toUnkeyable(error);
212
+ }
213
+ // NOTE(hash choice — DECIDED, TIM-1119): 64 bits of truncated sha256 is
214
+ // the SHIPPED artifact format; the capture pass writes .flight files and
215
+ // manifest entries named by this hash, so changing the width is now a
216
+ // build-artifact format break. The width is adequate: entries are
217
+ // namespaced per component AND per deploy, so birthday odds need ~2^32
218
+ // entries under ONE component to matter, and design/13 #24 already
219
+ // accepts 64-bit non-crypto FNV-1a for data-cache keys on the same
220
+ // namespacing argument. Only revisit (with a format migration) if the
221
+ // phase-3 runtime tier ever stores attacker-influenced keys more
222
+ // aggressively.
223
+ return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
224
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Build-time capture state — the channel between the post-build capture
3
+ * pass (server/prebuilt-builder.ts) and the `__prebuilt` wrapper
4
+ * (server/prebuilt-runtime.ts).
5
+ *
6
+ * During capture, a wrapped component's subtree may render OTHER wrapped
7
+ * components. With no payload source registered (the capture pass runs
8
+ * before artifacts exist), those inner wrappers render live and their
9
+ * output is baked flat into the outer entry — the nesting clamp of
10
+ * design/45-cache-lifetimes.md §Nesting Semantics (TIM-1136). The capture
11
+ * pass is the designated detection point for that clamp, so the wrapper
12
+ * reports every render that happens while a capture is active:
13
+ *
14
+ * - **Cycle**: the inner wrapper IS the component being captured (directly
15
+ * or transitively — the transitive case still renders the outer's own
16
+ * wrapper, so the identity check catches both). Left alone this recurses
17
+ * until the render times out; instead the wrapper throws
18
+ * `PrebuiltCaptureCycleError`, failing that entry with an error naming
19
+ * the cycle while the build continues (doc 44 skip-and-report policy).
20
+ * - **Nesting**: any other wrapper render is recorded as an (outer, inner)
21
+ * pair. The builder warns after the entry completes when the inner
22
+ * declares a refresh channel the outer cannot honor (doc 45 clamp rule).
23
+ *
24
+ * Captures run strictly sequentially (one entry at a time), so a single
25
+ * module-level slot is sufficient — there is never a second active capture
26
+ * to confuse renders between. At request time `active` is null and
27
+ * `noteWrapperRender` is a single null-check.
28
+ */
29
+
30
+ import type { PrebuiltComponentOptions } from '../prebuilt-runtime.js';
31
+
32
+ /** A wrapped component that rendered inside an active capture. */
33
+ export interface NestedWrapperRender {
34
+ innerId: string;
35
+ options: PrebuiltComponentOptions;
36
+ }
37
+
38
+ /** Thrown by the wrapper when a capture renders its own component. */
39
+ export class PrebuiltCaptureCycleError extends Error {
40
+ constructor(componentId: string) {
41
+ super(
42
+ `[timber] cache.component "${componentId}" renders itself (directly or through ` +
43
+ `other components) — prerendering it would recurse forever. Remove the cycle ` +
44
+ `or drop { prerender: true } on this component.`
45
+ );
46
+ this.name = 'PrebuiltCaptureCycleError';
47
+ }
48
+ }
49
+
50
+ let active: { outerId: string; nested: Map<string, PrebuiltComponentOptions> } | null = null;
51
+
52
+ /**
53
+ * Mark a capture as active. The builder calls this immediately before
54
+ * rendering an entry and MUST pair it with `endCapture()` in a finally.
55
+ */
56
+ export function beginCapture(outerId: string): void {
57
+ active = { outerId, nested: new Map() };
58
+ }
59
+
60
+ /**
61
+ * End the active capture and return the wrapped components that rendered
62
+ * inside it (first options object wins per inner ID).
63
+ */
64
+ export function endCapture(): NestedWrapperRender[] {
65
+ const nested = active?.nested;
66
+ active = null;
67
+ if (!nested) return [];
68
+ return Array.from(nested, ([innerId, options]) => ({ innerId, options }));
69
+ }
70
+
71
+ /** Whether a build-time capture is currently rendering. */
72
+ export function isCaptureActive(): boolean {
73
+ return active !== null;
74
+ }
75
+
76
+ /**
77
+ * Called by the `__prebuilt` wrapper on every render. No-op unless a
78
+ * capture is active. Throws `PrebuiltCaptureCycleError` when the wrapper
79
+ * belongs to the component currently being captured.
80
+ */
81
+ export function noteWrapperRender(innerId: string, options: PrebuiltComponentOptions): void {
82
+ if (active === null) return;
83
+ if (innerId === active.outerId) {
84
+ throw new PrebuiltCaptureCycleError(innerId);
85
+ }
86
+ if (!active.nested.has(innerId)) {
87
+ active.nested.set(innerId, options);
88
+ }
89
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Manifest tag index — maps tags to seed cache keys for tombstone writes.
3
+ *
4
+ * When `revalidateTag(tag)` fires, the framework must write tombstones for
5
+ * every build-seed entry carrying that tag. The manifest records tags per
6
+ * entry; this module inverts the index at startup so the lookup is O(1) per
7
+ * tag instead of a full manifest scan per invalidation.
8
+ *
9
+ * The index is built lazily on first access and cached for the process
10
+ * lifetime — the manifest is immutable within a deploy.
11
+ *
12
+ * See design/45-cache-lifetimes.md §ISR mechanics.
13
+ */
14
+
15
+ import type { PrebuiltManifest } from './payload-source.js';
16
+
17
+ export interface SeedKeyInfo {
18
+ componentId: string;
19
+ cacheKey: string;
20
+ }
21
+
22
+ /**
23
+ * Build the inverted tag → seed key index from the manifest.
24
+ *
25
+ * Returns a Map where each tag maps to the set of (componentId, cacheKey)
26
+ * pairs whose manifest entries carry that tag. Only entries with non-empty
27
+ * `tags` arrays participate.
28
+ */
29
+ export function buildTagIndex(manifest: PrebuiltManifest): Map<string, SeedKeyInfo[]> {
30
+ const index = new Map<string, SeedKeyInfo[]>();
31
+ for (const [componentId, component] of Object.entries(manifest)) {
32
+ if (!component || typeof component !== 'object' || !('entries' in component)) continue;
33
+ for (const [cacheKey, entry] of Object.entries(component.entries)) {
34
+ if (!entry.tags || entry.tags.length === 0) continue;
35
+ for (const tag of entry.tags) {
36
+ let list = index.get(tag);
37
+ if (!list) {
38
+ list = [];
39
+ index.set(tag, list);
40
+ }
41
+ list.push({ componentId, cacheKey });
42
+ }
43
+ }
44
+ }
45
+ return index;
46
+ }