@bleedingdev/modern-js-app-tools-extensions 3.8.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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/cjs/cloudflare/artifacts.js +127 -0
  4. package/dist/cjs/cloudflare/constants.js +141 -0
  5. package/dist/cjs/cloudflare/delivery-unit.js +161 -0
  6. package/dist/cjs/cloudflare/i18n-worker.js +135 -0
  7. package/dist/cjs/cloudflare/index.js +156 -0
  8. package/dist/cjs/cloudflare/security-policies.js +205 -0
  9. package/dist/cjs/cloudflare/types.js +18 -0
  10. package/dist/cjs/cloudflare/utils.js +66 -0
  11. package/dist/cjs/cloudflare/worker-manifest.js +239 -0
  12. package/dist/cjs/cloudflare/wrangler-config.js +205 -0
  13. package/dist/cjs/cloudflare-builder.js +394 -0
  14. package/dist/cjs/cloudflare-output-contract.js +108 -0
  15. package/dist/cjs/cloudflare-output-plan.js +93 -0
  16. package/dist/cjs/cloudflare-output-verifier/identity.js +92 -0
  17. package/dist/cjs/cloudflare-output-verifier/index.js +188 -0
  18. package/dist/cjs/cloudflare-output-verifier/issues.js +54 -0
  19. package/dist/cjs/cloudflare-output-verifier/manifest.js +89 -0
  20. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +151 -0
  21. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +378 -0
  22. package/dist/cjs/config.js +18 -0
  23. package/dist/cjs/css-extract-runtime-plugin.js +74 -0
  24. package/dist/cjs/index.js +125 -0
  25. package/dist/cjs/read-template.js +56 -0
  26. package/dist/cjs/release-envelope/canonical.js +68 -0
  27. package/dist/cjs/release-envelope/framework-output.js +649 -0
  28. package/dist/cjs/release-envelope/index.js +399 -0
  29. package/dist/cjs/release-envelope/plugin.js +102 -0
  30. package/dist/cjs/release-envelope/types.js +51 -0
  31. package/dist/cjs/release-identity.js +110 -0
  32. package/dist/cjs/templates/cloudflare-entry.001-bootstrap-security.mjs +251 -0
  33. package/dist/cjs/templates/cloudflare-entry.002-assets-routes.mjs +191 -0
  34. package/dist/cjs/templates/cloudflare-entry.003-i18n-locales.mjs +514 -0
  35. package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +1194 -0
  36. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +421 -0
  37. package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +93 -0
  38. package/dist/cjs/templates/cloudflare-worker-fs-promises.mjs +11 -0
  39. package/dist/cjs/templates/cloudflare-worker-loadable-server.mjs +281 -0
  40. package/dist/cjs/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +14 -0
  41. package/dist/cjs/templates/cloudflare-worker-path.mjs +98 -0
  42. package/dist/esm/cloudflare/artifacts.mjs +64 -0
  43. package/dist/esm/cloudflare/constants.mjs +34 -0
  44. package/dist/esm/cloudflare/delivery-unit.mjs +110 -0
  45. package/dist/esm/cloudflare/i18n-worker.mjs +97 -0
  46. package/dist/esm/cloudflare/index.mjs +108 -0
  47. package/dist/esm/cloudflare/security-policies.mjs +167 -0
  48. package/dist/esm/cloudflare/types.mjs +0 -0
  49. package/dist/esm/cloudflare/utils.mjs +12 -0
  50. package/dist/esm/cloudflare/worker-manifest.mjs +179 -0
  51. package/dist/esm/cloudflare/wrangler-config.mjs +151 -0
  52. package/dist/esm/cloudflare-builder.mjs +327 -0
  53. package/dist/esm/cloudflare-output-contract.mjs +31 -0
  54. package/dist/esm/cloudflare-output-plan.mjs +45 -0
  55. package/dist/esm/cloudflare-output-verifier/identity.mjs +54 -0
  56. package/dist/esm/cloudflare-output-verifier/index.mjs +133 -0
  57. package/dist/esm/cloudflare-output-verifier/issues.mjs +10 -0
  58. package/dist/esm/cloudflare-output-verifier/manifest.mjs +51 -0
  59. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +99 -0
  60. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +313 -0
  61. package/dist/esm/config.mjs +0 -0
  62. package/dist/esm/css-extract-runtime-plugin.mjs +32 -0
  63. package/dist/esm/index.mjs +10 -0
  64. package/dist/esm/read-template.mjs +5 -0
  65. package/dist/esm/release-envelope/canonical.mjs +21 -0
  66. package/dist/esm/release-envelope/framework-output.mjs +573 -0
  67. package/dist/esm/release-envelope/index.mjs +335 -0
  68. package/dist/esm/release-envelope/plugin.mjs +54 -0
  69. package/dist/esm/release-envelope/types.mjs +7 -0
  70. package/dist/esm/release-identity.mjs +66 -0
  71. package/dist/esm/templates/cloudflare-entry.001-bootstrap-security.mjs +251 -0
  72. package/dist/esm/templates/cloudflare-entry.002-assets-routes.mjs +191 -0
  73. package/dist/esm/templates/cloudflare-entry.003-i18n-locales.mjs +514 -0
  74. package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +1194 -0
  75. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +421 -0
  76. package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +93 -0
  77. package/dist/esm/templates/cloudflare-worker-fs-promises.mjs +11 -0
  78. package/dist/esm/templates/cloudflare-worker-loadable-server.mjs +281 -0
  79. package/dist/esm/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +14 -0
  80. package/dist/esm/templates/cloudflare-worker-path.mjs +98 -0
  81. package/dist/esm-node/cloudflare/artifacts.mjs +65 -0
  82. package/dist/esm-node/cloudflare/constants.mjs +35 -0
  83. package/dist/esm-node/cloudflare/delivery-unit.mjs +111 -0
  84. package/dist/esm-node/cloudflare/i18n-worker.mjs +98 -0
  85. package/dist/esm-node/cloudflare/index.mjs +109 -0
  86. package/dist/esm-node/cloudflare/security-policies.mjs +168 -0
  87. package/dist/esm-node/cloudflare/types.mjs +1 -0
  88. package/dist/esm-node/cloudflare/utils.mjs +13 -0
  89. package/dist/esm-node/cloudflare/worker-manifest.mjs +180 -0
  90. package/dist/esm-node/cloudflare/wrangler-config.mjs +152 -0
  91. package/dist/esm-node/cloudflare-builder.mjs +328 -0
  92. package/dist/esm-node/cloudflare-output-contract.mjs +32 -0
  93. package/dist/esm-node/cloudflare-output-plan.mjs +46 -0
  94. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +55 -0
  95. package/dist/esm-node/cloudflare-output-verifier/index.mjs +134 -0
  96. package/dist/esm-node/cloudflare-output-verifier/issues.mjs +11 -0
  97. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +52 -0
  98. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +100 -0
  99. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +314 -0
  100. package/dist/esm-node/config.mjs +1 -0
  101. package/dist/esm-node/css-extract-runtime-plugin.mjs +33 -0
  102. package/dist/esm-node/index.mjs +11 -0
  103. package/dist/esm-node/read-template.mjs +9 -0
  104. package/dist/esm-node/release-envelope/canonical.mjs +22 -0
  105. package/dist/esm-node/release-envelope/framework-output.mjs +574 -0
  106. package/dist/esm-node/release-envelope/index.mjs +336 -0
  107. package/dist/esm-node/release-envelope/plugin.mjs +55 -0
  108. package/dist/esm-node/release-envelope/types.mjs +8 -0
  109. package/dist/esm-node/release-identity.mjs +67 -0
  110. package/dist/esm-node/templates/cloudflare-entry.001-bootstrap-security.mjs +251 -0
  111. package/dist/esm-node/templates/cloudflare-entry.002-assets-routes.mjs +191 -0
  112. package/dist/esm-node/templates/cloudflare-entry.003-i18n-locales.mjs +514 -0
  113. package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +1194 -0
  114. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +421 -0
  115. package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +93 -0
  116. package/dist/esm-node/templates/cloudflare-worker-fs-promises.mjs +11 -0
  117. package/dist/esm-node/templates/cloudflare-worker-loadable-server.mjs +281 -0
  118. package/dist/esm-node/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +14 -0
  119. package/dist/esm-node/templates/cloudflare-worker-path.mjs +98 -0
  120. package/dist/types/cloudflare/artifacts.d.ts +15 -0
  121. package/dist/types/cloudflare/constants.d.ts +24 -0
  122. package/dist/types/cloudflare/delivery-unit.d.ts +23 -0
  123. package/dist/types/cloudflare/i18n-worker.d.ts +22 -0
  124. package/dist/types/cloudflare/index.d.ts +2 -0
  125. package/dist/types/cloudflare/security-policies.d.ts +52 -0
  126. package/dist/types/cloudflare/types.d.ts +28 -0
  127. package/dist/types/cloudflare/utils.d.ts +6 -0
  128. package/dist/types/cloudflare/worker-manifest.d.ts +149 -0
  129. package/dist/types/cloudflare/wrangler-config.d.ts +51 -0
  130. package/dist/types/cloudflare-builder.d.ts +59 -0
  131. package/dist/types/cloudflare-output-contract.d.ts +14 -0
  132. package/dist/types/cloudflare-output-plan.d.ts +34 -0
  133. package/dist/types/cloudflare-output-verifier/identity.d.ts +8 -0
  134. package/dist/types/cloudflare-output-verifier/index.d.ts +18 -0
  135. package/dist/types/cloudflare-output-verifier/issues.d.ts +14 -0
  136. package/dist/types/cloudflare-output-verifier/manifest.d.ts +2 -0
  137. package/dist/types/cloudflare-output-verifier/mutation-policy.d.ts +14 -0
  138. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +20 -0
  139. package/dist/types/config.d.ts +175 -0
  140. package/dist/types/css-extract-runtime-plugin.d.ts +6 -0
  141. package/dist/types/index.d.ts +10 -0
  142. package/dist/types/read-template.d.ts +2 -0
  143. package/dist/types/release-envelope/canonical.d.ts +5 -0
  144. package/dist/types/release-envelope/framework-output.d.ts +32 -0
  145. package/dist/types/release-envelope/index.d.ts +6 -0
  146. package/dist/types/release-envelope/plugin.d.ts +30 -0
  147. package/dist/types/release-envelope/types.d.ts +62 -0
  148. package/dist/types/release-identity.d.ts +16 -0
  149. package/dist/types/templates/cloudflare-entry.001-bootstrap-security.d.mts +1 -0
  150. package/dist/types/templates/cloudflare-entry.002-assets-routes.d.mts +1 -0
  151. package/dist/types/templates/cloudflare-entry.003-i18n-locales.d.mts +1 -0
  152. package/dist/types/templates/cloudflare-entry.004-rendering-css.d.mts +1 -0
  153. package/dist/types/templates/cloudflare-entry.005-worker-dispatch.d.mts +1 -0
  154. package/dist/types/templates/cloudflare-entry.006-fetch-handler.d.mts +4 -0
  155. package/dist/types/templates/cloudflare-worker-fs-promises.d.mts +5 -0
  156. package/dist/types/templates/cloudflare-worker-loadable-server.d.mts +48 -0
  157. package/dist/types/templates/cloudflare-worker-mf-ssr-runtime-plugin.d.mts +13 -0
  158. package/dist/types/templates/cloudflare-worker-path.d.mts +21 -0
  159. package/package.json +131 -0
  160. package/src/cloudflare/artifacts.ts +165 -0
  161. package/src/cloudflare/constants.ts +50 -0
  162. package/src/cloudflare/delivery-unit.ts +195 -0
  163. package/src/cloudflare/i18n-worker.ts +197 -0
  164. package/src/cloudflare/index.ts +259 -0
  165. package/src/cloudflare/security-policies.ts +194 -0
  166. package/src/cloudflare/types.ts +33 -0
  167. package/src/cloudflare/utils.ts +40 -0
  168. package/src/cloudflare/worker-manifest.ts +313 -0
  169. package/src/cloudflare/wrangler-config.ts +338 -0
  170. package/src/cloudflare-builder.ts +653 -0
  171. package/src/cloudflare-output-contract.ts +30 -0
  172. package/src/cloudflare-output-plan.ts +108 -0
  173. package/src/cloudflare-output-verifier/identity.ts +87 -0
  174. package/src/cloudflare-output-verifier/index.ts +277 -0
  175. package/src/cloudflare-output-verifier/issues.ts +54 -0
  176. package/src/cloudflare-output-verifier/manifest.ts +76 -0
  177. package/src/cloudflare-output-verifier/mutation-policy.ts +159 -0
  178. package/src/cloudflare-output-verifier/worker-bundles.ts +664 -0
  179. package/src/config.ts +196 -0
  180. package/src/css-extract-runtime-plugin.ts +70 -0
  181. package/src/index.ts +10 -0
  182. package/src/read-template.ts +8 -0
  183. package/src/release-envelope/canonical.ts +58 -0
  184. package/src/release-envelope/framework-output.ts +1121 -0
  185. package/src/release-envelope/index.ts +656 -0
  186. package/src/release-envelope/plugin.ts +125 -0
  187. package/src/release-envelope/types.ts +83 -0
  188. package/src/release-identity.ts +134 -0
  189. package/src/templates/cloudflare-entry.001-bootstrap-security.mjs +251 -0
  190. package/src/templates/cloudflare-entry.002-assets-routes.mjs +191 -0
  191. package/src/templates/cloudflare-entry.003-i18n-locales.mjs +514 -0
  192. package/src/templates/cloudflare-entry.004-rendering-css.mjs +1194 -0
  193. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +421 -0
  194. package/src/templates/cloudflare-entry.006-fetch-handler.mjs +93 -0
  195. package/src/templates/cloudflare-worker-fs-promises.mjs +11 -0
  196. package/src/templates/cloudflare-worker-loadable-server.mjs +281 -0
  197. package/src/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +14 -0
  198. package/src/templates/cloudflare-worker-path.mjs +98 -0
