@timber-js/app 0.2.0-alpha.166 → 0.2.0-alpha.168

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 (188) hide show
  1. package/dist/_chunks/{actions-CDPfMp_I.js → actions-O_LsyCE4.js} +3 -3
  2. package/dist/_chunks/{actions-CDPfMp_I.js.map → actions-O_LsyCE4.js.map} +1 -1
  3. package/dist/_chunks/{cache-api-DygSeKCB.js → cache-api-B-lhk9p4.js} +107 -58
  4. package/dist/_chunks/cache-api-B-lhk9p4.js.map +1 -0
  5. package/dist/_chunks/{cli-schema-sync-EXGYPhI2.js → cli-schema-sync-B73L6pMq.js} +5 -3
  6. package/dist/_chunks/cli-schema-sync-B73L6pMq.js.map +1 -0
  7. package/dist/_chunks/cloudflare-AHoWYTYr.js +1188 -0
  8. package/dist/_chunks/cloudflare-AHoWYTYr.js.map +1 -0
  9. package/dist/_chunks/json-lossy-check-ClNvBM_3.js +63 -0
  10. package/dist/_chunks/json-lossy-check-ClNvBM_3.js.map +1 -0
  11. package/dist/_chunks/{logger-t3uxAmbX.js → logger-AWfuX-KJ.js} +2 -19
  12. package/dist/_chunks/logger-AWfuX-KJ.js.map +1 -0
  13. package/dist/_chunks/{walkers-DBVzXuWc.js → walkers-CoOC8Hga.js} +2 -2
  14. package/dist/_chunks/{walkers-DBVzXuWc.js.map → walkers-CoOC8Hga.js.map} +1 -1
  15. package/dist/adapters/cloudflare-dev.js +1 -1
  16. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
  17. package/dist/adapters/cloudflare-kv-cache.js +10 -3
  18. package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
  19. package/dist/adapters/cloudflare.d.ts +12 -1
  20. package/dist/adapters/cloudflare.d.ts.map +1 -1
  21. package/dist/adapters/cloudflare.js +2 -461
  22. package/dist/adapters/types.d.ts +2 -0
  23. package/dist/adapters/types.d.ts.map +1 -1
  24. package/dist/cache/cache-api.d.ts +33 -11
  25. package/dist/cache/cache-api.d.ts.map +1 -1
  26. package/dist/cache/index.d.ts +1 -1
  27. package/dist/cache/index.d.ts.map +1 -1
  28. package/dist/cache/index.js +1 -1
  29. package/dist/cache/json-lossy-check.d.ts +11 -0
  30. package/dist/cache/json-lossy-check.d.ts.map +1 -0
  31. package/dist/cache/redis-handler.d.ts +42 -0
  32. package/dist/cache/redis-handler.d.ts.map +1 -1
  33. package/dist/cache/singleflight.d.ts.map +1 -1
  34. package/dist/cache/stores/cloudflare-kv.d.ts +1 -1
  35. package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -1
  36. package/dist/cache/stores/memory.d.ts +1 -1
  37. package/dist/cache/stores/memory.d.ts.map +1 -1
  38. package/dist/cache/stores/redis.d.ts +1 -1
  39. package/dist/cache/stores/redis.d.ts.map +1 -1
  40. package/dist/cache/stores/vercel.d.ts +1 -1
  41. package/dist/cache/stores/vercel.d.ts.map +1 -1
  42. package/dist/cache/tag-aware-handler.d.ts.map +1 -1
  43. package/dist/cache/timber-cache.d.ts.map +1 -1
  44. package/dist/cdn/cloudflare-purge.d.ts.map +1 -1
  45. package/dist/cdn/fastly-purge.d.ts.map +1 -1
  46. package/dist/cdn/workers-cache-purge.d.ts.map +1 -1
  47. package/dist/cli.d.ts +1 -1
  48. package/dist/cli.d.ts.map +1 -1
  49. package/dist/cli.js +3 -2
  50. package/dist/cli.js.map +1 -1
  51. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  52. package/dist/client/browser-entry/router-init.d.ts +1 -0
  53. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  54. package/dist/client/child-segment-context.d.ts +2 -2
  55. package/dist/client/child-segment-context.d.ts.map +1 -1
  56. package/dist/client/error-boundary.d.ts.map +1 -1
  57. package/dist/client/history.d.ts +10 -0
  58. package/dist/client/history.d.ts.map +1 -1
  59. package/dist/client/internal.js +141 -116
  60. package/dist/client/internal.js.map +1 -1
  61. package/dist/client/router.d.ts +6 -0
  62. package/dist/client/router.d.ts.map +1 -1
  63. package/dist/client/rsc-fetch.d.ts.map +1 -1
  64. package/dist/client/segment-cache.d.ts.map +1 -1
  65. package/dist/client/segment-update-context.d.ts +3 -3
  66. package/dist/client/segment-update-context.d.ts.map +1 -1
  67. package/dist/client/slot-outlet.d.ts +1 -1
  68. package/dist/codec.d.ts.map +1 -1
  69. package/dist/codec.js +2 -1
  70. package/dist/codec.js.map +1 -1
  71. package/dist/config-types.d.ts +16 -37
  72. package/dist/config-types.d.ts.map +1 -1
  73. package/dist/config-validation.d.ts.map +1 -1
  74. package/dist/dev-tools/instrumentation.d.ts.map +1 -1
  75. package/dist/fonts/pipeline.d.ts +19 -0
  76. package/dist/fonts/pipeline.d.ts.map +1 -1
  77. package/dist/fonts/transform.d.ts.map +1 -1
  78. package/dist/fonts/virtual-modules.d.ts.map +1 -1
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +378 -109
  81. package/dist/index.js.map +1 -1
  82. package/dist/plugin-context.d.ts.map +1 -1
  83. package/dist/plugins/adapter-build.d.ts +1 -0
  84. package/dist/plugins/adapter-build.d.ts.map +1 -1
  85. package/dist/plugins/cache.d.ts +8 -8
  86. package/dist/plugins/cache.d.ts.map +1 -1
  87. package/dist/plugins/entries.d.ts +27 -3
  88. package/dist/plugins/entries.d.ts.map +1 -1
  89. package/dist/plugins/fonts.d.ts.map +1 -1
  90. package/dist/plugins/server-bundle.d.ts.map +1 -1
  91. package/dist/routing/index.js +2 -2
  92. package/dist/server/action-client.d.ts +1 -1
  93. package/dist/server/action-client.d.ts.map +1 -1
  94. package/dist/server/als-registry.d.ts +7 -0
  95. package/dist/server/als-registry.d.ts.map +1 -1
  96. package/dist/server/body-limits.d.ts.map +1 -1
  97. package/dist/server/deny-boundary.d.ts +3 -1
  98. package/dist/server/deny-boundary.d.ts.map +1 -1
  99. package/dist/server/form-data.d.ts.map +1 -1
  100. package/dist/server/html-injector-core.d.ts.map +1 -1
  101. package/dist/server/index.js +20 -3
  102. package/dist/server/index.js.map +1 -1
  103. package/dist/server/internal.js +28 -41
  104. package/dist/server/internal.js.map +1 -1
  105. package/dist/server/middleware-runner.d.ts +0 -17
  106. package/dist/server/middleware-runner.d.ts.map +1 -1
  107. package/dist/server/param-coercion.d.ts.map +1 -1
  108. package/dist/server/pipeline-phases.d.ts +6 -3
  109. package/dist/server/pipeline-phases.d.ts.map +1 -1
  110. package/dist/server/pipeline.d.ts +18 -0
  111. package/dist/server/pipeline.d.ts.map +1 -1
  112. package/dist/server/prebuilt/capture-state.d.ts.map +1 -1
  113. package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -1
  114. package/dist/server/primitives.d.ts.map +1 -1
  115. package/dist/server/render-timeout.d.ts.map +1 -1
  116. package/dist/server/route-element-builder.d.ts +9 -0
  117. package/dist/server/route-element-builder.d.ts.map +1 -1
  118. package/dist/server/rsc-entry/action-middleware-runner.d.ts +14 -14
  119. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  120. package/dist/server/rsc-entry/render-route.d.ts +1 -0
  121. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  122. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  123. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  124. package/dist/server/rsc-entry/rsc-stream.d.ts +8 -0
  125. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  126. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +44 -74
  127. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -1
  128. package/dist/server/safe-load.d.ts.map +1 -1
  129. package/dist/server/ssr-entry.d.ts.map +1 -1
  130. package/dist/shared/redirect-type.d.ts +2 -2
  131. package/dist/shared/redirect-type.d.ts.map +1 -1
  132. package/dist/shims/font-google.d.ts.map +1 -1
  133. package/dist/shims/image.d.ts +120 -120
  134. package/dist/shims/image.d.ts.map +1 -1
  135. package/docs/api/32-api-cache.mdx +116 -4
  136. package/docs/api/34-api-config.mdx +0 -26
  137. package/docs/learn/09-caching.mdx +126 -10
  138. package/docs/learn/12-client-navigation.mdx +9 -1
  139. package/docs/learn/13-configuration.mdx +6 -8
  140. package/docs/learn/14-deploying.mdx +18 -18
  141. package/docs/more/03-coming-from-nextjs.mdx +2 -2
  142. package/docs/more/04-metadata-and-fonts.mdx +1 -1
  143. package/docs/more/50-ai-agent-instructions.mdx +5 -5
  144. package/package.json +6 -5
  145. package/src/adapters/cloudflare-kv-cache.ts +27 -6
  146. package/src/adapters/cloudflare.ts +63 -25
  147. package/src/adapters/types.ts +2 -0
  148. package/src/cache/cache-api.ts +84 -84
  149. package/src/cache/index.ts +1 -1
  150. package/src/cache/json-lossy-check.ts +75 -0
  151. package/src/cache/redis-handler.ts +65 -14
  152. package/src/cache/timber-cache.ts +10 -2
  153. package/src/client/browser-entry/index.ts +2 -0
  154. package/src/client/browser-entry/post-hydration.ts +16 -9
  155. package/src/client/browser-entry/router-init.ts +2 -0
  156. package/src/client/history.ts +11 -0
  157. package/src/client/router.ts +224 -208
  158. package/src/codec.ts +3 -1
  159. package/src/config-types.ts +16 -37
  160. package/src/config-validation.ts +7 -5
  161. package/src/fonts/pipeline.ts +32 -0
  162. package/src/fonts/transform.ts +30 -24
  163. package/src/plugins/adapter-build.ts +132 -13
  164. package/src/plugins/cache.ts +45 -30
  165. package/src/plugins/entries.ts +40 -43
  166. package/src/plugins/fonts.ts +30 -0
  167. package/src/plugins/server-bundle.ts +7 -15
  168. package/src/routing/scanner.ts +7 -0
  169. package/src/server/action-client.ts +1 -1
  170. package/src/server/als-registry.ts +7 -0
  171. package/src/server/deny-boundary.ts +6 -3
  172. package/src/server/middleware-runner.ts +0 -44
  173. package/src/server/param-coercion.ts +1 -0
  174. package/src/server/pipeline-phases.ts +11 -13
  175. package/src/server/pipeline.ts +51 -3
  176. package/src/server/route-element-builder.ts +59 -9
  177. package/src/server/rsc-entry/action-middleware-runner.ts +14 -14
  178. package/src/server/rsc-entry/index.ts +30 -40
  179. package/src/server/rsc-entry/render-route.ts +6 -2
  180. package/src/server/rsc-entry/rsc-payload.ts +21 -4
  181. package/src/server/rsc-entry/rsc-stream.ts +8 -0
  182. package/src/server/rsc-entry/wrap-action-dispatch.ts +109 -372
  183. package/dist/_chunks/cache-api-DygSeKCB.js.map +0 -1
  184. package/dist/_chunks/cli-schema-sync-EXGYPhI2.js.map +0 -1
  185. package/dist/_chunks/logger-t3uxAmbX.js.map +0 -1
  186. package/dist/_chunks/tree-match-D2l830j2.js +0 -102
  187. package/dist/_chunks/tree-match-D2l830j2.js.map +0 -1
  188. package/dist/adapters/cloudflare.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { a as __esmMin, o as __exportAll, s as __toCommonJS } from "./_chunks/dist-BA3u1z3W.js";
