@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.2

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 (275) hide show
  1. package/README.md +23 -0
  2. package/dist/cjs/backend-federation-build/codegen.js +29 -18
  3. package/dist/cjs/backend-federation-build/config.js +84 -45
  4. package/dist/cjs/backend-federation-build/emit.js +63 -22
  5. package/dist/cjs/build-config/build-environment.js +25 -162
  6. package/dist/cjs/build-config/internal-effect-discovery.js +143 -0
  7. package/dist/cjs/build-config/public.js +2 -5
  8. package/dist/cjs/build-config/remote-address.js +58 -0
  9. package/dist/cjs/cloudflare/artifacts.js +8 -19
  10. package/dist/cjs/cloudflare/delivery-unit.js +125 -43
  11. package/dist/cjs/cloudflare/index.js +14 -4
  12. package/dist/cjs/cloudflare/native-renderer.js +100 -0
  13. package/dist/cjs/cloudflare/renderer-identity.js +106 -0
  14. package/dist/cjs/cloudflare/worker-manifest.js +54 -13
  15. package/dist/cjs/cloudflare/worker-options.js +48 -0
  16. package/dist/cjs/cloudflare/wrangler-config.js +210 -13
  17. package/dist/cjs/cloudflare-builder.js +307 -89
  18. package/dist/cjs/cloudflare-output-contract.js +36 -4
  19. package/dist/cjs/cloudflare-output-verifier/identity.js +112 -23
  20. package/dist/cjs/cloudflare-output-verifier/index.js +10 -0
  21. package/dist/cjs/cloudflare-output-verifier/manifest.js +46 -0
  22. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +7 -1
  23. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +10 -5
  24. package/dist/cjs/config.js +10 -5
  25. package/dist/cjs/deploy-output/npmAliases.js +92 -6
  26. package/dist/cjs/deploy-output/plugin.js +27 -6
  27. package/dist/cjs/deploy-output/public-assets.js +146 -0
  28. package/dist/cjs/deploy-output/target.js +57 -1
  29. package/dist/cjs/deploy-output/trace-files.js +99 -0
  30. package/dist/cjs/module-federation-config/exposes.js +70 -0
  31. package/dist/cjs/module-federation-config/index.js +73 -0
  32. package/dist/cjs/module-federation-config/inspect.js +103 -0
  33. package/dist/cjs/module-federation-config/object-literal.js +87 -0
  34. package/dist/cjs/module-federation-config/syntax.js +151 -0
  35. package/dist/cjs/module-federation-config/types.js +18 -0
  36. package/dist/cjs/native-type-checker.js +130 -13
  37. package/dist/cjs/policy-defaults.js +38 -29
  38. package/dist/cjs/release-envelope/canonical.js +2 -9
  39. package/dist/cjs/release-envelope/contract.js +68 -0
  40. package/dist/cjs/release-envelope/framework-output.js +296 -105
  41. package/dist/cjs/release-envelope/index.js +150 -36
  42. package/dist/cjs/release-envelope/plugin.js +24 -15
  43. package/dist/cjs/release-envelope/renderer-output-stamp.js +170 -0
  44. package/dist/cjs/release-envelope/types.js +10 -8
  45. package/dist/cjs/release-identity.js +8 -5
  46. package/dist/cjs/renderer-build-identity.js +134 -0
  47. package/dist/cjs/templates/cloudflare-entry.001-bootstrap-security.mjs +14 -6
  48. package/dist/cjs/templates/cloudflare-entry.002-assets-routes.mjs +10 -2
  49. package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +403 -96
  50. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +530 -103
  51. package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +26 -7
  52. package/dist/cjs/templates/cloudflare-worker-route-data.mjs +1069 -0
  53. package/dist/cjs/workspace-topology.js +84 -0
  54. package/dist/esm/backend-federation-build/codegen.mjs +29 -18
  55. package/dist/esm/backend-federation-build/config.mjs +74 -38
  56. package/dist/esm/backend-federation-build/emit.mjs +64 -23
  57. package/dist/esm/build-config/build-environment.mjs +26 -160
  58. package/dist/esm/build-config/internal-effect-discovery.mjs +102 -0
  59. package/dist/esm/build-config/public.mjs +1 -1
  60. package/dist/esm/build-config/remote-address.mjs +20 -0
  61. package/dist/esm/cloudflare/artifacts.mjs +8 -19
  62. package/dist/esm/cloudflare/delivery-unit.mjs +124 -45
  63. package/dist/esm/cloudflare/index.mjs +15 -5
  64. package/dist/esm/cloudflare/native-renderer.mjs +48 -0
  65. package/dist/esm/cloudflare/renderer-identity.mjs +57 -0
  66. package/dist/esm/cloudflare/worker-manifest.mjs +55 -14
  67. package/dist/esm/cloudflare/worker-options.mjs +10 -0
  68. package/dist/esm/cloudflare/wrangler-config.mjs +203 -12
  69. package/dist/esm/cloudflare-builder.mjs +301 -89
  70. package/dist/esm/cloudflare-output-contract.mjs +29 -3
  71. package/dist/esm/cloudflare-output-verifier/identity.mjs +113 -24
  72. package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
  73. package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
  74. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  75. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +11 -6
  76. package/dist/esm/config.mjs +3 -1
  77. package/dist/esm/deploy-output/npmAliases.mjs +92 -6
  78. package/dist/esm/deploy-output/plugin.mjs +24 -6
  79. package/dist/esm/deploy-output/public-assets.mjs +82 -0
  80. package/dist/esm/deploy-output/target.mjs +50 -3
  81. package/dist/esm/deploy-output/trace-files.mjs +47 -0
  82. package/dist/esm/module-federation-config/exposes.mjs +18 -0
  83. package/dist/esm/module-federation-config/index.mjs +4 -0
  84. package/dist/esm/module-federation-config/inspect.mjs +65 -0
  85. package/dist/esm/module-federation-config/object-literal.mjs +43 -0
  86. package/dist/esm/module-federation-config/syntax.mjs +101 -0
  87. package/dist/esm/module-federation-config/types.mjs +0 -0
  88. package/dist/esm/native-type-checker.mjs +124 -13
  89. package/dist/esm/policy-defaults.mjs +36 -30
  90. package/dist/esm/release-envelope/canonical.mjs +2 -9
  91. package/dist/esm/release-envelope/contract.mjs +21 -0
  92. package/dist/esm/release-envelope/framework-output.mjs +297 -106
  93. package/dist/esm/release-envelope/index.mjs +149 -38
  94. package/dist/esm/release-envelope/plugin.mjs +24 -15
  95. package/dist/esm/release-envelope/renderer-output-stamp.mjs +112 -0
  96. package/dist/esm/release-envelope/types.mjs +2 -3
  97. package/dist/esm/release-identity.mjs +8 -5
  98. package/dist/esm/renderer-build-identity.mjs +82 -0
  99. package/dist/esm/templates/cloudflare-entry.001-bootstrap-security.mjs +14 -6
  100. package/dist/esm/templates/cloudflare-entry.002-assets-routes.mjs +10 -2
  101. package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +403 -96
  102. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +530 -103
  103. package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +26 -7
  104. package/dist/esm/templates/cloudflare-worker-route-data.mjs +1069 -0
  105. package/dist/esm/workspace-topology.mjs +21 -0
  106. package/dist/esm-node/backend-federation-build/codegen.mjs +29 -18
  107. package/dist/esm-node/backend-federation-build/config.mjs +74 -38
  108. package/dist/esm-node/backend-federation-build/emit.mjs +64 -23
  109. package/dist/esm-node/build-config/build-environment.mjs +26 -160
  110. package/dist/esm-node/build-config/internal-effect-discovery.mjs +103 -0
  111. package/dist/esm-node/build-config/public.mjs +1 -1
  112. package/dist/esm-node/build-config/remote-address.mjs +21 -0
  113. package/dist/esm-node/cloudflare/artifacts.mjs +8 -19
  114. package/dist/esm-node/cloudflare/delivery-unit.mjs +124 -45
  115. package/dist/esm-node/cloudflare/index.mjs +15 -5
  116. package/dist/esm-node/cloudflare/native-renderer.mjs +49 -0
  117. package/dist/esm-node/cloudflare/renderer-identity.mjs +58 -0
  118. package/dist/esm-node/cloudflare/worker-manifest.mjs +55 -14
  119. package/dist/esm-node/cloudflare/worker-options.mjs +11 -0
  120. package/dist/esm-node/cloudflare/wrangler-config.mjs +203 -12
  121. package/dist/esm-node/cloudflare-builder.mjs +301 -89
  122. package/dist/esm-node/cloudflare-output-contract.mjs +29 -3
  123. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +113 -24
  124. package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
  125. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
  126. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  127. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +11 -6
  128. package/dist/esm-node/config.mjs +3 -1
  129. package/dist/esm-node/deploy-output/npmAliases.mjs +92 -6
  130. package/dist/esm-node/deploy-output/plugin.mjs +24 -6
  131. package/dist/esm-node/deploy-output/public-assets.mjs +83 -0
  132. package/dist/esm-node/deploy-output/target.mjs +50 -3
  133. package/dist/esm-node/deploy-output/trace-files.mjs +48 -0
  134. package/dist/esm-node/module-federation-config/exposes.mjs +19 -0
  135. package/dist/esm-node/module-federation-config/index.mjs +5 -0
  136. package/dist/esm-node/module-federation-config/inspect.mjs +66 -0
  137. package/dist/esm-node/module-federation-config/object-literal.mjs +44 -0
  138. package/dist/esm-node/module-federation-config/syntax.mjs +102 -0
  139. package/dist/esm-node/module-federation-config/types.mjs +1 -0
  140. package/dist/esm-node/native-type-checker.mjs +124 -13
  141. package/dist/esm-node/policy-defaults.mjs +36 -30
  142. package/dist/esm-node/release-envelope/canonical.mjs +2 -9
  143. package/dist/esm-node/release-envelope/contract.mjs +22 -0
  144. package/dist/esm-node/release-envelope/framework-output.mjs +297 -106
  145. package/dist/esm-node/release-envelope/index.mjs +149 -38
  146. package/dist/esm-node/release-envelope/plugin.mjs +24 -15
  147. package/dist/esm-node/release-envelope/renderer-output-stamp.mjs +113 -0
  148. package/dist/esm-node/release-envelope/types.mjs +2 -3
  149. package/dist/esm-node/release-identity.mjs +8 -5
  150. package/dist/esm-node/renderer-build-identity.mjs +83 -0
  151. package/dist/esm-node/templates/cloudflare-entry.001-bootstrap-security.mjs +14 -6
  152. package/dist/esm-node/templates/cloudflare-entry.002-assets-routes.mjs +10 -2
  153. package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +403 -96
  154. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +530 -103
  155. package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +26 -7
  156. package/dist/esm-node/templates/cloudflare-worker-route-data.mjs +1069 -0
  157. package/dist/esm-node/workspace-topology.mjs +22 -0
  158. package/dist/types/backend-federation-build/codegen.d.ts +30 -0
  159. package/dist/types/backend-federation-build/config.d.ts +10 -57
  160. package/dist/types/backend-federation-build/emit.d.ts +12 -2
  161. package/dist/types/backend-federation-build/index.d.ts +1 -1
  162. package/dist/types/build-config/build-environment.d.ts +1 -9
  163. package/dist/types/build-config/internal-effect-discovery.d.ts +4 -0
  164. package/dist/types/build-config/public.d.ts +1 -1
  165. package/dist/types/build-config/remote-address.d.ts +3 -0
  166. package/dist/types/cloudflare/artifacts.d.ts +7 -6
  167. package/dist/types/cloudflare/constants.d.ts +1 -1
  168. package/dist/types/cloudflare/delivery-unit.d.ts +38 -7
  169. package/dist/types/cloudflare/native-renderer.d.ts +30 -0
  170. package/dist/types/cloudflare/renderer-identity.d.ts +25 -0
  171. package/dist/types/cloudflare/worker-manifest.d.ts +17 -2
  172. package/dist/types/cloudflare/worker-options.d.ts +20 -0
  173. package/dist/types/cloudflare/wrangler-config.d.ts +55 -2
  174. package/dist/types/cloudflare-builder.d.ts +51 -8
  175. package/dist/types/cloudflare-output-contract.d.ts +5 -3
  176. package/dist/types/cloudflare-output-verifier/identity.d.ts +7 -3
  177. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
  178. package/dist/types/config.d.ts +98 -13
  179. package/dist/types/deploy-output/plugin.d.ts +29 -9
  180. package/dist/types/deploy-output/public-assets.d.ts +51 -0
  181. package/dist/types/deploy-output/target.d.ts +28 -5
  182. package/dist/types/deploy-output/trace-files.d.ts +17 -0
  183. package/dist/types/module-federation-config/exposes.d.ts +11 -0
  184. package/dist/types/module-federation-config/index.d.ts +5 -0
  185. package/dist/types/module-federation-config/inspect.d.ts +2 -0
  186. package/dist/types/module-federation-config/object-literal.d.ts +4 -0
  187. package/dist/types/module-federation-config/syntax.d.ts +7 -0
  188. package/dist/types/module-federation-config/types.d.ts +20 -0
  189. package/dist/types/native-type-checker.d.ts +26 -3
  190. package/dist/types/policy-defaults.d.ts +12 -2
  191. package/dist/types/release-envelope/canonical.d.ts +5 -5
  192. package/dist/types/release-envelope/contract.d.mts +8 -0
  193. package/dist/types/release-envelope/framework-output.d.ts +23 -13
  194. package/dist/types/release-envelope/index.d.ts +17 -5
  195. package/dist/types/release-envelope/plugin.d.ts +10 -6
  196. package/dist/types/release-envelope/renderer-output-stamp.d.ts +34 -0
  197. package/dist/types/release-envelope/types.d.ts +42 -4
  198. package/dist/types/release-identity.d.ts +3 -2
  199. package/dist/types/renderer-build-identity.d.ts +30 -0
  200. package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
  201. package/dist/types/workspace-topology.d.ts +91 -0
  202. package/package.json +87 -27
  203. package/src/backend-federation-build/codegen.ts +28 -19
  204. package/src/backend-federation-build/config.ts +189 -123
  205. package/src/backend-federation-build/emit.ts +119 -38
  206. package/src/backend-federation-build/index.ts +4 -1
  207. package/src/build-config/build-environment.ts +59 -285
  208. package/src/build-config/internal-effect-discovery.ts +239 -0
  209. package/src/build-config/public.ts +0 -1
  210. package/src/build-config/remote-address.ts +33 -0
  211. package/src/cloudflare/artifacts.ts +22 -43
  212. package/src/cloudflare/delivery-unit.ts +283 -60
  213. package/src/cloudflare/index.ts +35 -19
  214. package/src/cloudflare/native-renderer.ts +117 -0
  215. package/src/cloudflare/renderer-identity.ts +171 -0
  216. package/src/cloudflare/worker-manifest.ts +108 -10
  217. package/src/cloudflare/worker-options.ts +60 -0
  218. package/src/cloudflare/wrangler-config.ts +390 -14
  219. package/src/cloudflare-builder.ts +543 -170
  220. package/src/cloudflare-output-contract.ts +45 -2
  221. package/src/cloudflare-output-verifier/identity.ts +138 -7
  222. package/src/cloudflare-output-verifier/index.ts +20 -0
  223. package/src/cloudflare-output-verifier/manifest.ts +77 -0
  224. package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
  225. package/src/cloudflare-output-verifier/worker-bundles.ts +27 -11
  226. package/src/config.ts +101 -19
  227. package/src/deploy-output/npmAliases.ts +141 -4
  228. package/src/deploy-output/plugin.ts +55 -18
  229. package/src/deploy-output/public-assets.ts +224 -0
  230. package/src/deploy-output/target.ts +97 -14
  231. package/src/deploy-output/trace-files.ts +80 -0
  232. package/src/module-federation-config/exposes.ts +46 -0
  233. package/src/module-federation-config/index.ts +22 -0
  234. package/src/module-federation-config/inspect.ts +157 -0
  235. package/src/module-federation-config/object-literal.ts +76 -0
  236. package/src/module-federation-config/syntax.ts +212 -0
  237. package/src/module-federation-config/types.ts +22 -0
  238. package/src/native-type-checker.ts +233 -20
  239. package/src/policy-defaults.ts +47 -17
  240. package/src/release-envelope/canonical.ts +6 -18
  241. package/src/release-envelope/contract.mjs +28 -0
  242. package/src/release-envelope/framework-output.ts +681 -156
  243. package/src/release-envelope/index.ts +315 -60
  244. package/src/release-envelope/plugin.ts +54 -38
  245. package/src/release-envelope/renderer-output-stamp.ts +210 -0
  246. package/src/release-envelope/types.ts +75 -6
  247. package/src/release-identity.ts +19 -2
  248. package/src/renderer-build-identity.ts +168 -0
  249. package/src/templates/cloudflare-entry.001-bootstrap-security.mjs +14 -6
  250. package/src/templates/cloudflare-entry.002-assets-routes.mjs +10 -2
  251. package/src/templates/cloudflare-entry.004-rendering-css.mjs +403 -96
  252. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +530 -103
  253. package/src/templates/cloudflare-entry.006-fetch-handler.mjs +26 -7
  254. package/src/templates/cloudflare-worker-route-data.mjs +1069 -0
  255. package/src/workspace-topology.ts +125 -0
  256. package/dist/cjs/templates/cloudflare-worker-fs-promises.mjs +0 -11
  257. package/dist/cjs/templates/cloudflare-worker-loadable-server.mjs +0 -281
  258. package/dist/cjs/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  259. package/dist/cjs/templates/cloudflare-worker-path.mjs +0 -98
  260. package/dist/esm/templates/cloudflare-worker-fs-promises.mjs +0 -11
  261. package/dist/esm/templates/cloudflare-worker-loadable-server.mjs +0 -281
  262. package/dist/esm/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  263. package/dist/esm/templates/cloudflare-worker-path.mjs +0 -98
  264. package/dist/esm-node/templates/cloudflare-worker-fs-promises.mjs +0 -11
  265. package/dist/esm-node/templates/cloudflare-worker-loadable-server.mjs +0 -281
  266. package/dist/esm-node/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  267. package/dist/esm-node/templates/cloudflare-worker-path.mjs +0 -98
  268. package/dist/types/templates/cloudflare-worker-fs-promises.d.mts +0 -5
  269. package/dist/types/templates/cloudflare-worker-loadable-server.d.mts +0 -48
  270. package/dist/types/templates/cloudflare-worker-mf-ssr-runtime-plugin.d.mts +0 -13
  271. package/dist/types/templates/cloudflare-worker-path.d.mts +0 -21
  272. package/src/templates/cloudflare-worker-fs-promises.mjs +0 -11
  273. package/src/templates/cloudflare-worker-loadable-server.mjs +0 -281
  274. package/src/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  275. package/src/templates/cloudflare-worker-path.mjs +0 -98
