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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/README.md +23 -0
  2. package/dist/cjs/backend-federation-build/codegen.js +29 -18
  3. package/dist/cjs/backend-federation-build/config.js +84 -45
  4. package/dist/cjs/backend-federation-build/emit.js +63 -22
  5. package/dist/cjs/build-config/build-environment.js +25 -162
  6. package/dist/cjs/build-config/internal-effect-discovery.js +143 -0
  7. package/dist/cjs/build-config/public.js +2 -5
  8. package/dist/cjs/build-config/remote-address.js +58 -0
  9. package/dist/cjs/cloudflare/artifacts.js +8 -19
  10. package/dist/cjs/cloudflare/delivery-unit.js +125 -43
  11. package/dist/cjs/cloudflare/index.js +14 -4
  12. package/dist/cjs/cloudflare/native-renderer.js +100 -0
  13. package/dist/cjs/cloudflare/renderer-identity.js +106 -0
  14. package/dist/cjs/cloudflare/worker-manifest.js +54 -13
  15. package/dist/cjs/cloudflare/worker-options.js +48 -0
  16. package/dist/cjs/cloudflare/wrangler-config.js +210 -13
  17. package/dist/cjs/cloudflare-builder.js +307 -89
  18. package/dist/cjs/cloudflare-output-contract.js +36 -4
  19. package/dist/cjs/cloudflare-output-verifier/identity.js +112 -23
  20. package/dist/cjs/cloudflare-output-verifier/index.js +10 -0
  21. package/dist/cjs/cloudflare-output-verifier/manifest.js +46 -0
  22. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +7 -1
  23. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +10 -5
  24. package/dist/cjs/config.js +10 -5
  25. package/dist/cjs/deploy-output/npmAliases.js +92 -6
  26. package/dist/cjs/deploy-output/plugin.js +27 -6
  27. package/dist/cjs/deploy-output/public-assets.js +146 -0
  28. package/dist/cjs/deploy-output/target.js +57 -1
  29. package/dist/cjs/deploy-output/trace-files.js +99 -0
  30. package/dist/cjs/module-federation-config/exposes.js +70 -0
  31. package/dist/cjs/module-federation-config/index.js +73 -0
  32. package/dist/cjs/module-federation-config/inspect.js +103 -0
  33. package/dist/cjs/module-federation-config/object-literal.js +87 -0
  34. package/dist/cjs/module-federation-config/syntax.js +151 -0
  35. package/dist/cjs/module-federation-config/types.js +18 -0
  36. package/dist/cjs/native-type-checker.js +130 -13
  37. package/dist/cjs/policy-defaults.js +38 -29
  38. package/dist/cjs/release-envelope/canonical.js +2 -9
  39. package/dist/cjs/release-envelope/contract.js +68 -0
  40. package/dist/cjs/release-envelope/framework-output.js +296 -105
  41. package/dist/cjs/release-envelope/index.js +150 -36
  42. package/dist/cjs/release-envelope/plugin.js +24 -15
  43. package/dist/cjs/release-envelope/renderer-output-stamp.js +170 -0
  44. package/dist/cjs/release-envelope/types.js +10 -8
  45. package/dist/cjs/release-identity.js +8 -5
  46. package/dist/cjs/renderer-build-identity.js +134 -0
  47. package/dist/cjs/templates/cloudflare-entry.001-bootstrap-security.mjs +14 -6
  48. package/dist/cjs/templates/cloudflare-entry.002-assets-routes.mjs +10 -2
  49. package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +403 -96
  50. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +530 -103
  51. package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +26 -7
  52. package/dist/cjs/templates/cloudflare-worker-route-data.mjs +1069 -0
  53. package/dist/cjs/workspace-topology.js +84 -0
  54. package/dist/esm/backend-federation-build/codegen.mjs +29 -18
  55. package/dist/esm/backend-federation-build/config.mjs +74 -38
  56. package/dist/esm/backend-federation-build/emit.mjs +64 -23
  57. package/dist/esm/build-config/build-environment.mjs +26 -160
  58. package/dist/esm/build-config/internal-effect-discovery.mjs +102 -0
  59. package/dist/esm/build-config/public.mjs +1 -1
  60. package/dist/esm/build-config/remote-address.mjs +20 -0
  61. package/dist/esm/cloudflare/artifacts.mjs +8 -19
  62. package/dist/esm/cloudflare/delivery-unit.mjs +124 -45
  63. package/dist/esm/cloudflare/index.mjs +15 -5
  64. package/dist/esm/cloudflare/native-renderer.mjs +48 -0
  65. package/dist/esm/cloudflare/renderer-identity.mjs +57 -0
  66. package/dist/esm/cloudflare/worker-manifest.mjs +55 -14
  67. package/dist/esm/cloudflare/worker-options.mjs +10 -0
  68. package/dist/esm/cloudflare/wrangler-config.mjs +203 -12
  69. package/dist/esm/cloudflare-builder.mjs +301 -89
  70. package/dist/esm/cloudflare-output-contract.mjs +29 -3
  71. package/dist/esm/cloudflare-output-verifier/identity.mjs +113 -24
  72. package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
  73. package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
  74. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  75. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +11 -6
  76. package/dist/esm/config.mjs +3 -1
  77. package/dist/esm/deploy-output/npmAliases.mjs +92 -6
  78. package/dist/esm/deploy-output/plugin.mjs +24 -6
  79. package/dist/esm/deploy-output/public-assets.mjs +82 -0
  80. package/dist/esm/deploy-output/target.mjs +50 -3
  81. package/dist/esm/deploy-output/trace-files.mjs +47 -0
  82. package/dist/esm/module-federation-config/exposes.mjs +18 -0
  83. package/dist/esm/module-federation-config/index.mjs +4 -0
  84. package/dist/esm/module-federation-config/inspect.mjs +65 -0
  85. package/dist/esm/module-federation-config/object-literal.mjs +43 -0
  86. package/dist/esm/module-federation-config/syntax.mjs +101 -0
  87. package/dist/esm/module-federation-config/types.mjs +0 -0
  88. package/dist/esm/native-type-checker.mjs +124 -13
  89. package/dist/esm/policy-defaults.mjs +36 -30
  90. package/dist/esm/release-envelope/canonical.mjs +2 -9
  91. package/dist/esm/release-envelope/contract.mjs +21 -0
  92. package/dist/esm/release-envelope/framework-output.mjs +297 -106
  93. package/dist/esm/release-envelope/index.mjs +149 -38
  94. package/dist/esm/release-envelope/plugin.mjs +24 -15
  95. package/dist/esm/release-envelope/renderer-output-stamp.mjs +112 -0
  96. package/dist/esm/release-envelope/types.mjs +2 -3
  97. package/dist/esm/release-identity.mjs +8 -5
  98. package/dist/esm/renderer-build-identity.mjs +82 -0
  99. package/dist/esm/templates/cloudflare-entry.001-bootstrap-security.mjs +14 -6
  100. package/dist/esm/templates/cloudflare-entry.002-assets-routes.mjs +10 -2
  101. package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +403 -96
  102. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +530 -103
  103. package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +26 -7
  104. package/dist/esm/templates/cloudflare-worker-route-data.mjs +1069 -0
  105. package/dist/esm/workspace-topology.mjs +21 -0
  106. package/dist/esm-node/backend-federation-build/codegen.mjs +29 -18
  107. package/dist/esm-node/backend-federation-build/config.mjs +74 -38
  108. package/dist/esm-node/backend-federation-build/emit.mjs +64 -23
  109. package/dist/esm-node/build-config/build-environment.mjs +26 -160
  110. package/dist/esm-node/build-config/internal-effect-discovery.mjs +103 -0
  111. package/dist/esm-node/build-config/public.mjs +1 -1
  112. package/dist/esm-node/build-config/remote-address.mjs +21 -0
  113. package/dist/esm-node/cloudflare/artifacts.mjs +8 -19
  114. package/dist/esm-node/cloudflare/delivery-unit.mjs +124 -45
  115. package/dist/esm-node/cloudflare/index.mjs +15 -5
  116. package/dist/esm-node/cloudflare/native-renderer.mjs +49 -0
  117. package/dist/esm-node/cloudflare/renderer-identity.mjs +58 -0
  118. package/dist/esm-node/cloudflare/worker-manifest.mjs +55 -14
  119. package/dist/esm-node/cloudflare/worker-options.mjs +11 -0
  120. package/dist/esm-node/cloudflare/wrangler-config.mjs +203 -12
  121. package/dist/esm-node/cloudflare-builder.mjs +301 -89
  122. package/dist/esm-node/cloudflare-output-contract.mjs +29 -3
  123. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +113 -24
  124. package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
  125. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
  126. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  127. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +11 -6
  128. package/dist/esm-node/config.mjs +3 -1
  129. package/dist/esm-node/deploy-output/npmAliases.mjs +92 -6
  130. package/dist/esm-node/deploy-output/plugin.mjs +24 -6
  131. package/dist/esm-node/deploy-output/public-assets.mjs +83 -0
  132. package/dist/esm-node/deploy-output/target.mjs +50 -3
  133. package/dist/esm-node/deploy-output/trace-files.mjs +48 -0
  134. package/dist/esm-node/module-federation-config/exposes.mjs +19 -0
  135. package/dist/esm-node/module-federation-config/index.mjs +5 -0
  136. package/dist/esm-node/module-federation-config/inspect.mjs +66 -0
  137. package/dist/esm-node/module-federation-config/object-literal.mjs +44 -0
  138. package/dist/esm-node/module-federation-config/syntax.mjs +102 -0
  139. package/dist/esm-node/module-federation-config/types.mjs +1 -0
  140. package/dist/esm-node/native-type-checker.mjs +124 -13
  141. package/dist/esm-node/policy-defaults.mjs +36 -30
  142. package/dist/esm-node/release-envelope/canonical.mjs +2 -9
  143. package/dist/esm-node/release-envelope/contract.mjs +22 -0
  144. package/dist/esm-node/release-envelope/framework-output.mjs +297 -106
  145. package/dist/esm-node/release-envelope/index.mjs +149 -38
  146. package/dist/esm-node/release-envelope/plugin.mjs +24 -15
  147. package/dist/esm-node/release-envelope/renderer-output-stamp.mjs +113 -0
  148. package/dist/esm-node/release-envelope/types.mjs +2 -3
  149. package/dist/esm-node/release-identity.mjs +8 -5
  150. package/dist/esm-node/renderer-build-identity.mjs +83 -0
  151. package/dist/esm-node/templates/cloudflare-entry.001-bootstrap-security.mjs +14 -6
  152. package/dist/esm-node/templates/cloudflare-entry.002-assets-routes.mjs +10 -2
  153. package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +403 -96
  154. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +530 -103
  155. package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +26 -7
  156. package/dist/esm-node/templates/cloudflare-worker-route-data.mjs +1069 -0
  157. package/dist/esm-node/workspace-topology.mjs +22 -0
  158. package/dist/types/backend-federation-build/codegen.d.ts +30 -0
  159. package/dist/types/backend-federation-build/config.d.ts +10 -57
  160. package/dist/types/backend-federation-build/emit.d.ts +12 -2
  161. package/dist/types/backend-federation-build/index.d.ts +1 -1
  162. package/dist/types/build-config/build-environment.d.ts +1 -9
  163. package/dist/types/build-config/internal-effect-discovery.d.ts +4 -0
  164. package/dist/types/build-config/public.d.ts +1 -1
  165. package/dist/types/build-config/remote-address.d.ts +3 -0
  166. package/dist/types/cloudflare/artifacts.d.ts +7 -6
  167. package/dist/types/cloudflare/constants.d.ts +1 -1
  168. package/dist/types/cloudflare/delivery-unit.d.ts +38 -7
  169. package/dist/types/cloudflare/native-renderer.d.ts +30 -0
  170. package/dist/types/cloudflare/renderer-identity.d.ts +25 -0
  171. package/dist/types/cloudflare/worker-manifest.d.ts +17 -2
  172. package/dist/types/cloudflare/worker-options.d.ts +20 -0
  173. package/dist/types/cloudflare/wrangler-config.d.ts +55 -2
  174. package/dist/types/cloudflare-builder.d.ts +51 -8
  175. package/dist/types/cloudflare-output-contract.d.ts +5 -3
  176. package/dist/types/cloudflare-output-verifier/identity.d.ts +7 -3
  177. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
  178. package/dist/types/config.d.ts +98 -13
  179. package/dist/types/deploy-output/plugin.d.ts +29 -9
  180. package/dist/types/deploy-output/public-assets.d.ts +51 -0
  181. package/dist/types/deploy-output/target.d.ts +28 -5
  182. package/dist/types/deploy-output/trace-files.d.ts +17 -0
  183. package/dist/types/module-federation-config/exposes.d.ts +11 -0
  184. package/dist/types/module-federation-config/index.d.ts +5 -0
  185. package/dist/types/module-federation-config/inspect.d.ts +2 -0
  186. package/dist/types/module-federation-config/object-literal.d.ts +4 -0
  187. package/dist/types/module-federation-config/syntax.d.ts +7 -0
  188. package/dist/types/module-federation-config/types.d.ts +20 -0
  189. package/dist/types/native-type-checker.d.ts +26 -3
  190. package/dist/types/policy-defaults.d.ts +12 -2
  191. package/dist/types/release-envelope/canonical.d.ts +5 -5
  192. package/dist/types/release-envelope/contract.d.mts +8 -0
  193. package/dist/types/release-envelope/framework-output.d.ts +23 -13
  194. package/dist/types/release-envelope/index.d.ts +17 -5
  195. package/dist/types/release-envelope/plugin.d.ts +10 -6
  196. package/dist/types/release-envelope/renderer-output-stamp.d.ts +34 -0
  197. package/dist/types/release-envelope/types.d.ts +42 -4
  198. package/dist/types/release-identity.d.ts +3 -2
  199. package/dist/types/renderer-build-identity.d.ts +30 -0
  200. package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
  201. package/dist/types/workspace-topology.d.ts +91 -0
  202. package/package.json +87 -27
  203. package/src/backend-federation-build/codegen.ts +28 -19
  204. package/src/backend-federation-build/config.ts +189 -123
  205. package/src/backend-federation-build/emit.ts +119 -38
  206. package/src/backend-federation-build/index.ts +4 -1
  207. package/src/build-config/build-environment.ts +59 -285
  208. package/src/build-config/internal-effect-discovery.ts +239 -0
  209. package/src/build-config/public.ts +0 -1
  210. package/src/build-config/remote-address.ts +33 -0
  211. package/src/cloudflare/artifacts.ts +22 -43
  212. package/src/cloudflare/delivery-unit.ts +283 -60
  213. package/src/cloudflare/index.ts +35 -19
  214. package/src/cloudflare/native-renderer.ts +117 -0
  215. package/src/cloudflare/renderer-identity.ts +171 -0
  216. package/src/cloudflare/worker-manifest.ts +108 -10
  217. package/src/cloudflare/worker-options.ts +60 -0
  218. package/src/cloudflare/wrangler-config.ts +390 -14
  219. package/src/cloudflare-builder.ts +543 -170
  220. package/src/cloudflare-output-contract.ts +45 -2
  221. package/src/cloudflare-output-verifier/identity.ts +138 -7
  222. package/src/cloudflare-output-verifier/index.ts +20 -0
  223. package/src/cloudflare-output-verifier/manifest.ts +77 -0
  224. package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
  225. package/src/cloudflare-output-verifier/worker-bundles.ts +27 -11
  226. package/src/config.ts +101 -19
  227. package/src/deploy-output/npmAliases.ts +141 -4
  228. package/src/deploy-output/plugin.ts +55 -18
  229. package/src/deploy-output/public-assets.ts +224 -0
  230. package/src/deploy-output/target.ts +97 -14
  231. package/src/deploy-output/trace-files.ts +80 -0
  232. package/src/module-federation-config/exposes.ts +46 -0
  233. package/src/module-federation-config/index.ts +22 -0
  234. package/src/module-federation-config/inspect.ts +157 -0
  235. package/src/module-federation-config/object-literal.ts +76 -0
  236. package/src/module-federation-config/syntax.ts +212 -0
  237. package/src/module-federation-config/types.ts +22 -0
  238. package/src/native-type-checker.ts +233 -20
  239. package/src/policy-defaults.ts +47 -17
  240. package/src/release-envelope/canonical.ts +6 -18
  241. package/src/release-envelope/contract.mjs +28 -0
  242. package/src/release-envelope/framework-output.ts +681 -156
  243. package/src/release-envelope/index.ts +315 -60
  244. package/src/release-envelope/plugin.ts +54 -38
  245. package/src/release-envelope/renderer-output-stamp.ts +210 -0
  246. package/src/release-envelope/types.ts +75 -6
  247. package/src/release-identity.ts +19 -2
  248. package/src/renderer-build-identity.ts +168 -0
  249. package/src/templates/cloudflare-entry.001-bootstrap-security.mjs +14 -6
  250. package/src/templates/cloudflare-entry.002-assets-routes.mjs +10 -2
  251. package/src/templates/cloudflare-entry.004-rendering-css.mjs +403 -96
  252. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +530 -103
  253. package/src/templates/cloudflare-entry.006-fetch-handler.mjs +26 -7
  254. package/src/templates/cloudflare-worker-route-data.mjs +1069 -0
  255. package/src/workspace-topology.ts +125 -0
  256. package/dist/cjs/templates/cloudflare-worker-fs-promises.mjs +0 -11
  257. package/dist/cjs/templates/cloudflare-worker-loadable-server.mjs +0 -281
  258. package/dist/cjs/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  259. package/dist/cjs/templates/cloudflare-worker-path.mjs +0 -98
  260. package/dist/esm/templates/cloudflare-worker-fs-promises.mjs +0 -11
  261. package/dist/esm/templates/cloudflare-worker-loadable-server.mjs +0 -281
  262. package/dist/esm/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  263. package/dist/esm/templates/cloudflare-worker-path.mjs +0 -98
  264. package/dist/esm-node/templates/cloudflare-worker-fs-promises.mjs +0 -11
  265. package/dist/esm-node/templates/cloudflare-worker-loadable-server.mjs +0 -281
  266. package/dist/esm-node/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  267. package/dist/esm-node/templates/cloudflare-worker-path.mjs +0 -98
  268. package/dist/types/templates/cloudflare-worker-fs-promises.d.mts +0 -5
  269. package/dist/types/templates/cloudflare-worker-loadable-server.d.mts +0 -48
  270. package/dist/types/templates/cloudflare-worker-mf-ssr-runtime-plugin.d.mts +0 -13
  271. package/dist/types/templates/cloudflare-worker-path.d.mts +0 -21
  272. package/src/templates/cloudflare-worker-fs-promises.mjs +0 -11
  273. package/src/templates/cloudflare-worker-loadable-server.mjs +0 -281
  274. package/src/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  275. package/src/templates/cloudflare-worker-path.mjs +0 -98