2
2
  import { c as isDynamicMetadataExtension, l as isMetadataRouteServePath, r as canonicalize } from "./_chunks/canonicalize-P41GR6tY.js";
3
- import { t as matchUrlParts } from "./_chunks/tree-match-D2l830j2.js";
4
- import { h as swallow } from "./_chunks/logger-t3uxAmbX.js";
5
- import { i as scanRoutes } from "./_chunks/cli-schema-sync-EXGYPhI2.js";
6
- import { a as generateRouteMap, c as fileHasDirective, d as getPrerenderExport, i as validateSchemaAgainstRoutes, l as fileHasExport, n as collectInterceptionRewrites, o as fileHasAnyExport, s as fileHasDefaultExport, t as collectLeafRoutes, u as fileHasStarExport } from "./_chunks/walkers-DBVzXuWc.js";
3
+ import { h as swallow } from "./_chunks/logger-AWfuX-KJ.js";
4
+ import { i as scanRoutes } from "./_chunks/cli-schema-sync-B73L6pMq.js";
5
+ import { a as generateRouteMap, c as fileHasDirective, d as getPrerenderExport, i as validateSchemaAgainstRoutes, l as fileHasExport, n as collectInterceptionRewrites, o as fileHasAnyExport, s as fileHasDefaultExport, t as collectLeafRoutes, u as fileHasStarExport } from "./_chunks/walkers-CoOC8Hga.js";
7
6
  import { a as resolveAppDir, c as createNoopTimer, n as loadTimberConfigFile, o as resolveBuildDir, r as mergeFileConfig, s as resolveClientJavascript, t as createPluginContext } from "./_chunks/plugin-context---kTF5v8.js";