@@ -12,6 +12,109 @@ async function loadWorkerModule(workerPath) {
12
12
  return workerModulePromises.get(workerPath);
13
13
  }
14
14
 
15
+ function createWorkerRendererGuardResponse(request) {
16
+ const deliveryUnit = MODERN_WORKER_MANIFEST.deliveryUnit;
17
+ const surfaces = deliveryUnit?.surfaces;
18
+ // API identity is universal. Only an explicit UI surface selects a renderer.
19
+ if (!surfaces || !Object.hasOwn(surfaces, 'ui')) {
20
+ return undefined;
21
+ }
22
+
23
+ const ui = surfaces.ui;
24
+ const identity = ui?.rendererIdentity;
25
+ const profile = ui?.rendererProfile;
26
+ // The built renderer's adapter decides whether documents are native.
27
+ const builtRenderer = MODERN_WORKER_MANIFEST.renderer;
28
+ const isNative =
29
+ builtRenderer?.nativeDocuments === true &&
30
+ [identity?.renderer, profile?.renderer].includes(builtRenderer.name);
31
+ if (
32
+ isNative &&
33
+ (request.headers.has('x-rsc-tree') || request.headers.has('x-rsc-action'))
34
+ ) {
35
+ return Response.json(
36
+ { code: 'unsupported-renderer-capability', capability: 'rsc' },
37
+ { status: 400, headers: { 'cache-control': 'no-store' } },
38
+ );
39
+ }
40
+
41
+ const isRecord = value =>
42
+ value !== null && typeof value === 'object' && !Array.isArray(value);
43
+ const isCanonicalString = value =>
44
+ typeof value === 'string' && value.length > 0 && value.trim() === value;
45
+ const hasExactFields = (value, fields) =>
46
+ isRecord(value) &&
47
+ Object.keys(value).every(field => fields.includes(field)) &&
48
+ fields.every(field => Object.hasOwn(value, field));
49
+ const exactVersion =
50
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u;
51
+ const isPackageIdentity = (value, router = false, hydration = false) => {
52
+ const fields = router
53
+ ? ['name', 'version', 'coreName', 'coreVersion']
54
+ : ['name', 'version'];
55
+ return (
56
+ hasExactFields(value, fields) &&
57
+ isCanonicalString(value.name) &&
58
+ (!router || isCanonicalString(value.coreName)) &&
59
+ (router ? ['version', 'coreVersion'] : ['version']).every(
60
+ field =>
61
+ isCanonicalString(value[field]) &&
62
+ (exactVersion.test(value[field]) ||
63
+ (hydration && /^[1-9]\d*$/u.test(value[field]))),
64
+ )
65
+ );
66
+ };
67
+ const validMetadata =
68
+ isRecord(ui) &&
69
+ ui.surface === 'ui' &&
70
+ hasExactFields(identity, [
71
+ 'renderer',
72
+ 'appId',
73
+ 'entryName',
74
+ 'protocolVersion',
75
+ 'buildId',
76
+ ]) &&
77
+ identity.renderer === builtRenderer?.name &&
78
+ identity.protocolVersion === 1 &&
79
+ ['appId', 'entryName', 'buildId'].every(field =>
80
+ isCanonicalString(identity[field]),
81
+ ) &&
82
+ identity.appId === ui.appId &&
83
+ ui.appId === deliveryUnit.appId &&
84
+ identity.buildId === ui.buildMarker &&
85
+ ui.buildMarker === deliveryUnit.buildMarker &&
86
+ hasExactFields(profile, [
87
+ 'renderer',
88
+ 'protocolVersion',
89
+ 'compiler',
90
+ 'hydration',
91
+ 'router',
92
+ ]) &&
93
+ profile.renderer === identity.renderer &&
94
+ profile.protocolVersion === identity.protocolVersion &&
95
+ isPackageIdentity(profile.compiler) &&
96
+ isPackageIdentity(profile.hydration, false, true) &&
97
+ isPackageIdentity(profile.router, true);
98
+
99
+ if (!validMetadata) {
100
+ return Response.json(
101
+ { code: 'invalid-renderer-metadata' },
102
+ { status: 500, headers: { 'cache-control': 'no-store' } },
103
+ );
104
+ }
105
+ if (isNative && !hasNativeWorkerRenderer(identity.renderer)) {
106
+ return Response.json(
107
+ {
108
+ code: 'unsupported-renderer-capability',
109
+ capability: 'cloudflare-worker',
110
+ renderer: identity.renderer,
111
+ },
112
+ { status: 501, headers: { 'cache-control': 'no-store' } },
113
+ );
114
+ }
115
+ return undefined;
116
+ }
117
+
15
118
  function getRuntimeModule(workerModule) {
16
119
  const defaultExport = workerModule.default;
17
120
  const nestedDefaultExport =
@@ -52,7 +155,249 @@ async function getRequestHandler(workerModule) {
52
155
  );
53
156
  }