@@ -2,8 +2,9 @@ import "node:module";
2
2
  import { createHash } from "node:crypto";
3
3
  import promises from "node:fs/promises";
4
4
  import node_path from "node:path";
5
+ import { formatBackendFederationValidationErrors, immutableRendererRouterBindings, validateRendererIdentity, validateRendererProfile, validateRendererProfileCompatibility, validateRendererRouterBindings } from "@modern-js/backend-federation-contracts";
5
6
  import { canonicalSerializeMicroVerticalReleaseEnvelope, digestMicroVerticalReleaseEnvelopePayload, releaseEnvelopePayload } from "./canonical.mjs";
6
- import { MICROVERTICAL_RELEASE_ENVELOPE_KIND, MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION, MICROVERTICAL_RELEASE_TARGETS } from "./types.mjs";
7
+ import { MICROVERTICAL_RELEASE_ENVELOPE_KIND, MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION, MICROVERTICAL_RELEASE_TARGETS, SHELL_RELEASE_ENVELOPE_KIND } from "./types.mjs";
7
8
  const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
8
9
  const SOURCE_REVISION_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u;
9
10
  const TARGETS = new Set(MICROVERTICAL_RELEASE_TARGETS);
@@ -27,6 +28,10 @@ const assertTarget = (value, location = 'target')=>{
27
28
  if (!TARGETS.has(target)) throw new Error(`${location} must be one of: ${MICROVERTICAL_RELEASE_TARGETS.join(', ')}.`);
28
29
  return target;
29
30
  };
