@timber-js/app 0.2.0-alpha.158 → 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 (286) hide show
  1. package/dist/_chunks/{actions-BA5tIpVu.js → actions-CBbP1hHO.js} +3 -3
  2. package/dist/_chunks/{actions-BA5tIpVu.js.map → actions-CBbP1hHO.js.map} +1 -1
  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/html-injectors.d.ts +1 -0
  139. package/dist/server/html-injectors.d.ts.map +1 -1
  140. package/dist/server/index.js +2 -2
  141. package/dist/server/internal.d.ts +6 -1
  142. package/dist/server/internal.d.ts.map +1 -1
  143. package/dist/server/internal.js +563 -74
  144. package/dist/server/internal.js.map +1 -1
  145. package/dist/server/prebuilt/cache-key.d.ts +58 -0
  146. package/dist/server/prebuilt/cache-key.d.ts.map +1 -0
  147. package/dist/server/prebuilt/capture-state.d.ts +57 -0
  148. package/dist/server/prebuilt/capture-state.d.ts.map +1 -0
  149. package/dist/server/prebuilt/manifest-tag-index.d.ts +27 -0
  150. package/dist/server/prebuilt/manifest-tag-index.d.ts.map +1 -0
  151. package/dist/server/prebuilt/overlay.d.ts +100 -0
  152. package/dist/server/prebuilt/overlay.d.ts.map +1 -0
  153. package/dist/server/prebuilt/payload-source.d.ts +75 -0
  154. package/dist/server/prebuilt/payload-source.d.ts.map +1 -0
  155. package/dist/server/prebuilt/synthetic-store.d.ts +64 -0
  156. package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -0
  157. package/dist/server/prebuilt-builder.d.ts +110 -0
  158. package/dist/server/prebuilt-builder.d.ts.map +1 -0
  159. package/dist/server/prebuilt-runtime.d.ts +101 -0
  160. package/dist/server/prebuilt-runtime.d.ts.map +1 -0
  161. package/dist/server/route-element-builder.d.ts +10 -8
  162. package/dist/server/route-element-builder.d.ts.map +1 -1
  163. package/dist/server/rsc-entry/helpers.d.ts +0 -5
  164. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  165. package/dist/server/rsc-entry/index.d.ts +10 -0
  166. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  167. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  168. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  169. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -4
  170. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  171. package/dist/server/sitemap-generator.d.ts +6 -6
  172. package/dist/server/sitemap-generator.d.ts.map +1 -1
  173. package/dist/server/tracing.d.ts +37 -3
  174. package/dist/server/tracing.d.ts.map +1 -1
  175. package/docs/api/32-api-cache.mdx +10 -4
  176. package/docs/learn/00-introduction.mdx +8 -13
  177. package/docs/learn/02-pages-and-layouts.mdx +9 -0
  178. package/docs/learn/06-forms-and-actions.mdx +0 -4
  179. package/docs/learn/07-typed-routes.mdx +127 -0
  180. package/docs/learn/09-caching.mdx +12 -1
  181. package/docs/learn/10-middleware.mdx +1 -0
  182. package/docs/learn/11-error-handling.mdx +9 -0
  183. package/docs/learn/13-configuration.mdx +28 -0
  184. package/docs/learn/14-deploying.mdx +52 -0
  185. package/docs/more/06-instrumentation.mdx +27 -1
  186. package/docs/more/07-security.mdx +8 -2
  187. package/docs/more/08-developer-experience.mdx +134 -0
  188. package/package.json +31 -2
  189. package/src/adapters/build-output-helper.ts +10 -0
  190. package/src/adapters/cloudflare.ts +178 -4
  191. package/src/cache/cache-api.ts +60 -0
  192. package/src/cache/handler-store.ts +8 -0
  193. package/src/cache/index.ts +4 -0
  194. package/src/cache/redis-handler.ts +25 -13
  195. package/src/cache/singleflight.ts +11 -11
  196. package/src/cache/store.ts +71 -0
  197. package/src/cache/stores/cloudflare-kv.ts +52 -0
  198. package/src/cache/stores/memory.ts +125 -0
  199. package/src/cache/stores/redis.ts +97 -0
  200. package/src/cache/stores/vercel.ts +59 -0
  201. package/src/cache/tag-aware-handler.ts +233 -0
  202. package/src/cache/timber-cache.ts +17 -7
  203. package/src/cdn/cloudflare-purge.ts +50 -0
  204. package/src/cdn/fastly-purge.ts +63 -0
  205. package/src/cdn/headers.ts +127 -0
  206. package/src/cdn/index.ts +16 -0
  207. package/src/cdn/purge-store.ts +34 -0
  208. package/src/client/browser-entry/action-dispatch.ts +1 -10
  209. package/src/client/browser-entry/post-hydration.ts +0 -1
  210. package/src/client/browser-entry/router-init.ts +0 -5
  211. package/src/client/history.ts +0 -4
  212. package/src/client/navigation-api.ts +5 -11
  213. package/src/client/router.ts +9 -43
  214. package/src/client/rsc-fetch.ts +5 -27
  215. package/src/client/segment-cache.ts +1 -4
  216. package/src/client/use-cookie.ts +18 -12
  217. package/src/config-types.ts +3 -1
  218. package/src/config-validation.ts +1 -0
  219. package/src/dev-tools/browser-logs.ts +37 -273
  220. package/src/dev-tools/index.ts +2 -2
  221. package/src/dev-tools/instrumentation.ts +1 -1
  222. package/src/dev-tools/logger.ts +33 -51
  223. package/src/dev-tools/terminal.ts +21 -22
  224. package/src/fonts/transform.ts +4 -43
  225. package/src/index.ts +16 -3
  226. package/src/plugin-context.ts +36 -0
  227. package/src/plugins/build-report.ts +133 -1
  228. package/src/plugins/cache.ts +32 -131
  229. package/src/plugins/callsite-ast.ts +167 -0
  230. package/src/plugins/callsite-jsx.ts +243 -0
  231. package/src/plugins/entries.ts +23 -4
  232. package/src/plugins/fonts.ts +16 -2
  233. package/src/plugins/prebuilt-capture.ts +208 -0
  234. package/src/plugins/prebuilt.ts +673 -0
  235. package/src/plugins/prerender-sugar.ts +329 -0
  236. package/src/plugins/server-bundle.ts +12 -0
  237. package/src/plugins/shims.ts +9 -0
  238. package/src/plugins/static-build.ts +0 -1
  239. package/src/routing/codegen.ts +1 -21
  240. package/src/routing/convention-lint.ts +85 -49
  241. package/src/routing/export-detect.ts +304 -0
  242. package/src/routing/manifest-codegen.ts +2 -11
  243. package/src/rsc-runtime/rsc.ts +7 -0
  244. package/src/server/action-handler.ts +11 -27
  245. package/src/server/actions.ts +3 -4
  246. package/src/server/als-registry.ts +7 -0
  247. package/src/server/cookie-parsing.ts +75 -59
  248. package/src/server/deny-renderer.ts +7 -15
  249. package/src/server/html-injectors.ts +11 -3
  250. package/src/server/internal.ts +17 -1
  251. package/src/server/prebuilt/cache-key.ts +224 -0
  252. package/src/server/prebuilt/capture-state.ts +89 -0
  253. package/src/server/prebuilt/manifest-tag-index.ts +46 -0
  254. package/src/server/prebuilt/overlay.ts +256 -0
  255. package/src/server/prebuilt/payload-source.ts +192 -0
  256. package/src/server/prebuilt/synthetic-store.ts +108 -0
  257. package/src/server/prebuilt-builder.ts +640 -0
  258. package/src/server/prebuilt-runtime.ts +706 -0
  259. package/src/server/route-element-builder.ts +40 -12
  260. package/src/server/rsc-entry/helpers.ts +0 -17
  261. package/src/server/rsc-entry/index.ts +83 -0
  262. package/src/server/rsc-entry/render-route.ts +6 -33
  263. package/src/server/rsc-entry/revalidate-renderer.ts +16 -11
  264. package/src/server/rsc-entry/rsc-payload.ts +5 -11
  265. package/src/server/sitemap-generator.ts +16 -10
  266. package/src/server/tracing.ts +88 -3
  267. package/dist/_chunks/cache-api-Cbyh3RW4.js +0 -401
  268. package/dist/_chunks/cache-api-Cbyh3RW4.js.map +0 -1
  269. package/dist/_chunks/logger-C5v1IMuh.js.map +0 -1
  270. package/dist/_chunks/plugin-context-DUl3dnCF.js.map +0 -1
  271. package/dist/_chunks/validation-CnKhGJsp.js +0 -113
  272. package/dist/_chunks/validation-CnKhGJsp.js.map +0 -1
  273. package/dist/_chunks/walkers-D5jCGPum.js.map +0 -1
  274. package/dist/cache/index.js.map +0 -1
  275. package/dist/client/head.d.ts +0 -16
  276. package/dist/client/head.d.ts.map +0 -1
  277. package/dist/client/navigation-api-types.d.ts +0 -90
  278. package/dist/client/navigation-api-types.d.ts.map +0 -1
  279. package/dist/server/client-module-map.d.ts +0 -47
  280. package/dist/server/client-module-map.d.ts.map +0 -1
  281. package/dist/shared/head-header.d.ts +0 -54
  282. package/dist/shared/head-header.d.ts.map +0 -1
  283. package/src/client/head.ts +0 -61
  284. package/src/client/navigation-api-types.ts +0 -112
  285. package/src/server/client-module-map.ts +0 -58
  286. package/src/shared/head-header.ts +0 -69
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Shared AST helpers for callsite-ID transforms.
3
+ *
4
+ * Both timber-cache-transform (plugins/cache.ts) and timber-prebuilt
5
+ * (plugins/prebuilt.ts) detect calls on bindings imported from the cache
6
+ * module and stamp them with content-derived stable IDs. The node shapes,
7
+ * import-binding resolution, recursive call walk, and ID derivation are
8
+ * identical — only the callee shape and rewrite differ per plugin.
9
+ *
10
+ * See TIM-997, TIM-1053, design/06-caching.md §"Cache Key Security".
11
+ */
12
+
13
+ import { fnv1aHash } from '../cache/fast-hash.js';
14
+
15
+ /** Module specifiers whose bindings the transforms track. */
16
+ export const CACHE_MODULE_SOURCES = new Set(['@timber-js/app/cache', 'timber/cache']);
17
+
18
+ export interface PositionedNode {
19
+ type: string;
20
+ start: number;
21
+ end: number;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ export interface IdentifierNode extends PositionedNode {
26
+ type: 'Identifier';
27
+ name: string;
28
+ }
29
+
30
+ export interface ImportDeclarationNode extends PositionedNode {
31
+ type: 'ImportDeclaration';
32
+ source: { value: string };
33
+ specifiers: Array<
34
+ PositionedNode & {
35
+ type: string;
36
+ local: IdentifierNode;
37
+ imported?: IdentifierNode;
38
+ }
39
+ >;
40
+ }
41
+
42
+ export interface CallExpressionNode extends PositionedNode {
43
+ type: 'CallExpression';
44
+ callee: PositionedNode;
45
+ arguments: PositionedNode[];
46
+ }
47
+
48
+ export interface MemberExpressionNode extends PositionedNode {
49
+ type: 'MemberExpression';
50
+ object: PositionedNode;
51
+ property: PositionedNode;
52
+ computed: boolean;
53
+ }
54
+
55
+ export interface ProgramNode extends PositionedNode {
56
+ type: 'Program';
57
+ body: PositionedNode[];
58
+ }
59
+
60
+ export interface ImportBindings {
61
+ /** Local names bound to the named import (`import { cache as c }`). */
62
+ named: Set<string>;
63
+ /** Local names of namespace imports (`import * as ns`) of a tracked module. */
64
+ namespaces: Set<string>;
65
+ }
66
+
67
+ /**
68
+ * Find the local binding name(s) for `importedName` imported from one of
69
+ * `sources` — both named imports (including aliases) and namespace imports
70
+ * (TIM-1054).
71
+ */
72
+ export function findImportBindings(
73
+ program: ProgramNode,
74
+ sources: ReadonlySet<string>,
75
+ importedName: string
76
+ ): ImportBindings {
77
+ const named = new Set<string>();
78
+ const namespaces = new Set<string>();
79
+ for (const stmt of program.body) {
80
+ if (stmt.type !== 'ImportDeclaration') continue;
81
+ const decl = stmt as unknown as ImportDeclarationNode;
82
+ if (!sources.has(decl.source.value)) continue;
83
+ for (const spec of decl.specifiers) {
84
+ if (spec.type === 'ImportNamespaceSpecifier') {
85
+ namespaces.add(spec.local.name);
86
+ continue;
87
+ }
88
+ if (spec.type !== 'ImportSpecifier') continue;
89
+ const specImportedName = spec.imported?.name ?? spec.local.name;
90
+ if (specImportedName === importedName) {
91
+ named.add(spec.local.name);
92
+ }
93
+ }
94
+ }
95
+ return { named, namespaces };
96
+ }
97
+
98
+ /**
99
+ * Recursively walk AST nodes collecting call expressions accepted by
100
+ * `matches`. Calls are typically at module scope, but calls inside
101
+ * functions/blocks are handled for completeness.
102
+ *
103
+ * NOTE: matching is by binding NAME, so nested scopes that shadow a tracked
104
+ * import can produce false positives for consumers of this walker (see
105
+ * TIM-1141 for the cache transform). The prebuilt transform avoids the
106
+ * class entirely by not using this walker — it allowlists top-level
107
+ * declarator positions instead (plugins/prebuilt.ts).
108
+ */
109
+ export function findCallSites(
110
+ node: PositionedNode,
111
+ matches: (call: CallExpressionNode) => boolean,
112
+ results: CallExpressionNode[]
113
+ ): void {
114
+ if (node.type === 'CallExpression') {
115
+ const call = node as unknown as CallExpressionNode;
116
+ if (matches(call)) {
117
+ results.push(call);
118
+ }
119
+ }
120
+ for (const key of Object.keys(node)) {
121
+ if (key === 'type' || key === 'start' || key === 'end') continue;
122
+ const val = node[key];
123
+ if (val && typeof val === 'object') {
124
+ if (Array.isArray(val)) {
125
+ for (const item of val) {
126
+ if (item && typeof item === 'object' && typeof item.type === 'string') {
127
+ findCallSites(item as PositionedNode, matches, results);
128
+ }
129
+ }
130
+ } else if (typeof (val as PositionedNode).type === 'string') {
131
+ findCallSites(val as PositionedNode, matches, results);
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Generate a stable callsite ID from a module path, a kind discriminator
139
+ * ('cache' | 'component'), the source text of the enclosing top-level
140
+ * statement, the source text of the call expression, and an occurrence index
141
+ * among fully byte-identical (statement, call) pairs in the same file.
142
+ *
143
+ * The ID is deterministic and:
144
+ * - Stable across cold starts AND across builds/deploys for unchanged calls —
145
+ * inserting, deleting, or reordering other calls does not affect it
146
+ * - Unique across different modules (relPath component)
147
+ * - Unique across distinct calls within a module (statement + span hashes),
148
+ * with fully identical duplicates disambiguated by source-order occurrence
149
+ * - Fresh when the wrapped fn, opts, or enclosing statement change, which
150
+ * intentionally invalidates the old persisted entries
151
+ *
152
+ * The enclosing-statement text is included so that byte-identical calls in
153
+ * different scopes — e.g. two factory helpers with identical call spans —
154
+ * get distinct, insertion-stable IDs. See plugins/cache.ts header for the
155
+ * full derivation rationale (TIM-1053).
156
+ */
157
+ export function generateStableCallsiteId(
158
+ relativeModulePath: string,
159
+ kind: string,
160
+ statementText: string,
161
+ callSpanText: string,
162
+ occurrence: number
163
+ ): string {
164
+ return fnv1aHash(
165
+ `${relativeModulePath}:${kind}:${fnv1aHash(statementText)}:${fnv1aHash(callSpanText)}:${occurrence}`
166
+ );
167
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * JSX call-site props extraction for prebuilt components (TIM-1142).
3
+ *
4
+ * At build time, the timber-prebuilt transform captures the exact props
5
+ * from each JSX usage site of a cache.component binding so the post-build
6
+ * capture pass can prerender entries for each distinct prop set. Only
7
+ * statically analyzable props (JSON-safe literals, arrays, and plain
8
+ * objects) are captured; dynamic/computed props bail — those call sites
9
+ * fall back to live rendering per design/44 §Props Derivation.
10
+ *
11
+ * See design/44-render-at-build-time.md §Props Derivation at Build Time.
12
+ */
13
+
14
+ import type { PluginContext } from '../plugin-context.js';
15
+ import type { IdentifierNode, PositionedNode, ProgramNode } from './callsite-ast.js';
16
+
17
+ interface JSXElementNode extends PositionedNode {
18
+ type: 'JSXElement';
19
+ openingElement: {
20
+ type: 'JSXOpeningElement';
21
+ name: PositionedNode;
22
+ attributes: PositionedNode[];
23
+ };
24
+ }
25
+
26
+ interface JSXAttributeNode extends PositionedNode {
27
+ type: 'JSXAttribute';
28
+ name: { type: string; name?: string };
29
+ value: PositionedNode | null;
30
+ }
31
+
32
+ interface JSXExpressionContainerNode extends PositionedNode {
33
+ type: 'JSXExpressionContainer';
34
+ expression: PositionedNode;
35
+ }
36
+
37
+ export type JsonValue =
38
+ | string
39
+ | number
40
+ | boolean
41
+ | null
42
+ | JsonValue[]
43
+ | { [key: string]: JsonValue };
44
+
45
+ /**
46
+ * Try to extract a static JSON-safe value from an AST expression node.
47
+ * Returns `undefined` (not null — null is a valid JSON value) when the
48
+ * expression is dynamic/computed and cannot be captured statically.
49
+ */
50
+ function tryExtractStaticValue(node: PositionedNode): JsonValue | undefined {
51
+ if (node.type === 'Literal') {
52
+ const val = (node as { value?: unknown }).value;
53
+ if (
54
+ val === null ||
55
+ typeof val === 'string' ||
56
+ typeof val === 'number' ||
57
+ typeof val === 'boolean'
58
+ ) {
59
+ return val as JsonValue;
60
+ }
61
+ return undefined; // regex or bigint literal
62
+ }
63
+ if (node.type === 'UnaryExpression') {
64
+ const unary = node as unknown as { operator: string; argument: PositionedNode };
65
+ if (unary.operator === '-' && unary.argument.type === 'Literal') {
66
+ const val = (unary.argument as { value?: unknown }).value;
67
+ if (typeof val === 'number') return -val;
68
+ }
69
+ return undefined;
70
+ }
71
+ if (node.type === 'TemplateLiteral') {
72
+ const tpl = node as unknown as {
73
+ quasis: { value: { cooked: string } }[];
74
+ expressions: unknown[];
75
+ };
76
+ if (tpl.expressions.length === 0) return tpl.quasis[0]?.value.cooked ?? undefined;
77
+ return undefined;
78
+ }
79
+ if (node.type === 'ArrayExpression') {
80
+ const elements = (node as unknown as { elements: (PositionedNode | null)[] }).elements;
81
+ const arr: JsonValue[] = [];
82
+ for (const el of elements) {
83
+ if (el === null) return undefined; // sparse
84
+ const val = tryExtractStaticValue(el);
85
+ if (val === undefined) return undefined;
86
+ arr.push(val);
87
+ }
88
+ return arr;
89
+ }
90
+ if (node.type === 'ObjectExpression') {
91
+ const properties = (node as unknown as { properties: PositionedNode[] }).properties;
92
+ const obj: Record<string, JsonValue> = {};
93
+ for (const prop of properties) {
94
+ if (prop.type === 'SpreadElement') return undefined;
95
+ if (prop.type !== 'Property') return undefined;
96
+ const p = prop as unknown as {
97
+ computed?: boolean;
98
+ key: PositionedNode & { name?: string; value?: unknown };
99
+ value: PositionedNode;
100
+ };
101
+ if (p.computed) return undefined;
102
+ const keyName =
103
+ p.key.type === 'Identifier'
104
+ ? p.key.name
105
+ : p.key.type === 'Literal'
106
+ ? String(p.key.value)
107
+ : undefined;
108
+ if (keyName === undefined) return undefined;
109
+ const val = tryExtractStaticValue(p.value);
110
+ if (val === undefined) return undefined;
111
+ obj[keyName] = val;
112
+ }
113
+ return obj;
114
+ }
115
+ return undefined;
116
+ }
117
+
118
+ /**
119
+ * Extract static props from a JSX element's attributes. Returns null if
120
+ * any attribute is dynamic (spread, computed expression, JSX element, etc.)
121
+ * — the component must fall back to dynamic rendering for that call site.
122
+ */
123
+ export function extractStaticJsxProps(
124
+ attributes: PositionedNode[]
125
+ ): Record<string, JsonValue> | null {
126
+ const props: Record<string, JsonValue> = {};
127
+ for (const attr of attributes) {
128
+ if (attr.type === 'JSXSpreadAttribute') return null;
129
+ if (attr.type !== 'JSXAttribute') return null;
130
+ const jsxAttr = attr as unknown as JSXAttributeNode;
131
+ const name = jsxAttr.name?.name;
132
+ if (!name) return null;
133
+ // React strips key and ref before passing to component props — including
134
+ // them would produce cache entries unaddressable at request time.
135
+ if (name === 'key' || name === 'ref') continue;
136
+ if (jsxAttr.value === null) {
137
+ props[name] = true;
138
+ continue;
139
+ }
140
+ if (jsxAttr.value.type === 'Literal') {
141
+ const val = tryExtractStaticValue(jsxAttr.value);
142
+ if (val === undefined) return null;
143
+ props[name] = val;
144
+ continue;
145
+ }
146
+ if (jsxAttr.value.type === 'JSXExpressionContainer') {
147
+ const container = jsxAttr.value as unknown as JSXExpressionContainerNode;
148
+ const val = tryExtractStaticValue(container.expression);
149
+ if (val === undefined) return null;
150
+ props[name] = val;
151
+ continue;
152
+ }
153
+ return null;
154
+ }
155
+ return props;
156
+ }
157
+
158
+ /**
159
+ * Walk an AST tree and collect JSX usages of the given binding names,
160
+ * returning the static props for each usage. Returns a map of
161
+ * bindingName → array of static prop objects (or null for dynamic props).
162
+ */
163
+ export function collectJsxUsages(
164
+ program: ProgramNode,
165
+ bindingNames: Set<string>
166
+ ): Map<string, Array<Record<string, JsonValue> | null>> {
167
+ const result = new Map<string, Array<Record<string, JsonValue> | null>>();
168
+ const walk = (node: unknown) => {
169
+ if (!node || typeof node !== 'object') return;
170
+ const n = node as PositionedNode;
171
+ if (n.type === 'JSXElement') {
172
+ const jsx = n as unknown as JSXElementNode;
173
+ const tagName = jsx.openingElement.name;
174
+ if (tagName.type === 'JSXIdentifier') {
175
+ const name = (tagName as IdentifierNode).name;
176
+ if (bindingNames.has(name)) {
177
+ // Children become props.children at runtime — if the element has
178
+ // children we can't capture the full props shape statically, so
179
+ // treat it as a dynamic call site (bail to null).
180
+ const children = (jsx as unknown as { children?: unknown[] }).children ?? [];
181
+ const hasChildren =
182
+ children.length > 0 &&
183
+ children.some((c: unknown) => {
184
+ const child = c as PositionedNode;
185
+ if (child.type === 'JSXText') {
186
+ // Whitespace-only text nodes are not passed as children
187
+ return (child as unknown as { value: string }).value.trim().length > 0;
188
+ }
189
+ return true;
190
+ });
191
+ const props = hasChildren ? null : extractStaticJsxProps(jsx.openingElement.attributes);
192
+ const usages = result.get(name) ?? [];
193
+ usages.push(props);
194
+ result.set(name, usages);
195
+ }
196
+ }
197
+ }
198
+ for (const value of Object.values(n)) {
199
+ if (Array.isArray(value)) {
200
+ for (const item of value) walk(item);
201
+ } else if (value && typeof value === 'object' && 'type' in value) {
202
+ walk(value);
203
+ }
204
+ }
205
+ };
206
+ for (const stmt of program.body) walk(stmt);
207
+ return result;
208
+ }
209
+
210
+ /**
211
+ * Deep stable JSON stringification with recursively sorted keys.
212
+ * `JSON.stringify` with an array replacer only includes listed keys at
213
+ * ALL nesting levels, so `{config:{theme:'dark'}}` and
214
+ * `{config:{theme:'light'}}` would both serialize as `{"config":{}}`.
215
+ */
216
+ function stableStringify(value: unknown): string {
217
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
218
+ if (Array.isArray(value)) return '[' + value.map(stableStringify).join(',') + ']';
219
+ const obj = value as Record<string, unknown>;
220
+ const parts = Object.keys(obj)
221
+ .sort()
222
+ .map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k]));
223
+ return '{' + parts.join(',') + '}';
224
+ }
225
+
226
+ /**
227
+ * Add a distinct props object to the call-site-props map for a component,
228
+ * deduplicating by deep JSON value (recursively sorted keys).
229
+ */
230
+ export function recordCallSiteProps(
231
+ ctx: PluginContext,
232
+ componentId: string,
233
+ props: Record<string, JsonValue>
234
+ ): void {
235
+ ctx.prebuiltCallSiteProps ??= new Map();
236
+ const existing = ctx.prebuiltCallSiteProps.get(componentId) ?? [];
237
+ const canonical = stableStringify(props);
238
+ const isDuplicate = existing.some((p) => stableStringify(p) === canonical);
239
+ if (!isDuplicate) {
240
+ existing.push(props);
241
+ ctx.prebuiltCallSiteProps.set(componentId, existing);
242
+ }
243
+ }
@@ -154,6 +154,10 @@ function generateConfigModule(ctx: PluginContext): string {
154
154
  // Per-build deployment ID for version skew detection (TIM-446).
155
155
  // Null in dev mode — HMR handles code updates without full reloads.
156
156
  deploymentId: ctx.deploymentId ?? null,
157
+ // Browser log forwarding threshold — used by buildClientScripts to
158
+ // decide whether /@vite/client is needed when clientJavascript is
159
+ // disabled. See TIM-1152.
160
+ devBrowserLogs: ctx.config.devBrowserLogs,
157
161
  // Note: project root is NOT included here — it was leaking server
158
162
  // filesystem paths to client bundles (TIM-787). Dev error pages get
159
163
  // the root through the dev server, not the runtime config.
@@ -244,8 +248,12 @@ function detectConfigFile(root: string): string | null {
244
248
  * cannot be JSON-serialized into virtual:timber-config, so it must
245
249
  * be loaded at runtime via dynamic import. See TIM-599.
246
250
  */
247
- function generateCacheHandlerModule(configPath: string | null, hasCacheHandler: boolean): string {
248
- if (configPath && hasCacheHandler) {
251
+ function generateCacheHandlerModule(
252
+ configPath: string | null,
253
+ hasCacheHandler: boolean,
254
+ hasCdnPurge: boolean
255
+ ): string {
256
+ if (configPath && (hasCacheHandler || hasCdnPurge)) {
249
257
  return [
250
258
  '// Auto-generated cache handler loader — do not edit.',
251
259
  '// Generated by timber-entries plugin.',
@@ -255,17 +263,27 @@ function generateCacheHandlerModule(configPath: string | null, hasCacheHandler:
255
263
  ` const config = mod.default ?? mod;`,
256
264
  ` return config.cacheHandler ?? null;`,
257
265
  `}`,
266
+ '',
267
+ `export async function loadCdnPurgeHandler() {`,
268
+ ` const mod = await import(${JSON.stringify(configPath)});`,
269
+ ` const config = mod.default ?? mod;`,
270
+ ` return config.cdnPurge ?? null;`,
271
+ `}`,
258
272
  ].join('\n');
259
273
  }
260
274
 
261
275
  return [
262
276
  '// Auto-generated cache handler loader — do not edit.',
263
277
  '// Generated by timber-entries plugin.',
264
- '// No cacheHandler configured in timber.config.',
278
+ '// No cacheHandler or cdnPurge configured in timber.config.',
265
279
  '',
266
280
  `export default async function loadCacheHandler() {`,
267
281
  ` return null;`,
268
282
  `}`,
283
+ '',
284
+ `export async function loadCdnPurgeHandler() {`,
285
+ ` return null;`,
286
+ `}`,
269
287
  ].join('\n');
270
288
  }
271
289
 
@@ -336,7 +354,8 @@ export function timberEntries(ctx: PluginContext): Plugin {
336
354
  if (id === RESOLVED_CACHE_HANDLER_ID) {
337
355
  const configPath = detectConfigFile(ctx.root);
338
356
  const hasCacheHandler = ctx.config.cacheHandler != null;
339
- return generateCacheHandlerModule(configPath, hasCacheHandler);
357
+ const hasCdnPurge = ctx.config.cdnPurge != null;
358
+ return generateCacheHandlerModule(configPath, hasCacheHandler, hasCdnPurge);
340
359
  }
341
360
  return null;
342
361
  },
@@ -221,9 +221,23 @@ export function timberFonts(ctx: PluginContext): Plugin {
221
221
  *
222
222
  * Delegates to `runFontsTransform`. Errors raised through the
223
223
  * `this.error` callback are surfaced as Vite plugin errors.
224
+ *
225
+ * The `filter` option uses Vite 8's rolldown-native hook filters to
226
+ * skip non-JS/TS files and files that don't mention any font module
227
+ * before crossing into JS — replacing the hand-rolled pre-filter that
228
+ * was previously inside `runFontsTransform` (TIM-840, TIM-1147).
224
229
  */
225
- transform(code: string, id: string) {
226
- return runFontsTransform(code, id, pipeline, (msg) => this.error(msg));
230
+ transform: {
231
+ filter: {
232
+ id: {
233
+ include: /^[^?]*\.(?:js|jsx|ts|tsx|mjs|cjs|mts|cts)(?:\?|$)/,
234
+ exclude: /(?:^\0|(?:^|\/)node_modules\/)/,
235
+ },
236
+ code: ['@timber/fonts/', '@timber-js/app/fonts/', 'next/font/'],
237
+ },
238
+ handler(code: string, id: string) {
239
+ return runFontsTransform(code, id, pipeline, (msg) => this.error(msg));
240
+ },
227
241
  },
228
242
 
229
243
  /**