@timber-js/app 0.2.0-alpha.147 → 0.2.0-alpha.148

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 (274) hide show
  1. package/dist/_chunks/{actions-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
  2. package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
  3. package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
  4. package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
  5. package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
  6. package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
  7. package/dist/_chunks/{cli-schema-sync-CQaxupHv.js → cli-schema-sync-DJDdyEae.js} +111 -260
  8. package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
  9. package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
  10. package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
  11. package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
  12. package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
  13. package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
  14. package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
  15. package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
  16. package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
  17. package/dist/_chunks/logger-CM0clugc.js.map +1 -0
  18. package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
  19. package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
  20. package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
  21. package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
  22. package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
  23. package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
  24. package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
  25. package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
  26. package/dist/_chunks/tree-match-D2l830j2.js +102 -0
  27. package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
  28. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
  29. package/dist/_chunks/{walkers-Ck4Y45XN.js → walkers-DvWDj453.js} +107 -82
  30. package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
  31. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
  32. package/dist/adapters/cloudflare-kv-cache.js +36 -14
  33. package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
  34. package/dist/adapters/compress-module.d.ts +5 -4
  35. package/dist/adapters/compress-module.d.ts.map +1 -1
  36. package/dist/adapters/nitro.d.ts +1 -3
  37. package/dist/adapters/nitro.d.ts.map +1 -1
  38. package/dist/adapters/nitro.js +14 -134
  39. package/dist/adapters/nitro.js.map +1 -1
  40. package/dist/cache/cache-api.d.ts.map +1 -1
  41. package/dist/cache/index.d.ts +3 -1
  42. package/dist/cache/index.d.ts.map +1 -1
  43. package/dist/cache/index.js +20 -235
  44. package/dist/cache/index.js.map +1 -1
  45. package/dist/cache/invalidation-epoch.d.ts +43 -0
  46. package/dist/cache/invalidation-epoch.d.ts.map +1 -0
  47. package/dist/cache/redis-handler.d.ts +13 -0
  48. package/dist/cache/redis-handler.d.ts.map +1 -1
  49. package/dist/cache/timber-cache.d.ts +17 -9
  50. package/dist/cache/timber-cache.d.ts.map +1 -1
  51. package/dist/cli-schema-sync.d.ts +21 -1
  52. package/dist/cli-schema-sync.d.ts.map +1 -1
  53. package/dist/cli.d.ts +12 -0
  54. package/dist/cli.d.ts.map +1 -1
  55. package/dist/cli.js +26 -31
  56. package/dist/cli.js.map +1 -1
  57. package/dist/client/browser-entry/hmr.d.ts.map +1 -1
  58. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  59. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  60. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  61. package/dist/client/history.d.ts +10 -17
  62. package/dist/client/history.d.ts.map +1 -1
  63. package/dist/client/index.js +1 -1
  64. package/dist/client/index.js.map +1 -1
  65. package/dist/client/internal.js +30 -33
  66. package/dist/client/internal.js.map +1 -1
  67. package/dist/client/navigation-api.d.ts +9 -2
  68. package/dist/client/navigation-api.d.ts.map +1 -1
  69. package/dist/client/navigation-context.d.ts +0 -11
  70. package/dist/client/navigation-context.d.ts.map +1 -1
  71. package/dist/client/navigation-root.d.ts +0 -5
  72. package/dist/client/navigation-root.d.ts.map +1 -1
  73. package/dist/client/router.d.ts.map +1 -1
  74. package/dist/client/rsc-fetch.d.ts.map +1 -1
  75. package/dist/client/use-search-params.d.ts +10 -5
  76. package/dist/client/use-search-params.d.ts.map +1 -1
  77. package/dist/codec.js +1 -1
  78. package/dist/config-validation.d.ts.map +1 -1
  79. package/dist/cookies/define-cookie.d.ts.map +1 -1
  80. package/dist/cookies/index.js +2 -4
  81. package/dist/cookies/index.js.map +1 -1
  82. package/dist/dev-tools/dev-page-shell.d.ts +1 -0
  83. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
  84. package/dist/dev-tools/hydration-overlay.d.ts +11 -0
  85. package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
  86. package/dist/dev-tools/index.d.ts +1 -2
  87. package/dist/dev-tools/index.d.ts.map +1 -1
  88. package/dist/fonts/ast-walk.d.ts +68 -0
  89. package/dist/fonts/ast-walk.d.ts.map +1 -0
  90. package/dist/fonts/ast.d.ts +23 -11
  91. package/dist/fonts/ast.d.ts.map +1 -1
  92. package/dist/fonts/bundle.d.ts +3 -1
  93. package/dist/fonts/bundle.d.ts.map +1 -1
  94. package/dist/fonts/dev-middleware.d.ts +1 -1
  95. package/dist/fonts/dev-middleware.d.ts.map +1 -1
  96. package/dist/fonts/fallbacks.d.ts +0 -7
  97. package/dist/fonts/fallbacks.d.ts.map +1 -1
  98. package/dist/fonts/google.d.ts +4 -2
  99. package/dist/fonts/google.d.ts.map +1 -1
  100. package/dist/fonts/local.d.ts +3 -3
  101. package/dist/fonts/local.d.ts.map +1 -1
  102. package/dist/fonts/paths.d.ts +43 -0
  103. package/dist/fonts/paths.d.ts.map +1 -0
  104. package/dist/fonts/pipeline.d.ts +7 -21
  105. package/dist/fonts/pipeline.d.ts.map +1 -1
  106. package/dist/fonts/transform.d.ts.map +1 -1
  107. package/dist/fonts/virtual-modules.d.ts +1 -18
  108. package/dist/fonts/virtual-modules.d.ts.map +1 -1
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +609 -406
  111. package/dist/index.js.map +1 -1
  112. package/dist/params/index.js +1 -1
  113. package/dist/params/resolve-schema.d.ts.map +1 -1
  114. package/dist/plugin-context.d.ts +28 -5
  115. package/dist/plugin-context.d.ts.map +1 -1
  116. package/dist/plugins/cache.d.ts +13 -5
  117. package/dist/plugins/cache.d.ts.map +1 -1
  118. package/dist/plugins/dev-server.d.ts.map +1 -1
  119. package/dist/plugins/fonts.d.ts +1 -2
  120. package/dist/plugins/fonts.d.ts.map +1 -1
  121. package/dist/plugins/routing.d.ts.map +1 -1
  122. package/dist/routing/codegen.d.ts.map +1 -1
  123. package/dist/routing/convention-lint.d.ts.map +1 -1
  124. package/dist/routing/file-cache.d.ts +8 -0
  125. package/dist/routing/file-cache.d.ts.map +1 -0
  126. package/dist/routing/index.js +2 -2
  127. package/dist/routing/interception.d.ts +0 -5
  128. package/dist/routing/interception.d.ts.map +1 -1
  129. package/dist/routing/manifest-codegen.d.ts +32 -0
  130. package/dist/routing/manifest-codegen.d.ts.map +1 -0
  131. package/dist/routing/scanner.d.ts.map +1 -1
  132. package/dist/schema-bridge.d.ts +15 -0
  133. package/dist/schema-bridge.d.ts.map +1 -1
  134. package/dist/search-params/define.d.ts.map +1 -1
  135. package/dist/search-params/index.js +7 -3
  136. package/dist/search-params/index.js.map +1 -1
  137. package/dist/search-params/wrappers.d.ts.map +1 -1
  138. package/dist/segment-params/define.d.ts.map +1 -1
  139. package/dist/segment-params/index.js +1 -1
  140. package/dist/server/access-gate.d.ts.map +1 -1
  141. package/dist/server/actions.d.ts +0 -14
  142. package/dist/server/actions.d.ts.map +1 -1
  143. package/dist/server/build-manifest.d.ts +0 -29
  144. package/dist/server/build-manifest.d.ts.map +1 -1
  145. package/dist/server/canonicalize.d.ts +10 -0
  146. package/dist/server/canonicalize.d.ts.map +1 -1
  147. package/dist/server/deny-renderer.d.ts +1 -1
  148. package/dist/server/deny-renderer.d.ts.map +1 -1
  149. package/dist/server/html-injector-core.d.ts +8 -0
  150. package/dist/server/html-injector-core.d.ts.map +1 -1
  151. package/dist/server/html-injectors.d.ts.map +1 -1
  152. package/dist/server/index.js +2 -2
  153. package/dist/server/internal.d.ts +1 -5
  154. package/dist/server/internal.d.ts.map +1 -1
  155. package/dist/server/internal.js +12 -171
  156. package/dist/server/internal.js.map +1 -1
  157. package/dist/server/node-stream-transforms.d.ts +0 -11
  158. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  159. package/dist/server/render-utils.d.ts +25 -0
  160. package/dist/server/render-utils.d.ts.map +1 -0
  161. package/dist/server/route-element-builder.d.ts +11 -25
  162. package/dist/server/route-element-builder.d.ts.map +1 -1
  163. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  164. package/dist/server/rsc-entry/helpers.d.ts +2 -18
  165. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  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/rsc-payload.d.ts.map +1 -1
  169. package/dist/server/ssr-render.d.ts.map +1 -1
  170. package/dist/shared/href-validation.d.ts.map +1 -1
  171. package/dist/shims/image.d.ts +15 -15
  172. package/package.json +2 -2
  173. package/src/adapters/cloudflare-kv-cache.ts +56 -19
  174. package/src/adapters/compress-module.ts +27 -118
  175. package/src/adapters/nitro.ts +3 -41
  176. package/src/adapters/raw-imports.d.ts +5 -0
  177. package/src/cache/cache-api.ts +9 -1
  178. package/src/cache/index.ts +5 -2
  179. package/src/cache/invalidation-epoch.ts +93 -0
  180. package/src/cache/redis-handler.ts +33 -2
  181. package/src/cache/timber-cache.ts +109 -53
  182. package/src/cli-schema-sync.ts +123 -30
  183. package/src/cli.ts +33 -37
  184. package/src/client/browser-entry/hmr.ts +0 -11
  185. package/src/client/browser-entry/hydrate.ts +39 -5
  186. package/src/client/browser-entry/post-hydration.ts +6 -15
  187. package/src/client/browser-entry/router-init.ts +98 -3
  188. package/src/client/history.ts +10 -24
  189. package/src/client/navigation-api.ts +14 -9
  190. package/src/client/navigation-context.ts +1 -21
  191. package/src/client/navigation-root.tsx +0 -8
  192. package/src/client/router.ts +18 -8
  193. package/src/client/rsc-fetch.ts +7 -2
  194. package/src/client/use-search-params.ts +10 -5
  195. package/src/config-validation.ts +33 -25
  196. package/src/cookies/define-cookie.ts +2 -18
  197. package/src/dev-tools/dev-page-shell.ts +6 -2
  198. package/src/dev-tools/hydration-overlay.ts +158 -0
  199. package/src/dev-tools/index.ts +3 -16
  200. package/src/fonts/ast-walk.ts +235 -0
  201. package/src/fonts/ast.ts +91 -249
  202. package/src/fonts/bundle.ts +16 -9
  203. package/src/fonts/dev-middleware.ts +16 -5
  204. package/src/fonts/fallbacks.ts +0 -23
  205. package/src/fonts/google.ts +7 -3
  206. package/src/fonts/local.ts +7 -6
  207. package/src/fonts/paths.ts +58 -0
  208. package/src/fonts/pipeline.ts +27 -48
  209. package/src/fonts/transform.ts +27 -0
  210. package/src/fonts/virtual-modules.ts +1 -57
  211. package/src/index.ts +5 -11
  212. package/src/params/resolve-schema.ts +2 -34
  213. package/src/plugin-context.ts +95 -17
  214. package/src/plugins/cache.ts +64 -17
  215. package/src/plugins/dev-server.ts +61 -9
  216. package/src/plugins/fonts.ts +21 -6
  217. package/src/plugins/routing.ts +5 -348
  218. package/src/routing/codegen.ts +3 -2
  219. package/src/routing/convention-lint.ts +4 -3
  220. package/src/routing/file-cache.ts +23 -0
  221. package/src/routing/interception.ts +2 -21
  222. package/src/routing/manifest-codegen.ts +290 -0
  223. package/src/routing/scanner.ts +5 -17
  224. package/src/routing/schema-validation.ts +2 -2
  225. package/src/schema-bridge.ts +63 -2
  226. package/src/search-params/define.ts +34 -2
  227. package/src/search-params/wrappers.ts +21 -3
  228. package/src/segment-params/define.ts +2 -62
  229. package/src/server/access-gate.tsx +8 -0
  230. package/src/server/actions.ts +8 -31
  231. package/src/server/build-manifest.ts +0 -56
  232. package/src/server/canonicalize.ts +6 -3
  233. package/src/server/deny-renderer.ts +6 -0
  234. package/src/server/html-injector-core.ts +19 -0
  235. package/src/server/html-injectors.ts +14 -4
  236. package/src/server/internal.ts +1 -18
  237. package/src/server/node-stream-transforms.ts +5 -72
  238. package/src/server/render-utils.ts +31 -0
  239. package/src/server/route-element-builder.ts +77 -47
  240. package/src/server/rsc-entry/error-renderer.ts +1 -0
  241. package/src/server/rsc-entry/helpers.ts +6 -34
  242. package/src/server/rsc-entry/index.ts +11 -0
  243. package/src/server/rsc-entry/render-route.ts +7 -9
  244. package/src/server/rsc-entry/rsc-payload.ts +2 -1
  245. package/src/server/rsc-entry/ssr-renderer.ts +2 -2
  246. package/src/server/ssr-render.ts +1 -11
  247. package/src/shared/href-validation.ts +8 -0
  248. package/dist/_chunks/cli-schema-sync-CBiMEtqa.js +0 -2
  249. package/dist/_chunks/cli-schema-sync-CQaxupHv.js.map +0 -1
  250. package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
  251. package/dist/_chunks/define-DMVhsipD.js.map +0 -1
  252. package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
  253. package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
  254. package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
  255. package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
  256. package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
  257. package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
  258. package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
  259. package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
  260. package/dist/_chunks/walkers-Ck4Y45XN.js.map +0 -1
  261. package/dist/_chunks/warnings-CNPXaypM.js +0 -125
  262. package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
  263. package/dist/client/link-pending-store.d.ts +0 -65
  264. package/dist/client/link-pending-store.d.ts.map +0 -1
  265. package/dist/client/nav-link-store.d.ts +0 -27
  266. package/dist/client/nav-link-store.d.ts.map +0 -1
  267. package/dist/dev-tools/warnings.d.ts +0 -109
  268. package/dist/dev-tools/warnings.d.ts.map +0 -1
  269. package/dist/server/flush.d.ts +0 -74
  270. package/dist/server/flush.d.ts.map +0 -1
  271. package/src/client/link-pending-store.ts +0 -116
  272. package/src/client/nav-link-store.ts +0 -37
  273. package/src/dev-tools/warnings.ts +0 -238
  274. package/src/server/flush.ts +0 -183
@@ -2,13 +2,21 @@
2
2
  * timber-cache-transform — Vite plugin that injects stable callsite IDs
3
3
  * into `cache()` calls imported from `@timber-js/app/cache`.
4
4
  *
5
- * Without this plugin, cache keys use a process-local counter (fnIdCounter)
6
- * which is unstable across cold starts in serverless/multi-instance
7
- * deployments. With a shared CacheHandler (Redis/KV), colliding counters
8
- * cause cache key poisoning — serving one route's cached data to another.
5
+ * Detected callsite forms (TIM-1054):
6
+ * - named imports, including aliases: `import { cache as c }` + `c(fn, opts)`
7
+ * - namespace imports: `import * as ns` + `ns.cache(fn, opts)`
8
+ * Forms a per-module transform cannot trace — local variable aliases
9
+ * (`const c = cache`), user re-export wrapper modules, computed access
10
+ * (`ns['cache']`) — fall back to the runtime fnId derived from
11
+ * fnv1a(fn.toString()) (see cache/timber-cache.ts), which is deterministic
12
+ * across instances and cold starts of the same build. Without either
13
+ * mechanism, an ordinal counter would collide across instances with a shared
14
+ * CacheHandler (Redis/KV) — cache key poisoning, serving one route's cached
15
+ * data to another.
9
16
  *
10
17
  * The plugin transforms:
11
- * cache(fn, opts) → cache(fn, opts, "stable-id")
18
+ * cache(fn, opts) → cache(fn, opts, "stable-id")
19
+ * ns.cache(fn, opts) → ns.cache(fn, opts, "stable-id")
12
20
  *
13
21
  * where stable-id =
14
22
  * fnv1a(relPath + ":cache:" + fnv1a(stmtText) + ":" + fnv1a(callSpanText) + ":" + occurrence)
@@ -81,29 +89,72 @@ interface CallExpressionNode extends PositionedNode {
81
89
  arguments: PositionedNode[];
82
90
  }
83
91
 
92
+ interface MemberExpressionNode extends PositionedNode {
93
+ type: 'MemberExpression';
94
+ object: PositionedNode;
95
+ property: PositionedNode;
96
+ computed: boolean;
97
+ }
98
+
84
99
  interface ProgramNode extends PositionedNode {
85
100
  type: 'Program';
86
101
  body: PositionedNode[];
87
102
  }
88
103
 
104
+ interface CacheBindings {
105
+ /** Local names bound to `cache` via named imports (`import { cache as c }`). */
106
+ named: Set<string>;
107
+ /** Local names of namespace imports (`import * as ns`) of a cache module. */
108
+ namespaces: Set<string>;
109
+ }
110
+
89
111
  /**
90
- * Find the local binding name(s) for `cache` imported from the cache module.
112
+ * Find the local binding name(s) for `cache` imported from the cache module —
113
+ * both named imports and namespace imports (TIM-1054).
91
114
  */
92
- function findCacheBindings(program: ProgramNode): Set<string> {
93
- const bindings = new Set<string>();
115
+ function findCacheBindings(program: ProgramNode): CacheBindings {
116
+ const named = new Set<string>();
117
+ const namespaces = new Set<string>();
94
118
  for (const stmt of program.body) {
95
119
  if (stmt.type !== 'ImportDeclaration') continue;
96
120
  const decl = stmt as unknown as ImportDeclarationNode;
97
121
  if (!CACHE_MODULE_SOURCES.has(decl.source.value)) continue;
98
122
  for (const spec of decl.specifiers) {
123
+ if (spec.type === 'ImportNamespaceSpecifier') {
124
+ namespaces.add(spec.local.name);
125
+ continue;
126
+ }
99
127
  if (spec.type !== 'ImportSpecifier') continue;
100
128
  const importedName = spec.imported?.name ?? spec.local.name;
101
129
  if (importedName === 'cache') {
102
- bindings.add(spec.local.name);
130
+ named.add(spec.local.name);
103
131
  }
104
132
  }
105
133
  }
106
- return bindings;
134
+ return { named, namespaces };
135
+ }
136
+
137
+ /**
138
+ * Is this callee a reference to the imported `cache` function — either a bare
139
+ * identifier bound by a named import, or `ns.cache` on a namespace import?
140
+ * Computed access (`ns['cache']`) is deliberately not matched; it falls back
141
+ * to the runtime fnId derivation.
142
+ */
143
+ function isCacheCallee(callee: PositionedNode, bindings: CacheBindings): boolean {
144
+ if (callee.type === 'Identifier') {
145
+ return bindings.named.has((callee as IdentifierNode).name);
146
+ }
147
+ if (callee.type === 'MemberExpression') {
148
+ const member = callee as unknown as MemberExpressionNode;
149
+ return (
150
+ !member.computed &&
151
+ member.object.type === 'Identifier' &&
152
+ bindings.namespaces.has((member.object as IdentifierNode).name) &&
153
+ member.property.type === 'Identifier' &&
154
+ (member.property as IdentifierNode).name === 'cache'
155
+ );
156
+ }
157
+ return false;
107
158
  }
108
159
 
109
160
  /**
@@ -114,16 +165,12 @@ function findCacheBindings(program: ProgramNode): Set<string> {
114
165
  */
115
166
  function findCacheCallSites(
116
167
  node: PositionedNode,
117
- cacheBindings: Set<string>,
168
+ cacheBindings: CacheBindings,
118
169
  results: CallExpressionNode[]
119
170
  ): void {
120
171
  if (node.type === 'CallExpression') {
121
172
  const call = node as unknown as CallExpressionNode;
122
- if (
123
- call.callee.type === 'Identifier' &&
124
- cacheBindings.has((call.callee as IdentifierNode).name) &&
125
- call.arguments.length === 2
126
- ) {
173
+ if (call.arguments.length === 2 && isCacheCallee(call.callee, cacheBindings)) {
127
174
  results.push(call);
128
175
  }
129
176
  }
@@ -188,7 +235,7 @@ export function timberCacheTransform(ctx: PluginContext): Plugin {
188
235
  }
189
236
 
190
237
  const cacheBindings = findCacheBindings(program);
191
- if (cacheBindings.size === 0) return;
238
+ if (cacheBindings.named.size === 0 && cacheBindings.namespaces.size === 0) return;
192
239
 
193
240
  // Collect call sites per top-level statement so each call carries its
194
241
  // enclosing statement's source text into the ID hash.
@@ -15,9 +15,9 @@
15
15
  import type { Plugin, ViteDevServer, DevEnvironment } from 'vite';
16
16
  import type { IncomingMessage, ServerResponse } from 'node:http';
17
17
  import type { TLSSocket } from 'node:tls';
18
+ import { existsSync } from 'node:fs';
18
19
  import { join } from 'node:path';
19
20
  import type { PluginContext } from '../plugin-context.js';
20
- import { setViteServer } from '../dev-tools/warnings.js';
21
21
  import {
22
22
  sendErrorToOverlay,
23
23
  classifyErrorPhase,
@@ -30,6 +30,9 @@ import { extractHmrOptions, type DevErrorHmrOptions } from '../dev-tools/dev-pag
30
30
  import { addVirtualModuleContext } from '../config-validation.js';
31
31
  import { compressResponse } from '../server/compress.js';
32
32
  import { isMetadataRouteServePath } from '../server/metadata-routes.js';
33
+ import { canonicalize } from '../server/canonicalize.js';
34
+ import { matchUrlParts } from '../server/tree-match.js';
35
+ import type { RouteTree } from '../routing/types.js';
33
36
 
34
37
  // ─── Constants ────────────────────────────────────────────────────────────
35
38
 
@@ -93,10 +96,6 @@ export function timberDevServer(ctx: PluginContext): Plugin {
93
96
  const configPaths = CONFIG_FILE_NAMES.map((name) => join(ctx.root, name));
94
97
  server.watcher.add(configPaths);
95
98
 
96
- // Register Vite server for browser console warning forwarding.
97
- // See 21-dev-server.md §Dev-Mode Warnings.
98
- setViteServer(server);
99
-
100
99
  // Listen for client-side errors forwarded from the browser.
101
100
  // The browser entry sends 'timber:client-error' events via HMR
102
101
  // for uncaught errors and unhandled rejections. We echo them back
@@ -104,7 +103,7 @@ export function timberDevServer(ctx: PluginContext): Plugin {
104
103
  listenForClientErrors(server, ctx.root);
105
104
 
106
105
  // Pre-hook — registers middleware before Vite's internals
107
- server.middlewares.use(createTimberMiddleware(server, ctx.root));
106
+ server.middlewares.use(createTimberMiddleware(server, ctx));
108
107
 
109
108
  // Wrap server.listen() to close the holding server immediately
110
109
  // before Vite binds the port — no gap where nothing is listening.
@@ -146,7 +145,8 @@ export function timberDevServer(ctx: PluginContext): Plugin {
146
145
  * For non-route requests (assets, Vite internals, HMR), the middleware
147
146
  * calls next() to let Vite handle them.
148
147
  */
149
- function createTimberMiddleware(server: ViteDevServer, projectRoot: string) {
148
+ function createTimberMiddleware(server: ViteDevServer, ctx: PluginContext) {
149
+ const projectRoot = ctx.root;
150
150
  // Extract HMR connection options once so the error page can construct
151
151
  // a correct WebSocket URL (TIM-789).
152
152
  const hmrOptions = extractHmrOptions(server.config);
@@ -165,8 +165,16 @@ function createTimberMiddleware(server: ViteDevServer, projectRoot: string) {
165
165
  }
166
166
 
167
167
  // Pass through static asset requests — but not metadata routes like
168
- // /favicon.ico that match asset extensions but are handled by timber.
169
- if (isAssetRequest(url) && !isMetadataRouteServePath(url)) {
168
+ // /favicon.ico that match asset extensions but are handled by timber,
169
+ // and not registered routes like app/feed.json/route.ts whose URL ends
170
+ // in an asset extension. Production serves these through the RSC
171
+ // handler, so letting them fall through to Vite would 404 in dev only
172
+ // (TIM-1038).
173
+ if (
174
+ isAssetRequest(url) &&
175
+ !isMetadataRouteServePath(url) &&
176
+ !isRouteOwnedAsset(ctx.routeTree, server, url)
177
+ ) {
170
178
  next();
171
179
  return;
172
180
  }
@@ -513,6 +521,50 @@ function isAssetRequest(url: string): boolean {
513
521
  return ASSET_EXTENSIONS.test(url);
514
522
  }
515
523
 
524
+ /**
525
+ * Check whether an asset-extension URL belongs to a scanned route rather
526
+ * than a static file.
527
+ *
528
+ * Route handlers may live at paths ending in asset-like extensions
529
+ * (app/feed.json/route.ts → /feed.json), which the asset bypass would
530
+ * otherwise hand to Vite — 404ing in dev while working in production.
531
+ * Uses the same canonicalization and tree walker as the runtime matcher
532
+ * so dev classification agrees with what the pipeline would match.
533
+ *
534
+ * A file that actually exists on disk wins over the route match: the
535
+ * production server serves public/ statics before the RSC handler (see
536
+ * adapters/nitro.ts), and Vite serves root-relative source files (e.g.
537
+ * /app/global.css) in dev. Without this, a broad catch-all route like
538
+ * app/[...slug] would shadow every static asset. Joining the canonical
539
+ * pathname is traversal-safe — canonicalize() resolves `..` segments and
540
+ * rejects root escapes and encoded separators.
541
+ *
542
+ * Reads ctx.routeTree per request — the routing plugin rescans it on
543
+ * file add/unlink/change, so newly created routes are picked up live.
544
+ */
545
+ function isRouteOwnedAsset(
546
+ routeTree: RouteTree | null,
547
+ server: ViteDevServer,
548
+ url: string
549
+ ): boolean {
550
+ if (!routeTree) return false;
551
+ const rawPathname = url.split('?')[0];
552
+ const result = canonicalize(rawPathname);
553
+ // Malformed URLs can't match a route — let Vite handle them as before.
554
+ if (!result.ok) return false;
555
+ const parts = result.pathname === '/' ? [] : result.pathname.slice(1).split('/');
556
+ if (matchUrlParts(routeTree.root, parts) === null) return false;
557
+
558
+ // Real static files win over the route match (dev/prod parity: the
559
+ // production server tries public/ before the RSC handler).
560
+ const relativePath = result.pathname.slice(1);
561
+ const { publicDir, root } = server.config;
562
+ if (publicDir && existsSync(join(publicDir, relativePath))) return false;
563
+ if (existsSync(join(root, relativePath))) return false;
564
+
565
+ return true;
566
+ }
567
+
516
568
  // ─── Client Error Listener ─────────────────────────────────────────────
517
569
 
518
570
  interface ClientErrorPayload {
@@ -18,13 +18,14 @@
18
18
  * Design doc: 24-fonts.md
19
19
  */
20
20
 
21
- import type { Plugin, ViteDevServer } from 'vite';
21
+ import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';
22
22
  import type { PluginContext } from '../plugin-context.js';
23
23
  import {
24
24
  downloadAndCacheFonts,
25
25
  generateProductionFontFaces,
26
26
  resolveDevFontFaces,
27
27
  } from '../fonts/google.js';
28
+ import { fontUrlPrefix } from '../fonts/paths.js';
28
29
  import { FontPipeline } from '../fonts/pipeline.js';
29
30
  import { runFontsTransform } from '../fonts/transform.js';
30
31
  import { installFontDevMiddleware } from '../fonts/dev-middleware.js';
@@ -45,7 +46,7 @@ import {
45
46
  // updating every caller. New helpers should be added to the per-concern
46
47
  // module first and re-exported here only if external code needs them.
47
48
 
48
- export { FontPipeline, pruneRegistryEntries, type FontRegistry } from '../fonts/pipeline.js';
49
+ export { FontPipeline } from '../fonts/pipeline.js';
49
50
  export {
50
51
  generateFontId,
51
52
  extractFontConfig,
@@ -54,17 +55,27 @@ export {
54
55
  parseGoogleFontFamilies,
55
56
  parseLocalFontImportName,
56
57
  } from '../fonts/transform.js';
57
- export { generateFontCss, generateAllFontCss } from '../fonts/virtual-modules.js';
58
58
 
59
59
  /**
60
60
  * Create the timber-fonts Vite plugin.
61
61
  */
62
62
  export function timberFonts(ctx: PluginContext): Plugin {
63
63
  const pipeline = new FontPipeline({ hashOnRegister: !ctx.dev });
64
+ let resolvedBase = '/';
64
65
 
65
66
  return {
66
67
  name: 'timber-fonts',
67
68
 
69
+ /**
70
+ * Capture Vite's resolved `base` so every font URL the pipeline emits
71
+ * (@font-face src, manifest hrefs, dev middleware matching) carries
72
+ * the prefix. Emitted file locations are unaffected — only URLs.
73
+ */
74
+ configResolved(config: ResolvedConfig) {
75
+ resolvedBase = config.base;
76
+ pipeline.setBase(config.base);
77
+ },
78
+
68
79
  /**
69
80
  * `enforce: 'pre'` is load-bearing for the font CSS pipeline (TIM-828).
70
81
  *
@@ -181,7 +192,7 @@ export function timberFonts(ctx: PluginContext): Plugin {
181
192
  * this middleware.
182
193
  */
183
194
  configureServer(server: ViteDevServer) {
184
- installFontDevMiddleware(server, pipeline);
195
+ installFontDevMiddleware(server, pipeline, resolvedBase);
185
196
  },
186
197
 
187
198
  /**
@@ -208,10 +219,14 @@ export function timberFonts(ctx: PluginContext): Plugin {
208
219
  // same `CachedFont` objects; the bundle emitter dedupes by
209
220
  // `hashedFilename` so each binary is written to disk exactly once.
210
221
  const cachedByFamily = groupCachedFontsByFamily(cachedFonts);
222
+ const urlPrefix = fontUrlPrefix(resolvedBase);
211
223
  for (const font of googleFonts) {
212
224
  const familyCached = cachedByFamily.get(font.family.toLowerCase()) ?? [];
213
225
  pipeline.attachCachedFiles(font.id, familyCached);
214
- pipeline.attachFaces(font.id, generateProductionFontFaces(familyCached, font.display));
226
+ pipeline.attachFaces(
227
+ font.id,
228
+ generateProductionFontFaces(familyCached, font.display, urlPrefix)
229
+ );
215
230
  }
216
231
  },
217
232
 
@@ -239,7 +254,7 @@ export function timberFonts(ctx: PluginContext): Plugin {
239
254
  (msg) => this.warn(msg)
240
255
  );
241
256
  if (!ctx.buildManifest) return;
242
- writeFontManifest(pipeline, ctx.buildManifest);
257
+ writeFontManifest(pipeline, ctx.buildManifest, resolvedBase);
243
258
  },
244
259
  };
245
260
  }