31
+ const assertEnvelopeKind = (value)=>{
32
+ if (value !== MICROVERTICAL_RELEASE_ENVELOPE_KIND && value !== SHELL_RELEASE_ENVELOPE_KIND) throw new Error(`envelope.kind must be "${MICROVERTICAL_RELEASE_ENVELOPE_KIND}" or "${SHELL_RELEASE_ENVELOPE_KIND}".`);
33
+ return value;
34
+ };
30
35
  const assertReleaseIdentity = (value, location = 'identity')=>{
31
36
  const identity = assertRecord(value, location);
32
37
  assertExactKeys(identity, [
@@ -185,7 +190,7 @@ const assertUniqueSortedArtifacts = (artifacts, location)=>{
185
190
  if (paths.some((logicalPath, index)=>logicalPath !== sorted[index])) throw new Error(`${location} must be sorted by logicalPath.`);
186
191
  };
187
192
  const assertSurfacePaths = (value, location)=>{
188
- if (!Array.isArray(value) || 0 === value.length) throw new Error(`${location} must contain at least one artifact path.`);
193
+ if (!Array.isArray(value)) throw new Error(`${location} must be an array of artifact paths.`);
189
194
  const paths = value.map((item, index)=>assertNormalizedLogicalPath(item, `${location}[${index}]`));
190
195
  const sorted = [
191
196
  ...paths
@@ -194,26 +199,51 @@ const assertSurfacePaths = (value, location)=>{
194
199
  if (new Set(paths).size !== paths.length) throw new Error(`${location} must not contain duplicate artifact paths.`);
195
200
  return paths;
196
201
  };
197
- const assertSurfaces = (value)=>{
202
+ const assertSurfaces = (value, kind)=>{
198
203
  const surfaces = assertRecord(value, 'surfaces');
199
204
  assertExactKeys(surfaces, [
200
205
  'uiClient',
201
206
  'ssr',
202
207
  'apiBackend',
203
- 'backendFederation'
208
+ ...kind !== SHELL_RELEASE_ENVELOPE_KIND && Object.hasOwn(surfaces, 'backendFederation') ? [
209
+ 'backendFederation'
210
+ ] : []
204
211
  ], 'surfaces');
205
- const backendFederation = assertRecord(surfaces.backendFederation, 'surfaces.backendFederation');
206
- assertExactKeys(backendFederation, [
212
+ const backendFederation = Object.hasOwn(surfaces, 'backendFederation') ? assertRecord(surfaces.backendFederation, 'surfaces.backendFederation') : void 0;
213
+ if (backendFederation) assertExactKeys(backendFederation, [
207
214
  'manifest',
208
215
  'container'
209
216
  ], 'surfaces.backendFederation');
217
+ const uiClient = assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient');
218
+ const ssr = assertSurfacePaths(surfaces.ssr, 'surfaces.ssr');
219
+ const apiBackend = assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend');
220
+ if (uiClient.length > 0 && 0 === ssr.length) throw new Error('surfaces.ssr must contain at least one artifact path when UI/client is declared.');
221
+ if (ssr.length > 0 && 0 === uiClient.length) throw new Error('surfaces.uiClient must contain at least one artifact path when SSR is declared.');
222
+ if (0 === apiBackend.length && (backendFederation || 0 === uiClient.length)) throw new Error('surfaces.apiBackend must contain at least one artifact path.');
223
+ if (kind === SHELL_RELEASE_ENVELOPE_KIND) {
224
+ if (0 === uiClient.length || 0 === ssr.length) throw new Error('Shell surfaces.uiClient and surfaces.ssr must each contain at least one artifact path.');
225
+ return {
226
+ kind,
227
+ surfaces: {
228
+ uiClient,
229
+ ssr,
230
+ apiBackend
231
+ }
232
+ };
233
+ }
234
+ if (apiBackend.length > 0 && !backendFederation) throw new Error('surfaces.backendFederation is required when API/backend is declared.');
210
235
  return {
211
- uiClient: assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient'),
212
- ssr: assertSurfacePaths(surfaces.ssr, 'surfaces.ssr'),
213
- apiBackend: assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend'),
214
- backendFederation: {
215
- manifest: assertNormalizedLogicalPath(backendFederation.manifest, 'surfaces.backendFederation.manifest'),
216
- container: assertNormalizedLogicalPath(backendFederation.container, 'surfaces.backendFederation.container')
236
+ kind,
237
+ surfaces: {
238
+ uiClient,
239
+ ssr,
240
+ apiBackend,
241
+ ...backendFederation ? {
242
+ backendFederation: {
243
+ manifest: assertNormalizedLogicalPath(backendFederation.manifest, 'surfaces.backendFederation.manifest'),
244
+ container: assertNormalizedLogicalPath(backendFederation.container, 'surfaces.backendFederation.container')
245
+ }
246
+ } : {}
217
247
  }
218
248
  };
219
249
  };
@@ -223,12 +253,14 @@ const assertSurfaceReferences = (artifacts, surfaces)=>{
223
253
  uiClient: surfaces.uiClient,
224
254
  ssr: surfaces.ssr,
225
255
  apiBackend: surfaces.apiBackend,
226
- 'backendFederation.manifest': [
227
- surfaces.backendFederation.manifest
228
- ],
229
- 'backendFederation.container': [
230
- surfaces.backendFederation.container
231
- ]
256
+ ...'backendFederation' in surfaces && surfaces.backendFederation ? {
257
+ 'backendFederation.manifest': [
258
+ surfaces.backendFederation.manifest
259
+ ],
260
+ 'backendFederation.container': [
261
+ surfaces.backendFederation.container
262
+ ]
263
+ } : {}
232
264
  }))for (const logicalPath of paths)if (!artifactPaths.has(logicalPath)) throw new Error(`surfaces.${surface} references unbound artifact "${logicalPath}".`);
233
265
  };
234
266
  const assertTargetSurfaceContract = (target, artifacts, surfaces)=>{
@@ -246,12 +278,14 @@ const assertTargetSurfaceContract = (target, artifacts, surfaces)=>{
246
278
  assertRuntime(surfaces.uiClient, 'browser', 'UI/client');
247
279
  assertRuntime(surfaces.ssr, 'node' === target ? 'nodejs' : 'workerd', 'SSR');
248
280
  assertRuntime(surfaces.apiBackend, 'node' === target ? 'nodejs' : 'workerd-effect', 'API/backend');
249
- assertRuntime([
250
- surfaces.backendFederation.manifest
251
- ], 'module-federation-manifest', 'backend federation manifest');
252
- assertRuntime([
253
- surfaces.backendFederation.container
254
- ], 'node' === target ? 'nodejs' : 'commonjs-module', 'backend federation container');
281
+ if ('backendFederation' in surfaces && surfaces.backendFederation) {
282
+ assertRuntime([
283
+ surfaces.backendFederation.manifest
284
+ ], 'module-federation-manifest', 'backend federation manifest');
285
+ assertRuntime([
286
+ surfaces.backendFederation.container
287
+ ], 'node' === target ? 'nodejs' : 'commonjs-module', 'backend federation container');
288
+ }
255
289
  };
256
290
  const deepFreeze = (value)=>{
257
291
  if (null !== value && 'object' == typeof value && !Object.isFrozen(value)) {
@@ -260,6 +294,51 @@ const deepFreeze = (value)=>{
260
294
  }
261
295
  return value;
262
296
  };
297
+ const assertReleaseUi = (value, identity, surfaces)=>{
298
+ if (0 === surfaces.uiClient.length) {
299
+ if (void 0 !== value) throw new Error('envelope.ui is forbidden for an API-only release.');
300
+ return;
301
+ }
302
+ const ui = assertRecord(value, 'envelope.ui');
303
+ assertExactKeys(ui, [
304
+ 'rendererIdentity',
305
+ 'rendererProfile',
306
+ 'routerBindings'
307
+ ], 'envelope.ui');
308
+ const routerBindings = assertRecord(ui.routerBindings, 'envelope.ui.routerBindings');
309
+ const errors = [
310
+ ...validateRendererIdentity(ui.rendererIdentity, 'envelope.ui.rendererIdentity').errors,
311
+ ...validateRendererProfile(ui.rendererProfile, 'envelope.ui.rendererProfile').errors,
312
+ ...validateRendererRouterBindings(routerBindings, Object.keys(routerBindings), 'envelope.ui.routerBindings').errors
313
+ ];
314
+ if (errors.length > 0) throw new Error(formatBackendFederationValidationErrors(errors));
315
+ const rendererIdentity = ui.rendererIdentity;
316
+ const rendererProfile = ui.rendererProfile;
317
+ if (!Object.hasOwn(routerBindings, rendererIdentity.entryName)) throw new Error('envelope.ui.routerBindings must include the primary rendererIdentity.entryName.');
318
+ if (rendererIdentity.buildId !== identity.buildMarker) throw new Error('envelope.ui.rendererIdentity.buildId must match identity.buildMarker.');
319
+ for (const field of [
320
+ 'renderer',
321
+ 'protocolVersion'
322
+ ])if (rendererIdentity[field] !== rendererProfile[field]) throw new Error(`envelope.ui.rendererProfile.${field} must match rendererIdentity.${field}.`);
323
+ return {
324
+ rendererIdentity: {
325
+ ...rendererIdentity
326
+ },
327
+ rendererProfile: {
328
+ ...rendererProfile,
329
+ compiler: {
330
+ ...rendererProfile.compiler
331
+ },
332
+ hydration: {
333
+ ...rendererProfile.hydration
334
+ },
335
+ router: {
336
+ ...rendererProfile.router
337
+ }
338
+ },
339
+ routerBindings: immutableRendererRouterBindings(routerBindings)
340
+ };
341
+ };
263
342
  const assertEnvelope = (value)=>{
264
343
  const envelope = assertRecord(value, 'envelope');
265
344
  assertExactKeys(envelope, [
@@ -267,26 +346,35 @@ const assertEnvelope = (value)=>{
267
346
  'kind',
268
347
  'target',
269
348
  'identity',
349
+ ...Object.hasOwn(envelope, 'ui') ? [
350
+ 'ui'
351
+ ] : [],
270
352
  'artifacts',
271
353
  'surfaces',
272
354
  'envelopeDigest'
273
355
  ], 'envelope');
274
356
  if (envelope.schemaVersion !== MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION) throw new Error(`envelope.schemaVersion must be ${MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION}.`);
275
- if (envelope.kind !== MICROVERTICAL_RELEASE_ENVELOPE_KIND) throw new Error(`envelope.kind must be "${MICROVERTICAL_RELEASE_ENVELOPE_KIND}".`);
357
+ const kind = assertEnvelopeKind(envelope.kind);
276
358
  if (!Array.isArray(envelope.artifacts) || 0 === envelope.artifacts.length) throw new Error('envelope.artifacts must contain at least one artifact.');
277
359
  const artifacts = envelope.artifacts.map((artifact, index)=>assertReleaseArtifact(artifact, `envelope.artifacts[${index}]`));
278
360
  assertUniqueSortedArtifacts(artifacts, 'envelope.artifacts');
279
361
  const target = assertTarget(envelope.target, 'envelope.target');
280
- const surfaces = assertSurfaces(envelope.surfaces);
362
+ const releaseSurfaces = assertSurfaces(envelope.surfaces, kind);
363
+ const { surfaces } = releaseSurfaces;
364
+ if (0 === surfaces.uiClient.length && Object.hasOwn(envelope, 'ui')) throw new Error('envelope.ui is forbidden for an API-only release.');
281
365
  assertSurfaceReferences(artifacts, surfaces);
282
366
  assertTargetSurfaceContract(target, artifacts, surfaces);
367
+ const identity = assertReleaseIdentity(envelope.identity, 'envelope.identity');
368
+ const ui = assertReleaseUi(envelope.ui, identity, surfaces);
283
369
  const parsed = {
370
+ ...releaseSurfaces,
284
371
  schemaVersion: MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION,
285
- kind: MICROVERTICAL_RELEASE_ENVELOPE_KIND,
286
372
  target,
287
- identity: assertReleaseIdentity(envelope.identity, 'envelope.identity'),
373
+ identity,
374
+ ...ui ? {
375
+ ui
376
+ } : {},
288
377
  artifacts,
289
- surfaces,
290
378
  envelopeDigest: assertNonEmptyString(envelope.envelopeDigest, 'envelope.envelopeDigest')
291
379
  };
292
380
  if (!SHA256_PATTERN.test(parsed.envelopeDigest)) throw new Error('envelope.envelopeDigest must be a lowercase SHA-256 digest.');
@@ -294,36 +382,59 @@ const assertEnvelope = (value)=>{
294
382
  if (parsed.envelopeDigest !== expectedDigest) throw new Error('envelope.envelopeDigest does not match canonical payload.');
295
383
  return parsed;
296
384
  };
297
- const createMicroVerticalReleaseEnvelope = async (input)=>{
385
+ async function createMicroVerticalReleaseEnvelope(input) {
298
386
  const artifactRoot = await promises.realpath(node_path.resolve(input.artifactRoot));
299
387
  const target = assertTarget(input.target);
300
388
  const identity = assertReleaseIdentity(input.identity);
389
+ const kind = assertEnvelopeKind(void 0 === input.kind ? MICROVERTICAL_RELEASE_ENVELOPE_KIND : input.kind);
301
390
  if (!Array.isArray(input.artifacts) || 0 === input.artifacts.length) throw new Error('artifacts must contain at least one artifact.');
302
391
  const inputs = input.artifacts.map((artifact, index)=>assertArtifactInput(artifact, `artifacts[${index}]`));
303
392
  const sortedInputs = [
304
393
  ...inputs
305
394
  ].sort((left, right)=>left.logicalPath.localeCompare(right.logicalPath));
306
395
  assertUniqueSortedArtifacts(sortedInputs, 'artifacts');
307
- const surfaces = assertSurfaces(input.surfaces);
396
+ const releaseSurfaces = assertSurfaces(input.surfaces, kind);
397
+ const { surfaces } = releaseSurfaces;
398
+ if (0 === surfaces.uiClient.length && Object.hasOwn(input, 'ui')) throw new Error('envelope.ui is forbidden for an API-only release.');
308
399
  assertSurfaceReferences(sortedInputs, surfaces);
309
400
  assertTargetSurfaceContract(target, sortedInputs, surfaces);
401
+ const ui = assertReleaseUi(input.ui, identity, surfaces);
310
402
  const artifacts = await Promise.all(sortedInputs.map((artifact)=>readFinalArtifact(artifactRoot, artifact)));
311
403
  assertTargetSurfaceContract(target, artifacts, surfaces);
312
404
  const payload = {
405
+ ...releaseSurfaces,
313
406
  schemaVersion: MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION,
314
- kind: MICROVERTICAL_RELEASE_ENVELOPE_KIND,
315
407
  target,
316
408
  identity,
317
- artifacts,
318
- surfaces
409
+ ...ui ? {
410
+ ui
411
+ } : {},
412
+ artifacts
319
413
  };
320
414
  return deepFreeze({
321
415
  ...payload,
322
416
  envelopeDigest: digestMicroVerticalReleaseEnvelopePayload(payload)
323
417
  });
324
- };
325
- const verifyMicroVerticalReleaseEnvelope = async (value, options)=>{
418
+ }
419
+ async function verifyMicroVerticalReleaseEnvelope(value, options) {
326
420
  const envelope = assertEnvelope(value);
421
+ if (void 0 !== options.expectedRendererProfile) {
422
+ const result = validateRendererProfileCompatibility(options.expectedRendererProfile, envelope.ui?.rendererProfile, 'envelope.ui.rendererProfile');
423
+ if (!result.ok) throw new Error(formatBackendFederationValidationErrors(result.errors));
424
+ }
425
+ if (void 0 !== options.expectedRendererIdentity) {
426
+ const expected = options.expectedRendererIdentity;
427
+ const result = validateRendererIdentity(expected, 'expectedRendererIdentity');
428
+ if (!result.ok) throw new Error(formatBackendFederationValidationErrors(result.errors));
429
+ for (const field of [
430
+ 'renderer',
431
+ 'protocolVersion',
432
+ 'appId',
433
+ 'entryName',
434
+ 'buildId'
435
+ ])if (envelope.ui?.rendererIdentity[field] !== expected[field]) throw new Error(`envelope.ui.rendererIdentity.${field} must match the consuming renderer identity.`);
436
+ }
437
+ if (void 0 !== options.expectedKind && envelope.kind !== options.expectedKind) throw new Error(`envelope.kind must be "${options.expectedKind}" for this release owner; received "${envelope.kind}".`);
327
438
  if (void 0 !== options.expectedTarget && envelope.target !== options.expectedTarget) throw new Error(`envelope.target must be "${options.expectedTarget}" for this staging target; received "${envelope.target}".`);
328
439
  const artifactRoot = await promises.realpath(node_path.resolve(options.artifactRoot));
329
440
  for (const artifact of envelope.artifacts){
@@ -332,5 +443,5 @@ const verifyMicroVerticalReleaseEnvelope = async (value, options)=>{
332
443
  if (!matches) throw new Error('file' === artifact.kind && 'file' === finalArtifact.kind ? `Artifact "${artifact.logicalPath}" digest does not match final artifact bytes.` : `Artifact "${artifact.logicalPath}" does not match its final filesystem binding.`);
333
444
  }
334
445
  return deepFreeze(envelope);
335
- };
336
- export { MICROVERTICAL_RELEASE_ENVELOPE_KIND, MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION, MICROVERTICAL_RELEASE_TARGETS, canonicalSerializeMicroVerticalReleaseEnvelope, createMicroVerticalReleaseEnvelope, verifyMicroVerticalReleaseEnvelope };
446
+ }
447
+ export { MICROVERTICAL_RELEASE_ENVELOPE_KIND, MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION, MICROVERTICAL_RELEASE_TARGETS, SHELL_RELEASE_ENVELOPE_KIND, canonicalSerializeMicroVerticalReleaseEnvelope, createMicroVerticalReleaseEnvelope, verifyMicroVerticalReleaseEnvelope };
@@ -1,49 +1,58 @@
1
1
  import "node:module";
2
2
  import node_path from "node:path";
3
+ import { NODE_PUBLIC_ASSET_SCOPE, normalizeDeclaredPublicAssets, resolveAddedDeclaredPublicAssetPaths } from "../deploy-output/public-assets.mjs";
4
+ import { getDeployingTarget } from "../deploy-output/target.mjs";
3
5
  import { emitFrameworkMicroVerticalReleaseEnvelope, emitNodeStagedReleaseEnvelope, verifyBuildOutputReleaseEnvelope, verifyNodeReleaseEnvelopeStaging } from "./framework-output.mjs";
4
- const resolveActiveDeployTarget = (api, resolveDeployTarget)=>{
5
- const { metaName } = api.getAppContext();
6
- const config = api.getNormalizedConfig();
7
- if ('modern-js' !== metaName && !config.deploy?.target && !process.env.MODERNJS_DEPLOY) return;
8
- return resolveDeployTarget(config);
9
- };
10
- const createUltramodernReleaseEnvelopePlugin = ({ resolveDeployTarget })=>({
6
+ const createUltramodernReleaseEnvelopePlugin = ({ resolveRendererProfile } = {})=>({
11
7
  name: '@modern-js/ultramodern-release-envelope',
12
8
  pre: [
13
9
  '@modern-js/backend-federation-build',
14
10
  '@modern-js/plugin-bff',
15
- '@modern-js/deploy-output-aliases'
11
+ '@modern-js/deploy-output-aliases',
12
+ '@modern-js/deploy-output-public-assets'
16
13
  ],
17
14
  post: [
18
15
  '@modern-js/plugin-deploy'
19
16
  ],
20
17
  setup (api) {
21
- const emitBuildEnvelope = async (target)=>{
22
- const { apiOnly, distDirectory } = api.getAppContext();
18
+ const emitBuildEnvelope = async (target, requirePromotable = true)=>{
19
+ const { apiOnly, appDirectory, distDirectory } = api.getAppContext();
23
20
  await emitFrameworkMicroVerticalReleaseEnvelope({
24
21
  apiOnly,
22
+ appDirectory,
25
23
  distDirectory,
26
- target
24
+ role: api.getNormalizedConfig().deploy?.releaseEnvelopeRole,
25
+ requirePromotable,
26
+ target,
27
+ ...!apiOnly && resolveRendererProfile ? {
28
+ expectedRendererProfile: resolveRendererProfile(api.getNormalizedConfig())
29
+ } : {}
27
30
  });
28
31
  };
29
32
  api.onAfterBuild(async ()=>{
30
- const configuredTarget = resolveDeployTarget(api.getNormalizedConfig());
33
+ const configuredTarget = api.getAppContext().deployTarget?.target;
31
34
  if ('node' !== configuredTarget && 'cloudflare' !== configuredTarget) return;
32
- await emitBuildEnvelope(configuredTarget);
35
+ await emitBuildEnvelope(configuredTarget, false);
33
36
  });
34
37
  api.onBeforeDeploy(async ()=>{
35
- const configuredTarget = resolveActiveDeployTarget(api, resolveDeployTarget);
38
+ const configuredTarget = getDeployingTarget(api.getAppContext());
36
39
  if ('node' !== configuredTarget) return;
37
40
  await emitBuildEnvelope(configuredTarget);
38
41
  const { distDirectory } = api.getAppContext();
39
42
  await verifyBuildOutputReleaseEnvelope(distDirectory, configuredTarget);
40
43
  });
41
44
  api.onAfterDeploy(async ()=>{
42
- const configuredTarget = resolveActiveDeployTarget(api, resolveDeployTarget);
45
+ const configuredTarget = getDeployingTarget(api.getAppContext());
43
46
  if ('node' !== configuredTarget) return;
44
47
  const { appDirectory, distDirectory } = api.getAppContext();
45
48
  const outputDirectory = node_path.join(appDirectory, '.output');
46
49
  const releaseEnvelope = await emitNodeStagedReleaseEnvelope({
50
+ declaredPublicAssets: await resolveAddedDeclaredPublicAssetPaths({
51
+ appDirectory,
52
+ assets: normalizeDeclaredPublicAssets(api.getNormalizedConfig().deploy?.node?.publicAssets, NODE_PUBLIC_ASSET_SCOPE),
53
+ generatedRoot: distDirectory,
54
+ scope: NODE_PUBLIC_ASSET_SCOPE
55
+ }),
47
56
  distDirectory,
48
57
  outputDirectory
49
58
  });
@@ -0,0 +1,113 @@
1
+ import "node:module";
2
+ import { randomUUID } from "node:crypto";
3
+ import promises from "node:fs/promises";
4
+ import node_path from "node:path";
5
+ import { isDeepStrictEqual } from "node:util";
6
+ import { ULTRAMODERN_BUILD_ARTIFACT_FILE, ULTRAMODERN_BUILD_ARTIFACT_PATH, assertUltramodernBuildArtifact, formatBackendFederationValidationErrors, stampUltramodernBuildArtifactIdentity, validateRendererIdentity, validateRendererProfile, validateRendererRouterBindings } from "@modern-js/backend-federation-contracts";
7
+ const validateFinalizedRendererBuild = (output, context, appId)=>{
8
+ const entryNames = context.entrypoints.map((entry)=>entry.entryName);
9
+ const primary = context.entrypoints.find((entry)=>entry.isMainEntry) ?? context.entrypoints[0];
10
+ const errors = [
11
+ ...validateRendererIdentity(output.ui.rendererIdentity).errors,
12
+ ...validateRendererProfile(output.ui.rendererProfile).errors,
13
+ ...validateRendererRouterBindings(output.ui.routerBindings, entryNames, 'routerBindings').errors
14
+ ];
15
+ if (errors.length) throw new Error(formatBackendFederationValidationErrors(errors));
16
+ if (!primary || new Set(entryNames).size !== entryNames.length || context.entrypoints.filter((entry)=>entry.isMainEntry).length > 1 || output.ui.rendererIdentity.entryName !== primary.entryName || output.ui.rendererIdentity.appId !== appId || output.ui.rendererIdentity.buildId !== output.buildMarker || output.ui.rendererIdentity.renderer !== output.ui.rendererProfile.renderer) throw new Error('Finalized renderer output conflicts with the actual application entries and release identity.');
17
+ };
18
+ const stampFinalizedRendererBuildArtifact = (artifact, output, context)=>{
19
+ assertUltramodernBuildArtifact(artifact);
20
+ const ui = artifact.surfaces.ui;
21
+ if (!ui) throw new Error('Finalized renderer output requires a declared UI artifact');
22
+ validateFinalizedRendererBuild(output, context, artifact.deliveryUnit.appId);
23
+ const drifted = [
24
+ !isDeepStrictEqual({
25
+ ...ui.rendererIdentity,
26
+ buildId: output.ui.rendererIdentity.buildId
27
+ }, output.ui.rendererIdentity) && `rendererIdentity (captured entry ${ui.rendererIdentity.entryName}, built entry ${output.ui.rendererIdentity.entryName})`,
28
+ !isDeepStrictEqual(ui.rendererProfile, output.ui.rendererProfile) && 'rendererProfile',
29
+ !isDeepStrictEqual(ui.routerBindings, output.ui.routerBindings) && `routerBindings (captured entries ${Object.keys(ui.routerBindings).join(', ')}, built entries ${Object.keys(output.ui.routerBindings).join(', ')})`
30
+ ].filter(Boolean);
31
+ if (drifted.length) throw new Error(`Finalized renderer output conflicts with the captured application profile or router bindings: ${drifted.join('; ')} differ from ${ULTRAMODERN_BUILD_ARTIFACT_PATH}. After changing modern.config entries, renderer or router, recapture it with \`ultramodern-create ultramodern sync-delivery-unit\`.`);
32
+ const identity = {
33
+ buildMarker: output.buildMarker,
34
+ sourceRevision: output.sourceRevision
35
+ };
36
+ const stamped = stampUltramodernBuildArtifactIdentity(artifact, identity);
37
+ return stampUltramodernBuildArtifactIdentity({
38
+ ...stamped,
39
+ surfaces: {
40
+ ...stamped.surfaces,
41
+ ui: {
42
+ ...stamped.surfaces.ui,
43
+ ...output.ui
44
+ }
45
+ }
46
+ }, identity);
47
+ };
48
+ async function writeOwnedArtifact(file, bytes) {
49
+ await promises.mkdir(node_path.dirname(file), {
50
+ recursive: true
51
+ });
52
+ const temporary = `${file}.${randomUUID()}.tmp`;
53
+ let created = false;
54
+ try {
55
+ const handle = await promises.open(temporary, 'wx');
56
+ created = true;
57
+ try {
58
+ await handle.writeFile(bytes);
59
+ } finally{
60
+ await handle.close();
61
+ }
62
+ await promises.rename(temporary, file);
63
+ } finally{
64
+ if (created) await promises.rm(temporary, {
65
+ force: true
66
+ });
67
+ }
68
+ }
69
+ async function emitRendererBuildArtifact(context, options) {
70
+ const source = node_path.join(context.appDirectory, ULTRAMODERN_BUILD_ARTIFACT_PATH);
71
+ const bytes = await promises.readFile(source).catch((error)=>{
72
+ if ('ENOENT' === error.code) return;
73
+ throw error;
74
+ });
75
+ if (!bytes) {
76
+ const declaration = await promises.stat(source.replace(/\.json$/u, '.ts')).catch((error)=>{
77
+ if ('ENOENT' === error.code) return;
78
+ throw error;
79
+ });
80
+ if (declaration) throw new Error('Generated UI artifact declaration is missing its build carrier');
81
+ return;
82
+ }
83
+ const artifact = JSON.parse(bytes.toString('utf8'));
84
+ assertUltramodernBuildArtifact(artifact);
85
+ const finalized = await options.resolveRendererBuild(context);
86
+ const stamped = stampFinalizedRendererBuildArtifact(artifact, finalized, context);
87
+ const serialized = `${JSON.stringify(stamped, null, 2)}\n`;
88
+ for (const directory of [
89
+ context.distDirectory,
90
+ node_path.join(context.distDirectory, 'public')
91
+ ])await writeOwnedArtifact(node_path.join(directory, ULTRAMODERN_BUILD_ARTIFACT_FILE), serialized);
92
+ return stamped;
93
+ }
94
+ function rendererBuildArtifactStampPlugin(options) {
95
+ if ('string' != typeof options.rendererBuildPlugin || !options.rendererBuildPlugin || options.rendererBuildPlugin.trim() !== options.rendererBuildPlugin || 'function' != typeof options.resolveRendererBuild) throw new Error('Renderer artifact stamping requires its owning metadata producer');
96
+ return {
97
+ name: '@modern-js/renderer-build-artifact-stamp',
98
+ pre: [
99
+ options.rendererBuildPlugin
100
+ ],
101
+ required: [
102
+ options.rendererBuildPlugin
103
+ ],
104
+ setup (api) {
105
+ api.onAfterBuild(async ()=>{
106
+ const context = api.getAppContext();
107
+ if (context.apiOnly) return;
108
+ await emitRendererBuildArtifact(context, options);
109
+ });
110
+ }
111
+ };
112
+ }
113
+ export { emitRendererBuildArtifact, rendererBuildArtifactStampPlugin, stampFinalizedRendererBuildArtifact, validateFinalizedRendererBuild };
@@ -1,8 +1,7 @@
1
1
  import "node:module";
2
- const MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION = 3;
3
- const MICROVERTICAL_RELEASE_ENVELOPE_KIND = 'ultramodern-target-microvertical-release-envelope';
4
2
  const MICROVERTICAL_RELEASE_TARGETS = [
5
3
  'node',
6
4
  'cloudflare'
7
5
  ];
8
- export { MICROVERTICAL_RELEASE_ENVELOPE_KIND, MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION, MICROVERTICAL_RELEASE_TARGETS };
6
+ export { MICROVERTICAL_RELEASE_ENVELOPE_KIND, MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION, SHELL_RELEASE_ENVELOPE_KIND } from "./contract.mjs";
7
+ export { MICROVERTICAL_RELEASE_TARGETS };
@@ -1,13 +1,16 @@
1
1
  import "node:module";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
+ const invokeGit = execFileSync;
4
5
  const BUILD_MARKER_NAMESPACE = 'ultramodern-delivery-unit-release-build-marker:v1';
5
6
  const PROMOTABLE_SOURCE_REVISION_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u;
7
+ const workspaceGitEnv = ()=>Object.fromEntries(Object.entries(process.env).filter(([name])=>!name.toUpperCase().startsWith('GIT_')));
6
8
  const gitOutput = (workspaceRoot, args)=>{
7
9
  try {
8
- return execFileSync('git', args, {
10
+ return invokeGit('git', args, {
9
11
  cwd: workspaceRoot,
10
12
  encoding: 'utf8',
13
+ env: workspaceGitEnv(),
11
14
  stdio: [
12
15
  'ignore',
13
16
  'pipe',
@@ -19,9 +22,9 @@ const gitOutput = (workspaceRoot, args)=>{
19
22
  }
20
23
  };
21
24
  const createUltramodernReleaseBuildMarker = ({ generationBuildMarker, sourceRevision, unitId })=>createHash('sha256').update(`${BUILD_MARKER_NAMESPACE}:${unitId}:${generationBuildMarker}:${sourceRevision}`).digest('hex').slice(0, 16);
22
- const resolveUltramodernSourceRevision = (workspaceRoot = process.cwd(), configuredSourceRevision)=>{
25
+ const resolveUltramodernSourceRevision = (workspaceRoot = process.cwd(), configuredSourceRevision, environment = process.env)=>{
23
26
  const configuredArgumentValue = configuredSourceRevision?.trim();
24
- const configuredEnvironmentValue = process.env.ULTRAMODERN_SOURCE_REVISION?.trim();
27
+ const configuredEnvironmentValue = environment.ULTRAMODERN_SOURCE_REVISION?.trim();
25
28
  const configuredArgument = configuredArgumentValue && 'workspace' !== configuredArgumentValue ? configuredArgumentValue : void 0;
26
29
  const configuredEnvironment = configuredEnvironmentValue && 'workspace' !== configuredEnvironmentValue ? configuredEnvironmentValue : void 0;
27
30
  if (configuredArgument && configuredEnvironment && configuredArgument !== configuredEnvironment) throw new Error(`Configured source revision ${configuredArgument} does not match environment source revision ${configuredEnvironment}.`);
@@ -49,9 +52,9 @@ const resolveUltramodernSourceRevision = (workspaceRoot = process.cwd(), configu
49
52
  if (configured && configured !== revision) throw new Error(`Configured source revision ${configured} does not match clean Git HEAD ${revision}.`);
50
53
  return revision;
51
54
  };
52
- const resolveUltramodernReleaseIdentity = ({ generationBuildMarker, sourceRevision: configuredSourceRevision, unitId, workspaceRoot })=>{
55
+ const resolveUltramodernReleaseIdentity = ({ generationBuildMarker, sourceRevision: configuredSourceRevision, unitId, workspaceRoot, environment })=>{
53
56
  const explicitSourceRevision = configuredSourceRevision?.trim();
54
- const sourceRevision = resolveUltramodernSourceRevision(workspaceRoot, explicitSourceRevision);
57
+ const sourceRevision = resolveUltramodernSourceRevision(workspaceRoot, explicitSourceRevision, environment);
55
58
  return {
56
59
  buildMarker: 'workspace' === sourceRevision ? generationBuildMarker : createUltramodernReleaseBuildMarker({
57
60
  generationBuildMarker,
@@ -0,0 +1,83 @@
1
+ import "node:module";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import node_fs from "node:fs";
4
+ import node_path from "node:path";
5
+ import { immutableRendererRouterBindings, validateRendererProfile, validateRendererRouterBindings } from "@modern-js/backend-federation-contracts";
6
+ import { resolveUltramodernSourceRevision } from "./release-identity.mjs";
7
+ const LOCKFILES = [
8
+ 'pnpm-lock.yaml',
9
+ 'package-lock.json',
10
+ 'yarn.lock',
11
+ 'bun.lock',
12
+ 'bun.lockb'
13
+ ];
14
+ const processNonce = randomUUID();
15
+ const sha256 = (value)=>createHash('sha256').update(value).digest('hex');
16
+ function rendererProfileKey(profile) {
17
+ return sha256(JSON.stringify([
18
+ profile.renderer,
19
+ profile.protocolVersion,
20
+ profile.compiler,
21
+ profile.hydration,
22
+ profile.router,
23
+ Object.entries(profile.dependencies ?? {}).sort(([left], [right])=>left < right ? -1 : left > right ? 1 : 0)
24
+ ]));
25
+ }
26
+ function lockfileHash(projectRoot) {
27
+ let directory = node_path.resolve(projectRoot);
28
+ for(;;){
29
+ for (const name of LOCKFILES)try {
30
+ return sha256(node_fs.readFileSync(node_path.join(directory, name)));
31
+ } catch (error) {
32
+ if ('ENOENT' !== error.code) throw error;
33
+ }
34
+ const parent = node_path.dirname(directory);
35
+ if (parent === directory) return '';
36
+ directory = parent;
37
+ }
38
+ }
39
+ function resolveRendererBuildIdentities(options) {
40
+ const profile = {
41
+ renderer: options.profile.renderer,
42
+ protocolVersion: options.profile.protocolVersion,
43
+ compiler: options.profile.compiler,
44
+ hydration: options.profile.hydration,
45
+ router: options.profile.router
46
+ };
47
+ const validation = validateRendererProfile(profile);
48
+ if (!validation.ok || options.renderer !== profile.renderer) throw new Error(`Renderer/profile identity mismatch: ${JSON.stringify(validation.errors)}.`);
49
+ const entries = [
50
+ ...options.entryNames
51
+ ].sort();
52
+ if (0 === entries.length || new Set(entries).size !== entries.length || entries.some((value)=>!value || '__proto__' === value)) throw new Error('Renderer build identity requires unique nonempty entry names.');
53
+ const routerValidation = validateRendererRouterBindings(options.routerBindings, entries, 'routerBindings', options.routerFrameworks);
54
+ if (!routerValidation.ok) throw new Error(`Invalid renderer router bindings: ${JSON.stringify(routerValidation.errors)}.`);
55
+ const projectRoot = node_path.resolve(options.projectRoot);
56
+ const appId = options.appId ?? JSON.parse(node_fs.readFileSync(node_path.join(projectRoot, 'package.json'), 'utf8')).name;
57
+ if ('string' != typeof appId || !appId || appId.trim() !== appId) throw new Error('Renderer application identity requires a delivery artifact appId or a package name.');
58
+ const profileKey = rendererProfileKey(options.profile);
59
+ const sourceRevision = resolveUltramodernSourceRevision(projectRoot, options.sourceRevision);
60
+ const buildId = sha256(JSON.stringify([
61
+ profileKey,
62
+ lockfileHash(projectRoot),
63
+ 'development' === options.mode || 'workspace' === sourceRevision ? processNonce : sourceRevision
64
+ ]));
65
+ const identities = Object.fromEntries(entries.map((entryName)=>[
66
+ entryName,
67
+ Object.freeze({
68
+ renderer: options.renderer,
69
+ appId,
70
+ entryName,
71
+ protocolVersion: 1,
72
+ buildId
73
+ })
74
+ ]));
75
+ return Object.freeze({
76
+ identities: Object.freeze(identities),
77
+ buildId,
78
+ profileKey,
79
+ sourceRevision,
80
+ routerBindings: immutableRendererRouterBindings(options.routerBindings)
81
+ });
82
+ }
83
+ export { rendererProfileKey, resolveRendererBuildIdentities };