8
7
  import { t as fnv1aHash } from "./_chunks/fast-hash-D6hIVt1Y.js";
9
8
  import { t as formatSize } from "./_chunks/format-CfwjgPz9.js";
@@ -107,11 +106,18 @@ function timberContent(ctx) {
107
106
  };
108
107
  }
109
108
  //#endregion
110
- //#region ../../node_modules/.pnpm/srvx@0.11.17/node_modules/srvx/dist/adapters/node.mjs
111
- async function sendNodeResponse(nodeRes, webRes) {
109
+ //#region ../../node_modules/.pnpm/srvx@0.11.22/node_modules/srvx/dist/adapters/node.mjs
110
+ function sendNodeResponse(nodeRes, webRes) {
111
+ try {
112
+ return _sendNodeResponse(nodeRes, webRes, false) || Promise.resolve();
113
+ } catch (error) {
114
+ return Promise.reject(error);
115
+ }
116
+ }
117
+ function _sendNodeResponse(nodeRes, webRes, detached) {
112
118
  if (!webRes) {
113
119
  nodeRes.statusCode = 500;
114
- return endNodeResponse(nodeRes);
120
+ return endNodeResponse(nodeRes, detached);
115
121
  }
116
122
  if (webRes._toNodeResponse) {
117
123
  const res = webRes._toNodeResponse();
@@ -123,18 +129,22 @@ async function sendNodeResponse(nodeRes, webRes) {
123
129
  writeHead(nodeRes, res.status, res.statusText, res.headers);
124
130
  nodeRes.write(res.body);
125
131
  } else writeHead(nodeRes, res.status, res.statusText, res.headers);
126
- return endNodeResponse(nodeRes);
132
+ return endNodeResponse(nodeRes, detached);
127
133
  }
128
- const rawHeaders = [...webRes.headers];
134
+ const rawHeaders = [];
135
+ for (const [key, value] of webRes.headers) rawHeaders.push(key, value);
129
136
  writeHead(nodeRes, webRes.status, webRes.statusText, rawHeaders);
130
- return webRes.body ? streamBody(webRes.body, nodeRes) : endNodeResponse(nodeRes);
137
+ return webRes.body ? streamBody(webRes.body, nodeRes) : endNodeResponse(nodeRes, detached);
131
138
  }
132
139
  function writeHead(nodeRes, status, statusText, rawHeaders) {
133
- const writeHeaders = rawHeaders.flat();
134
- if (!nodeRes.headersSent) if (nodeRes.req?.httpVersion === "2.0") nodeRes.writeHead(status, writeHeaders);
135
- else nodeRes.writeHead(status, statusText, writeHeaders);
140
+ if (!nodeRes.headersSent) if (nodeRes.req?.httpVersion === "2.0") nodeRes.writeHead(status, rawHeaders);
141
+ else nodeRes.writeHead(status, statusText, rawHeaders);
136
142
  }
137
- function endNodeResponse(nodeRes) {
143
+ function endNodeResponse(nodeRes, detached) {
144
+ if (detached) {
145
+ nodeRes.end();
146
+ return;
147
+ }
138
148
  return new Promise((resolve) => nodeRes.end(resolve));
139
149
  }
140
150
  function pipeBody(stream, nodeRes, status, statusText, headers) {
@@ -1227,10 +1237,11 @@ function validateConfig(config) {
1227
1237
  suggestion: "Example: budget: { firstLoadJs: 150_000 } for a ~150 kB limit."
1228
1238
  });
1229
1239
  }
1240
+ const MOVED_TO_CACHE_FILE = /* @__PURE__ */ new Set(["cacheHandler", "cdnPurge"]);
1230
1241
  for (const key of Object.keys(config)) if (!knownKeys.has(key)) errors.push({
1231
1242
  field: key,
1232
1243
  message: `Unknown config option: "${key}".`,
1233
- suggestion: `Check for typos. Known options: ${[...knownKeys].sort().join(", ")}.`
1244
+ suggestion: MOVED_TO_CACHE_FILE.has(key) ? `"${key}" has moved to timber.cache.ts. Export cacheHandler as default, cdnPurge as a named export.` : `Check for typos. Known options: ${[...knownKeys].sort().join(", ")}.`
1234
1245
  });
1235
1246
  return errors;
1236
1247
  }
