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

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 +30 -165
  6. package/dist/cjs/build-config/internal-effect-discovery.js +231 -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 +180 -17
  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 +9 -6
  46. package/dist/cjs/renderer-build-identity.js +144 -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 +547 -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 +31 -163
  58. package/dist/esm/build-config/internal-effect-discovery.mjs +187 -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 +175 -18
  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 +9 -6
  98. package/dist/esm/renderer-build-identity.mjs +92 -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 +547 -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 +31 -163
  110. package/dist/esm-node/build-config/internal-effect-discovery.mjs +188 -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 +175 -18
  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 +9 -6
  150. package/dist/esm-node/renderer-build-identity.mjs +93 -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 +547 -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 +4 -10
  163. package/dist/types/build-config/internal-effect-discovery.d.ts +6 -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 +20 -4
  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 +102 -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 +28 -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 +13 -2
  199. package/dist/types/renderer-build-identity.d.ts +41 -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 +69 -293
  208. package/src/build-config/internal-effect-discovery.ts +458 -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 +115 -13
  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 +105 -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 +284 -27
  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 +30 -4
  248. package/src/renderer-build-identity.ts +208 -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 +547 -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,266 @@ 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
+ ? {
310
+ ...nativeRenderer.entries[route.entryName],
311
+ ...(Object.hasOwn(nativeRenderer, 'workerFederation')
312
+ ? { workerFederation: nativeRenderer.workerFederation }
313
+ : {}),
314
+ ...(Object.hasOwn(nativeRenderer, 'workerFederationManifestUrl')
315
+ ? {
316
+ workerFederationManifestUrl:
317
+ nativeRenderer.workerFederationManifestUrl,
318
+ }
319
+ : {}),
320
+ ...(Object.hasOwn(nativeRenderer, 'workerFederationServices')
321
+ ? {
322
+ workerFederationServices:
323
+ nativeRenderer.workerFederationServices,
324
+ }
325
+ : {}),
326
+ }
327
+ : undefined;
328
+ // Native renderers have no Flight transport. Reject before the bundle loads.
329
+ if (
330
+ request.headers.has('x-rsc-tree') ||
331
+ request.headers.has('x-rsc-action')
332
+ ) {
333
+ return Response.json(
334
+ { code: 'unsupported-renderer-capability', capability: 'rsc' },
335
+ { status: 400, headers: { 'cache-control': 'no-store' } },
336
+ );
337
+ }
338
+ if (!resources) {
339
+ return createWorkerRendererErrorResponse(
340
+ 'missing-native-renderer-resources',
341
+ route.entryName,
342
+ );
343
+ }
344
+ try {
345
+ const workerModule = route.worker
346
+ ? await loadWorkerModule(route.worker)
347
+ : undefined;
348
+ if (
349
+ !workerModule ||
350
+ typeof workerModule.dispatchNativeWorkerRequest !== 'function'
351
+ ) {
352
+ return createWorkerRendererErrorResponse(
353
+ 'missing-native-worker-bundle',
354
+ route.entryName,
355
+ );
356
+ }
357
+ return await workerModule.dispatchNativeWorkerRequest(request, {
358
+ identity,
359
+ bundle: workerModule,
360
+ resources,
361
+ bindings: env,
362
+ executionContext: ctx,
363
+ });
364
+ } catch (error) {
365
+ // A bundle evaluation or pre-commit native failure must not escape fetch.
366
+ console.error(error);
367
+ return createWorkerRendererErrorResponse(
368
+ 'native-render-failed',
369
+ route.entryName,
370
+ );
371
+ }
372
+ }
373
+
374
+ /**
375
+ * The Node host's ASCII-only identity header bytes: fixed field order and
376
+ * non-ASCII code units escaped, since Fetch header values are ByteStrings.
377
+ */
378
+ function serializeRendererIdentityHeader(identity) {
379
+ return JSON.stringify({
380
+ renderer: identity.renderer,
381
+ appId: identity.appId,
382
+ entryName: identity.entryName,
383
+ protocolVersion: identity.protocolVersion,
384
+ buildId: identity.buildId,
385
+ }).replace(/[\u007f-\u{10ffff}]/gu, character =>
386
+ Array.from(
387
+ { length: character.length },
388
+ (_, index) =>
389
+ `\\u${character.charCodeAt(index).toString(16).padStart(4, '0')}`,
390
+ ).join(''),
391
+ );
392
+ }
393
+
394
+ function withWorkerRendererIdentity(response, route) {
395
+ const identities = MODERN_WORKER_MANIFEST.rendererIdentities;
396
+ if (!identities) return response;
397
+ if (!Object.hasOwn(identities, route.entryName)) {
398
+ // Do not let a manifest/route mismatch escape fetch and fail the isolate.
399
+ response.body?.cancel().catch(() => {});
400
+ return createWorkerRendererErrorResponse(
401
+ 'missing-renderer-identity',
402
+ route.entryName,
403
+ );
404
+ }
405
+ const headers = new Headers(response.headers);
406
+ headers.set(
407
+ 'x-ultramodern-renderer-identity',
408
+ serializeRendererIdentityHeader(identities[route.entryName]),
409
+ );
410
+ return new Response(response.body, {
411
+ headers,
412
+ status: response.status,
413
+ statusText: response.statusText,
414
+ });
415
+ }
416
+
417
+ async function invokeRouteWorker(route, request, env, ctx) {
56
418
  const workerPath = route.worker;
57
419
  if (!workerPath) {
58
420
  return new Response('Worker bundle not configured for SSR route', {
@@ -81,6 +443,30 @@ async function dispatchRouteWorker(route, request, env, ctx) {
81
443
  return fetchHandler(request, env, ctx);
82
444
  }
83
445
 
446
+ const runtime = getRuntimeModule(workerModule);
447
+ // Match the native server dispatcher: actions precede Flight trees, and
448
+ // neither path may fall back to HTML rendering or rewrite the Request body.
449
+ if (request.headers.get('x-rsc-action')) {
450
+ const handleAction = await runtime.handleAction;
451
+ if (typeof handleAction !== 'function') {
452
+ return new Response('Cannot find server action handler', { status: 500 });
453
+ }
454
+ return handleAction(request);
455
+ }
456
+
457
+ if (request.headers.get('x-rsc-tree')) {
458
+ const rscPayloadHandler = await runtime.rscPayloadHandler;
459
+ if (typeof rscPayloadHandler !== 'function') {
460
+ return new Response('Cannot find request handler for RSC', {
461
+ status: 500,
462
+ });
463
+ }
464
+ return rscPayloadHandler(
465
+ request,
466
+ await getRequestHandlerOptions(route, request, env, ctx, false),
467
+ );
468
+ }
469
+
84
470
  const requestHandler = await getRequestHandler(workerModule);
85
471
 
86
472
  if (typeof requestHandler === 'function') {
@@ -88,6 +474,7 @@ async function dispatchRouteWorker(route, request, env, ctx) {
88
474
  route,
89
475
  request,
90
476
  env,
477
+ ctx,
91
478
  );
92
479
 
93
480
  return withRouteCssLinks(
@@ -96,9 +483,8 @@ async function dispatchRouteWorker(route, request, env, ctx) {
96
483
  requestHandlerOptions.resource.routeManifest,
97
484
  request,
98
485
  env,
99
- requestHandlerOptions.locals[
100
- DISTRIBUTED_SSR_FRAGMENTS_LOCALS_KEY
101
- ]?.getStylesheetHrefs(),
486
+ undefined,
487
+ requestHandlerOptions.locals[DISTRIBUTED_SSR_FRAGMENTS_LOCALS_KEY],
102
488
  );
103
489
  }
104
490
 
@@ -114,14 +500,46 @@ async function dispatchRouteWorker(route, request, env, ctx) {
114
500
  );
115
501
  }
116
502
 
117
- function matchesPrefix(pathname, prefix) {
503
+ // Worker Static Assets decode percent-encoded paths, so an encoded spelling
504
+ // of a mounted prefix (`/%70refix/...`, `/prefix%2F...`) would otherwise skip
505
+ // the BFF or service binding that owns the prefix and reach ASSETS directly.
506
+ // Resolve such spellings to the canonical pathname the owner sees; a
507
+ // backslash or undecodable spelling becomes a path the owner rejects.
508
+ function resolvePrefixPathname(pathname, prefix) {
118
509
  if (!prefix || prefix === '/') {
119
- return true;
510
+ return pathname;
120
511
  }
121
512
 
122
513
  const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
514
+ let candidate = pathname;
515
+
516
+ for (let attempt = 0; attempt < 4; attempt += 1) {
517
+ if (candidate === normalized || candidate.startsWith(`${normalized}/`)) {
518
+ return candidate;
519
+ }
520
+ if (candidate.startsWith(`${normalized}\\`)) {
521
+ return `${normalized}/__invalid_encoded_path__`;
522
+ }
523
+
524
+ let decoded;
525
+ try {
526
+ decoded = decodeURIComponent(candidate);
527
+ } catch {
528
+ return candidate.toLowerCase().startsWith(normalized.toLowerCase())
529
+ ? `${normalized}/__invalid_encoded_path__`
530
+ : null;
531
+ }
532
+ if (decoded === candidate) {
533
+ return null;
534
+ }
535
+ candidate = decoded;
536
+ }
537
+
538
+ return null;
539
+ }
123
540
 
124
- return pathname === normalized || pathname.startsWith(`${normalized}/`);
541
+ function matchesPrefix(pathname, prefix) {
542
+ return resolvePrefixPathname(pathname, prefix) !== null;
125
543
  }
126
544
 
127
545
  function createRequestForMountedPrefix(request, prefix) {
@@ -131,12 +549,13 @@ function createRequestForMountedPrefix(request, prefix) {
131
549
 
132
550
  const url = new URL(request.url);
133
551
  const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
552
+ const matchedPathname = resolvePrefixPathname(url.pathname, normalized);
134
553
 
135
- if (!matchesPrefix(url.pathname, normalized)) {
554
+ if (matchedPathname === null) {
136
555
  return request;
137
556
  }
138
557
 
139
- const nextPath = url.pathname.slice(normalized.length) || '/';
558
+ const nextPath = matchedPathname.slice(normalized.length) || '/';
140
559
  url.pathname = nextPath.startsWith('/') ? nextPath : `/${nextPath}`;
141
560
 
142
561
  return new Request(url, request);
@@ -158,117 +577,131 @@ function createEffectBffDispatcherErrorResponse(bff, error) {
158
577
  );
159
578
  }
160
579
 
161
- function getEffectBffDispatcher(bff, runtime) {
162
- let effectDispatcherPromise = effectBffDispatcherPromises.get(bff.worker);
163
-
164
- if (effectDispatcherPromise) {
165
- return effectDispatcherPromise;
580
+ // workerd binds every I/O object (sockets, timers, pending promises) to the request that
581
+ // created it, so an Effect runtime built while serving one request cannot serve the next: a
582
+ // pooled PostgreSQL connection or a cached in-flight effect from an earlier request never
583
+ // completes. Each BFF request therefore builds its own dispatcher and disposes it once the
584
+ // response body has been delivered; platform pools such as Hyperdrive keep connections warm.
585
+ async function createEffectBffDispatcher(bff, runtime) {
586
+ if (
587
+ typeof bff.dispatcherExport !== 'string' ||
588
+ bff.dispatcherExport.length === 0
589
+ ) {
590
+ throw new Error('manifest does not declare dispatcherExport');
166
591
  }
167
592
 
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];
593
+ const effectDispatcherFactory = runtime[bff.dispatcherExport];
177
594
 
178
- if (typeof effectDispatcherFactory !== 'function') {
179
- throw new Error(`worker bundle does not export ${bff.dispatcherExport}`);
180
- }
595
+ if (typeof effectDispatcherFactory !== 'function') {
596
+ throw new Error(`worker bundle does not export ${bff.dispatcherExport}`);
597
+ }
181
598
 
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
- ) {
599
+ const effectConfig = bff.effect;
600
+ if (
601
+ !effectConfig ||
602
+ typeof effectConfig !== 'object' ||
603
+ Array.isArray(effectConfig)
604
+ ) {
605
+ throw new Error('manifest declares invalid Effect BFF runtime config');
606
+ }
607
+ const crossProjectPolicy = effectConfig.crossProjectPolicy;
608
+ if (
609
+ !crossProjectPolicy ||
610
+ typeof crossProjectPolicy !== 'object' ||
611
+ Array.isArray(crossProjectPolicy)
612
+ ) {
613
+ throw new Error(
614
+ 'manifest declares invalid Effect BFF cross-project policy',
615
+ );
616
+ }
617
+ for (const field of [
618
+ 'enabled',
619
+ 'requireEnvelope',
620
+ 'requireOperationContext',
621
+ 'requireOperationContextDetails',
622
+ 'requireOperationSchemaHash',
623
+ 'requireOperationVersion',
624
+ 'allowUnknownOperations',
625
+ ]) {
626
+ if (typeof crossProjectPolicy[field] !== 'boolean') {
220
627
  throw new Error(
221
- 'manifest Effect BFF cross-project policy requires expectedOperationContracts object',
628
+ `manifest Effect BFF cross-project policy requires boolean ${field}`,
222
629
  );
223
630
  }
631
+ }
632
+ if (
633
+ !crossProjectPolicy.expectedOperationContracts ||
634
+ typeof crossProjectPolicy.expectedOperationContracts !== 'object' ||
635
+ Array.isArray(crossProjectPolicy.expectedOperationContracts)
636
+ ) {
637
+ throw new Error(
638
+ 'manifest Effect BFF cross-project policy requires expectedOperationContracts object',
639
+ );
640
+ }
224
641
 
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 {}
642
+ const effectDispatcher = await effectDispatcherFactory({
643
+ prefix: bff.prefix,
644
+ ...(effectConfig?.openapi === undefined
645
+ ? {}
646
+ : { openapi: effectConfig.openapi }),
647
+ ...(effectConfig?.dataPlatform === undefined
648
+ ? {}
649
+ : { dataPlatform: effectConfig.dataPlatform }),
650
+ ...(effectConfig?.crossProjectPolicy === undefined
651
+ ? {}
652
+ : { crossProjectPolicy }),
653
+ });
242
654
 
243
- throw new Error(
244
- `worker export ${bff.dispatcherExport} did not return a dispatcher with a dispatch function`,
245
- );
246
- }
655
+ if (!effectDispatcher || typeof effectDispatcher.dispatch !== 'function') {
656
+ try {
657
+ await effectDispatcher?.dispose?.();
658
+ } catch {}
247
659
 
248
- return effectDispatcher;
249
- });
660
+ throw new Error(
661
+ `worker export ${bff.dispatcherExport} did not return a dispatcher with a dispatch function`,
662
+ );
663
+ }
250
664
 
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
- });
665
+ return effectDispatcher;
666
+ }
259
667
 
260
- return effectDispatcherPromise;
668
+ function disposeEffectBffDispatcherAfterResponse(response, dispatcher, ctx) {
669
+ if (response.body === null) {
670
+ ctx.waitUntil(dispatcher.dispose());
671
+ return response;
672
+ }
673
+ const { readable, writable } = new TransformStream();
674
+ // The body pipe settles when the client has the whole body or the stream failed; a failure
675
+ // already reached the client through `readable`, so it only has to release the runtime here.
676
+ ctx.waitUntil(
677
+ response.body
678
+ .pipeTo(writable)
679
+ .catch(() => undefined)
680
+ .then(() => dispatcher.dispose()),
681
+ );
682
+ return new Response(readable, response);
261
683
  }
262
684
 
263
- async function dispatchBffRequest(request, env) {
685
+ async function dispatchBffRequest(request, env, ctx) {
686
+ const rendererRejection = createWorkerRendererGuardResponse(request);
687
+ if (rendererRejection) return rendererRejection;
264
688
  const bff = MODERN_WORKER_MANIFEST.bff;
265
689
 
266
- if (
267
- !bff?.worker ||
268
- !matchesPrefix(new URL(request.url).pathname, bff.prefix)
269
- ) {
690
+ const requestUrl = new URL(request.url);
691
+ const matchedPathname = resolvePrefixPathname(
692
+ requestUrl.pathname,
693
+ bff?.prefix,
694
+ );
695
+ if (!bff?.worker || matchedPathname === null) {
270
696
  return null;
271
697
  }
698
+ const canonicalRequest =
699
+ matchedPathname === requestUrl.pathname
700
+ ? request
701
+ : new Request(
702
+ Object.assign(requestUrl, { pathname: matchedPathname }),
703
+ request,
704
+ );
272
705
  if (bff.runtimeFramework !== 'effect') {
273
706
  return createEffectBffDispatcherErrorResponse(
274
707
  bff,
@@ -296,12 +729,23 @@ async function dispatchBffRequest(request, env) {
296
729
  let effectDispatcher;
297
730
 
298
731
  try {
299
- effectDispatcher = await getEffectBffDispatcher(bff, runtime);
732
+ effectDispatcher = await createEffectBffDispatcher(bff, runtime);
300
733
  } catch (error) {
301
734
  return createEffectBffDispatcherErrorResponse(bff, error);
302
735
  }
303
736
 
304
- return effectDispatcher.dispatch(request, { env });
737
+ let response;
738
+ try {
739
+ response = await effectDispatcher.dispatch(canonicalRequest, { env });
740
+ } catch (error) {
741
+ ctx.waitUntil(effectDispatcher.dispose());
742
+ throw error;
743
+ }
744
+ return disposeEffectBffDispatcherAfterResponse(
745
+ response,
746
+ effectDispatcher,
747
+ ctx,
748
+ );
305
749
  }
306
750
 
307
751
  const directHandler =