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

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
@@ -1,25 +1,37 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { isDeepStrictEqual } from 'node:util';
4
5
  import {
6
+ assertRendererProfileCompatibility,
5
7
  BACKEND_FEDERATION_MANIFEST_FILE,
6
8
  BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
7
9
  isUltramodernBuildArtifact,
10
+ type RendererProfile,
8
11
  ULTRAMODERN_BUILD_ARTIFACT_FILE,
12
+ ULTRAMODERN_BUILD_ARTIFACT_PATH,
9
13
  type UltramodernBuildArtifact,
10
14
  } from '@modern-js/backend-federation-contracts';
15
+ import { readBuildIdentity } from '../backend-federation-build/config';
16
+ import {
17
+ resolveTopologyDeliveryUnit,
18
+ resolveTopologyReleaseRole,
19
+ resolveWorkerDeliveryUnitStamp,
20
+ } from '../cloudflare/delivery-unit';
11
21
  import {
12
22
  canonicalSerializeMicroVerticalReleaseEnvelope,
13
23
  createMicroVerticalReleaseEnvelope,
14
24
  verifyMicroVerticalReleaseEnvelope,
15
25
  } from './index';
16
- import type {
17
- MicroVerticalReleaseArtifact,
18
- MicroVerticalReleaseArtifactInput,
19
- MicroVerticalReleaseEnvelope,
20
- MicroVerticalReleaseIdentity,
21
- MicroVerticalReleaseSurfaces,
22
- MicroVerticalReleaseTarget,
26
+ import {
27
+ type MicroVerticalReleaseArtifact,
28
+ type MicroVerticalReleaseArtifactInput,
29
+ type MicroVerticalReleaseIdentity,
30
+ type MicroVerticalReleaseSurfaces,
31
+ type MicroVerticalReleaseTarget,
32
+ type MicroVerticalReleaseUi,
33
+ type ReleaseEnvelope,
34
+ SHELL_RELEASE_ENVELOPE_KIND,
23
35
  } from './types';
24
36
 
25
37
  export const MICROVERTICAL_RELEASE_ENVELOPE_PATH =
@@ -33,6 +45,42 @@ const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_SCHEMA_VERSION = 1;
33
45
 
34
46
  const COMPILED_MODULE_PATTERN = /\.(?:c|m)?js$/u;
35
47
  const EFFECT_BFF_WORKER_PATTERN = /^worker\/__modern_bff_effect\.(?:c|m)?js$/u;
48
+ const EFFECT_BFF_WORKER_SUPPORT_PATTERN =
49
+ /^worker\/__modern_worker_(?:runtime|shared)_*\.(?:c|m)?js$/u;
50
+ const isEffectBffWorkerArtifact = (logicalPath: string) =>
51
+ EFFECT_BFF_WORKER_PATTERN.test(logicalPath) ||
52
+ EFFECT_BFF_WORKER_SUPPORT_PATTERN.test(logicalPath);
53
+ const effectBffWorkerArtifacts = (files: string[]) =>
54
+ files.some(logicalPath => EFFECT_BFF_WORKER_PATTERN.test(logicalPath))
55
+ ? files.filter(isEffectBffWorkerArtifact)
56
+ : [];
57
+ const CRAWLER_POLICY_PATH = 'public/robots.txt';
58
+ const isPublicMetadataPath = (logicalPath: string) =>
59
+ logicalPath === 'public/_headers' ||
60
+ /^public\/\.well-known\/[^/]+\.json$/u.test(logicalPath);
61
+ const PUBLIC_BUILD_ARTIFACT_PATH = `public/${ULTRAMODERN_BUILD_ARTIFACT_FILE}`;
62
+ // Files an app declared through `deploy.<target>.publicAssets`. The deploy
63
+ // output stages them from app-root sources; they are served data, not a
64
+ // UI/client surface, so API-only units may carry them.
65
+ const DECLARED_PUBLIC_ASSET_RUNTIME = 'public-asset';
66
+
67
+ const resolveDeclaredPublicAssets = (
68
+ files: readonly string[],
69
+ declaredPublicAssets: readonly string[],
70
+ target: 'Node' | 'Cloudflare',
71
+ ) => {
72
+ const staged = new Set(files);
73
+ const declared = new Set<string>();
74
+ for (const logicalPath of declaredPublicAssets) {
75
+ if (!logicalPath.startsWith('public/') || !staged.has(logicalPath)) {
76
+ throw new Error(
77
+ `[ultramodern-release-envelope] final ${target} staging has no declared public asset file ${JSON.stringify(logicalPath)}.`,
78
+ );
79
+ }
80
+ declared.add(logicalPath);
81
+ }
82
+ return declared;
83
+ };
36
84
 
37
85
  const isRecord = (value: unknown): value is Record<string, unknown> =>
38
86
  typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -57,8 +105,8 @@ const collectNestedStrings = (value: unknown, key: string): string[] => {
57
105
  ]);
58
106
  };
59
107
 