@@ -1304,12 +1315,9 @@ var init_config_validation = __esmMin((() => {
1304
1315
  debug: true,
1305
1316
  clientJavascript: true,
1306
1317
  adapter: true,
1307
- cacheHandler: true,
1308
- cdnPurge: true,
1309
1318
  allowedOrigins: true,
1310
1319
  csrf: true,
1311
1320
  limits: true,
1312
- actions: true,
1313
1321
  forms: true,
1314
1322
  pageExtensions: true,
1315
1323
  slowRequestMs: true,
@@ -1323,6 +1331,7 @@ var init_config_validation = __esmMin((() => {
1323
1331
  reactCompiler: true,
1324
1332
  sitemap: true,
1325
1333
  buildDir: true,
1334
+ clientSegmentCache: true,
1326
1335
  topLoader: true,
1327
1336
  budget: true
1328
1337
  }));
@@ -1337,7 +1346,7 @@ var init_config_validation = __esmMin((() => {
1337
1346
  "virtual:timber-config": "Runtime config (timber.config.ts)",
1338
1347
  "virtual:timber-route-manifest": "Route manifest (app/ file tree)",
1339
1348
  "virtual:timber-instrumentation": "Instrumentation (instrumentation.ts)",
1340
- "virtual:timber-cache-handler": "Cache handler (cacheHandler config)",
1349
+ "virtual:timber-cache-handler": "Cache handler (timber.cache.ts)",
1341
1350
  "virtual:timber-build-manifest": "Build manifest (asset mapping)"
1342
1351
  };
1343
1352
  REQUIRED_PEERS = [
@@ -1466,6 +1475,105 @@ function compressWithGzip(body) {
1466
1475
  return Readable.toWeb(gzip);
1467
1476
  }
1468
1477
  //#endregion
1478
+ //#region src/server/tree-match.ts
1479
+ /**
1480
+ * Match URL path parts against a segment tree.
1481
+ *
1482
+ * Walks the tree depth-first with backtracking, applying priority
1483
+ * ordering globally across group boundaries. Returns the matched
1484
+ * segment chain and collected params, or null if no route matches.
1485
+ *
1486
+ * By default, a match requires a terminal node (one with `page` or
1487
+ * `route`) at the leaf. Pass `{ requireTerminal: false }` to match
1488
+ * any segment (used by metadata route matching).
1489
+ *
1490
+ * @param root The root segment node to start matching from.
1491
+ * @param parts URL path parts (e.g. ['blog', 'hello-world']).
1492
+ * Empty array matches the root itself (if terminal).
1493
+ */
1494
+ function matchUrlParts(root, parts, options) {
1495
+ const chain = [];
1496
+ const params = {};
1497
+ if (matchNode(root, parts, 0, chain, params, options?.requireTerminal ?? true)) return {
1498
+ chain,
1499
+ params
1500
+ };
1501
+ return null;
1502
+ }
1503
+ /**
1504
+ * Flatten children through group segments.
1505
+ *
1506
+ * Groups are transparent for URL matching — their non-group descendants
1507
+ * are returned with the chain of group nodes leading to them. This lets
1508
+ * the caller apply priority ordering (static > dynamic > catch-all >
1509
+ * optional-catch-all) across all groups uniformly instead of per-group.
1510
+ */
1511
+ function collectEffectiveChildren(node, groupChain = []) {
1512
+ const result = [];
1513
+ for (const child of node.children) if (child.segmentType === "group") {
1514
+ const nested = collectEffectiveChildren(child, [...groupChain, child]);
1515
+ result.push(...nested);
1516
+ } else result.push({
1517
+ child,
1518
+ groupChain
1519
+ });
1520
+ return result;
1521
+ }
1522
+ /** A node is terminal if it can serve a response (has a page or route handler). */
1523
+ function isTerminal(node) {
1524
+ return !!(node.page || node.route);
1525
+ }
1526
+ /**
1527
+ * Recursive depth-first matcher with backtracking.
1528
+ *
1529
+ * Mutates `chain` and `params` in place for performance; backtracks
1530
+ * (restores previous state) when a candidate doesn't lead to a match.
1531
+ */
1532
+ function matchNode(node, parts, index, chain, params, requireTerminal) {
1533
+ chain.push(node);
1534
+ if (index >= parts.length) {
1535
+ if (!requireTerminal || isTerminal(node)) return true;
1536
+ for (const child of node.children) if (child.segmentType === "group") {
1537
+ if (matchNode(child, parts, index, chain, params, requireTerminal)) return true;
1538
+ }
1539
+ const effective = collectEffectiveChildren(node);
1540
+ for (const { child, groupChain } of effective) if (child.segmentType === "optional-catch-all" && (!requireTerminal || isTerminal(child))) {
1541
+ chain.push(...groupChain, child);
1542
+ return true;
1543
+ }
1544
+ chain.pop();
1545
+ return false;
1546
+ }
1547
+ const part = parts[index];
1548
+ const effective = collectEffectiveChildren(node);
1549
+ for (const { child, groupChain } of effective) if (child.segmentType === "static" && child.segmentName === part) {
1550
+ chain.push(...groupChain);
1551
+ if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
1552
+ chain.length -= groupChain.length;
1553
+ }
1554
+ for (const { child, groupChain } of effective) if (child.segmentType === "dynamic" && child.paramName) {
1555
+ chain.push(...groupChain);
1556
+ const prev = params[child.paramName];
1557
+ params[child.paramName] = part;
1558
+ if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
1559
+ chain.length -= groupChain.length;
1560
+ if (prev !== void 0) params[child.paramName] = prev;
1561
+ else delete params[child.paramName];
1562
+ }
1563
+ for (const { child, groupChain } of effective) if (child.segmentType === "catch-all" && child.paramName && (!requireTerminal || isTerminal(child))) {
1564
+ chain.push(...groupChain, child);
1565
+ params[child.paramName] = parts.slice(index);
1566
+ return true;
1567
+ }
1568
+ for (const { child, groupChain } of effective) if (child.segmentType === "optional-catch-all" && child.paramName && (!requireTerminal || isTerminal(child))) {
1569
+ chain.push(...groupChain, child);
1570
+ params[child.paramName] = parts.slice(index);
1571
+ return true;
1572
+ }
1573
+ chain.pop();
1574
+ return false;
1575
+ }
1576
+ //#endregion
1469
1577
  //#region src/plugins/dev-server.ts
1470
1578
  var RSC_ENTRY_ID = "virtual:timber-rsc-entry";
1471
1579
  /**
@@ -1892,6 +2000,7 @@ function generateConfigModule(ctx) {
1892
2000
  sitemap: ctx.config.sitemap,
1893
2001
  forms: serializeFormsConfig(ctx.config.forms),
1894
2002
  deploymentId: ctx.deploymentId ?? null,
2003
+ clientSegmentCache: ctx.config.clientSegmentCache ?? false,
1895
2004
  devBrowserLogs: ctx.config.devBrowserLogs
1896
2005
  };
1897
2006
  return [
@@ -1904,12 +2013,12 @@ function generateConfigModule(ctx) {
1904
2013
  ].join("\n");
1905
2014
  }
1906
2015
  /**
1907
- * Detect the user's instrumentation.ts file at the project root.
2016
+ * Detect a user convention file at both root and src/.
1908
2017
  *
1909
- * Checks for instrumentation.ts, .js, and .mjs — matching the same
1910
- * extensions as timber.config.ts detection.
2018
+ * Root takes precedence over src/, extension priority: .ts > .js > .mjs.
2019
+ * Used for instrumentation.ts, timber.cache.ts, and similar file conventions.
1911
2020
  */
1912
- function detectInstrumentationFile(root) {
2021
+ function detectConventionFile(root, basename) {
1913
2022
  const extensions = [
1914
2023
  ".ts",
1915
2024
  ".js",
@@ -1917,11 +2026,25 @@ function detectInstrumentationFile(root) {
1917
2026
  ];
1918
2027
  const dirs = [root, resolve(root, "src")];
1919
2028
  for (const dir of dirs) for (const ext of extensions) {
1920
- const candidate = resolve(dir, `instrumentation${ext}`);
2029
+ const candidate = resolve(dir, `${basename}${ext}`);
1921
2030
  if (existsSync(candidate)) return candidate;
1922
2031
  }
1923
2032
  return null;
1924
2033
  }
2034
+ function detectInstrumentationFile(root) {
2035
+ return detectConventionFile(root, "instrumentation");
2036
+ }
2037
+ /**
2038
+ * Detect the user's timber.cache file.
2039
+ *
2040
+ * This file holds runtime cache instances (cacheHandler, cdnPurge)
2041
+ * that cannot be JSON-serialized. Separating them from timber.config.ts
2042
+ * prevents the RSC build from pulling build-time deps (adapters, MDX,
2043
+ * rollup) into the runtime bundle. See TIM-1211.
2044
+ */
2045
+ function detectCacheFile(root) {
2046
+ return detectConventionFile(root, "timber.cache");
2047
+ }
1925
2048
  /**
1926
2049
  * Generate the virtual:timber-instrumentation module source.
1927
2050
  *
@@ -1951,50 +2074,37 @@ function generateInstrumentationModule(instrumentationPath) {
1951
2074
  ].join("\n");
1952
2075
  }
1953
2076
  /**
1954
- * Detect the user's timber.config file at the project root.
1955
- * Returns the absolute path or null if not found.
1956
- */
1957
- function detectConfigFile(root) {
1958
- for (const name of [
1959
- "timber.config.ts",
1960
- "timber.config.js",
1961
- "timber.config.mjs"
1962
- ]) {
1963
- const candidate = resolve(root, name);
1964
- if (existsSync(candidate)) return candidate;
1965
- }
1966
- return null;
1967
- }
1968
- /**
1969
2077
  * Generate the virtual:timber-cache-handler module source.
1970
2078
  *
1971
- * When the user's config has a cacheHandler, generates a module that
1972
- * dynamically imports the config file and extracts the cacheHandler.
1973
- * The cacheHandler is a class instance (e.g. RedisCacheHandler) that
1974
- * cannot be JSON-serialized into virtual:timber-config, so it must
1975
- * be loaded at runtime via dynamic import. See TIM-599.
2079
+ * When the user's timber.cache.ts exists, generates a module that
2080
+ * dynamically imports it and extracts cacheHandler and cdnPurge.
2081
+ * These are class instances (e.g. RedisCacheHandler) that cannot be
2082
+ * JSON-serialized into virtual:timber-config, so they must be loaded
2083
+ * at runtime via dynamic import.
2084
+ *
2085
+ * timber.cache.ts exports: default cacheHandler, named cdnPurge.
2086
+ *
2087
+ * See TIM-1211, TIM-599.
1976
2088
  */
1977
- function generateCacheHandlerModule(configPath, hasCacheHandler, hasCdnPurge) {
1978
- if (configPath && (hasCacheHandler || hasCdnPurge)) return [
2089
+ function generateCacheHandlerModule(cachePath) {
2090
+ if (cachePath) return [
1979
2091
  "// Auto-generated cache handler loader — do not edit.",
1980
2092
  "// Generated by timber-entries plugin.",
1981
2093
  "",
1982
2094
  `export default async function loadCacheHandler() {`,
1983
- ` const mod = await import(${JSON.stringify(configPath)});`,
1984
- ` const config = mod.default ?? mod;`,
1985
- ` return config.cacheHandler ?? null;`,
2095
+ ` const mod = await import(${JSON.stringify(cachePath)});`,
2096
+ ` return mod.default ?? null;`,
1986
2097
  `}`,
1987
2098
  "",
1988
2099
  `export async function loadCdnPurgeHandler() {`,
1989
- ` const mod = await import(${JSON.stringify(configPath)});`,
1990
- ` const config = mod.default ?? mod;`,
1991
- ` return config.cdnPurge ?? null;`,
2100
+ ` const mod = await import(${JSON.stringify(cachePath)});`,
2101
+ ` return mod.cdnPurge ?? null;`,
1992
2102
  `}`
1993
2103
  ].join("\n");
1994
2104
  return [
1995
2105
  "// Auto-generated cache handler loader — do not edit.",
1996
2106
  "// Generated by timber-entries plugin.",
1997
- "// No cacheHandler or cdnPurge configured in timber.config.",
2107
+ "// No timber.cache.ts found.",
1998
2108
  "",
1999
2109
  `export default async function loadCacheHandler() {`,
2000
2110
  ` return null;`,
@@ -2042,7 +2152,7 @@ function timberEntries(ctx) {
2042
2152
  load(id) {
2043
2153
  if (id === RESOLVED_CONFIG_ID) return generateConfigModule(ctx);
2044
2154
  if (id === RESOLVED_INSTRUMENTATION_ID) return generateInstrumentationModule(detectInstrumentationFile(ctx.root));
2045
- if (id === RESOLVED_CACHE_HANDLER_ID) return generateCacheHandlerModule(detectConfigFile(ctx.root), ctx.config.cacheHandler != null, ctx.config.cdnPurge != null);
2155
+ if (id === RESOLVED_CACHE_HANDLER_ID) return generateCacheHandlerModule(detectCacheFile(ctx.root));
2046
2156
  return null;
2047
2157
  }
2048
2158
  };
@@ -4610,6 +4720,30 @@ var FontPipeline = class {
4610
4720
  const familyKey = family.toLowerCase();
4611
4721
  for (const [id, entry] of this._entries) if (entry.importer === importer && entry.family.toLowerCase() === familyKey) this._entries.delete(id);
4612
4722
  }
4723
+ /** True if at least one entry is registered under `importer`. */
4724
+ hasImporter(importer) {
4725
+ for (const entry of this._entries.values()) if (entry.importer === importer) return true;
4726
+ return false;
4727
+ }
4728
+ /**
4729
+ * Drop **every** registry entry that originated from `importer`,
4730
+ * regardless of family.
4731
+ *
4732
+ * Called at the start of the transform hook before re-registering the
4733
+ * current extraction set. This handles the cases `pruneFor` misses:
4734
+ *
4735
+ * - **Family rename** (Inter → Roboto): the old family's entries are
4736
+ * cleared because we prune by importer, not by (importer, family).
4737
+ * - **Font call deleted**: the old entries are cleared even though no
4738
+ * new call provides a family to prune against.
4739
+ *
4740
+ * Other importers' fonts are untouched.
4741
+ *
4742
+ * See TIM-1087.
4743
+ */
4744
+ pruneImporter(importer) {
4745
+ for (const [id, entry] of this._entries) if (entry.importer === importer) this._entries.delete(id);
4746
+ }
4613
4747
  /** Drop every registered font. Used by tests and rebuild flows. */
4614
4748
  clear() {
4615
4749
  this._entries.clear();
@@ -5788,6 +5922,7 @@ function runFontsTransform(code, id, pipeline, emitError) {
5788
5922
  }
5789
5923
  if (analysis.callSites.length === 0 && analysis.bindings.length === 0) return null;
5790
5924
  const s = new MagicString(code);
5925
+ const pending = [];
5791
5926
  for (const site of analysis.callSites) if (site.binding.provider === "google") {
5792
5927
  const family = site.binding.importedName.replace(/_/g, " ");
5793
5928
  const config = extractGoogleFontConfigFromNode(site.configNode);
@@ -5796,27 +5931,32 @@ function runFontsTransform(code, id, pipeline, emitError) {
5796
5931
  const className = familyToClassName(family);
5797
5932
  const fontStack = buildFontStack(family);
5798
5933
  const display = config.display ?? "swap";
5799
- const extracted = {
5800
- id: fontId,
5801
- family,
5802
- provider: "google",
5803
- weights: normalizeWeightArray(config.weight),
5804
- styles: normalizeStyleArray(config.style),
5805
- subsets: config.subsets ?? ["latin"],
5806
- display,
5807
- variable: config.variable,
5808
- className,
5809
- fontFamily: fontStack,
5810
- importer: id
5811
- };
5812
- pipeline.pruneFor(id, family);
5813
- pipeline.register(extracted);
5814
- s.overwrite(site.replaceStart, site.replaceEnd, buildFontResultLiteral(extracted));
5934
+ pending.push({
5935
+ site,
5936
+ extracted: {
5937
+ id: fontId,
5938
+ family,
5939
+ provider: "google",
5940
+ weights: normalizeWeightArray(config.weight),
5941
+ styles: normalizeStyleArray(config.style),
5942
+ subsets: config.subsets ?? ["latin"],
5943
+ display,
5944
+ variable: config.variable,
5945
+ className,
5946
+ fontFamily: fontStack,
5947
+ importer: id
5948
+ }
5949
+ });
5815
5950
  } else {
5816
5951
  const config = extractLocalFontConfigFromNode(site.configNode);
5817
5952
  if (!config) emitError(`Could not statically analyze local font config at line ${site.line}, column ${site.column}. Ensure src is a string or array of { path, weight?, style? } objects.`);
5818
- const extracted = processLocalFont(config, id);
5819
- pipeline.pruneFor(id, extracted.family);
5953
+ pending.push({
5954
+ site,
5955
+ extracted: processLocalFont(config, id)
5956
+ });
5957
+ }
5958
+ pipeline.pruneImporter(id);
5959
+ for (const { extracted, site } of pending) {
5820
5960
  pipeline.register(extracted);
5821
5961
  s.overwrite(site.replaceStart, site.replaceEnd, buildFontResultLiteral(extracted));
5822
5962
  }
@@ -5986,6 +6126,25 @@ function writeFontManifest(pipeline, buildManifest, base = "/") {
5986
6126
  //#endregion
5987
6127
  //#region src/plugins/fonts.ts
5988
6128
  /**
6129
+ * timber-fonts — Vite sub-plugin shell for build-time font processing.
6130
+ *
6131
+ * This file is intentionally thin: it wires the Vite plugin hooks to the
6132
+ * pipeline + per-concern modules under `../fonts/`. All real logic lives
6133
+ * in:
6134
+ *
6135
+ * - `../fonts/pipeline.ts` — `FontPipeline` (state, mutators)
6136
+ * - `../fonts/transform.ts` — transform-hook logic, parsing helpers
6137
+ * - `../fonts/dev-middleware.ts` — dev font binary server
6138
+ * - `../fonts/virtual-modules.ts` — virtual module ID constants + source
6139
+ *
6140
+ * The public exports below are the stable API that the test suite and
6141
+ * other parts of the framework import. Anything new should be added to
6142
+ * one of the per-concern modules and re-exported here only if external
6143
+ * consumers need it.
6144
+ *
6145
+ * Design doc: 24-fonts.md
6146
+ */
6147
+ /**
5989
6148
  * Create the timber-fonts Vite plugin.
5990
6149
  */
5991
6150
  function timberFonts(ctx) {
@@ -6119,6 +6278,27 @@ function timberFonts(ctx) {
6119
6278
  */
6120
6279
  buildStart() {},
6121
6280
  /**
6281
+ * Prune stale font entries when a module that previously imported fonts
6282
+ * no longer contains any font import. The transform hook's `filter.code`
6283
+ * skips such modules, so without this hook the old entries would persist
6284
+ * until dev restart. See TIM-1087.
6285
+ */
6286
+ async hotUpdate({ type, file, read }) {
6287
+ if (!pipeline.hasImporter(file)) return;
6288
+ if (type === "delete") {
6289
+ pipeline.pruneImporter(file);
6290
+ return;
6291
+ }
6292
+ const code = await read();
6293
+ if (code.includes("@timber/fonts/") || code.includes("@timber-js/app/fonts/") || code.includes("next/font/")) return;
6294
+ try {
6295
+ parseAst(code, { lang: "tsx" });
6296
+ } catch {
6297
+ return;
6298
+ }
6299
+ pipeline.pruneImporter(file);
6300
+ },
6301
+ /**
6122
6302
  * Scan source files for font function calls and extract static config.
6123
6303
  *
6124
6304
  * Delegates to `runFontsTransform`. Errors raised through the
@@ -7383,14 +7563,14 @@ function generateStableCallsiteId(relativeModulePath, kind, statementText, callS
7383
7563
  //#region src/plugins/cache.ts
7384
7564
  /**
7385
7565
  * timber-cache-transform — Vite plugin that injects stable callsite IDs
7386
- * into `cache()` calls imported from `@timber-js/app/cache`.
7566
+ * into `cache.data()` calls imported from `@timber-js/app/cache`.
7387
7567
  *
7388
7568
  * Detected callsite forms (TIM-1054):
7389
- * - named imports, including aliases: `import { cache as c }` + `c(fn, opts)`
7390
- * - namespace imports: `import * as ns` + `ns.cache(fn, opts)`
7569
+ * - named imports, including aliases: `import { cache as c }` + `c.data(fn, opts)`
7570
+ * - namespace imports: `import * as ns` + `ns.cache.data(fn, opts)`
7391
7571
  * Forms a per-module transform cannot trace — local variable aliases
7392
7572
  * (`const c = cache`), user re-export wrapper modules, computed access
7393
- * (`ns['cache']`) — fall back to the runtime fnId derived from
7573
+ * (`cache['data']`) — fall back to the runtime fnId derived from
7394
7574
  * fnv1a(fn.toString()) (see cache/timber-cache.ts), which is deterministic
7395
7575
  * across instances and cold starts of the same build. Without either
7396
7576
  * mechanism, an ordinal counter would collide across instances with a shared
@@ -7398,8 +7578,8 @@ function generateStableCallsiteId(relativeModulePath, kind, statementText, callS
7398
7578
  * data to another.
7399
7579
  *
7400
7580
  * The plugin transforms:
7401
- * cache(fn, opts) → cache(fn, opts, "stable-id")
7402
- * ns.cache(fn, opts) → ns.cache(fn, opts, "stable-id")
7581
+ * cache.data(fn, opts) → cache.data(fn, opts, "stable-id")
7582
+ * ns.cache.data(fn, opts) → ns.cache.data(fn, opts, "stable-id")
7403
7583
  *
7404
7584
  * where stable-id =
7405
7585
  * fnv1a(relPath + ":cache:" + fnv1a(stmtText) + ":" + fnv1a(callSpanText) + ":" + occurrence)
@@ -7417,9 +7597,9 @@ function generateStableCallsiteId(relativeModulePath, kind, statementText, callS
7417
7597
  * unrelated edits, while a changed call gets a fresh ID — which is exactly
7418
7598
  * when invalidation is wanted.
7419
7599
  *
7420
- * The enclosing-statement text is included so that byte-identical cache()
7600
+ * The enclosing-statement text is included so that byte-identical cache.data()
7421
7601
  * calls in different scopes — e.g. two factory helpers that each `return
7422
- * cache(fn, { ttl: 60 })` — get distinct, insertion-stable IDs: their
7602
+ * cache.data(fn, { ttl: 60 })` — get distinct, insertion-stable IDs: their
7423
7603
  * enclosing declarations differ by name/body even when the call spans are
7424
7604
  * identical. Without it, a source-order occurrence counter alone would shift
7425
7605
  * when an identical span is inserted above, reassigning IDs across deploys.
@@ -7436,32 +7616,40 @@ function generateStableCallsiteId(relativeModulePath, kind, statementText, callS
7436
7616
  * See TIM-997, TIM-1053, design/06-caching.md §"Cache Key Security".
7437
7617
  */
7438
7618
  /**
7439
- * Is this callee a reference to the imported `cache` function — either a bare
7440
- * identifier bound by a named import, or `ns.cache` on a namespace import?
7441
- * Computed access (`ns['cache']`) is deliberately not matched; it falls back
7619
+ * Is this callee a `cache.data(...)` call? Matches two forms:
7620
+ * - Named import: `cache.data(fn, opts)` where `cache` is the imported binding
7621
+ * - Namespace import: `ns.cache.data(fn, opts)` where `ns` is the namespace
7622
+ * Computed access (`cache['data']`) is deliberately not matched; it falls back
7442
7623
  * to the runtime fnId derivation.
7443
7624
  */
7444
- function isCacheCallee(callee, bindings) {
7445
- if (callee.type === "Identifier") return bindings.named.has(callee.name);
7446
- if (callee.type === "MemberExpression") {
7447
- const member = callee;
7448
- return !member.computed && member.object.type === "Identifier" && bindings.namespaces.has(member.object.name) && member.property.type === "Identifier" && member.property.name === "cache";
7625
+ function isCacheDataCallee(callee, bindings) {
7626
+ if (callee.type !== "MemberExpression") return false;
7627
+ const member = callee;
7628
+ if (member.computed) return false;
7629
+ if (member.property.type !== "Identifier" || member.property.name !== "data") return false;
7630
+ const obj = member.object;
7631
+ if (obj.type === "Identifier") return bindings.named.has(obj.name);
7632
+ if (obj.type === "MemberExpression") {
7633
+ const outer = obj;
7634
+ return !outer.computed && outer.object.type === "Identifier" && bindings.namespaces.has(outer.object.name) && outer.property.type === "Identifier" && outer.property.name === "cache";
7449
7635
  }
7450
7636
  return false;
7451
7637
  }
7452
7638
  /**
7453
- * Extract the local binding name that a cache callee resolves to — the
7454
- * identifier for named imports, or the namespace object for `ns.cache()`.
7639
+ * Extract the local binding name that a cache.data callee resolves to — the
7640
+ * `cache` identifier for named imports, or the namespace object for `ns.cache.data()`.
7455
7641
  */
7456
7642
  function calleeBindingName(callee, bindings) {
7457
- if (callee.type === "Identifier") {
7458
- const name = callee.name;
7643
+ if (callee.type !== "MemberExpression") return null;
7644
+ const obj = callee.object;
7645
+ if (obj.type === "Identifier") {
7646
+ const name = obj.name;
7459
7647
  return bindings.named.has(name) ? name : null;
7460
7648
  }
7461
- if (callee.type === "MemberExpression") {
7462
- const member = callee;
7463
- if (member.object.type === "Identifier") {
7464
- const name = member.object.name;
7649
+ if (obj.type === "MemberExpression") {
7650
+ const outer = obj;
7651
+ if (outer.object.type === "Identifier") {
7652
+ const name = outer.object.name;
7465
7653
  return bindings.namespaces.has(name) ? name : null;
7466
7654
  }
7467
7655
  }
@@ -7499,7 +7687,7 @@ function timberCacheTransform(ctx) {
7499
7687
  const callSites = [];
7500
7688
  for (const stmt of program.body) {
7501
7689
  const calls = [];
7502
- findCallSites(stmt, (call) => call.arguments.length === 2 && isCacheCallee(call.callee, cacheBindings) && !isCalleeShadowed(call), calls);
7690
+ findCallSites(stmt, (call) => call.arguments.length === 2 && isCacheDataCallee(call.callee, cacheBindings) && !isCalleeShadowed(call), calls);
7503
7691
  for (const call of calls) callSites.push({
7504
7692
  call,
7505
7693
  stmt
@@ -8650,6 +8838,7 @@ function timberAdapterBuild(ctx) {
8650
8838
  }
8651
8839
  if (ctx.clientJavascript.disabled) await stripJsFromRscAssetsManifests(buildDir);
8652
8840
  const adapterConfig = {
8841
+ root: ctx.root,
8653
8842
  output: ctx.config.output ?? "server",
8654
8843
  clientJavascriptDisabled: ctx.clientJavascript.disabled,
8655
8844
  manifestInit
@@ -8660,6 +8849,84 @@ function timberAdapterBuild(ctx) {
8660
8849
  };
8661
8850
  }
8662
8851
  /**
8852
+ * Parse the RSC assets manifest at `manifestPath`. Tries JSON.parse first
8853
+ * (fast path for plain-string manifests); falls back to a dynamic import()
8854
+ * for manifests that contain runtime expressions from renderBuiltUrl.
8855
+ * Returns null if both paths fail.
8856
+ */
8857
+ async function parseRscManifest(manifestPath, content) {
8858
+ const jsonStr = content.replace(/^export default\s*/, "").replace(/;?\s*$/, "");
8859
+ try {
8860
+ return JSON.parse(jsonStr);
8861
+ } catch {}
8862
+ try {
8863
+ return (await import(`${pathToFileURL(manifestPath).href}?t=${Date.now()}`)).default ?? null;
8864
+ } catch (err) {
8865
+ swallow(err, `failed to evaluate RSC assets manifest: ${manifestPath}`, { level: "warn" });
8866
+ return null;
8867
+ }
8868
+ }
8869
+ /**
8870
+ * Replace all `"js": [...]` array contents with `"js": []` using balanced
8871
+ * bracket matching. Handles nested brackets from runtime expressions like
8872
+ * `__rbu("path/to/[slug]/file.js")`.
8873
+ */
8874
+ function clearJsArrays(src) {
8875
+ const pattern = /"js"\s*:\s*\[/g;
8876
+ let result = "";
8877
+ let lastEnd = 0;
8878
+ let match;
8879
+ while ((match = pattern.exec(src)) !== null) {
8880
+ const arrayStart = match.index + match[0].length;
8881
+ let depth = 1;
8882
+ let i = arrayStart;
8883
+ while (i < src.length && depth > 0) {
8884
+ if (src[i] === "[") depth++;
8885
+ else if (src[i] === "]") depth--;
8886
+ i++;
8887
+ }
8888
+ result += src.slice(lastEnd, match.index) + "\"js\": []";
8889
+ lastEnd = i;
8890
+ }
8891
+ result += src.slice(lastEnd);
8892
+ return result;
8893
+ }
8894
+ /**
8895
+ * Replace `"bootstrapScriptContent": <value>` with an empty string. The
8896
+ * value may be a JS expression (e.g., concatenation with runtime helpers),
8897
+ * not a simple string literal. Scans forward from the colon, tracking
8898
+ * balanced parens and brackets, and stops at the next unbalanced `,` or `}`.
8899
+ */
8900
+ function clearBootstrapScriptContent(src) {
8901
+ const idx = src.indexOf("\"bootstrapScriptContent\"");
8902
+ if (idx === -1) return src;
8903
+ const colonIdx = src.indexOf(":", idx + 24);
8904
+ if (colonIdx === -1) return src;
8905
+ let valStart = colonIdx + 1;
8906
+ while (valStart < src.length && /\s/.test(src[valStart])) valStart++;
8907
+ let i = valStart;
8908
+ let depth = 0;
8909
+ let inString = false;
8910
+ let stringChar = "";
8911
+ while (i < src.length) {
8912
+ const ch = src[i];
8913
+ if (inString) {
8914
+ if (ch === "\\") {
8915
+ i += 2;
8916
+ continue;
8917
+ }
8918
+ if (ch === stringChar) inString = false;
8919
+ } else if (ch === "\"" || ch === "'") {
8920
+ inString = true;
8921
+ stringChar = ch;
8922
+ } else if (ch === "(" || ch === "[") depth++;
8923
+ else if (ch === ")" || ch === "]") depth--;
8924
+ else if (depth === 0 && (ch === "," || ch === "}" || ch === "\n")) break;
8925
+ i++;
8926
+ }
8927
+ return src.slice(0, idx) + "\"bootstrapScriptContent\": \"\"" + src.slice(i);
8928
+ }
8929
+ /**
8663
8930
  * Strip JS references from the RSC plugin's assets manifest files.
8664
8931
  *
8665
8932
  * The RSC plugin writes `__vite_rsc_assets_manifest.js` to rsc/ and ssr/
@@ -8684,13 +8951,8 @@ async function mergeRscAssetsManifest(buildDir, root, buildManifest, routeClient
8684
8951
  } catch {
8685
8952
  return;
8686
8953
  }
8687
- const jsonStr = content.replace(/^export default\s*/, "").replace(/;?\s*$/, "");
8688
- let manifest;
8689
- try {
8690
- manifest = JSON.parse(jsonStr);
8691
- } catch {
8692
- return;
8693
- }
8954
+ const manifest = await parseRscManifest(manifestPath, content);
8955
+ if (!manifest) return;
8694
8956
  const serverResources = manifest.serverResources;
8695
8957
  if (serverResources) {
8696
8958
  for (const [relativePath, deps] of Object.entries(serverResources)) if (deps.css && deps.css.length > 0) {
@@ -8721,16 +8983,23 @@ async function stripJsFromRscAssetsManifests(buildDir) {
8721
8983
  continue;
8722
8984
  }
8723
8985
  const jsonStr = content.replace(/^export default\s*/, "").replace(/;?\s*$/, "");
8724
- let manifest;
8986
+ let isStrictJson = true;
8987
+ let manifest = null;
8725
8988
  try {
8726
8989
  manifest = JSON.parse(jsonStr);
8727
- } catch (err) {
8728
- swallow(err, `corrupted RSC assets manifest: ${path}`, { level: "warn" });
8990
+ } catch {
8991
+ isStrictJson = false;
8992
+ }
8993
+ if (!isStrictJson) {
8994
+ let rewritten = content;
8995
+ rewritten = clearJsArrays(rewritten);
8996
+ rewritten = clearBootstrapScriptContent(rewritten);
8997
+ await writeFile(path, rewritten);
8729
8998
  continue;
8730
8999
  }
8731
- const deps = manifest.clientReferenceDeps;
9000
+ const deps = manifest?.clientReferenceDeps;
8732
9001
  if (deps) for (const entry of Object.values(deps)) entry.js = [];
8733
- manifest.bootstrapScriptContent = "";
9002
+ if (manifest) manifest.bootstrapScriptContent = "";
8734
9003
  await writeFile(path, `export default ${JSON.stringify(manifest, null, 2)};\n`);
8735
9004
  }
8736
9005
  }