54
157
 
158
+ function hasNativeWorkerRenderer(renderer) {
159
+ return MODERN_WORKER_MANIFEST.nativeRenderer?.renderer === renderer;
160
+ }
161
+
162
+ function getNativeRouteIdentity(route) {
163
+ const identities = MODERN_WORKER_MANIFEST.rendererIdentities;
164
+ const identity =
165
+ identities && Object.hasOwn(identities, route.entryName)
166
+ ? identities[route.entryName]
167
+ : undefined;
168
+ // The built native-document renderer, or one that shipped native resources.
169
+ const builtRenderer = MODERN_WORKER_MANIFEST.renderer;
170
+ return identity &&
171
+ ((builtRenderer?.nativeDocuments === true &&
172
+ identity.renderer === builtRenderer.name) ||
173
+ hasNativeWorkerRenderer(identity.renderer))
174
+ ? identity
175
+ : undefined;
176
+ }
177
+
178
+ const ROUTE_DATA_REQUEST_PARAM = '__loader';
179
+
180
+ // Route data requests (`?__loader=`) are answered by the entry's route data
181
+ // worker, like the Node server's data handler. Without a match the request
182
+ // falls through to page rendering, as it does on Node.
183
+ async function dispatchRouteDataRequest(route, request) {
184
+ const routeDataWorkerPath = route.routeDataWorker;
185
+ // Native entries answer their own data protocol through the native
186
+ // dispatcher; the legacy route-data worker speaks the React encoding.
187
+ if (
188
+ !routeDataWorkerPath ||
189
+ getNativeRouteIdentity(route) ||
190
+ !new URL(request.url).searchParams.has(ROUTE_DATA_REQUEST_PARAM)
191
+ ) {
192
+ return undefined;
193
+ }
194
+
195
+ const routeDataWorkerModule = await loadWorkerModule(routeDataWorkerPath);
196
+ const handleRouteDataRequest = routeDataWorkerModule
197
+ ? getRuntimeModule(routeDataWorkerModule).handleRouteDataRequest
198
+ : undefined;
199
+
200
+ if (typeof handleRouteDataRequest !== 'function') {
201
+ return new Response(
202
+ `Route data worker bundle has no handleRouteDataRequest export: ${routeDataWorkerPath}`,
203
+ {
204
+ status: 500,
205
+ headers: {
206
+ 'content-type': 'text/plain; charset=utf-8',
207
+ 'x-modern-js-route-data-worker': routeDataWorkerPath,
208
+ },
209
+ },
210
+ );
211
+ }
212
+
213
+ return handleRouteDataRequest({
214
+ request,
215
+ serverRoutes: MODERN_WORKER_MANIFEST.routeSpec.routes,
216
+ context: {
217
+ loaderContext: new Map(),
218
+ monitors: createNoopMonitors(),
219
+ reporter: {
220
+ reportTiming: () => {},
221
+ },
222
+ },
223
+ onTiming() {},
224
+ });
225
+ }
226
+
55
227
  async function dispatchRouteWorker(route, request, env, ctx) {
228
+ const rendererRejection = createWorkerRendererGuardResponse(request);
229
+ if (rendererRejection) return rendererRejection;
230
+
231
+ const nativeIdentity = getNativeRouteIdentity(route);
232
+ return withWorkerRendererIdentity(
233
+ nativeIdentity
234
+ ? applyRouteResponseHeaders(
235
+ await invokeNativeRouteWorker(
236
+ route,
237
+ nativeIdentity,
238
+ request,
239
+ env,
240
+ ctx,
241
+ ),
242
+ route,
243
+ )
244
+ : await invokeRouteWorker(route, request, env, ctx),
245
+ route,
246
+ );
247
+ }
248
+
249
+ const CUMULATIVE_RESPONSE_HEADERS = new Set([
250
+ 'content-security-policy',
251
+ 'content-security-policy-report-only',
252
+ 'link',
253
+ 'server-timing',
254
+ 'set-cookie',
255
+ ]);
256
+
257
+ /**
258
+ * Merge a route's configured `responseHeaders` into a native response, as the
259
+ * Node host does: CSP, Link, Server-Timing and cookies append, `Vary` unions, the
260
+ * renderer's Content-Type stays, and other fields take the configured value.
261
+ */
262
+ function applyRouteResponseHeaders(response, route) {
263
+ const configured = Object.entries(route.responseHeaders ?? {});
264
+ if (configured.length === 0) return response;
265
+ const headers = new Headers(response.headers);
266
+ for (const [rawName, rawValue] of configured) {
267
+ const name = rawName.toLowerCase();
268
+ const value = String(rawValue);
269
+ // The renderer owns its body framing and content coding.
270
+ if (
271
+ name === 'content-length' ||
272
+ name === 'transfer-encoding' ||
273
+ name === 'content-encoding'
274
+ )
275
+ continue;
276
+ if (CUMULATIVE_RESPONSE_HEADERS.has(name)) headers.append(name, value);
277
+ else if (name === 'vary') {
278
+ const fields = new Map();
279
+ for (const field of `${headers.get('vary') ?? ''},${value}`.split(',')) {
280
+ const item = field.trim();
281
+ if (item && !fields.has(item.toLowerCase()))
282
+ fields.set(item.toLowerCase(), item);
283
+ }
284
+ headers.set('vary', [...fields.values()].join(', '));
285
+ } else if (name !== 'content-type' || !headers.has(name))
286
+ headers.set(name, value);
287
+ }
288
+ return new Response(response.body, {
289
+ headers,
290
+ status: response.status,
291
+ statusText: response.statusText,
292
+ });
293
+ }
294
+
295
+ function createWorkerRendererErrorResponse(code, entryName) {
296
+ return Response.json(
297
+ { code, entryName: String(entryName) },
298
+ { status: 500, headers: { 'cache-control': 'no-store' } },
299
+ );
300
+ }
301
+
302
+ // Native-document renderers bundle their server handler with the renderer-core
303
+ // worker dispatcher. Build-validated document inputs come from the manifest.
304
+ async function invokeNativeRouteWorker(route, identity, request, env, ctx) {
305
+ const nativeRenderer = MODERN_WORKER_MANIFEST.nativeRenderer;
306
+ const resources =
307
+ nativeRenderer?.renderer === identity.renderer &&
308
+ Object.hasOwn(nativeRenderer.entries, route.entryName)
309
+ ? nativeRenderer.entries[route.entryName]
310
+ : undefined;
311
+ // Native renderers have no Flight transport. Reject before the bundle loads.
312
+ if (
313
+ request.headers.has('x-rsc-tree') ||
314
+ request.headers.has('x-rsc-action')
315
+ ) {
316
+ return Response.json(
317
+ { code: 'unsupported-renderer-capability', capability: 'rsc' },
318
+ { status: 400, headers: { 'cache-control': 'no-store' } },
319
+ );
320
+ }
321
+ if (!resources) {
322
+ return createWorkerRendererErrorResponse(
323
+ 'missing-native-renderer-resources',
324
+ route.entryName,
325
+ );
326
+ }
327
+ try {
328
+ const workerModule = route.worker
329
+ ? await loadWorkerModule(route.worker)
330
+ : undefined;
331
+ if (
332
+ !workerModule ||
333
+ typeof workerModule.dispatchNativeWorkerRequest !== 'function'
334
+ ) {
335
+ return createWorkerRendererErrorResponse(
336
+ 'missing-native-worker-bundle',
337
+ route.entryName,
338
+ );
339
+ }
340
+ return await workerModule.dispatchNativeWorkerRequest(request, {
341
+ identity,
342
+ bundle: workerModule,
343
+ resources,
344
+ bindings: env,
345
+ executionContext: ctx,
346
+ });
347
+ } catch (error) {
348
+ // A bundle evaluation or pre-commit native failure must not escape fetch.
349
+ console.error(error);
350
+ return createWorkerRendererErrorResponse(
351
+ 'native-render-failed',
352
+ route.entryName,
353
+ );
354
+ }
355
+ }
356
+
357
+ /**
358
+ * The Node host's ASCII-only identity header bytes: fixed field order and
359
+ * non-ASCII code units escaped, since Fetch header values are ByteStrings.
360
+ */
361
+ function serializeRendererIdentityHeader(identity) {
362
+ return JSON.stringify({
363
+ renderer: identity.renderer,
364
+ appId: identity.appId,
365
+ entryName: identity.entryName,
366
+ protocolVersion: identity.protocolVersion,
367
+ buildId: identity.buildId,
368
+ }).replace(/[\u007f-\u{10ffff}]/gu, character =>
369
+ Array.from(
370
+ { length: character.length },
371
+ (_, index) =>
372
+ `\\u${character.charCodeAt(index).toString(16).padStart(4, '0')}`,
373
+ ).join(''),
374
+ );
375
+ }
376
+
377
+ function withWorkerRendererIdentity(response, route) {
378
+ const identities = MODERN_WORKER_MANIFEST.rendererIdentities;
379
+ if (!identities) return response;
380
+ if (!Object.hasOwn(identities, route.entryName)) {
381
+ // Do not let a manifest/route mismatch escape fetch and fail the isolate.
382
+ response.body?.cancel().catch(() => {});
383
+ return createWorkerRendererErrorResponse(
384
+ 'missing-renderer-identity',
385
+ route.entryName,
386
+ );
387
+ }
388
+ const headers = new Headers(response.headers);
389
+ headers.set(
390
+ 'x-ultramodern-renderer-identity',
391
+ serializeRendererIdentityHeader(identities[route.entryName]),
392
+ );
393
+ return new Response(response.body, {
394
+ headers,
395
+ status: response.status,
396
+ statusText: response.statusText,
397
+ });
398
+ }
399
+
400
+ async function invokeRouteWorker(route, request, env, ctx) {
56
401
  const workerPath = route.worker;
57
402
  if (!workerPath) {
58
403
  return new Response('Worker bundle not configured for SSR route', {
@@ -81,6 +426,30 @@ async function dispatchRouteWorker(route, request, env, ctx) {
81
426
  return fetchHandler(request, env, ctx);
82
427
  }
83
428
 
429
+ const runtime = getRuntimeModule(workerModule);
430
+ // Match the native server dispatcher: actions precede Flight trees, and
431
+ // neither path may fall back to HTML rendering or rewrite the Request body.
432
+ if (request.headers.get('x-rsc-action')) {
433
+ const handleAction = await runtime.handleAction;
434
+ if (typeof handleAction !== 'function') {
435
+ return new Response('Cannot find server action handler', { status: 500 });
436
+ }
437
+ return handleAction(request);
438
+ }
439
+
440
+ if (request.headers.get('x-rsc-tree')) {
441
+ const rscPayloadHandler = await runtime.rscPayloadHandler;
442
+ if (typeof rscPayloadHandler !== 'function') {
443
+ return new Response('Cannot find request handler for RSC', {
444
+ status: 500,
445
+ });
446
+ }
447
+ return rscPayloadHandler(
448
+ request,
449
+ await getRequestHandlerOptions(route, request, env, ctx, false),
450
+ );
451
+ }
452
+
84
453
  const requestHandler = await getRequestHandler(workerModule);
85
454
 
86
455
  if (typeof requestHandler === 'function') {
@@ -88,6 +457,7 @@ async function dispatchRouteWorker(route, request, env, ctx) {
88
457
  route,
89
458
  request,
90
459
  env,
460
+ ctx,
91
461
  );
92
462
 
93
463
  return withRouteCssLinks(
@@ -96,9 +466,8 @@ async function dispatchRouteWorker(route, request, env, ctx) {
96
466
  requestHandlerOptions.resource.routeManifest,
97
467
  request,
98
468
  env,
99
- requestHandlerOptions.locals[
100
- DISTRIBUTED_SSR_FRAGMENTS_LOCALS_KEY
101
- ]?.getStylesheetHrefs(),
469
+ undefined,
470
+ requestHandlerOptions.locals[DISTRIBUTED_SSR_FRAGMENTS_LOCALS_KEY],
102
471
  );
103
472
  }
104
473
 
@@ -114,14 +483,46 @@ async function dispatchRouteWorker(route, request, env, ctx) {
114
483
  );
115
484
  }
116
485
 
117
- function matchesPrefix(pathname, prefix) {
486
+ // Worker Static Assets decode percent-encoded paths, so an encoded spelling
487
+ // of a mounted prefix (`/%70refix/...`, `/prefix%2F...`) would otherwise skip
488
+ // the BFF or service binding that owns the prefix and reach ASSETS directly.
489
+ // Resolve such spellings to the canonical pathname the owner sees; a
490
+ // backslash or undecodable spelling becomes a path the owner rejects.
491
+ function resolvePrefixPathname(pathname, prefix) {
118
492
  if (!prefix || prefix === '/') {
119
- return true;
493
+ return pathname;
120
494
  }
121
495
 
122
496
  const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
497
+ let candidate = pathname;
498
+
499
+ for (let attempt = 0; attempt < 4; attempt += 1) {
500
+ if (candidate === normalized || candidate.startsWith(`${normalized}/`)) {
501
+ return candidate;
502
+ }
503
+ if (candidate.startsWith(`${normalized}\\`)) {
504
+ return `${normalized}/__invalid_encoded_path__`;
505
+ }
506
+
507
+ let decoded;
508
+ try {
509
+ decoded = decodeURIComponent(candidate);
510
+ } catch {
511
+ return candidate.toLowerCase().startsWith(normalized.toLowerCase())
512
+ ? `${normalized}/__invalid_encoded_path__`
513
+ : null;
514
+ }
515
+ if (decoded === candidate) {
516
+ return null;
517
+ }
518
+ candidate = decoded;
519
+ }
520
+
521
+ return null;
522
+ }
123
523
 
124
- return pathname === normalized || pathname.startsWith(`${normalized}/`);
524
+ function matchesPrefix(pathname, prefix) {
525
+ return resolvePrefixPathname(pathname, prefix) !== null;
125
526
  }
126
527
 
127
528
  function createRequestForMountedPrefix(request, prefix) {
@@ -131,12 +532,13 @@ function createRequestForMountedPrefix(request, prefix) {
131
532
 
132
533
  const url = new URL(request.url);
133
534
  const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
535
+ const matchedPathname = resolvePrefixPathname(url.pathname, normalized);
134
536
 
135
- if (!matchesPrefix(url.pathname, normalized)) {
537
+ if (matchedPathname === null) {
136
538
  return request;
137
539
  }
138
540
 
139
- const nextPath = url.pathname.slice(normalized.length) || '/';
541
+ const nextPath = matchedPathname.slice(normalized.length) || '/';
140
542
  url.pathname = nextPath.startsWith('/') ? nextPath : `/${nextPath}`;
141
543
 
142
544
  return new Request(url, request);
@@ -158,117 +560,131 @@ function createEffectBffDispatcherErrorResponse(bff, error) {
158
560
  );
159
561
  }
160
562
 
161
- function getEffectBffDispatcher(bff, runtime) {
162
- let effectDispatcherPromise = effectBffDispatcherPromises.get(bff.worker);
163
-
164
- if (effectDispatcherPromise) {
165
- return effectDispatcherPromise;
563
+ // workerd binds every I/O object (sockets, timers, pending promises) to the request that
564
+ // created it, so an Effect runtime built while serving one request cannot serve the next: a
565
+ // pooled PostgreSQL connection or a cached in-flight effect from an earlier request never
566
+ // completes. Each BFF request therefore builds its own dispatcher and disposes it once the
567
+ // response body has been delivered; platform pools such as Hyperdrive keep connections warm.
568
+ async function createEffectBffDispatcher(bff, runtime) {
569
+ if (
570
+ typeof bff.dispatcherExport !== 'string' ||
571
+ bff.dispatcherExport.length === 0
572
+ ) {
573
+ throw new Error('manifest does not declare dispatcherExport');
166
574
  }
167
575
 
168
- effectDispatcherPromise = Promise.resolve().then(async () => {
169
- if (
170
- typeof bff.dispatcherExport !== 'string' ||
171
- bff.dispatcherExport.length === 0
172
- ) {
173
- throw new Error('manifest does not declare dispatcherExport');
174
- }
175
-
176
- const effectDispatcherFactory = runtime[bff.dispatcherExport];
576
+ const effectDispatcherFactory = runtime[bff.dispatcherExport];
177
577
 
178
- if (typeof effectDispatcherFactory !== 'function') {
179
- throw new Error(`worker bundle does not export ${bff.dispatcherExport}`);
180
- }
578
+ if (typeof effectDispatcherFactory !== 'function') {
579
+ throw new Error(`worker bundle does not export ${bff.dispatcherExport}`);
580
+ }
181
581
 
182
- const effectConfig = bff.effect;
183
- if (
184
- !effectConfig ||
185
- typeof effectConfig !== 'object' ||
186
- Array.isArray(effectConfig)
187
- ) {
188
- throw new Error('manifest declares invalid Effect BFF runtime config');
189
- }
190
- const crossProjectPolicy = effectConfig.crossProjectPolicy;
191
- if (
192
- !crossProjectPolicy ||
193
- typeof crossProjectPolicy !== 'object' ||
194
- Array.isArray(crossProjectPolicy)
195
- ) {
196
- throw new Error(
197
- 'manifest declares invalid Effect BFF cross-project policy',
198
- );
199
- }
200
- for (const field of [
201
- 'enabled',
202
- 'requireEnvelope',
203
- 'requireOperationContext',
204
- 'requireOperationContextDetails',
205
- 'requireOperationSchemaHash',
206
- 'requireOperationVersion',
207
- 'allowUnknownOperations',
208
- ]) {
209
- if (typeof crossProjectPolicy[field] !== 'boolean') {
210
- throw new Error(
211
- `manifest Effect BFF cross-project policy requires boolean ${field}`,
212
- );
213
- }
214
- }
215
- if (
216
- !crossProjectPolicy.expectedOperationContracts ||
217
- typeof crossProjectPolicy.expectedOperationContracts !== 'object' ||
218
- Array.isArray(crossProjectPolicy.expectedOperationContracts)
219
- ) {
582
+ const effectConfig = bff.effect;
583
+ if (
584
+ !effectConfig ||
585
+ typeof effectConfig !== 'object' ||
586
+ Array.isArray(effectConfig)
587
+ ) {
588
+ throw new Error('manifest declares invalid Effect BFF runtime config');
589
+ }
590
+ const crossProjectPolicy = effectConfig.crossProjectPolicy;
591
+ if (
592
+ !crossProjectPolicy ||
593
+ typeof crossProjectPolicy !== 'object' ||
594
+ Array.isArray(crossProjectPolicy)
595
+ ) {
596
+ throw new Error(
597
+ 'manifest declares invalid Effect BFF cross-project policy',
598
+ );
599
+ }
600
+ for (const field of [
601
+ 'enabled',
602
+ 'requireEnvelope',
603
+ 'requireOperationContext',
604
+ 'requireOperationContextDetails',
605
+ 'requireOperationSchemaHash',
606
+ 'requireOperationVersion',
607
+ 'allowUnknownOperations',
608
+ ]) {
609
+ if (typeof crossProjectPolicy[field] !== 'boolean') {
220
610
  throw new Error(
221
- 'manifest Effect BFF cross-project policy requires expectedOperationContracts object',
611
+ `manifest Effect BFF cross-project policy requires boolean ${field}`,
222
612
  );
223
613
  }
614
+ }
615
+ if (
616
+ !crossProjectPolicy.expectedOperationContracts ||
617
+ typeof crossProjectPolicy.expectedOperationContracts !== 'object' ||
618
+ Array.isArray(crossProjectPolicy.expectedOperationContracts)
619
+ ) {
620
+ throw new Error(
621
+ 'manifest Effect BFF cross-project policy requires expectedOperationContracts object',
622
+ );
623
+ }
224
624
 
225
- const effectDispatcher = await effectDispatcherFactory({
226
- prefix: bff.prefix,
227
- ...(effectConfig?.openapi === undefined
228
- ? {}
229
- : { openapi: effectConfig.openapi }),
230
- ...(effectConfig?.dataPlatform === undefined
231
- ? {}
232
- : { dataPlatform: effectConfig.dataPlatform }),
233
- ...(effectConfig?.crossProjectPolicy === undefined
234
- ? {}
235
- : { crossProjectPolicy }),
236
- });
237
-
238
- if (!effectDispatcher || typeof effectDispatcher.dispatch !== 'function') {
239
- try {
240
- await effectDispatcher?.dispose?.();
241
- } catch {}
625
+ const effectDispatcher = await effectDispatcherFactory({
626
+ prefix: bff.prefix,
627
+ ...(effectConfig?.openapi === undefined
628
+ ? {}
629
+ : { openapi: effectConfig.openapi }),
630
+ ...(effectConfig?.dataPlatform === undefined
631
+ ? {}
632
+ : { dataPlatform: effectConfig.dataPlatform }),
633
+ ...(effectConfig?.crossProjectPolicy === undefined
634
+ ? {}
635
+ : { crossProjectPolicy }),
636
+ });
242
637
 
243
- throw new Error(
244
- `worker export ${bff.dispatcherExport} did not return a dispatcher with a dispatch function`,
245
- );
246
- }
638
+ if (!effectDispatcher || typeof effectDispatcher.dispatch !== 'function') {
639
+ try {
640
+ await effectDispatcher?.dispose?.();
641
+ } catch {}
247
642
 
248
- return effectDispatcher;
249
- });
643
+ throw new Error(
644
+ `worker export ${bff.dispatcherExport} did not return a dispatcher with a dispatch function`,
645
+ );
646
+ }
250
647
 
251
- effectBffDispatcherPromises.set(bff.worker, effectDispatcherPromise);
252
- effectDispatcherPromise.catch(() => {
253
- if (
254
- effectBffDispatcherPromises.get(bff.worker) === effectDispatcherPromise
255
- ) {
256
- effectBffDispatcherPromises.delete(bff.worker);
257
- }
258
- });
648
+ return effectDispatcher;
649
+ }
259
650
 
260
- return effectDispatcherPromise;
651
+ function disposeEffectBffDispatcherAfterResponse(response, dispatcher, ctx) {
652
+ if (response.body === null) {
653
+ ctx.waitUntil(dispatcher.dispose());
654
+ return response;
655
+ }
656
+ const { readable, writable } = new TransformStream();
657
+ // The body pipe settles when the client has the whole body or the stream failed; a failure
658
+ // already reached the client through `readable`, so it only has to release the runtime here.
659
+ ctx.waitUntil(
660
+ response.body
661
+ .pipeTo(writable)
662
+ .catch(() => undefined)
663
+ .then(() => dispatcher.dispose()),
664
+ );
665
+ return new Response(readable, response);
261
666
  }
262
667
 
263
- async function dispatchBffRequest(request, env) {
668
+ async function dispatchBffRequest(request, env, ctx) {
669
+ const rendererRejection = createWorkerRendererGuardResponse(request);
670
+ if (rendererRejection) return rendererRejection;
264
671
  const bff = MODERN_WORKER_MANIFEST.bff;
265
672
 
266
- if (
267
- !bff?.worker ||
268
- !matchesPrefix(new URL(request.url).pathname, bff.prefix)
269
- ) {
673
+ const requestUrl = new URL(request.url);
674
+ const matchedPathname = resolvePrefixPathname(
675
+ requestUrl.pathname,
676
+ bff?.prefix,
677
+ );
678
+ if (!bff?.worker || matchedPathname === null) {
270
679
  return null;
271
680
  }
681
+ const canonicalRequest =
682
+ matchedPathname === requestUrl.pathname
683
+ ? request
684
+ : new Request(
685
+ Object.assign(requestUrl, { pathname: matchedPathname }),
686
+ request,
687
+ );
272
688
  if (bff.runtimeFramework !== 'effect') {
273
689
  return createEffectBffDispatcherErrorResponse(
274
690
  bff,
@@ -296,12 +712,23 @@ async function dispatchBffRequest(request, env) {
296
712
  let effectDispatcher;
297
713
 
298
714
  try {
299
- effectDispatcher = await getEffectBffDispatcher(bff, runtime);
715
+ effectDispatcher = await createEffectBffDispatcher(bff, runtime);
300
716
  } catch (error) {
301
717
  return createEffectBffDispatcherErrorResponse(bff, error);
302
718
  }
303
719
 
304
- return effectDispatcher.dispatch(request, { env });
720
+ let response;
721
+ try {
722
+ response = await effectDispatcher.dispatch(canonicalRequest, { env });
723
+ } catch (error) {
724
+ ctx.waitUntil(effectDispatcher.dispose());
725
+ throw error;
726
+ }
727
+ return disposeEffectBffDispatcherAfterResponse(
728
+ response,
729
+ effectDispatcher,
730
+ ctx,
731
+ );
305
732
  }
306
733
 
307
734
  const directHandler =