60
- // Empty native MF producers emit no remote entry or exposed execution assets.
61
- // Only their routeAssets declarations may supply the missing browser evidence.
108
+ // Native MF consumers can import remotes without emitting their own remote entry.
109
+ // Only local routeAssets declarations may supply their browser evidence.
62
110
  const emptyProducerRouteModules = async (
63
111
  distDirectory: string,
64
112
  files: string[],
@@ -70,7 +118,14 @@ const emptyProducerRouteModules = async (
70
118
  !Array.isArray(manifest.exposes) ||
71
119
  manifest.exposes.length !== 0 ||
72
120
  !Array.isArray(manifest.remotes) ||
73
- manifest.remotes.length !== 0 ||
121
+ manifest.remotes.some(
122
+ remote =>
123
+ !isRecord(remote) ||
124
+ ['federationContainerName', 'moduleName', 'alias', 'entry'].some(
125
+ key =>
126
+ typeof remote[key] !== 'string' || remote[key].trim().length === 0,
127
+ ),
128
+ ) ||
74
129
  !isRecord(manifest.metaData) ||
75
130
  !isRecord(manifest.metaData.remoteEntry) ||
76
131
  manifest.metaData.remoteEntry.name !== '' ||
@@ -303,7 +358,7 @@ const resolveSafeReleaseEnvelopePath = async ({
303
358
 
304
359
  const writeReleaseEnvelope = async (
305
360
  artifactRoot: string,
306
- envelope: MicroVerticalReleaseEnvelope,
361
+ envelope: ReleaseEnvelope,
307
362
  ) => {
308
363
  const envelopePath = await resolveSafeReleaseEnvelopePath({
309
364
  artifactRoot,
@@ -322,7 +377,8 @@ const writeReleaseEnvelope = async (
322
377
 
323
378
  const readBuildArtifact = async (
324
379
  distDirectory: string,
325
- ): Promise<UltramodernBuildArtifact> => {
380
+ requirePromotable = true,
381
+ ): Promise<UltramodernBuildArtifact | undefined> => {
326
382
  const artifactPath = path.join(
327
383
  distDirectory,
328
384
  ULTRAMODERN_BUILD_ARTIFACT_FILE,
@@ -334,8 +390,17 @@ const readBuildArtifact = async (
334
390
  );
335
391
  }
336
392
  if (artifact.deliveryUnit.sourceRevision === 'workspace') {
393
+ // `resolveUltramodernSourceRevision` deliberately labels a dirty or
394
+ // non-Git checkout `workspace` so development stays usable; only a
395
+ // promotion may not carry that identity. A plain `modern build` is the
396
+ // development path, so it skips the envelope instead of failing the
397
+ // build. The deploy path still asks for a promotable artifact, which is
398
+ // where the release gate belongs.
399
+ if (!requirePromotable) {
400
+ return undefined;
401
+ }
337
402
  throw new Error(
338
- '[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable full-stack envelope.',
403
+ '[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable envelope.',
339
404
  );
340
405
  }
341
406
  return artifact;
@@ -491,9 +556,8 @@ type ReleaseIdentityCarrierSurface =
491
556
  | 'ssr'
492
557
  | 'uiClient';
493
558
 
494
- type ReleaseIdentityCarrierPaths = Record<
495
- ReleaseIdentityCarrierSurface,
496
- string[]
559
+ type ReleaseIdentityCarrierPaths = Partial<
560
+ Record<ReleaseIdentityCarrierSurface, string[]>
497
561
  >;
498
562
 
499
563
  const writeReleaseIdentityCarrierMetadata = async (
@@ -506,7 +570,11 @@ const writeReleaseIdentityCarrierMetadata = async (
506
570
  ReleaseIdentityCarrierSurface,
507
571
  string[],
508
572
  ][]) {
509
- if (logicalPaths.length === 0) {
573
+ if (
574
+ logicalPaths.length === 0 &&
575
+ surface !== 'ssr' &&
576
+ surface !== 'uiClient'
577
+ ) {
510
578
  throw new Error(
511
579
  `[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`,
512
580
  );
@@ -581,12 +649,18 @@ const createReleaseArtifactInputs = async (
581
649
  distDirectory: string,
582
650
  identity: MicroVerticalReleaseIdentity,
583
651
  target: MicroVerticalReleaseTarget,
652
+ apiOnly: boolean,
653
+ uiOnly: boolean,
654
+ shellConsumer: boolean,
584
655
  ) => {
656
+ // A Shell consumes remotes and a UI-only unit has no backend; neither
657
+ // publishes a backend federation pair.
658
+ const backendProducer = !uiOnly && !shellConsumer;
585
659
  const files = await collectFiles(distDirectory);
586
- if (
587
- target === 'node' &&
588
- files.some(logicalPath => EFFECT_BFF_WORKER_PATTERN.test(logicalPath))
589
- ) {
660
+ // Rspack emits these runtime/shared chunks for UI SSR too. Only an actual
661
+ // Effect BFF entry makes the shared worker support part of that API surface.
662
+ const effectWorkerPaths = effectBffWorkerArtifacts(files);
663
+ if (target === 'node' && files.some(isEffectBffWorkerArtifact)) {
590
664
  throw new Error(
591
665
  '[ultramodern-release-envelope] Node target conflicts with a Cloudflare Effect API/BFF worker artifact; resolve the canonical deploy target instead of emitting a Node envelope for Cloudflare output.',
592
666
  );
@@ -596,7 +670,10 @@ const createReleaseArtifactInputs = async (
596
670
  return (
597
671
  topLevel === 'static' ||
598
672
  topLevel === 'html' ||
599
- topLevel === 'public' ||
673
+ (topLevel === 'public' &&
674
+ logicalPath !== CRAWLER_POLICY_PATH &&
675
+ logicalPath !== PUBLIC_BUILD_ARTIFACT_PATH &&
676
+ !isPublicMetadataPath(logicalPath)) ||
600
677
  [
601
678
  'index.html',
602
679
  'mf-manifest.json',
@@ -618,7 +695,7 @@ const createReleaseArtifactInputs = async (
618
695
  logicalPath =>
619
696
  logicalPath.startsWith('worker/') &&
620
697
  COMPILED_MODULE_PATTERN.test(logicalPath) &&
621
- !EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
698
+ !effectWorkerPaths.includes(logicalPath),
622
699
  );
623
700
  const nodeApiEntryPaths = files.filter(
624
701
  logicalPath =>
@@ -633,44 +710,59 @@ const createReleaseArtifactInputs = async (
633
710
  logicalPath.startsWith('shared/')) &&
634
711
  COMPILED_MODULE_PATTERN.test(logicalPath),
635
712
  )
636
- : files.filter(logicalPath =>
637
- EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
638
- );
713
+ : effectWorkerPaths;
714
+
715
+ if (
716
+ backendProducer &&
717
+ target === 'cloudflare' &&
718
+ effectWorkerPaths.length === 0
719
+ ) {
720
+ throw new Error(
721
+ '[ultramodern-release-envelope] cloudflare MicroVertical has no actual Effect API/BFF worker artifact.',
722
+ );
723
+ }
639
724
 
640
- if (uiClientPaths.length === 0) {
725
+ if (apiOnly && (uiClientPaths.length > 0 || ssrPaths.length > 0)) {
726
+ throw new Error(
727
+ '[ultramodern-release-envelope] API-only MicroVertical emitted an undeclared UI/client or SSR surface.',
728
+ );
729
+ }
730
+ if (!apiOnly && uiClientPaths.length === 0) {
641
731
  throw new Error(
642
732
  `[ultramodern-release-envelope] ${target} full-stack MicroVertical has no UI/client artifacts.`,
643
733
  );
644
734
  }
645
- if (ssrPaths.length === 0) {
735
+ if (!apiOnly && ssrPaths.length === 0) {
646
736
  throw new Error(
647
737
  `[ultramodern-release-envelope] ${target} full-stack MicroVertical has no ${target === 'node' ? 'Node' : 'workerd'} SSR artifacts.`,
648
738
  );
649
739
  }
740
+ if (uiOnly && (apiBackendPaths.length > 0 || nodeApiEntryPaths.length > 0))
741
+ throw new Error(
742
+ '[ultramodern-release-envelope] UI-only application emitted an undeclared API/backend artifact.',
743
+ );
650
744
  if (
651
- (target === 'node' && nodeApiEntryPaths.length === 0) ||
652
- apiBackendPaths.length === 0
745
+ backendProducer &&
746
+ ((target === 'node' && nodeApiEntryPaths.length === 0) ||
747
+ apiBackendPaths.length === 0)
653
748
  ) {
654
749
  throw new Error(
655
- `[ultramodern-release-envelope] ${target} full-stack MicroVertical has no actual ${target === 'node' ? 'compiled Node Effect API artifact' : 'Effect API/BFF worker artifact'}.`,
750
+ `[ultramodern-release-envelope] ${target} MicroVertical has no actual ${target === 'node' ? 'compiled Node Effect API artifact' : 'Effect API/BFF worker artifact'}.`,
656
751
  );
657
752
  }
658
753
 
659
- const clientExecutionPaths = await manifestReferencedClientModules(
660
- distDirectory,
661
- files,
662
- );
663
- if (clientExecutionPaths.length === 0) {
754
+ const clientExecutionPaths = apiOnly
755
+ ? []
756
+ : await manifestReferencedClientModules(distDirectory, files);
757
+ if (!apiOnly && clientExecutionPaths.length === 0) {
664
758
  throw new Error(
665
759
  '[ultramodern-release-envelope] UI/client manifest references no compiled execution module.',
666
760
  );
667
761
  }
668
- const ssrExecutionPaths = await routeReferencedSsrModules(
669
- distDirectory,
670
- files,
671
- target,
672
- );
673
- if (ssrExecutionPaths.length === 0) {
762
+ const ssrExecutionPaths = apiOnly
763
+ ? []
764
+ : await routeReferencedSsrModules(distDirectory, files, target);
765
+ if (!apiOnly && ssrExecutionPaths.length === 0) {
674
766
  throw new Error(
675
767
  `[ultramodern-release-envelope] route manifest references no emitted ${target === 'node' ? 'Node' : 'Cloudflare'} SSR execution module.`,
676
768
  );
@@ -689,13 +781,34 @@ const createReleaseArtifactInputs = async (
689
781
  }
690
782
  };
691
783
  add(uiClientPaths, 'browser');
784
+ add(
785
+ files.filter(logicalPath => logicalPath === CRAWLER_POLICY_PATH),
786
+ 'crawler-policy',
787
+ );
788
+ add(files.filter(isPublicMetadataPath), 'public-metadata');
692
789
  add(ssrPaths, target === 'node' ? 'nodejs' : 'workerd');
693
790
  add(apiBackendPaths, target === 'node' ? 'nodejs' : 'workerd-effect');
694
- add([BACKEND_FEDERATION_MANIFEST_FILE], 'module-federation-manifest');
791
+ add([ULTRAMODERN_BUILD_ARTIFACT_FILE], 'release-identity-metadata');
695
792
  add(
696
- [BACKEND_FEDERATION_REMOTE_ENTRY_FILE],
697
- target === 'node' ? 'nodejs' : 'commonjs-module',
793
+ files.filter(logicalPath => logicalPath === PUBLIC_BUILD_ARTIFACT_PATH),
794
+ 'release-identity-metadata',
698
795
  );
796
+ if (backendProducer) {
797
+ add([BACKEND_FEDERATION_MANIFEST_FILE], 'module-federation-manifest');
798
+ add(
799
+ [BACKEND_FEDERATION_REMOTE_ENTRY_FILE],
800
+ target === 'node' ? 'nodejs' : 'commonjs-module',
801
+ );
802
+ }
803
+ if (shellConsumer) {
804
+ // The Shell is the deployed entry point: every remaining build file is
805
+ // part of its deployment and must be bound.
806
+ for (const logicalPath of files) {
807
+ if (!runtimeByPath.has(logicalPath)) {
808
+ runtimeByPath.set(logicalPath, `${target}-deployment`);
809
+ }
810
+ }
811
+ }
699
812
 
700
813
  const surfaces: MicroVerticalReleaseSurfaces = {
701
814
  uiClient: [...uiClientPaths].sort((left, right) =>
@@ -705,10 +818,14 @@ const createReleaseArtifactInputs = async (
705
818
  apiBackend: [...apiBackendPaths].sort((left, right) =>
706
819
  left.localeCompare(right),
707
820
  ),
708
- backendFederation: {
709
- manifest: BACKEND_FEDERATION_MANIFEST_FILE,
710
- container: BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
711
- },
821
+ ...(backendProducer
822
+ ? {
823
+ backendFederation: {
824
+ manifest: BACKEND_FEDERATION_MANIFEST_FILE,
825
+ container: BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
826
+ },
827
+ }
828
+ : {}),
712
829
  };
713
830
  const artifacts = [...runtimeByPath]
714
831
  .map(([logicalPath, runtime]) => releaseArtifact(logicalPath, runtime))
@@ -717,11 +834,15 @@ const createReleaseArtifactInputs = async (
717
834
  distDirectory,
718
835
  identity,
719
836
  {
720
- apiBackend: apiBackendPaths,
721
- backendFederation: [
722
- BACKEND_FEDERATION_MANIFEST_FILE,
723
- BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
724
- ],
837
+ ...(apiBackendPaths.length > 0 ? { apiBackend: apiBackendPaths } : {}),
838
+ ...(backendProducer
839
+ ? {
840
+ backendFederation: [
841
+ BACKEND_FEDERATION_MANIFEST_FILE,
842
+ BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
843
+ ],
844
+ }
845
+ : {}),
725
846
  ssr: [...new Set([...ssrExecutionPaths, ...ssrPaths])],
726
847
  uiClient: uiClientPaths.filter(logicalPath =>
727
848
  COMPILED_MODULE_PATTERN.test(logicalPath),
@@ -736,6 +857,43 @@ const createReleaseArtifactInputs = async (
736
857
  };
737
858
  };
738
859
 
860
+ const releaseEnvelopeSurfaces = (
861
+ surfaces: MicroVerticalReleaseSurfaces,
862
+ shellConsumer: boolean,
863
+ ) => {
864
+ if (!shellConsumer) {
865
+ return { surfaces };
866
+ }
867
+ const { uiClient, ssr, apiBackend } = surfaces;
868
+ return {
869
+ kind: SHELL_RELEASE_ENVELOPE_KIND,
870
+ surfaces: { uiClient, ssr, apiBackend },
871
+ };
872
+ };
873
+
874
+ /**
875
+ * A Shell publishes no backend federation pair, so no producer restamps its
876
+ * identity. Its finalized build artifact (stamped by the renderer build from the
877
+ * declared delivery unit) must belong to this app; the build-artifact contract
878
+ * requires every identity record to carry that one stamped identity.
879
+ */
880
+ const assertShellBuildIdentity = async (
881
+ appDirectory: string,
882
+ distDirectory: string,
883
+ ) => {
884
+ const source = await readBuildIdentity(appDirectory);
885
+ const finalized = (await pathExists(
886
+ path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE),
887
+ ))
888
+ ? await resolveWorkerDeliveryUnitStamp(appDirectory, distDirectory)
889
+ : undefined;
890
+ if (!source.artifact || !finalized || finalized.unitId !== source.unitId) {
891
+ throw new Error(
892
+ '[ultramodern-release-envelope] Shell is missing its native compiled delivery-unit identity.',
893
+ );
894
+ }
895
+ };
896
+
739
897
  const pathExists = async (filePath: string) => {
740
898
  try {
741
899
  await fs.access(filePath);
@@ -747,16 +905,34 @@ const pathExists = async (filePath: string) => {
747
905
 
748
906
  export const emitFrameworkMicroVerticalReleaseEnvelope = async ({
749
907
  apiOnly,
908
+ appDirectory,
750
909
  distDirectory,
910
+ requirePromotable = true,
911
+ expectedRendererProfile,
912
+ role,
751
913
  target,
752
914
  }: {
753
915
  apiOnly: boolean;
916
+ appDirectory?: string;
754
917
  distDirectory: string;
918
+ requirePromotable?: boolean;
919
+ expectedRendererProfile?: RendererProfile;
920
+ role?: 'microvertical' | 'shell';
755
921
  target: MicroVerticalReleaseTarget;
756
- }): Promise<MicroVerticalReleaseEnvelope | undefined> => {
757
- if (apiOnly) {
758
- return undefined;
922
+ }): Promise<ReleaseEnvelope | undefined> => {
923
+ const declaredRole = appDirectory
924
+ ? await resolveTopologyReleaseRole(appDirectory)
925
+ : undefined;
926
+ if (
927
+ role !== undefined &&
928
+ declaredRole !== undefined &&
929
+ role !== declaredRole
930
+ ) {
931
+ throw new Error(
932
+ '[ultramodern-release-envelope] Configured release role disagrees with the declared topology app.',
933
+ );
759
934
  }
935
+ const shellConsumer = (declaredRole ?? role ?? 'microvertical') === 'shell';
760
936
  const backendManifestPath = path.join(
761
937
  distDirectory,
762
938
  BACKEND_FEDERATION_MANIFEST_FILE,
@@ -769,35 +945,115 @@ export const emitFrameworkMicroVerticalReleaseEnvelope = async ({
769
945
  pathExists(backendManifestPath),
770
946
  pathExists(backendContainerPath),
771
947
  ]);
772
- if (!hasBackendManifest && !hasBackendContainer) {
773
- return undefined;
948
+ if (shellConsumer) {
949
+ if (apiOnly || hasBackendManifest || hasBackendContainer || !appDirectory) {
950
+ throw new Error(
951
+ '[ultramodern-release-envelope] Shell requires a full-stack application source and cannot declare a backend federation producer.',
952
+ );
953
+ }
954
+ await assertShellBuildIdentity(appDirectory, distDirectory);
955
+ } else if (!apiOnly && !hasBackendManifest && !hasBackendContainer) {
956
+ const source = appDirectory
957
+ ? path.join(appDirectory, ULTRAMODERN_BUILD_ARTIFACT_PATH)
958
+ : undefined;
959
+ const participationPaths = [
960
+ path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE),
961
+ backendManifestPath,
962
+ backendContainerPath,
963
+ ...(source ? [source, source.replace(/\.json$/u, '.ts')] : []),
964
+ ];
965
+ const hasParticipationEntry = (
966
+ await Promise.all(
967
+ participationPaths.map(async filePath => {
968
+ try {
969
+ await fs.lstat(filePath);
970
+ return true;
971
+ } catch (error) {
972
+ if (isMissingPathError(error)) return false;
973
+ throw error;
974
+ }
975
+ }),
976
+ )
977
+ ).some(Boolean);
978
+ if (
979
+ !hasParticipationEntry &&
980
+ (!appDirectory || !(await resolveTopologyDeliveryUnit(appDirectory)))
981
+ )
982
+ return undefined;
774
983
  }
775
- if (!hasBackendManifest || !hasBackendContainer) {
984
+ const buildArtifact = await readBuildArtifact(
985
+ distDirectory,
986
+ requirePromotable,
987
+ );
988
+ const declared =
989
+ appDirectory && buildArtifact
990
+ ? await resolveWorkerDeliveryUnitStamp(appDirectory, distDirectory)
991
+ : undefined;
992
+ const uiOnly = Boolean(declared?.surfaces.ui && !declared.surfaces.api);
993
+ if (uiOnly && (apiOnly || hasBackendManifest || hasBackendContainer))
776
994
  throw new Error(
777
- '[ultramodern-release-envelope] full-stack MicroVertical backend federation manifest and container must be emitted together.',
995
+ '[ultramodern-release-envelope] UI-only application cannot declare API-only configuration or backend federation output.',
996
+ );
997
+ if (!hasBackendManifest && !hasBackendContainer && !apiOnly && !buildArtifact)
998
+ return undefined;
999
+ if (
1000
+ !shellConsumer &&
1001
+ !uiOnly &&
1002
+ (!hasBackendManifest || !hasBackendContainer)
1003
+ ) {
1004
+ throw new Error(
1005
+ '[ultramodern-release-envelope] MicroVertical backend federation manifest and container must be emitted together.',
778
1006
  );
779
1007
  }
780
1008
 
781
- const buildArtifact = await readBuildArtifact(distDirectory);
1009
+ if (!buildArtifact) {
1010
+ return undefined;
1011
+ }
1012
+ const builtUi = buildArtifact.surfaces.ui;
1013
+ if (Boolean(builtUi) === apiOnly) {
1014
+ throw new Error(
1015
+ '[ultramodern-release-envelope] build artifact UI surface must match the configured application surface.',
1016
+ );
1017
+ }
1018
+ if (expectedRendererProfile !== undefined) {
1019
+ assertRendererProfileCompatibility(
1020
+ expectedRendererProfile,
1021
+ builtUi?.rendererProfile,
1022
+ );
1023
+ }
1024
+ const ui: MicroVerticalReleaseUi | undefined = builtUi
1025
+ ? {
1026
+ rendererIdentity: builtUi.rendererIdentity,
1027
+ rendererProfile: builtUi.rendererProfile,
1028
+ routerBindings: builtUi.routerBindings,
1029
+ }
1030
+ : undefined;
782
1031
  const identity = {
783
1032
  unitId: buildArtifact.deliveryUnit.unitId,
784
1033
  buildMarker: buildArtifact.deliveryUnit.buildMarker,
785
1034
  sourceRevision: buildArtifact.deliveryUnit.sourceRevision,
786
1035
  releaseVersion: buildArtifact.deliveryUnit.version,
787
1036
  };
788
- await readBackendManifest(distDirectory, identity);
1037
+ if (!shellConsumer && !uiOnly) {
1038
+ await readBackendManifest(distDirectory, identity);
1039
+ }
789
1040
  const { artifacts, surfaces } = await createReleaseArtifactInputs(
790
1041
  distDirectory,
791
1042
  identity,
792
1043
  target,
1044
+ apiOnly,
1045
+ uiOnly,
1046
+ shellConsumer,
793
1047
  );
794
1048
  const envelope = await createMicroVerticalReleaseEnvelope({
795
1049
  artifactRoot: distDirectory,
796
1050
  target,
797
1051
  identity,
1052
+ ...(ui ? { ui } : {}),
798
1053
  artifacts,
799
- surfaces,
1054
+ ...releaseEnvelopeSurfaces(surfaces, shellConsumer),
800
1055
  });
1056
+ await verifyBuildArtifactCarriers(distDirectory, envelope);
801
1057
  await writeReleaseEnvelope(distDirectory, envelope);
802
1058
  return envelope;
803
1059
  };
@@ -806,13 +1062,15 @@ export const readFrameworkMicroVerticalReleaseEnvelope = async ({
806
1062
  artifactRoot,
807
1063
  expectedTarget,
808
1064
  logicalPathForArtifact,
1065
+ expectedRendererProfile,
809
1066
  required = false,
810
1067
  }: {
811
1068
  artifactRoot: string;
812
1069
  expectedTarget?: MicroVerticalReleaseTarget;
813
1070
  logicalPathForArtifact?: (artifact: MicroVerticalReleaseArtifact) => string;
1071
+ expectedRendererProfile?: RendererProfile;
814
1072
  required?: boolean;
815
- }): Promise<MicroVerticalReleaseEnvelope | undefined> => {
1073
+ }): Promise<ReleaseEnvelope | undefined> => {
816
1074
  const envelopePath = await resolveSafeReleaseEnvelopePath({
817
1075
  artifactRoot,
818
1076
  createDirectory: false,
@@ -825,32 +1083,113 @@ export const readFrameworkMicroVerticalReleaseEnvelope = async ({
825
1083
  }
826
1084
  return undefined;
827
1085
  }
828
- return verifyMicroVerticalReleaseEnvelope(await readJson(envelopePath), {
1086
+ const envelope = await verifyMicroVerticalReleaseEnvelope(
1087
+ await readJson(envelopePath),
1088
+ {
1089
+ artifactRoot,
1090
+ ...(expectedTarget ? { expectedTarget } : {}),
1091
+ ...(logicalPathForArtifact ? { logicalPathForArtifact } : {}),
1092
+ ...(expectedRendererProfile ? { expectedRendererProfile } : {}),
1093
+ },
1094
+ );
1095
+ await verifyBuildArtifactCarriers(
829
1096
  artifactRoot,
830
- ...(expectedTarget ? { expectedTarget } : {}),
831
- ...(logicalPathForArtifact ? { logicalPathForArtifact } : {}),
832
- });
1097
+ envelope,
1098
+ logicalPathForArtifact,
1099
+ );
1100
+ return envelope;
1101
+ };
1102
+
1103
+ const verifyBuildArtifactCarriers = async (
1104
+ artifactRoot: string,
1105
+ envelope: ReleaseEnvelope,
1106
+ logicalPathForArtifact?: (artifact: MicroVerticalReleaseArtifact) => string,
1107
+ ) => {
1108
+ const carriers = envelope.artifacts.filter(
1109
+ artifact =>
1110
+ artifact.logicalPath === ULTRAMODERN_BUILD_ARTIFACT_FILE ||
1111
+ artifact.logicalPath === PUBLIC_BUILD_ARTIFACT_PATH,
1112
+ );
1113
+ if (carriers.length === 0) {
1114
+ throw new Error(
1115
+ '[ultramodern-release-envelope] immutable build artifact must be bound to the release envelope.',
1116
+ );
1117
+ }
1118
+ for (const artifact of carriers) {
1119
+ if (
1120
+ artifact.kind !== 'file' ||
1121
+ artifact.runtime !== 'release-identity-metadata'
1122
+ ) {
1123
+ throw new Error(
1124
+ '[ultramodern-release-envelope] immutable build artifact must be a real file with runtime "release-identity-metadata".',
1125
+ );
1126
+ }
1127
+ const logicalPath =
1128
+ logicalPathForArtifact?.(artifact) ?? artifact.logicalPath;
1129
+ const built = await readJson(path.join(artifactRoot, logicalPath));
1130
+ if (!isUltramodernBuildArtifact(built)) {
1131
+ throw new Error(
1132
+ '[ultramodern-release-envelope] bound build artifact is invalid.',
1133
+ );
1134
+ }
1135
+ assertIdentityBlock(
1136
+ built.deliveryUnit,
1137
+ envelope.identity,
1138
+ 'buildArtifact.deliveryUnit',
1139
+ true,
1140
+ );
1141
+ if (Boolean(built.surfaces.ui) !== Boolean(envelope.ui)) {
1142
+ throw new Error(
1143
+ '[ultramodern-release-envelope] bound build artifact UI surface differs from the release envelope.',
1144
+ );
1145
+ }
1146
+ if (envelope.ui && built.surfaces.ui) {
1147
+ if (
1148
+ !isDeepStrictEqual(
1149
+ envelope.ui.routerBindings,
1150
+ built.surfaces.ui.routerBindings,
1151
+ )
1152
+ ) {
1153
+ throw new Error(
1154
+ '[ultramodern-release-envelope] buildArtifact.ui.routerBindings differ from the release envelope.',
1155
+ );
1156
+ }
1157
+ assertRendererProfileCompatibility(
1158
+ envelope.ui.rendererProfile,
1159
+ built.surfaces.ui.rendererProfile,
1160
+ );
1161
+ for (const field of [
1162
+ 'renderer',
1163
+ 'protocolVersion',
1164
+ 'appId',
1165
+ 'entryName',
1166
+ 'buildId',
1167
+ ] as const) {
1168
+ if (
1169
+ envelope.ui.rendererIdentity[field] !==
1170
+ built.surfaces.ui.rendererIdentity[field]
1171
+ ) {
1172
+ throw new Error(
1173
+ `[ultramodern-release-envelope] buildArtifact.ui.rendererIdentity.${field} differs from the release envelope.`,
1174
+ );
1175
+ }
1176
+ }
1177
+ }
1178
+ }
833
1179
  };
834
1180
 
835
1181
  export const verifyBuildOutputReleaseEnvelope = async (
836
1182
  distDirectory: string,
837
1183
  expectedTarget?: MicroVerticalReleaseTarget,
838
1184
  ) => {
839
- const [hasEnvelope, hasBuildArtifact, hasBackendManifest, hasBackendEntry] =
840
- await Promise.all([
841
- pathExists(path.join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH)),
842
- pathExists(path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE)),
843
- pathExists(path.join(distDirectory, BACKEND_FEDERATION_MANIFEST_FILE)),
844
- pathExists(
845
- path.join(distDirectory, BACKEND_FEDERATION_REMOTE_ENTRY_FILE),
846
- ),
847
- ]);
848
- const isFullStackMicroVertical =
849
- hasBuildArtifact && (hasBackendManifest || hasBackendEntry);
1185
+ const [hasEnvelope, hasBuildArtifact] = await Promise.all([
1186
+ pathExists(path.join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH)),
1187
+ pathExists(path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE)),
1188
+ ]);
850
1189
  return readFrameworkMicroVerticalReleaseEnvelope({
851
1190
  artifactRoot: distDirectory,
852
1191
  ...(expectedTarget ? { expectedTarget } : {}),
853
- required: hasEnvelope || isFullStackMicroVertical,
1192
+ required: hasEnvelope || hasBuildArtifact,
854
1193
  });
855
1194
  };
856
1195
 
@@ -869,13 +1208,24 @@ export const verifyNodeReleaseEnvelopeStaging = async ({
869
1208
  const createNodeStagedReleaseArtifactInputs = async (
870
1209
  outputDirectory: string,
871
1210
  identity: MicroVerticalReleaseIdentity,
1211
+ apiOnly: boolean,
1212
+ uiOnly: boolean,
1213
+ declaredPublicAssets: readonly string[],
1214
+ shellConsumer: boolean,
872
1215
  ) => {
1216
+ const backendProducer = !uiOnly && !shellConsumer;
873
1217
  const files = await collectFiles(outputDirectory);
1218
+ const declaredPublicAssetPaths = resolveDeclaredPublicAssets(
1219
+ files,
1220
+ declaredPublicAssets,
1221
+ 'Node',
1222
+ );
874
1223
  for (const requiredPath of [
875
1224
  'index.js',
876
1225
  'package.json',
877
- BACKEND_FEDERATION_MANIFEST_FILE,
878
- BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
1226
+ ...(backendProducer
1227
+ ? [BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_REMOTE_ENTRY_FILE]
1228
+ : []),
879
1229
  ]) {
880
1230
  if (!files.includes(requiredPath)) {
881
1231
  throw new Error(
@@ -888,12 +1238,24 @@ const createNodeStagedReleaseArtifactInputs = async (
888
1238
  BACKEND_FEDERATION_MANIFEST_FILE,
889
1239
  BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
890
1240
  ]);
1241
+ if (
1242
+ shellConsumer &&
1243
+ files.some(logicalPath => backendFederationPaths.has(logicalPath))
1244
+ ) {
1245
+ throw new Error(
1246
+ '[ultramodern-release-envelope] final Shell Node staging cannot contain a backend federation producer artifact.',
1247
+ );
1248
+ }
891
1249
  const uiClientPaths = files.filter(logicalPath => {
892
1250
  const topLevel = logicalPath.split('/')[0];
893
1251
  return (
894
1252
  topLevel === 'static' ||
895
1253
  topLevel === 'html' ||
896
- topLevel === 'public' ||
1254
+ (topLevel === 'public' &&
1255
+ logicalPath !== CRAWLER_POLICY_PATH &&
1256
+ logicalPath !== PUBLIC_BUILD_ARTIFACT_PATH &&
1257
+ !isPublicMetadataPath(logicalPath) &&
1258
+ !declaredPublicAssetPaths.has(logicalPath)) ||
897
1259
  [
898
1260
  'index.html',
899
1261
  'mf-manifest.json',
@@ -915,27 +1277,46 @@ const createNodeStagedReleaseArtifactInputs = async (
915
1277
  (logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) &&
916
1278
  COMPILED_MODULE_PATTERN.test(logicalPath),
917
1279
  );
1280
+ if (
1281
+ uiOnly &&
1282
+ (apiBackendPaths.length > 0 ||
1283
+ files.some(logicalPath => backendFederationPaths.has(logicalPath)))
1284
+ )
1285
+ throw new Error(
1286
+ '[ultramodern-release-envelope] final UI-only Node staging contains an undeclared API/backend or backend federation artifact.',
1287
+ );
1288
+ if (
1289
+ apiOnly &&
1290
+ (uiClientPaths.length > 0 || ssrPaths.some(path => path !== 'index.js'))
1291
+ ) {
1292
+ throw new Error(
1293
+ '[ultramodern-release-envelope] final API-only Node staging contains an undeclared UI/client or SSR surface.',
1294
+ );
1295
+ }
918
1296
  for (const [surface, paths] of [
919
1297
  ['UI/client', uiClientPaths],
920
- ['Node SSR', ssrPaths],
1298
+ ['Node SSR', apiOnly ? [] : ssrPaths],
921
1299
  ['Node API/backend', apiBackendPaths],
922
1300
  ] as const) {
923
- if (paths.length === 0) {
1301
+ if (
1302
+ paths.length === 0 &&
1303
+ (surface === 'Node API/backend' ? backendProducer : !apiOnly)
1304
+ ) {
924
1305
  throw new Error(
925
1306
  `[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`,
926
1307
  );
927
1308
  }
928
1309
  }
929
- const clientExecutionPaths = await manifestReferencedClientModules(
930
- outputDirectory,
931
- files,
932
- );
933
- const ssrExecutionPaths = await routeReferencedSsrModules(
934
- outputDirectory,
935
- files,
936
- 'node',
937
- );
938
- if (clientExecutionPaths.length === 0 || ssrExecutionPaths.length === 0) {
1310
+ const clientExecutionPaths = apiOnly
1311
+ ? []
1312
+ : await manifestReferencedClientModules(outputDirectory, files);
1313
+ const ssrExecutionPaths = apiOnly
1314
+ ? []
1315
+ : await routeReferencedSsrModules(outputDirectory, files, 'node');
1316
+ if (
1317
+ !apiOnly &&
1318
+ (clientExecutionPaths.length === 0 || ssrExecutionPaths.length === 0)
1319
+ ) {
939
1320
  throw new Error(
940
1321
  '[ultramodern-release-envelope] final Node manifests must reference emitted UI/client and SSR execution modules.',
941
1322
  );
@@ -949,33 +1330,57 @@ const createNodeStagedReleaseArtifactInputs = async (
949
1330
  for (const logicalPath of uiClientPaths) {
950
1331
  runtimeByPath.set(logicalPath, 'browser');
951
1332
  }
952
- for (const logicalPath of ssrPaths) {
1333
+ for (const logicalPath of declaredPublicAssetPaths) {
1334
+ runtimeByPath.set(logicalPath, DECLARED_PUBLIC_ASSET_RUNTIME);
1335
+ }
1336
+ if (files.includes(CRAWLER_POLICY_PATH)) {
1337
+ runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
1338
+ }
1339
+ for (const logicalPath of files.filter(isPublicMetadataPath)) {
1340
+ runtimeByPath.set(logicalPath, 'public-metadata');
1341
+ }
1342
+ for (const logicalPath of apiOnly ? [] : ssrPaths) {
953
1343
  runtimeByPath.set(logicalPath, 'nodejs');
954
1344
  }
955
1345
  for (const logicalPath of apiBackendPaths) {
956
1346
  runtimeByPath.set(logicalPath, 'nodejs');
957
1347
  }
958
- runtimeByPath.set(
959
- BACKEND_FEDERATION_MANIFEST_FILE,
960
- 'module-federation-manifest',
961
- );
962
- runtimeByPath.set(BACKEND_FEDERATION_REMOTE_ENTRY_FILE, 'nodejs');
1348
+ if (backendProducer) {
1349
+ runtimeByPath.set(
1350
+ BACKEND_FEDERATION_MANIFEST_FILE,
1351
+ 'module-federation-manifest',
1352
+ );
1353
+ runtimeByPath.set(BACKEND_FEDERATION_REMOTE_ENTRY_FILE, 'nodejs');
1354
+ }
1355
+ for (const logicalPath of [
1356
+ ULTRAMODERN_BUILD_ARTIFACT_FILE,
1357
+ PUBLIC_BUILD_ARTIFACT_PATH,
1358
+ ]) {
1359
+ if (files.includes(logicalPath))
1360
+ runtimeByPath.set(logicalPath, 'release-identity-metadata');
1361
+ }
963
1362
 
964
1363
  const identityCarrierMetadata = await writeReleaseIdentityCarrierMetadata(
965
1364
  outputDirectory,
966
1365
  identity,
967
1366
  {
968
- apiBackend: apiBackendPaths,
969
- backendFederation: [
970
- BACKEND_FEDERATION_MANIFEST_FILE,
971
- BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
972
- ],
973
- ssr: [
974
- ...new Set([
975
- ...ssrExecutionPaths,
976
- ...ssrPaths.filter(logicalPath => logicalPath !== 'index.js'),
977
- ]),
978
- ],
1367
+ ...(apiBackendPaths.length > 0 ? { apiBackend: apiBackendPaths } : {}),
1368
+ ...(backendProducer
1369
+ ? {
1370
+ backendFederation: [
1371
+ BACKEND_FEDERATION_MANIFEST_FILE,
1372
+ BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
1373
+ ],
1374
+ }
1375
+ : {}),
1376
+ ssr: apiOnly
1377
+ ? []
1378
+ : [
1379
+ ...new Set([
1380
+ ...ssrExecutionPaths,
1381
+ ...ssrPaths.filter(logicalPath => logicalPath !== 'index.js'),
1382
+ ]),
1383
+ ],
979
1384
  uiClient: uiClientPaths.filter(logicalPath =>
980
1385
  COMPILED_MODULE_PATTERN.test(logicalPath),
981
1386
  ),
@@ -992,22 +1397,31 @@ const createNodeStagedReleaseArtifactInputs = async (
992
1397
  uiClient: [...uiClientPaths].sort((left, right) =>
993
1398
  left.localeCompare(right),
994
1399
  ),
995
- ssr: [...ssrPaths].sort((left, right) => left.localeCompare(right)),
1400
+ ssr: apiOnly
1401
+ ? []
1402
+ : [...ssrPaths].sort((left, right) => left.localeCompare(right)),
996
1403
  apiBackend: [...apiBackendPaths].sort((left, right) =>
997
1404
  left.localeCompare(right),
998
1405
  ),
999
- backendFederation: {
1000
- manifest: BACKEND_FEDERATION_MANIFEST_FILE,
1001
- container: BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
1002
- },
1406
+ ...(backendProducer
1407
+ ? {
1408
+ backendFederation: {
1409
+ manifest: BACKEND_FEDERATION_MANIFEST_FILE,
1410
+ container: BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
1411
+ },
1412
+ }
1413
+ : {}),
1003
1414
  },
1004
1415
  };
1005
1416
  };
1006
1417
 
1007
1418
  export const emitNodeStagedReleaseEnvelope = async ({
1419
+ declaredPublicAssets = [],
1008
1420
  distDirectory,
1009
1421
  outputDirectory,
1010
1422
  }: {
1423
+ /** Staged `deploy.node.publicAssets` files, relative to `outputDirectory`. */
1424
+ declaredPublicAssets?: readonly string[];
1011
1425
  distDirectory: string;
1012
1426
  outputDirectory: string;
1013
1427
  }) => {
@@ -1015,17 +1429,24 @@ export const emitNodeStagedReleaseEnvelope = async ({
1015
1429
  if (!source) {
1016
1430
  return undefined;
1017
1431
  }
1432
+ const shellConsumer = source.kind === SHELL_RELEASE_ENVELOPE_KIND;
1018
1433
  const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(
1019
1434
  outputDirectory,
1020
1435
  source.identity,
1436
+ source.surfaces.uiClient.length === 0,
1437
+ source.surfaces.apiBackend.length === 0,
1438
+ declaredPublicAssets,
1439
+ shellConsumer,
1021
1440
  );
1022
1441
  const staged = await createMicroVerticalReleaseEnvelope({
1023
1442
  artifactRoot: outputDirectory,
1024
1443
  target: 'node',
1025
1444
  identity: source.identity,
1445
+ ...(source.ui ? { ui: source.ui } : {}),
1026
1446
  artifacts,
1027
- surfaces,
1447
+ ...releaseEnvelopeSurfaces(surfaces, shellConsumer),
1028
1448
  });
1449
+ await verifyBuildArtifactCarriers(outputDirectory, staged);
1029
1450
  await writeReleaseEnvelope(outputDirectory, staged);
1030
1451
  return staged;
1031
1452
  };
@@ -1059,14 +1480,24 @@ export const stageCloudflareReleaseEnvelope = async ({
1059
1480
  const createCloudflareStagedReleaseArtifactInputs = async (
1060
1481
  outputDirectory: string,
1061
1482
  identity: MicroVerticalReleaseIdentity,
1483
+ apiOnly: boolean,
1484
+ uiOnly: boolean,
1485
+ declaredPublicAssets: readonly string[],
1486
+ shellConsumer: boolean,
1062
1487
  ) => {
1488
+ const backendProducer = !uiOnly && !shellConsumer;
1063
1489
  const files = await collectFiles(outputDirectory);
1490
+ const declaredPublicAssetPaths = resolveDeclaredPublicAssets(
1491
+ files,
1492
+ declaredPublicAssets,
1493
+ 'Cloudflare',
1494
+ );
1064
1495
  for (const requiredPath of [
1065
1496
  'server/index.mjs',
1066
1497
  'server/modern-worker-manifest.json',
1067
1498
  'wrangler.json',
1068
1499
  'package.json',
1069
- 'worker/package.json',
1500
+ ...(apiOnly ? [] : ['worker/package.json']),
1070
1501
  ]) {
1071
1502
  if (!files.includes(requiredPath)) {
1072
1503
  throw new Error(
@@ -1087,34 +1518,84 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1087
1518
  }
1088
1519
  const backendManifestPath = `public/${BACKEND_FEDERATION_MANIFEST_FILE}`;
1089
1520
  const backendContainerPath = `public/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`;
1090
- const apiBackendPaths = files.filter(logicalPath =>
1091
- EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
1092
- );
1521
+ const apiBackendPaths = effectBffWorkerArtifacts(files);
1522
+ if (
1523
+ backendProducer &&
1524
+ !files.some(logicalPath => EFFECT_BFF_WORKER_PATTERN.test(logicalPath))
1525
+ ) {
1526
+ throw new Error(
1527
+ '[ultramodern-release-envelope] final Cloudflare staging has no actual Effect API/BFF worker artifact.',
1528
+ );
1529
+ }
1093
1530
  const ssrPaths = files.filter(
1094
1531
  logicalPath =>
1095
1532
  (logicalPath.startsWith('server/') ||
1096
1533
  logicalPath.startsWith('worker/')) &&
1097
1534
  COMPILED_MODULE_PATTERN.test(logicalPath) &&
1098
- !EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
1535
+ !apiBackendPaths.includes(logicalPath),
1099
1536
  );
1100
1537
  const uiClientPaths = files.filter(
1101
1538
  logicalPath =>
1102
1539
  logicalPath.startsWith('public/') &&
1540
+ logicalPath !== CRAWLER_POLICY_PATH &&
1541
+ !isPublicMetadataPath(logicalPath) &&
1542
+ !declaredPublicAssetPaths.has(logicalPath) &&
1543
+ logicalPath !== PUBLIC_BUILD_ARTIFACT_PATH &&
1103
1544
  logicalPath !== backendManifestPath &&
1104
1545
  logicalPath !== backendContainerPath,
1105
1546
  );
1547
+ if (
1548
+ shellConsumer &&
1549
+ files.some(
1550
+ logicalPath =>
1551
+ logicalPath === backendManifestPath ||
1552
+ logicalPath === backendContainerPath,
1553
+ )
1554
+ ) {
1555
+ throw new Error(
1556
+ '[ultramodern-release-envelope] final Shell Cloudflare staging cannot contain a backend federation producer artifact.',
1557
+ );
1558
+ }
1559
+ if (
1560
+ uiOnly &&
1561
+ (apiBackendPaths.length > 0 ||
1562
+ files.some(
1563
+ logicalPath =>
1564
+ logicalPath === backendManifestPath ||
1565
+ logicalPath === backendContainerPath ||
1566
+ (logicalPath.startsWith('api/') &&
1567
+ COMPILED_MODULE_PATTERN.test(logicalPath)),
1568
+ ))
1569
+ )
1570
+ throw new Error(
1571
+ '[ultramodern-release-envelope] final UI-only Cloudflare staging contains an undeclared API/backend or backend federation artifact.',
1572
+ );
1573
+ if (
1574
+ apiOnly &&
1575
+ (uiClientPaths.length > 0 ||
1576
+ ssrPaths.some(path => path !== 'server/index.mjs'))
1577
+ ) {
1578
+ throw new Error(
1579
+ '[ultramodern-release-envelope] final API-only Cloudflare staging contains an undeclared UI/client or SSR surface.',
1580
+ );
1581
+ }
1106
1582
  for (const [surface, paths] of [
1107
1583
  ['UI/client', uiClientPaths],
1108
- ['Cloudflare SSR', ssrPaths],
1584
+ ['Cloudflare SSR', apiOnly ? [] : ssrPaths],
1109
1585
  ['Cloudflare API/backend', apiBackendPaths],
1110
1586
  ] as const) {
1111
- if (paths.length === 0) {
1587
+ if (
1588
+ paths.length === 0 &&
1589
+ (surface === 'Cloudflare API/backend' ? backendProducer : !apiOnly)
1590
+ ) {
1112
1591
  throw new Error(
1113
1592
  `[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`,
1114
1593
  );
1115
1594
  }
1116
1595
  }
1117
- for (const logicalPath of [backendManifestPath, backendContainerPath]) {
1596
+ for (const logicalPath of backendProducer
1597
+ ? [backendManifestPath, backendContainerPath]
1598
+ : []) {
1118
1599
  if (!files.includes(logicalPath)) {
1119
1600
  throw new Error(
1120
1601
  `[ultramodern-release-envelope] final Cloudflare staging is missing ${logicalPath}.`,
@@ -1122,18 +1603,25 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1122
1603
  }
1123
1604
  }
1124
1605
 
1125
- const clientExecutionPaths = await manifestReferencedClientModules(
1126
- outputDirectory,
1127
- files,
1128
- 'public/mf-manifest.json',
1129
- );
1130
- const ssrExecutionPaths = await routeReferencedSsrModules(
1131
- outputDirectory,
1132
- files,
1133
- 'cloudflare',
1134
- 'server/route.json',
1135
- );
1136
- if (clientExecutionPaths.length === 0 || ssrExecutionPaths.length === 0) {
1606
+ const clientExecutionPaths = apiOnly
1607
+ ? []
1608
+ : await manifestReferencedClientModules(
1609
+ outputDirectory,
1610
+ files,
1611
+ 'public/mf-manifest.json',
1612
+ );
1613
+ const ssrExecutionPaths = apiOnly
1614
+ ? []
1615
+ : await routeReferencedSsrModules(
1616
+ outputDirectory,
1617
+ files,
1618
+ 'cloudflare',
1619
+ 'server/route.json',
1620
+ );
1621
+ if (
1622
+ !apiOnly &&
1623
+ (clientExecutionPaths.length === 0 || ssrExecutionPaths.length === 0)
1624
+ ) {
1137
1625
  throw new Error(
1138
1626
  '[ultramodern-release-envelope] final Cloudflare manifests must reference emitted UI/client and SSR execution modules.',
1139
1627
  );
@@ -1149,23 +1637,44 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1149
1637
  }
1150
1638
  };
1151
1639
  add(uiClientPaths, 'browser');
1152
- add(ssrPaths, 'workerd');
1640
+ add([...declaredPublicAssetPaths], DECLARED_PUBLIC_ASSET_RUNTIME);
1641
+ if (files.includes(CRAWLER_POLICY_PATH)) {
1642
+ runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
1643
+ }
1644
+ add(files.filter(isPublicMetadataPath), 'public-metadata');
1645
+ add(apiOnly ? [] : ssrPaths, 'workerd');
1153
1646
  add(apiBackendPaths, 'workerd-effect');
1154
- add([backendManifestPath], 'module-federation-manifest');
1155
- add([backendContainerPath], 'commonjs-module');
1647
+ if (backendProducer) {
1648
+ add([backendManifestPath], 'module-federation-manifest');
1649
+ add([backendContainerPath], 'commonjs-module');
1650
+ }
1651
+ add(
1652
+ files.filter(
1653
+ logicalPath =>
1654
+ logicalPath === ULTRAMODERN_BUILD_ARTIFACT_FILE ||
1655
+ logicalPath === PUBLIC_BUILD_ARTIFACT_PATH,
1656
+ ),
1657
+ 'release-identity-metadata',
1658
+ );
1156
1659
 
1157
1660
  const identityCarrierMetadata = await writeReleaseIdentityCarrierMetadata(
1158
1661
  outputDirectory,
1159
1662
  identity,
1160
1663
  {
1161
- apiBackend: apiBackendPaths,
1162
- backendFederation: [backendManifestPath, backendContainerPath],
1163
- ssr: [
1164
- ...new Set([
1165
- ...ssrExecutionPaths,
1166
- ...ssrPaths.filter(logicalPath => logicalPath.startsWith('worker/')),
1167
- ]),
1168
- ],
1664
+ ...(apiBackendPaths.length > 0 ? { apiBackend: apiBackendPaths } : {}),
1665
+ ...(backendProducer
1666
+ ? { backendFederation: [backendManifestPath, backendContainerPath] }
1667
+ : {}),
1668
+ ssr: apiOnly
1669
+ ? []
1670
+ : [
1671
+ ...new Set([
1672
+ ...ssrExecutionPaths,
1673
+ ...ssrPaths.filter(logicalPath =>
1674
+ logicalPath.startsWith('worker/'),
1675
+ ),
1676
+ ]),
1677
+ ],
1169
1678
  uiClient: uiClientPaths.filter(logicalPath =>
1170
1679
  COMPILED_MODULE_PATTERN.test(logicalPath),
1171
1680
  ),
@@ -1182,22 +1691,31 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1182
1691
  uiClient: [...uiClientPaths].sort((left, right) =>
1183
1692
  left.localeCompare(right),
1184
1693
  ),
1185
- ssr: [...ssrPaths].sort((left, right) => left.localeCompare(right)),
1694
+ ssr: apiOnly
1695
+ ? []
1696
+ : [...ssrPaths].sort((left, right) => left.localeCompare(right)),
1186
1697
  apiBackend: [...apiBackendPaths].sort((left, right) =>
1187
1698
  left.localeCompare(right),
1188
1699
  ),
1189
- backendFederation: {
1190
- manifest: backendManifestPath,
1191
- container: backendContainerPath,
1192
- },
1700
+ ...(backendProducer
1701
+ ? {
1702
+ backendFederation: {
1703
+ manifest: backendManifestPath,
1704
+ container: backendContainerPath,
1705
+ },
1706
+ }
1707
+ : {}),
1193
1708
  },
1194
1709
  };
1195
1710
  };
1196
1711
 
1197
1712
  export const emitCloudflareStagedReleaseEnvelope = async ({
1713
+ declaredPublicAssets = [],
1198
1714
  distDirectory,
1199
1715
  outputDirectory,
1200
1716
  }: {
1717
+ /** Staged `deploy.worker.publicAssets` files, relative to `outputDirectory`. */
1718
+ declaredPublicAssets?: readonly string[];
1201
1719
  distDirectory: string;
1202
1720
  outputDirectory: string;
1203
1721
  }) => {
@@ -1208,18 +1726,25 @@ export const emitCloudflareStagedReleaseEnvelope = async ({
1208
1726
  if (!source) {
1209
1727
  return undefined;
1210
1728
  }
1729
+ const shellConsumer = source.kind === SHELL_RELEASE_ENVELOPE_KIND;
1211
1730
  const { artifacts, surfaces } =
1212
1731
  await createCloudflareStagedReleaseArtifactInputs(
1213
1732
  outputDirectory,
1214
1733
  source.identity,
1734
+ source.surfaces.uiClient.length === 0,
1735
+ source.surfaces.apiBackend.length === 0,
1736
+ declaredPublicAssets,
1737
+ shellConsumer,
1215
1738
  );
1216
1739
  const staged = await createMicroVerticalReleaseEnvelope({
1217
1740
  artifactRoot: outputDirectory,
1218
1741
  target: 'cloudflare',
1219
1742
  identity: source.identity,
1743
+ ...(source.ui ? { ui: source.ui } : {}),
1220
1744
  artifacts,
1221
- surfaces,
1745
+ ...releaseEnvelopeSurfaces(surfaces, shellConsumer),
1222
1746
  });
1747
+ await verifyBuildArtifactCarriers(outputDirectory, staged);
1223
1748
  await writeReleaseEnvelope(outputDirectory, staged);
1224
1749
  return staged;
1225
1750
  };