@@ -0,0 +1,1121 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import {
5
+ BACKEND_FEDERATION_MANIFEST_FILE,
6
+ BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
7
+ isUltramodernBuildArtifact,
8
+ ULTRAMODERN_BUILD_ARTIFACT_FILE,
9
+ type UltramodernBuildArtifact,
10
+ } from '@modern-js/utils/universal';
11
+ import {
12
+ canonicalSerializeMicroVerticalReleaseEnvelope,
13
+ createMicroVerticalReleaseEnvelope,
14
+ verifyMicroVerticalReleaseEnvelope,
15
+ } from './index';
16
+ import type {
17
+ MicroVerticalReleaseArtifact,
18
+ MicroVerticalReleaseArtifactInput,
19
+ MicroVerticalReleaseEnvelope,
20
+ MicroVerticalReleaseIdentity,
21
+ MicroVerticalReleaseSurfaces,
22
+ MicroVerticalReleaseTarget,
23
+ } from './types';
24
+
25
+ export const MICROVERTICAL_RELEASE_ENVELOPE_PATH =
26
+ 'release/microvertical-release-envelope.json';
27
+ export const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_PATH =
28
+ 'release/microvertical-release-identity-carriers.json';
29
+
30
+ const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_KIND =
31
+ 'ultramodern-release-identity-carriers';
32
+ const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_SCHEMA_VERSION = 1;
33
+
34
+ const COMPILED_MODULE_PATTERN = /\.(?:c|m)?js$/u;
35
+ const EFFECT_BFF_WORKER_PATTERN = /^worker\/__modern_bff_effect\.(?:c|m)?js$/u;
36
+
37
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
38
+ typeof value === 'object' && value !== null && !Array.isArray(value);
39
+
40
+ const flattenStrings = (value: unknown): string[] =>
41
+ typeof value === 'string'
42
+ ? [value]
43
+ : Array.isArray(value)
44
+ ? value.flatMap(flattenStrings)
45
+ : [];
46
+
47
+ const collectNestedStrings = (value: unknown, key: string): string[] => {
48
+ if (Array.isArray(value)) {
49
+ return value.flatMap(item => collectNestedStrings(item, key));
50
+ }
51
+ if (!isRecord(value)) {
52
+ return [];
53
+ }
54
+ return Object.entries(value).flatMap(([entryKey, entryValue]) => [
55
+ ...(entryKey === key ? flattenStrings(entryValue) : []),
56
+ ...collectNestedStrings(entryValue, key),
57
+ ]);
58
+ };
59
+
60
+ const manifestReferencedClientModules = async (
61
+ distDirectory: string,
62
+ files: string[],
63
+ manifestLogicalPath = 'mf-manifest.json',
64
+ ) => {
65
+ const manifest = await readJson(
66
+ path.join(distDirectory, manifestLogicalPath),
67
+ );
68
+ const references = [
69
+ ...collectNestedStrings(manifest, 'path'),
70
+ ...collectNestedStrings(manifest, 'name'),
71
+ ...collectNestedStrings(manifest, 'sync'),
72
+ ...collectNestedStrings(manifest, 'async'),
73
+ ];
74
+ const normalizedReferences = new Set(
75
+ references
76
+ .filter((value): value is string => typeof value === 'string')
77
+ .map(value => value.replace(/^https?:\/\/[^/]+/u, ''))
78
+ .map(value => value.replace(/^\/+/u, '')),
79
+ );
80
+ return files.filter(
81
+ logicalPath =>
82
+ COMPILED_MODULE_PATTERN.test(logicalPath) &&
83
+ [...normalizedReferences].some(
84
+ reference =>
85
+ reference === logicalPath ||
86
+ reference.endsWith(`/${logicalPath}`) ||
87
+ logicalPath.endsWith(`/${reference}`),
88
+ ),
89
+ );
90
+ };
91
+
92
+ const routeReferencedSsrModules = async (
93
+ distDirectory: string,
94
+ files: string[],
95
+ target: MicroVerticalReleaseTarget,
96
+ routeSpecLogicalPath = 'route.json',
97
+ ) => {
98
+ const routeSpec = await readJson(
99
+ path.join(distDirectory, routeSpecLogicalPath),
100
+ );
101
+ const field = target === 'node' ? 'bundle' : 'worker';
102
+ const references = collectNestedStrings(routeSpec, field).map(value =>
103
+ value.replace(/^\/+/u, ''),
104
+ );
105
+ return files.filter(
106
+ logicalPath =>
107
+ COMPILED_MODULE_PATTERN.test(logicalPath) &&
108
+ references.includes(logicalPath),
109
+ );
110
+ };
111
+
112
+ const readJson = async (filePath: string): Promise<unknown> =>
113
+ JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;
114
+
115
+ const isFilesystemPathInside = (root: string, candidate: string) => {
116
+ const relative = path.relative(root, candidate);
117
+ return (
118
+ relative === '' ||
119
+ (!relative.startsWith(`..${path.sep}`) &&
120
+ relative !== '..' &&
121
+ !path.isAbsolute(relative))
122
+ );
123
+ };
124
+
125
+ const isMissingPathError = (error: unknown) =>
126
+ isRecord(error) && error.code === 'ENOENT';
127
+
128
+ const resolveSafeReleaseEnvelopePath = async ({
129
+ artifactRoot,
130
+ createDirectory,
131
+ }: {
132
+ artifactRoot: string;
133
+ createDirectory: boolean;
134
+ }): Promise<string | undefined> => {
135
+ const resolvedRoot = path.resolve(artifactRoot);
136
+ const realRoot = await fs.realpath(resolvedRoot);
137
+ const releaseDirectory = path.join(resolvedRoot, 'release');
138
+ let releaseStat;
139
+ try {
140
+ releaseStat = await fs.lstat(releaseDirectory);
141
+ } catch (error) {
142
+ if (!isMissingPathError(error)) {
143
+ throw error;
144
+ }
145
+ if (!createDirectory) {
146
+ return undefined;
147
+ }
148
+ await fs.mkdir(releaseDirectory);
149
+ releaseStat = await fs.lstat(releaseDirectory);
150
+ }
151
+ if (releaseStat.isSymbolicLink() || !releaseStat.isDirectory()) {
152
+ throw new Error(
153
+ '[ultramodern-release-envelope] release must be a real directory inside artifactRoot, not a symlink or file.',
154
+ );
155
+ }
156
+ const realReleaseDirectory = await fs.realpath(releaseDirectory);
157
+ if (!isFilesystemPathInside(realRoot, realReleaseDirectory)) {
158
+ throw new Error(
159
+ '[ultramodern-release-envelope] release directory resolves outside artifactRoot.',
160
+ );
161
+ }
162
+
163
+ const envelopePath = path.join(
164
+ releaseDirectory,
165
+ path.basename(MICROVERTICAL_RELEASE_ENVELOPE_PATH),
166
+ );
167
+ try {
168
+ const envelopeStat = await fs.lstat(envelopePath);
169
+ if (envelopeStat.isSymbolicLink() || !envelopeStat.isFile()) {
170
+ throw new Error(
171
+ '[ultramodern-release-envelope] release envelope must be a real file inside artifactRoot, not a symlink or directory.',
172
+ );
173
+ }
174
+ const realEnvelopePath = await fs.realpath(envelopePath);
175
+ if (!isFilesystemPathInside(realRoot, realEnvelopePath)) {
176
+ throw new Error(
177
+ '[ultramodern-release-envelope] release envelope resolves outside artifactRoot.',
178
+ );
179
+ }
180
+ } catch (error) {
181
+ if (!isMissingPathError(error)) {
182
+ throw error;
183
+ }
184
+ if (!createDirectory) {
185
+ return undefined;
186
+ }
187
+ }
188
+ return envelopePath;
189
+ };
190
+
191
+ const writeReleaseEnvelope = async (
192
+ artifactRoot: string,
193
+ envelope: MicroVerticalReleaseEnvelope,
194
+ ) => {
195
+ const envelopePath = await resolveSafeReleaseEnvelopePath({
196
+ artifactRoot,
197
+ createDirectory: true,
198
+ });
199
+ if (!envelopePath) {
200
+ throw new Error(
201
+ '[ultramodern-release-envelope] could not resolve release envelope path.',
202
+ );
203
+ }
204
+ await fs.writeFile(
205
+ envelopePath,
206
+ `${canonicalSerializeMicroVerticalReleaseEnvelope(envelope)}\n`,
207
+ );
208
+ };
209
+
210
+ const readBuildArtifact = async (
211
+ distDirectory: string,
212
+ ): Promise<UltramodernBuildArtifact> => {
213
+ const artifactPath = path.join(
214
+ distDirectory,
215
+ ULTRAMODERN_BUILD_ARTIFACT_FILE,
216
+ );
217
+ const artifact = await readJson(artifactPath);
218
+ if (!isUltramodernBuildArtifact(artifact)) {
219
+ throw new Error(
220
+ `[ultramodern-release-envelope] ${ULTRAMODERN_BUILD_ARTIFACT_FILE} is missing or invalid.`,
221
+ );
222
+ }
223
+ if (artifact.deliveryUnit.sourceRevision === 'workspace') {
224
+ throw new Error(
225
+ '[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable full-stack envelope.',
226
+ );
227
+ }
228
+ return artifact;
229
+ };
230
+
231
+ const assertIdentityBlock = (
232
+ value: unknown,
233
+ identity: MicroVerticalReleaseIdentity,
234
+ location: string,
235
+ includeReleaseVersion: boolean,
236
+ ) => {
237
+ if (!isRecord(value)) {
238
+ throw new Error(
239
+ `[ultramodern-release-envelope] ${location} is missing delivery identity.`,
240
+ );
241
+ }
242
+ const expected = {
243
+ unitId: identity.unitId,
244
+ buildMarker: identity.buildMarker,
245
+ sourceRevision: identity.sourceRevision,
246
+ ...(includeReleaseVersion
247
+ ? { version: identity.releaseVersion }
248
+ : undefined),
249
+ };
250
+ for (const [field, expectedValue] of Object.entries(expected)) {
251
+ if (value[field] !== expectedValue) {
252
+ throw new Error(
253
+ `[ultramodern-release-envelope] ${location}.${field} must match ${expectedValue}; received ${String(value[field])}.`,
254
+ );
255
+ }
256
+ }
257
+ };
258
+
259
+ const readBackendManifest = async (
260
+ distDirectory: string,
261
+ identity: MicroVerticalReleaseIdentity,
262
+ ) => {
263
+ const manifest = await readJson(
264
+ path.join(distDirectory, BACKEND_FEDERATION_MANIFEST_FILE),
265
+ );
266
+ if (!isRecord(manifest) || !isRecord(manifest.backendFederation)) {
267
+ throw new Error(
268
+ '[ultramodern-release-envelope] backend federation manifest is missing backendFederation metadata.',
269
+ );
270
+ }
271
+ assertIdentityBlock(
272
+ manifest.backendFederation.deliveryUnit,
273
+ identity,
274
+ 'backendFederation.deliveryUnit',
275
+ true,
276
+ );
277
+ const versionBoundary = manifest.backendFederation.versionBoundary;
278
+ assertIdentityBlock(
279
+ isRecord(versionBoundary) ? versionBoundary.deliveryUnit : undefined,
280
+ identity,
281
+ 'backendFederation.versionBoundary.deliveryUnit',
282
+ false,
283
+ );
284
+ };
285
+
286
+ const collectFiles = async (root: string): Promise<string[]> => {
287
+ const resolvedRoot = path.resolve(root);
288
+ const realRoot = await fs.realpath(resolvedRoot);
289
+
290
+ const collectDirectory = async (
291
+ relativeDirectory: string,
292
+ ): Promise<string[]> => {
293
+ const directory = path.join(
294
+ resolvedRoot,
295
+ ...relativeDirectory.split('/').filter(Boolean),
296
+ );
297
+ const entries = await fs.readdir(directory, { withFileTypes: true });
298
+ const files: string[] = [];
299
+ for (const entry of entries) {
300
+ const logicalPath = path.posix.join(relativeDirectory, entry.name);
301
+ if (logicalPath === 'release' || logicalPath.startsWith('release/')) {
302
+ continue;
303
+ }
304
+ if (entry.isDirectory()) {
305
+ files.push(...(await collectDirectory(logicalPath)));
306
+ continue;
307
+ }
308
+ if (entry.isFile()) {
309
+ files.push(logicalPath);
310
+ continue;
311
+ }
312
+ if (!entry.isSymbolicLink()) {
313
+ continue;
314
+ }
315
+
316
+ const lexicalPath = path.join(resolvedRoot, ...logicalPath.split('/'));
317
+ let realPath: string;
318
+ try {
319
+ realPath = await fs.realpath(lexicalPath);
320
+ } catch {
321
+ throw new Error(
322
+ `[ultramodern-release-envelope] symbolic link ${logicalPath} cannot be resolved.`,
323
+ );
324
+ }
325
+ if (!isFilesystemPathInside(realRoot, realPath)) {
326
+ throw new Error(
327
+ `[ultramodern-release-envelope] symbolic link ${logicalPath} resolves outside artifactRoot.`,
328
+ );
329
+ }
330
+ const targetStat = await fs.stat(realPath);
331
+ if (!targetStat.isFile() && !targetStat.isDirectory()) {
332
+ throw new Error(
333
+ `[ultramodern-release-envelope] symbolic link ${logicalPath} must resolve to a file or directory.`,
334
+ );
335
+ }
336
+ const targetLogicalPath = path
337
+ .relative(realRoot, realPath)
338
+ .split(path.sep)
339
+ .join('/');
340
+ if (
341
+ targetLogicalPath === 'release' ||
342
+ targetLogicalPath.startsWith('release/')
343
+ ) {
344
+ throw new Error(
345
+ `[ultramodern-release-envelope] symbolic link ${logicalPath} targets private release metadata.`,
346
+ );
347
+ }
348
+ if (
349
+ targetStat.isDirectory() &&
350
+ isFilesystemPathInside(
351
+ realPath,
352
+ await fs.realpath(path.dirname(lexicalPath)),
353
+ )
354
+ ) {
355
+ throw new Error(
356
+ `[ultramodern-release-envelope] symbolic link ${logicalPath} targets an ancestor directory.`,
357
+ );
358
+ }
359
+ files.push(logicalPath);
360
+ }
361
+ return files;
362
+ };
363
+
364
+ return (await collectDirectory('')).sort();
365
+ };
366
+
367
+ const releaseArtifact = (
368
+ logicalPath: string,
369
+ runtime: string,
370
+ ): MicroVerticalReleaseArtifactInput => ({
371
+ logicalPath,
372
+ runtime,
373
+ });
374
+
375
+ type ReleaseIdentityCarrierSurface =
376
+ | 'apiBackend'
377
+ | 'backendFederation'
378
+ | 'ssr'
379
+ | 'uiClient';
380
+
381
+ type ReleaseIdentityCarrierPaths = Record<
382
+ ReleaseIdentityCarrierSurface,
383
+ string[]
384
+ >;
385
+
386
+ const writeReleaseIdentityCarrierMetadata = async (
387
+ artifactRoot: string,
388
+ identity: MicroVerticalReleaseIdentity,
389
+ carrierPaths: ReleaseIdentityCarrierPaths,
390
+ ) => {
391
+ const surfacesByPath = new Map<string, Set<ReleaseIdentityCarrierSurface>>();
392
+ for (const [surface, logicalPaths] of Object.entries(carrierPaths) as [
393
+ ReleaseIdentityCarrierSurface,
394
+ string[],
395
+ ][]) {
396
+ if (logicalPaths.length === 0) {
397
+ throw new Error(
398
+ `[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`,
399
+ );
400
+ }
401
+ for (const logicalPath of logicalPaths) {
402
+ const surfaces = surfacesByPath.get(logicalPath) ?? new Set();
403
+ surfaces.add(surface);
404
+ surfacesByPath.set(logicalPath, surfaces);
405
+ }
406
+ }
407
+
408
+ const carriers = await Promise.all(
409
+ [...surfacesByPath]
410
+ .sort(([left], [right]) => left.localeCompare(right))
411
+ .map(async ([logicalPath, surfaces]) => {
412
+ const bytes = await fs.readFile(path.join(artifactRoot, logicalPath));
413
+ return {
414
+ logicalPath,
415
+ byteLength: bytes.byteLength,
416
+ sha256: createHash('sha256').update(bytes).digest('hex'),
417
+ surfaces: [...surfaces].sort((left, right) =>
418
+ left.localeCompare(right),
419
+ ),
420
+ };
421
+ }),
422
+ );
423
+ const releaseEnvelopePath = await resolveSafeReleaseEnvelopePath({
424
+ artifactRoot,
425
+ createDirectory: true,
426
+ });
427
+ if (!releaseEnvelopePath) {
428
+ throw new Error(
429
+ '[ultramodern-release-envelope] could not resolve private release metadata directory.',
430
+ );
431
+ }
432
+ const metadataPath = path.join(
433
+ path.dirname(releaseEnvelopePath),
434
+ path.basename(MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_PATH),
435
+ );
436
+ try {
437
+ const metadataStat = await fs.lstat(metadataPath);
438
+ if (metadataStat.isSymbolicLink() || !metadataStat.isFile()) {
439
+ throw new Error(
440
+ '[ultramodern-release-envelope] release identity carrier metadata must be a real file inside artifactRoot.',
441
+ );
442
+ }
443
+ } catch (error) {
444
+ if (!isMissingPathError(error)) {
445
+ throw error;
446
+ }
447
+ }
448
+ await fs.writeFile(
449
+ metadataPath,
450
+ `${JSON.stringify(
451
+ {
452
+ schemaVersion: MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_SCHEMA_VERSION,
453
+ kind: MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_KIND,
454
+ identity,
455
+ carriers,
456
+ },
457
+ null,
458
+ 2,
459
+ )}\n`,
460
+ );
461
+ return releaseArtifact(
462
+ MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_PATH,
463
+ 'release-identity-metadata',
464
+ );
465
+ };
466
+
467
+ const createReleaseArtifactInputs = async (
468
+ distDirectory: string,
469
+ identity: MicroVerticalReleaseIdentity,
470
+ target: MicroVerticalReleaseTarget,
471
+ ) => {
472
+ const files = await collectFiles(distDirectory);
473
+ if (
474
+ target === 'node' &&
475
+ files.some(logicalPath => EFFECT_BFF_WORKER_PATTERN.test(logicalPath))
476
+ ) {
477
+ throw new Error(
478
+ '[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.',
479
+ );
480
+ }
481
+ const uiClientPaths = files.filter(logicalPath => {
482
+ const topLevel = logicalPath.split('/')[0];
483
+ return (
484
+ topLevel === 'static' ||
485
+ topLevel === 'html' ||
486
+ topLevel === 'public' ||
487
+ [
488
+ 'index.html',
489
+ 'mf-manifest.json',
490
+ 'mf-stats.json',
491
+ 'remoteEntry.js',
492
+ 'routes-manifest.json',
493
+ 'loadable-stats.json',
494
+ ].includes(logicalPath)
495
+ );
496
+ });
497
+ const ssrPaths =
498
+ target === 'node'
499
+ ? files.filter(
500
+ logicalPath =>
501
+ logicalPath.startsWith('bundles/') &&
502
+ COMPILED_MODULE_PATTERN.test(logicalPath),
503
+ )
504
+ : files.filter(
505
+ logicalPath =>
506
+ logicalPath.startsWith('worker/') &&
507
+ COMPILED_MODULE_PATTERN.test(logicalPath) &&
508
+ !EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
509
+ );
510
+ const nodeApiEntryPaths = files.filter(
511
+ logicalPath =>
512
+ logicalPath.startsWith('api/') &&
513
+ COMPILED_MODULE_PATTERN.test(logicalPath),
514
+ );
515
+ const apiBackendPaths =
516
+ target === 'node'
517
+ ? files.filter(
518
+ logicalPath =>
519
+ (logicalPath.startsWith('api/') ||
520
+ logicalPath.startsWith('shared/')) &&
521
+ COMPILED_MODULE_PATTERN.test(logicalPath),
522
+ )
523
+ : files.filter(logicalPath =>
524
+ EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
525
+ );
526
+
527
+ if (uiClientPaths.length === 0) {
528
+ throw new Error(
529
+ `[ultramodern-release-envelope] ${target} full-stack MicroVertical has no UI/client artifacts.`,
530
+ );
531
+ }
532
+ if (ssrPaths.length === 0) {
533
+ throw new Error(
534
+ `[ultramodern-release-envelope] ${target} full-stack MicroVertical has no ${target === 'node' ? 'Node' : 'workerd'} SSR artifacts.`,
535
+ );
536
+ }
537
+ if (
538
+ (target === 'node' && nodeApiEntryPaths.length === 0) ||
539
+ apiBackendPaths.length === 0
540
+ ) {
541
+ throw new Error(
542
+ `[ultramodern-release-envelope] ${target} full-stack MicroVertical has no actual ${target === 'node' ? 'compiled Node Effect API artifact' : 'Effect API/BFF worker artifact'}.`,
543
+ );
544
+ }
545
+
546
+ const clientExecutionPaths = await manifestReferencedClientModules(
547
+ distDirectory,
548
+ files,
549
+ );
550
+ if (clientExecutionPaths.length === 0) {
551
+ throw new Error(
552
+ '[ultramodern-release-envelope] UI/client manifest references no compiled execution module.',
553
+ );
554
+ }
555
+ const ssrExecutionPaths = await routeReferencedSsrModules(
556
+ distDirectory,
557
+ files,
558
+ target,
559
+ );
560
+ if (ssrExecutionPaths.length === 0) {
561
+ throw new Error(
562
+ `[ultramodern-release-envelope] route manifest references no emitted ${target === 'node' ? 'Node' : 'Cloudflare'} SSR execution module.`,
563
+ );
564
+ }
565
+
566
+ const runtimeByPath = new Map<string, string>();
567
+ const add = (paths: string[], runtime: string) => {
568
+ for (const logicalPath of paths) {
569
+ const existing = runtimeByPath.get(logicalPath);
570
+ if (existing && existing !== runtime) {
571
+ throw new Error(
572
+ `[ultramodern-release-envelope] artifact "${logicalPath}" cannot carry conflicting runtimes "${existing}" and "${runtime}".`,
573
+ );
574
+ }
575
+ runtimeByPath.set(logicalPath, runtime);
576
+ }
577
+ };
578
+ add(uiClientPaths, 'browser');
579
+ add(ssrPaths, target === 'node' ? 'nodejs' : 'workerd');
580
+ add(apiBackendPaths, target === 'node' ? 'nodejs' : 'workerd-effect');
581
+ add([BACKEND_FEDERATION_MANIFEST_FILE], 'module-federation-manifest');
582
+ add(
583
+ [BACKEND_FEDERATION_REMOTE_ENTRY_FILE],
584
+ target === 'node' ? 'nodejs' : 'commonjs-module',
585
+ );
586
+
587
+ const surfaces: MicroVerticalReleaseSurfaces = {
588
+ uiClient: [...uiClientPaths].sort((left, right) =>
589
+ left.localeCompare(right),
590
+ ),
591
+ ssr: [...ssrPaths].sort((left, right) => left.localeCompare(right)),
592
+ apiBackend: [...apiBackendPaths].sort((left, right) =>
593
+ left.localeCompare(right),
594
+ ),
595
+ backendFederation: {
596
+ manifest: BACKEND_FEDERATION_MANIFEST_FILE,
597
+ container: BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
598
+ },
599
+ };
600
+ const artifacts = [...runtimeByPath]
601
+ .map(([logicalPath, runtime]) => releaseArtifact(logicalPath, runtime))
602
+ .sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
603
+ const identityCarrierMetadata = await writeReleaseIdentityCarrierMetadata(
604
+ distDirectory,
605
+ identity,
606
+ {
607
+ apiBackend: apiBackendPaths,
608
+ backendFederation: [
609
+ BACKEND_FEDERATION_MANIFEST_FILE,
610
+ BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
611
+ ],
612
+ ssr: [...new Set([...ssrExecutionPaths, ...ssrPaths])],
613
+ uiClient: uiClientPaths.filter(logicalPath =>
614
+ COMPILED_MODULE_PATTERN.test(logicalPath),
615
+ ),
616
+ },
617
+ );
618
+ return {
619
+ artifacts: [...artifacts, identityCarrierMetadata].sort((left, right) =>
620
+ left.logicalPath.localeCompare(right.logicalPath),
621
+ ),
622
+ surfaces,
623
+ };
624
+ };
625
+
626
+ const pathExists = async (filePath: string) => {
627
+ try {
628
+ await fs.access(filePath);
629
+ return true;
630
+ } catch {
631
+ return false;
632
+ }
633
+ };
634
+
635
+ export const emitFrameworkMicroVerticalReleaseEnvelope = async ({
636
+ apiOnly,
637
+ distDirectory,
638
+ target,
639
+ }: {
640
+ apiOnly: boolean;
641
+ distDirectory: string;
642
+ target: MicroVerticalReleaseTarget;
643
+ }): Promise<MicroVerticalReleaseEnvelope | undefined> => {
644
+ if (apiOnly) {
645
+ return undefined;
646
+ }
647
+ const backendManifestPath = path.join(
648
+ distDirectory,
649
+ BACKEND_FEDERATION_MANIFEST_FILE,
650
+ );
651
+ const backendContainerPath = path.join(
652
+ distDirectory,
653
+ BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
654
+ );
655
+ const [hasBackendManifest, hasBackendContainer] = await Promise.all([
656
+ pathExists(backendManifestPath),
657
+ pathExists(backendContainerPath),
658
+ ]);
659
+ if (!hasBackendManifest && !hasBackendContainer) {
660
+ return undefined;
661
+ }
662
+ if (!hasBackendManifest || !hasBackendContainer) {
663
+ throw new Error(
664
+ '[ultramodern-release-envelope] full-stack MicroVertical backend federation manifest and container must be emitted together.',
665
+ );
666
+ }
667
+
668
+ const buildArtifact = await readBuildArtifact(distDirectory);
669
+ const identity = {
670
+ unitId: buildArtifact.deliveryUnit.unitId,
671
+ buildMarker: buildArtifact.deliveryUnit.buildMarker,
672
+ sourceRevision: buildArtifact.deliveryUnit.sourceRevision,
673
+ releaseVersion: buildArtifact.deliveryUnit.version,
674
+ };
675
+ await readBackendManifest(distDirectory, identity);
676
+ const { artifacts, surfaces } = await createReleaseArtifactInputs(
677
+ distDirectory,
678
+ identity,
679
+ target,
680
+ );
681
+ const envelope = await createMicroVerticalReleaseEnvelope({
682
+ artifactRoot: distDirectory,
683
+ target,
684
+ identity,
685
+ artifacts,
686
+ surfaces,
687
+ });
688
+ await writeReleaseEnvelope(distDirectory, envelope);
689
+ return envelope;
690
+ };
691
+
692
+ export const readFrameworkMicroVerticalReleaseEnvelope = async ({
693
+ artifactRoot,
694
+ expectedTarget,
695
+ logicalPathForArtifact,
696
+ required = false,
697
+ }: {
698
+ artifactRoot: string;
699
+ expectedTarget?: MicroVerticalReleaseTarget;
700
+ logicalPathForArtifact?: (artifact: MicroVerticalReleaseArtifact) => string;
701
+ required?: boolean;
702
+ }): Promise<MicroVerticalReleaseEnvelope | undefined> => {
703
+ const envelopePath = await resolveSafeReleaseEnvelopePath({
704
+ artifactRoot,
705
+ createDirectory: false,
706
+ });
707
+ if (!envelopePath) {
708
+ if (required) {
709
+ throw new Error(
710
+ `[ultramodern-release-envelope] required envelope is missing at ${MICROVERTICAL_RELEASE_ENVELOPE_PATH}.`,
711
+ );
712
+ }
713
+ return undefined;
714
+ }
715
+ return verifyMicroVerticalReleaseEnvelope(await readJson(envelopePath), {
716
+ artifactRoot,
717
+ ...(expectedTarget ? { expectedTarget } : {}),
718
+ ...(logicalPathForArtifact ? { logicalPathForArtifact } : {}),
719
+ });
720
+ };
721
+
722
+ export const verifyBuildOutputReleaseEnvelope = async (
723
+ distDirectory: string,
724
+ expectedTarget?: MicroVerticalReleaseTarget,
725
+ ) => {
726
+ const [hasEnvelope, hasBuildArtifact, hasBackendManifest, hasBackendEntry] =
727
+ await Promise.all([
728
+ pathExists(path.join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH)),
729
+ pathExists(path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE)),
730
+ pathExists(path.join(distDirectory, BACKEND_FEDERATION_MANIFEST_FILE)),
731
+ pathExists(
732
+ path.join(distDirectory, BACKEND_FEDERATION_REMOTE_ENTRY_FILE),
733
+ ),
734
+ ]);
735
+ const isFullStackMicroVertical =
736
+ hasBuildArtifact && (hasBackendManifest || hasBackendEntry);
737
+ return readFrameworkMicroVerticalReleaseEnvelope({
738
+ artifactRoot: distDirectory,
739
+ ...(expectedTarget ? { expectedTarget } : {}),
740
+ required: hasEnvelope || isFullStackMicroVertical,
741
+ });
742
+ };
743
+
744
+ export const verifyNodeReleaseEnvelopeStaging = async ({
745
+ outputDirectory,
746
+ }: {
747
+ distDirectory?: string;
748
+ outputDirectory: string;
749
+ }) =>
750
+ readFrameworkMicroVerticalReleaseEnvelope({
751
+ artifactRoot: outputDirectory,
752
+ expectedTarget: 'node',
753
+ required: true,
754
+ });
755
+
756
+ const createNodeStagedReleaseArtifactInputs = async (
757
+ outputDirectory: string,
758
+ identity: MicroVerticalReleaseIdentity,
759
+ ) => {
760
+ const files = await collectFiles(outputDirectory);
761
+ for (const requiredPath of [
762
+ 'index.js',
763
+ 'package.json',
764
+ BACKEND_FEDERATION_MANIFEST_FILE,
765
+ BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
766
+ ]) {
767
+ if (!files.includes(requiredPath)) {
768
+ throw new Error(
769
+ `[ultramodern-release-envelope] final Node staging is missing ${requiredPath}.`,
770
+ );
771
+ }
772
+ }
773
+
774
+ const backendFederationPaths = new Set([
775
+ BACKEND_FEDERATION_MANIFEST_FILE,
776
+ BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
777
+ ]);
778
+ const uiClientPaths = files.filter(logicalPath => {
779
+ const topLevel = logicalPath.split('/')[0];
780
+ return (
781
+ topLevel === 'static' ||
782
+ topLevel === 'html' ||
783
+ topLevel === 'public' ||
784
+ [
785
+ 'index.html',
786
+ 'mf-manifest.json',
787
+ 'mf-stats.json',
788
+ 'remoteEntry.js',
789
+ 'routes-manifest.json',
790
+ 'loadable-stats.json',
791
+ ].includes(logicalPath)
792
+ );
793
+ });
794
+ const ssrPaths = files.filter(
795
+ logicalPath =>
796
+ logicalPath === 'index.js' ||
797
+ (logicalPath.startsWith('bundles/') &&
798
+ COMPILED_MODULE_PATTERN.test(logicalPath)),
799
+ );
800
+ const apiBackendPaths = files.filter(
801
+ logicalPath =>
802
+ (logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) &&
803
+ COMPILED_MODULE_PATTERN.test(logicalPath),
804
+ );
805
+ for (const [surface, paths] of [
806
+ ['UI/client', uiClientPaths],
807
+ ['Node SSR', ssrPaths],
808
+ ['Node API/backend', apiBackendPaths],
809
+ ] as const) {
810
+ if (paths.length === 0) {
811
+ throw new Error(
812
+ `[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`,
813
+ );
814
+ }
815
+ }
816
+ const clientExecutionPaths = await manifestReferencedClientModules(
817
+ outputDirectory,
818
+ files,
819
+ );
820
+ const ssrExecutionPaths = await routeReferencedSsrModules(
821
+ outputDirectory,
822
+ files,
823
+ 'node',
824
+ );
825
+ if (clientExecutionPaths.length === 0 || ssrExecutionPaths.length === 0) {
826
+ throw new Error(
827
+ '[ultramodern-release-envelope] final Node manifests must reference emitted UI/client and SSR execution modules.',
828
+ );
829
+ }
830
+
831
+ const runtimeByPath = new Map(
832
+ files
833
+ .filter(logicalPath => !backendFederationPaths.has(logicalPath))
834
+ .map(logicalPath => [logicalPath, 'nodejs-deployment']),
835
+ );
836
+ for (const logicalPath of uiClientPaths) {
837
+ runtimeByPath.set(logicalPath, 'browser');
838
+ }
839
+ for (const logicalPath of ssrPaths) {
840
+ runtimeByPath.set(logicalPath, 'nodejs');
841
+ }
842
+ for (const logicalPath of apiBackendPaths) {
843
+ runtimeByPath.set(logicalPath, 'nodejs');
844
+ }
845
+ runtimeByPath.set(
846
+ BACKEND_FEDERATION_MANIFEST_FILE,
847
+ 'module-federation-manifest',
848
+ );
849
+ runtimeByPath.set(BACKEND_FEDERATION_REMOTE_ENTRY_FILE, 'nodejs');
850
+
851
+ const identityCarrierMetadata = await writeReleaseIdentityCarrierMetadata(
852
+ outputDirectory,
853
+ identity,
854
+ {
855
+ apiBackend: apiBackendPaths,
856
+ backendFederation: [
857
+ BACKEND_FEDERATION_MANIFEST_FILE,
858
+ BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
859
+ ],
860
+ ssr: [
861
+ ...new Set([
862
+ ...ssrExecutionPaths,
863
+ ...ssrPaths.filter(logicalPath => logicalPath !== 'index.js'),
864
+ ]),
865
+ ],
866
+ uiClient: uiClientPaths.filter(logicalPath =>
867
+ COMPILED_MODULE_PATTERN.test(logicalPath),
868
+ ),
869
+ },
870
+ );
871
+ return {
872
+ artifacts: [
873
+ ...[...runtimeByPath].map(([logicalPath, runtime]) =>
874
+ releaseArtifact(logicalPath, runtime),
875
+ ),
876
+ identityCarrierMetadata,
877
+ ].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath)),
878
+ surfaces: {
879
+ uiClient: [...uiClientPaths].sort((left, right) =>
880
+ left.localeCompare(right),
881
+ ),
882
+ ssr: [...ssrPaths].sort((left, right) => left.localeCompare(right)),
883
+ apiBackend: [...apiBackendPaths].sort((left, right) =>
884
+ left.localeCompare(right),
885
+ ),
886
+ backendFederation: {
887
+ manifest: BACKEND_FEDERATION_MANIFEST_FILE,
888
+ container: BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
889
+ },
890
+ },
891
+ };
892
+ };
893
+
894
+ export const emitNodeStagedReleaseEnvelope = async ({
895
+ distDirectory,
896
+ outputDirectory,
897
+ }: {
898
+ distDirectory: string;
899
+ outputDirectory: string;
900
+ }) => {
901
+ const source = await verifyBuildOutputReleaseEnvelope(distDirectory, 'node');
902
+ if (!source) {
903
+ return undefined;
904
+ }
905
+ const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(
906
+ outputDirectory,
907
+ source.identity,
908
+ );
909
+ const staged = await createMicroVerticalReleaseEnvelope({
910
+ artifactRoot: outputDirectory,
911
+ target: 'node',
912
+ identity: source.identity,
913
+ artifacts,
914
+ surfaces,
915
+ });
916
+ await writeReleaseEnvelope(outputDirectory, staged);
917
+ return staged;
918
+ };
919
+
920
+ export const stageCloudflareReleaseEnvelope = async ({
921
+ distDirectory,
922
+ outputDirectory,
923
+ }: {
924
+ distDirectory: string;
925
+ outputDirectory: string;
926
+ }) => {
927
+ const source = await verifyBuildOutputReleaseEnvelope(
928
+ distDirectory,
929
+ 'cloudflare',
930
+ );
931
+ if (!source) {
932
+ return undefined;
933
+ }
934
+ const stagedEnvelopePath = path.join(
935
+ outputDirectory,
936
+ MICROVERTICAL_RELEASE_ENVELOPE_PATH,
937
+ );
938
+ if (await pathExists(stagedEnvelopePath)) {
939
+ throw new Error(
940
+ '[ultramodern-release-envelope] Cloudflare staging contains a stale envelope before final output generation.',
941
+ );
942
+ }
943
+ return source;
944
+ };
945
+
946
+ const createCloudflareStagedReleaseArtifactInputs = async (
947
+ outputDirectory: string,
948
+ identity: MicroVerticalReleaseIdentity,
949
+ ) => {
950
+ const files = await collectFiles(outputDirectory);
951
+ for (const requiredPath of [
952
+ 'server/index.mjs',
953
+ 'server/modern-worker-manifest.json',
954
+ 'wrangler.json',
955
+ 'package.json',
956
+ 'worker/package.json',
957
+ ]) {
958
+ if (!files.includes(requiredPath)) {
959
+ throw new Error(
960
+ `[ultramodern-release-envelope] final Cloudflare staging is missing ${requiredPath}.`,
961
+ );
962
+ }
963
+ }
964
+ if (
965
+ files.some(
966
+ logicalPath =>
967
+ logicalPath === 'public/release' ||
968
+ logicalPath.startsWith('public/release/'),
969
+ )
970
+ ) {
971
+ throw new Error(
972
+ '[ultramodern-release-envelope] final Cloudflare envelope must remain private and cannot be staged under public/release.',
973
+ );
974
+ }
975
+ const backendManifestPath = `public/${BACKEND_FEDERATION_MANIFEST_FILE}`;
976
+ const backendContainerPath = `public/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`;
977
+ const apiBackendPaths = files.filter(logicalPath =>
978
+ EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
979
+ );
980
+ const ssrPaths = files.filter(
981
+ logicalPath =>
982
+ (logicalPath.startsWith('server/') ||
983
+ logicalPath.startsWith('worker/')) &&
984
+ COMPILED_MODULE_PATTERN.test(logicalPath) &&
985
+ !EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
986
+ );
987
+ const uiClientPaths = files.filter(
988
+ logicalPath =>
989
+ logicalPath.startsWith('public/') &&
990
+ logicalPath !== backendManifestPath &&
991
+ logicalPath !== backendContainerPath,
992
+ );
993
+ for (const [surface, paths] of [
994
+ ['UI/client', uiClientPaths],
995
+ ['Cloudflare SSR', ssrPaths],
996
+ ['Cloudflare API/backend', apiBackendPaths],
997
+ ] as const) {
998
+ if (paths.length === 0) {
999
+ throw new Error(
1000
+ `[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`,
1001
+ );
1002
+ }
1003
+ }
1004
+ for (const logicalPath of [backendManifestPath, backendContainerPath]) {
1005
+ if (!files.includes(logicalPath)) {
1006
+ throw new Error(
1007
+ `[ultramodern-release-envelope] final Cloudflare staging is missing ${logicalPath}.`,
1008
+ );
1009
+ }
1010
+ }
1011
+
1012
+ const clientExecutionPaths = await manifestReferencedClientModules(
1013
+ outputDirectory,
1014
+ files,
1015
+ 'public/mf-manifest.json',
1016
+ );
1017
+ const ssrExecutionPaths = await routeReferencedSsrModules(
1018
+ outputDirectory,
1019
+ files,
1020
+ 'cloudflare',
1021
+ 'server/route.json',
1022
+ );
1023
+ if (clientExecutionPaths.length === 0 || ssrExecutionPaths.length === 0) {
1024
+ throw new Error(
1025
+ '[ultramodern-release-envelope] final Cloudflare manifests must reference emitted UI/client and SSR execution modules.',
1026
+ );
1027
+ }
1028
+
1029
+ const runtimeByPath = new Map<string, string>();
1030
+ for (const logicalPath of files) {
1031
+ runtimeByPath.set(logicalPath, 'cloudflare-deployment');
1032
+ }
1033
+ const add = (paths: string[], runtime: string) => {
1034
+ for (const logicalPath of paths) {
1035
+ runtimeByPath.set(logicalPath, runtime);
1036
+ }
1037
+ };
1038
+ add(uiClientPaths, 'browser');
1039
+ add(ssrPaths, 'workerd');
1040
+ add(apiBackendPaths, 'workerd-effect');
1041
+ add([backendManifestPath], 'module-federation-manifest');
1042
+ add([backendContainerPath], 'commonjs-module');
1043
+
1044
+ const identityCarrierMetadata = await writeReleaseIdentityCarrierMetadata(
1045
+ outputDirectory,
1046
+ identity,
1047
+ {
1048
+ apiBackend: apiBackendPaths,
1049
+ backendFederation: [backendManifestPath, backendContainerPath],
1050
+ ssr: [
1051
+ ...new Set([
1052
+ ...ssrExecutionPaths,
1053
+ ...ssrPaths.filter(logicalPath => logicalPath.startsWith('worker/')),
1054
+ ]),
1055
+ ],
1056
+ uiClient: uiClientPaths.filter(logicalPath =>
1057
+ COMPILED_MODULE_PATTERN.test(logicalPath),
1058
+ ),
1059
+ },
1060
+ );
1061
+ return {
1062
+ artifacts: [
1063
+ ...[...runtimeByPath].map(([logicalPath, runtime]) =>
1064
+ releaseArtifact(logicalPath, runtime),
1065
+ ),
1066
+ identityCarrierMetadata,
1067
+ ].sort((left, right) => left.logicalPath.localeCompare(right.logicalPath)),
1068
+ surfaces: {
1069
+ uiClient: [...uiClientPaths].sort((left, right) =>
1070
+ left.localeCompare(right),
1071
+ ),
1072
+ ssr: [...ssrPaths].sort((left, right) => left.localeCompare(right)),
1073
+ apiBackend: [...apiBackendPaths].sort((left, right) =>
1074
+ left.localeCompare(right),
1075
+ ),
1076
+ backendFederation: {
1077
+ manifest: backendManifestPath,
1078
+ container: backendContainerPath,
1079
+ },
1080
+ },
1081
+ };
1082
+ };
1083
+
1084
+ export const emitCloudflareStagedReleaseEnvelope = async ({
1085
+ distDirectory,
1086
+ outputDirectory,
1087
+ }: {
1088
+ distDirectory: string;
1089
+ outputDirectory: string;
1090
+ }) => {
1091
+ const source = await verifyBuildOutputReleaseEnvelope(
1092
+ distDirectory,
1093
+ 'cloudflare',
1094
+ );
1095
+ if (!source) {
1096
+ return undefined;
1097
+ }
1098
+ const { artifacts, surfaces } =
1099
+ await createCloudflareStagedReleaseArtifactInputs(
1100
+ outputDirectory,
1101
+ source.identity,
1102
+ );
1103
+ const staged = await createMicroVerticalReleaseEnvelope({
1104
+ artifactRoot: outputDirectory,
1105
+ target: 'cloudflare',
1106
+ identity: source.identity,
1107
+ artifacts,
1108
+ surfaces,
1109
+ });
1110
+ await writeReleaseEnvelope(outputDirectory, staged);
1111
+ return staged;
1112
+ };
1113
+
1114
+ export const verifyCloudflareReleaseEnvelopeStaging = async (
1115
+ outputDirectory: string,
1116
+ ) =>
1117
+ readFrameworkMicroVerticalReleaseEnvelope({
1118
+ artifactRoot: outputDirectory,
1119
+ expectedTarget: 'cloudflare',
1120
+ required: true,
1121
+ });