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

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 (213) hide show
  1. package/dist/cjs/backend-federation-build/codegen.js +267 -0
  2. package/dist/cjs/backend-federation-build/config.js +239 -0
  3. package/dist/cjs/backend-federation-build/emit.js +154 -0
  4. package/dist/cjs/backend-federation-build/index.js +53 -0
  5. package/dist/cjs/build-config/build-environment.js +129 -0
  6. package/dist/cjs/build-config/precompress/plugin.js +124 -0
  7. package/dist/cjs/build-config/precompress/types.js +18 -0
  8. package/dist/cjs/build-config/public.js +43 -0
  9. package/dist/cjs/build-config/remote-address.js +58 -0
  10. package/dist/cjs/cloudflare/artifacts.js +8 -19
  11. package/dist/cjs/cloudflare/constants.js +1 -1
  12. package/dist/cjs/cloudflare/delivery-unit.js +23 -42
  13. package/dist/cjs/cloudflare/index.js +10 -2
  14. package/dist/cjs/cloudflare/worker-manifest.js +13 -4
  15. package/dist/cjs/cloudflare/wrangler-config.js +35 -7
  16. package/dist/cjs/cloudflare-builder.js +251 -75
  17. package/dist/cjs/cloudflare-output-contract.js +69 -3
  18. package/dist/cjs/cloudflare-output-verifier/identity.js +29 -27
  19. package/dist/cjs/cloudflare-output-verifier/index.js +10 -0
  20. package/dist/cjs/cloudflare-output-verifier/manifest.js +46 -0
  21. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +7 -1
  22. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +14 -6
  23. package/dist/cjs/config.js +34 -1
  24. package/dist/cjs/deploy-output/npmAliases.js +251 -0
  25. package/dist/cjs/deploy-output/plugin.js +97 -0
  26. package/dist/cjs/deploy-output/public-assets.js +146 -0
  27. package/dist/cjs/deploy-output/target.js +104 -0
  28. package/dist/cjs/deploy-output/trace-files.js +90 -0
  29. package/dist/cjs/native-type-checker.js +193 -0
  30. package/dist/cjs/policy-defaults.js +101 -0
  31. package/dist/cjs/release-envelope/framework-output.js +153 -79
  32. package/dist/cjs/release-envelope/index.js +10 -4
  33. package/dist/cjs/release-envelope/plugin.js +18 -13
  34. package/dist/cjs/release-identity.js +5 -6
  35. package/dist/cjs/runtime-package-resolution.js +118 -0
  36. package/dist/cjs/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
  37. package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
  38. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
  39. package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  40. package/dist/cjs/templates/cloudflare-worker-route-data.mjs +1069 -0
  41. package/dist/esm/backend-federation-build/codegen.mjs +216 -0
  42. package/dist/esm/backend-federation-build/config.mjs +163 -0
  43. package/dist/esm/backend-federation-build/emit.mjs +103 -0
  44. package/dist/esm/backend-federation-build/index.mjs +1 -0
  45. package/dist/esm/build-config/build-environment.mjs +88 -0
  46. package/dist/esm/build-config/precompress/plugin.mjs +76 -0
  47. package/dist/esm/build-config/precompress/types.mjs +0 -0
  48. package/dist/esm/build-config/public.mjs +1 -0
  49. package/dist/esm/build-config/remote-address.mjs +20 -0
  50. package/dist/esm/cloudflare/artifacts.mjs +8 -19
  51. package/dist/esm/cloudflare/constants.mjs +1 -1
  52. package/dist/esm/cloudflare/delivery-unit.mjs +17 -36
  53. package/dist/esm/cloudflare/index.mjs +10 -2
  54. package/dist/esm/cloudflare/worker-manifest.mjs +13 -4
  55. package/dist/esm/cloudflare/wrangler-config.mjs +35 -7
  56. package/dist/esm/cloudflare-builder.mjs +245 -75
  57. package/dist/esm/cloudflare-output-contract.mjs +59 -2
  58. package/dist/esm/cloudflare-output-verifier/identity.mjs +27 -25
  59. package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
  60. package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
  61. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  62. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +15 -7
  63. package/dist/esm/config.mjs +3 -0
  64. package/dist/esm/deploy-output/npmAliases.mjs +199 -0
  65. package/dist/esm/deploy-output/plugin.mjs +46 -0
  66. package/dist/esm/deploy-output/public-assets.mjs +82 -0
  67. package/dist/esm/deploy-output/target.mjs +57 -0
  68. package/dist/esm/deploy-output/trace-files.mjs +38 -0
  69. package/dist/esm/native-type-checker.mjs +142 -0
  70. package/dist/esm/policy-defaults.mjs +45 -0
  71. package/dist/esm/release-envelope/framework-output.mjs +130 -56
  72. package/dist/esm/release-envelope/index.mjs +10 -4
  73. package/dist/esm/release-envelope/plugin.mjs +18 -13
  74. package/dist/esm/release-identity.mjs +5 -6
  75. package/dist/esm/runtime-package-resolution.mjs +60 -0
  76. package/dist/esm/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
  77. package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
  78. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
  79. package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  80. package/dist/esm/templates/cloudflare-worker-route-data.mjs +1069 -0
  81. package/dist/esm-node/backend-federation-build/codegen.mjs +217 -0
  82. package/dist/esm-node/backend-federation-build/config.mjs +164 -0
  83. package/dist/esm-node/backend-federation-build/emit.mjs +104 -0
  84. package/dist/esm-node/backend-federation-build/index.mjs +2 -0
  85. package/dist/esm-node/build-config/build-environment.mjs +89 -0
  86. package/dist/esm-node/build-config/precompress/plugin.mjs +77 -0
  87. package/dist/esm-node/build-config/precompress/types.mjs +1 -0
  88. package/dist/esm-node/build-config/public.mjs +2 -0
  89. package/dist/esm-node/build-config/remote-address.mjs +21 -0
  90. package/dist/esm-node/cloudflare/artifacts.mjs +8 -19
  91. package/dist/esm-node/cloudflare/constants.mjs +1 -1
  92. package/dist/esm-node/cloudflare/delivery-unit.mjs +17 -36
  93. package/dist/esm-node/cloudflare/index.mjs +10 -2
  94. package/dist/esm-node/cloudflare/worker-manifest.mjs +13 -4
  95. package/dist/esm-node/cloudflare/wrangler-config.mjs +35 -7
  96. package/dist/esm-node/cloudflare-builder.mjs +245 -75
  97. package/dist/esm-node/cloudflare-output-contract.mjs +59 -2
  98. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +27 -25
  99. package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
  100. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
  101. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  102. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +15 -7
  103. package/dist/esm-node/config.mjs +3 -0
  104. package/dist/esm-node/deploy-output/npmAliases.mjs +200 -0
  105. package/dist/esm-node/deploy-output/plugin.mjs +49 -0
  106. package/dist/esm-node/deploy-output/public-assets.mjs +83 -0
  107. package/dist/esm-node/deploy-output/target.mjs +58 -0
  108. package/dist/esm-node/deploy-output/trace-files.mjs +39 -0
  109. package/dist/esm-node/native-type-checker.mjs +143 -0
  110. package/dist/esm-node/policy-defaults.mjs +46 -0
  111. package/dist/esm-node/release-envelope/framework-output.mjs +130 -56
  112. package/dist/esm-node/release-envelope/index.mjs +10 -4
  113. package/dist/esm-node/release-envelope/plugin.mjs +18 -13
  114. package/dist/esm-node/release-identity.mjs +5 -6
  115. package/dist/esm-node/runtime-package-resolution.mjs +61 -0
  116. package/dist/esm-node/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
  117. package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
  118. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
  119. package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  120. package/dist/esm-node/templates/cloudflare-worker-route-data.mjs +1069 -0
  121. package/dist/types/backend-federation-build/codegen.d.ts +116 -0
  122. package/dist/types/backend-federation-build/config.d.ts +121 -0
  123. package/dist/types/backend-federation-build/emit.d.ts +21 -0
  124. package/dist/types/backend-federation-build/index.d.ts +2 -0
  125. package/dist/types/build-config/build-environment.d.ts +14 -0
  126. package/dist/types/build-config/precompress/plugin.d.ts +4 -0
  127. package/dist/types/build-config/precompress/types.d.ts +19 -0
  128. package/dist/types/build-config/public.d.ts +2 -0
  129. package/dist/types/build-config/remote-address.d.ts +3 -0
  130. package/dist/types/cloudflare/artifacts.d.ts +7 -6
  131. package/dist/types/cloudflare/constants.d.ts +1 -1
  132. package/dist/types/cloudflare/delivery-unit.d.ts +3 -3
  133. package/dist/types/cloudflare/worker-manifest.d.ts +2 -2
  134. package/dist/types/cloudflare/wrangler-config.d.ts +6 -0
  135. package/dist/types/cloudflare-builder.d.ts +49 -9
  136. package/dist/types/cloudflare-output-contract.d.ts +4 -1
  137. package/dist/types/cloudflare-output-verifier/identity.d.ts +1 -1
  138. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
  139. package/dist/types/config.d.ts +29 -1
  140. package/dist/types/deploy-output/npmAliases.d.ts +14 -0
  141. package/dist/types/deploy-output/plugin.d.ts +35 -0
  142. package/dist/types/deploy-output/public-assets.d.ts +51 -0
  143. package/dist/types/deploy-output/target.d.ts +29 -0
  144. package/dist/types/deploy-output/trace-files.d.ts +17 -0
  145. package/dist/types/native-type-checker.d.ts +15 -0
  146. package/dist/types/policy-defaults.d.ts +46 -0
  147. package/dist/types/precompress/LICENSE +20 -0
  148. package/dist/types/precompress/index.d.ts +76 -0
  149. package/dist/types/release-envelope/framework-output.d.ts +8 -3
  150. package/dist/types/release-envelope/plugin.d.ts +7 -7
  151. package/dist/types/runtime-package-resolution.d.ts +28 -0
  152. package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
  153. package/package.json +97 -15
  154. package/src/backend-federation-build/codegen.ts +254 -0
  155. package/src/backend-federation-build/config.ts +492 -0
  156. package/src/backend-federation-build/emit.ts +213 -0
  157. package/src/backend-federation-build/index.ts +2 -0
  158. package/src/build-config/build-environment.ts +160 -0
  159. package/src/build-config/precompress/plugin.ts +140 -0
  160. package/src/build-config/precompress/types.ts +26 -0
  161. package/src/build-config/public.ts +5 -0
  162. package/src/build-config/remote-address.ts +33 -0
  163. package/src/cloudflare/artifacts.ts +22 -43
  164. package/src/cloudflare/constants.ts +1 -1
  165. package/src/cloudflare/delivery-unit.ts +42 -53
  166. package/src/cloudflare/index.ts +24 -11
  167. package/src/cloudflare/worker-manifest.ts +20 -3
  168. package/src/cloudflare/wrangler-config.ts +86 -10
  169. package/src/cloudflare-builder.ts +472 -151
  170. package/src/cloudflare-output-contract.ts +70 -0
  171. package/src/cloudflare-output-verifier/identity.ts +5 -1
  172. package/src/cloudflare-output-verifier/index.ts +20 -0
  173. package/src/cloudflare-output-verifier/manifest.ts +77 -0
  174. package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
  175. package/src/cloudflare-output-verifier/worker-bundles.ts +32 -14
  176. package/src/config.ts +34 -6
  177. package/src/deploy-output/npmAliases.ts +382 -0
  178. package/src/deploy-output/plugin.ts +86 -0
  179. package/src/deploy-output/public-assets.ts +224 -0
  180. package/src/deploy-output/target.ts +106 -0
  181. package/src/deploy-output/trace-files.ts +67 -0
  182. package/src/native-type-checker.ts +183 -0
  183. package/src/policy-defaults.ts +127 -0
  184. package/src/release-envelope/framework-output.ts +346 -94
  185. package/src/release-envelope/index.ts +26 -5
  186. package/src/release-envelope/plugin.ts +43 -40
  187. package/src/release-identity.ts +13 -2
  188. package/src/runtime-package-resolution.ts +115 -0
  189. package/src/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
  190. package/src/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
  191. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
  192. package/src/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  193. package/src/templates/cloudflare-worker-route-data.mjs +1069 -0
  194. package/dist/cjs/templates/cloudflare-worker-fs-promises.mjs +0 -11
  195. package/dist/cjs/templates/cloudflare-worker-loadable-server.mjs +0 -281
  196. package/dist/cjs/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  197. package/dist/cjs/templates/cloudflare-worker-path.mjs +0 -98
  198. package/dist/esm/templates/cloudflare-worker-fs-promises.mjs +0 -11
  199. package/dist/esm/templates/cloudflare-worker-loadable-server.mjs +0 -281
  200. package/dist/esm/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  201. package/dist/esm/templates/cloudflare-worker-path.mjs +0 -98
  202. package/dist/esm-node/templates/cloudflare-worker-fs-promises.mjs +0 -11
  203. package/dist/esm-node/templates/cloudflare-worker-loadable-server.mjs +0 -281
  204. package/dist/esm-node/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  205. package/dist/esm-node/templates/cloudflare-worker-path.mjs +0 -98
  206. package/dist/types/templates/cloudflare-worker-fs-promises.d.mts +0 -5
  207. package/dist/types/templates/cloudflare-worker-loadable-server.d.mts +0 -48
  208. package/dist/types/templates/cloudflare-worker-mf-ssr-runtime-plugin.d.mts +0 -13
  209. package/dist/types/templates/cloudflare-worker-path.d.mts +0 -21
  210. package/src/templates/cloudflare-worker-fs-promises.mjs +0 -11
  211. package/src/templates/cloudflare-worker-loadable-server.mjs +0 -281
  212. package/src/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  213. package/src/templates/cloudflare-worker-path.mjs +0 -98
@@ -7,7 +7,7 @@ import {
7
7
  isUltramodernBuildArtifact,
8
8
  ULTRAMODERN_BUILD_ARTIFACT_FILE,
9
9
  type UltramodernBuildArtifact,
10
- } from '@modern-js/utils/universal';
10
+ } from '@modern-js/backend-federation-contracts';
11
11
  import {
12
12
  canonicalSerializeMicroVerticalReleaseEnvelope,
13
13
  createMicroVerticalReleaseEnvelope,
@@ -33,6 +33,38 @@ const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_SCHEMA_VERSION = 1;
33
33
 
34
34
  const COMPILED_MODULE_PATTERN = /\.(?:c|m)?js$/u;
35
35
  const EFFECT_BFF_WORKER_PATTERN = /^worker\/__modern_bff_effect\.(?:c|m)?js$/u;
36
+ const EFFECT_BFF_WORKER_SUPPORT_PATTERN =
37
+ /^worker\/__modern_worker_(?:runtime|shared)_*\.(?:c|m)?js$/u;
38
+ const isEffectBffWorkerArtifact = (logicalPath: string) =>
39
+ EFFECT_BFF_WORKER_PATTERN.test(logicalPath) ||
40
+ EFFECT_BFF_WORKER_SUPPORT_PATTERN.test(logicalPath);
41
+ const CRAWLER_POLICY_PATH = 'public/robots.txt';
42
+ const isPublicMetadataPath = (logicalPath: string) =>
43
+ logicalPath === 'public/_headers' ||
44
+ /^public\/\.well-known\/[^/]+\.json$/u.test(logicalPath);
45
+ const PUBLIC_BUILD_ARTIFACT_PATH = `public/${ULTRAMODERN_BUILD_ARTIFACT_FILE}`;
46
+ // Files an app declared through `deploy.<target>.publicAssets`. The deploy
47
+ // output stages them from app-root sources; they are served data, not a
48
+ // UI/client surface, so API-only units may carry them.
49
+ const DECLARED_PUBLIC_ASSET_RUNTIME = 'public-asset';
50
+
51
+ const resolveDeclaredPublicAssets = (
52
+ files: readonly string[],
53
+ declaredPublicAssets: readonly string[],
54
+ target: 'Node' | 'Cloudflare',
55
+ ) => {
56
+ const staged = new Set(files);
57
+ const declared = new Set<string>();
58
+ for (const logicalPath of declaredPublicAssets) {
59
+ if (!logicalPath.startsWith('public/') || !staged.has(logicalPath)) {
60
+ throw new Error(
61
+ `[ultramodern-release-envelope] final ${target} staging has no declared public asset file ${JSON.stringify(logicalPath)}.`,
62
+ );
63
+ }
64
+ declared.add(logicalPath);
65
+ }
66
+ return declared;
67
+ };
36
68
 
37
69
  const isRecord = (value: unknown): value is Record<string, unknown> =>
38
70
  typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -57,6 +89,111 @@ const collectNestedStrings = (value: unknown, key: string): string[] => {
57
89
  ]);
58
90
  };
59
91
 
92
+ // Empty native MF producers emit no remote entry or exposed execution assets.
93
+ // Only their routeAssets declarations may supply the missing browser evidence.
94
+ const emptyProducerRouteModules = async (
95
+ distDirectory: string,
96
+ files: string[],
97
+ manifest: unknown,
98
+ manifestLogicalPath: string,
99
+ ): Promise<string[]> => {
100
+ if (
101
+ !isRecord(manifest) ||
102
+ !Array.isArray(manifest.exposes) ||
103
+ manifest.exposes.length !== 0 ||
104
+ !Array.isArray(manifest.remotes) ||
105
+ manifest.remotes.length !== 0 ||
106
+ !isRecord(manifest.metaData) ||
107
+ !isRecord(manifest.metaData.remoteEntry) ||
108
+ manifest.metaData.remoteEntry.name !== '' ||
109
+ manifest.metaData.remoteEntry.path !== '' ||
110
+ manifest.metaData.remoteEntry.type !== 'global'
111
+ ) {
112
+ return [];
113
+ }
114
+ const publicPath = manifest.metaData.publicPath;
115
+ // Reject encoded/ambiguous paths before URL parsing can normalize traversal.
116
+ const safeReference = (value: unknown): value is string =>
117
+ typeof value === 'string' &&
118
+ value.length > 0 &&
119
+ !/[\\\\%?#\s]/u.test(value) &&
120
+ !value.split('/').some(segment => segment === '.' || segment === '..');
121
+ if (!safeReference(publicPath)) {
122
+ return [];
123
+ }
124
+ const absoluteBase = /^https?:\/\//u.test(publicPath);
125
+ if (
126
+ publicPath !== 'auto' &&
127
+ (!publicPath.endsWith('/') ||
128
+ (!absoluteBase &&
129
+ (!publicPath.startsWith('/') || publicPath.startsWith('//'))))
130
+ ) {
131
+ return [];
132
+ }
133
+ if (absoluteBase) {
134
+ const base = new URL(publicPath);
135
+ if (base.username || base.password || base.href !== publicPath) {
136
+ return [];
137
+ }
138
+ }
139
+ const directory = path.posix.dirname(manifestLogicalPath);
140
+ const routesManifest = await readJson(
141
+ path.join(distDirectory, directory, 'routes-manifest.json'),
142
+ );
143
+ if (!isRecord(routesManifest) || !isRecord(routesManifest.routeAssets)) {
144
+ return [];
145
+ }
146
+ const modules = new Set<string>();
147
+ for (const route of Object.values(routesManifest.routeAssets)) {
148
+ if (!isRecord(route) || !Array.isArray(route.assets)) {
149
+ return [];
150
+ }
151
+ for (const reference of route.assets) {
152
+ if (!safeReference(reference) || reference.startsWith('//')) {
153
+ return [];
154
+ }
155
+ let relative = reference;
156
+ if (/^https?:\/\//u.test(reference)) {
157
+ if (!absoluteBase || !reference.startsWith(publicPath)) {
158
+ return [];
159
+ }
160
+ relative = reference.slice(publicPath.length);
161
+ } else if (reference.startsWith('/')) {
162
+ const prefix =
163
+ publicPath === 'auto'
164
+ ? '/'
165
+ : absoluteBase
166
+ ? new URL(publicPath).pathname
167
+ : publicPath;
168
+ if (!reference.startsWith(prefix)) {
169
+ return [];
170
+ }
171
+ relative = reference.slice(prefix.length);
172
+ }
173
+ // Never allow API, SSR, backend containers, or unrelated on-disk modules.
174
+ if (
175
+ !/^(?:static|html|public)\/[^:]+$/u.test(relative) ||
176
+ relative.includes('//')
177
+ ) {
178
+ return [];
179
+ }
180
+ const logicalPath = path.posix.join(directory, relative);
181
+ if (!files.includes(logicalPath)) {
182
+ return [];
183
+ }
184
+ // A browser-named symlink to server/private bytes is not browser evidence.
185
+ const assetStat = await fs.lstat(path.join(distDirectory, logicalPath));
186
+ if (!assetStat.isFile()) {
187
+ return [];
188
+ }
189
+ if (COMPILED_MODULE_PATTERN.test(logicalPath)) {
190
+ modules.add(logicalPath);
191
+ }
192
+ }
193
+ }
194
+ return [...modules];
195
+ };
196
+
60
197
  const manifestReferencedClientModules = async (
61
198
  distDirectory: string,
62
199
  files: string[],
@@ -77,7 +214,7 @@ const manifestReferencedClientModules = async (
77
214
  .map(value => value.replace(/^https?:\/\/[^/]+/u, ''))
78
215
  .map(value => value.replace(/^\/+/u, '')),
79
216
  );
80
- return files.filter(
217
+ const modules = files.filter(
81
218
  logicalPath =>
82
219
  COMPILED_MODULE_PATTERN.test(logicalPath) &&
83
220
  [...normalizedReferences].some(
@@ -87,6 +224,14 @@ const manifestReferencedClientModules = async (
87
224
  logicalPath.endsWith(`/${reference}`),
88
225
  ),
89
226
  );
227
+ return modules.length > 0
228
+ ? modules
229
+ : emptyProducerRouteModules(
230
+ distDirectory,
231
+ files,
232
+ manifest,
233
+ manifestLogicalPath,
234
+ );
90
235
  };
91
236
 
92
237
  const routeReferencedSsrModules = async (
@@ -209,7 +354,8 @@ const writeReleaseEnvelope = async (
209
354
 
210
355
  const readBuildArtifact = async (
211
356
  distDirectory: string,
212
- ): Promise<UltramodernBuildArtifact> => {
357
+ requirePromotable = true,
358
+ ): Promise<UltramodernBuildArtifact | undefined> => {
213
359
  const artifactPath = path.join(
214
360
  distDirectory,
215
361
  ULTRAMODERN_BUILD_ARTIFACT_FILE,
@@ -221,8 +367,17 @@ const readBuildArtifact = async (
221
367
  );
222
368
  }
223
369
  if (artifact.deliveryUnit.sourceRevision === 'workspace') {
370
+ // `resolveUltramodernSourceRevision` deliberately labels a dirty or
371
+ // non-Git checkout `workspace` so development stays usable; only a
372
+ // promotion may not carry that identity. A plain `modern build` is the
373
+ // development path, so it skips the envelope instead of failing the
374
+ // build. The deploy path still asks for a promotable artifact, which is
375
+ // where the release gate belongs.
376
+ if (!requirePromotable) {
377
+ return undefined;
378
+ }
224
379
  throw new Error(
225
- '[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable full-stack envelope.',
380
+ '[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable envelope.',
226
381
  );
227
382
  }
228
383
  return artifact;
@@ -393,7 +548,11 @@ const writeReleaseIdentityCarrierMetadata = async (
393
548
  ReleaseIdentityCarrierSurface,
394
549
  string[],
395
550
  ][]) {
396
- if (logicalPaths.length === 0) {
551
+ if (
552
+ logicalPaths.length === 0 &&
553
+ surface !== 'ssr' &&
554
+ surface !== 'uiClient'
555
+ ) {
397
556
  throw new Error(
398
557
  `[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`,
399
558
  );
@@ -468,12 +627,10 @@ const createReleaseArtifactInputs = async (
468
627
  distDirectory: string,
469
628
  identity: MicroVerticalReleaseIdentity,
470
629
  target: MicroVerticalReleaseTarget,
630
+ apiOnly: boolean,
471
631
  ) => {
472
632
  const files = await collectFiles(distDirectory);
473
- if (
474
- target === 'node' &&
475
- files.some(logicalPath => EFFECT_BFF_WORKER_PATTERN.test(logicalPath))
476
- ) {
633
+ if (target === 'node' && files.some(isEffectBffWorkerArtifact)) {
477
634
  throw new Error(
478
635
  '[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
636
  );
@@ -483,7 +640,9 @@ const createReleaseArtifactInputs = async (
483
640
  return (
484
641
  topLevel === 'static' ||
485
642
  topLevel === 'html' ||
486
- topLevel === 'public' ||
643
+ (topLevel === 'public' &&
644
+ logicalPath !== CRAWLER_POLICY_PATH &&
645
+ !isPublicMetadataPath(logicalPath)) ||
487
646
  [
488
647
  'index.html',
489
648
  'mf-manifest.json',
@@ -505,7 +664,7 @@ const createReleaseArtifactInputs = async (
505
664
  logicalPath =>
506
665
  logicalPath.startsWith('worker/') &&
507
666
  COMPILED_MODULE_PATTERN.test(logicalPath) &&
508
- !EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
667
+ !isEffectBffWorkerArtifact(logicalPath),
509
668
  );
510
669
  const nodeApiEntryPaths = files.filter(
511
670
  logicalPath =>
@@ -520,44 +679,48 @@ const createReleaseArtifactInputs = async (
520
679
  logicalPath.startsWith('shared/')) &&
521
680
  COMPILED_MODULE_PATTERN.test(logicalPath),
522
681
  )
523
- : files.filter(logicalPath =>
524
- EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
525
- );
682
+ : files.filter(isEffectBffWorkerArtifact);
526
683
 
527
- if (uiClientPaths.length === 0) {
684
+ if (apiOnly && (uiClientPaths.length > 0 || ssrPaths.length > 0)) {
685
+ throw new Error(
686
+ '[ultramodern-release-envelope] API-only MicroVertical emitted an undeclared UI/client or SSR surface.',
687
+ );
688
+ }
689
+ if (!apiOnly && uiClientPaths.length === 0) {
528
690
  throw new Error(
529
691
  `[ultramodern-release-envelope] ${target} full-stack MicroVertical has no UI/client artifacts.`,
530
692
  );
531
693
  }
532
- if (ssrPaths.length === 0) {
694
+ if (!apiOnly && ssrPaths.length === 0) {
533
695
  throw new Error(
534
696
  `[ultramodern-release-envelope] ${target} full-stack MicroVertical has no ${target === 'node' ? 'Node' : 'workerd'} SSR artifacts.`,
535
697
  );
536
698
  }
537
699
  if (
538
700
  (target === 'node' && nodeApiEntryPaths.length === 0) ||
701
+ (target === 'cloudflare' &&
702
+ !files.some(logicalPath =>
703
+ EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
704
+ )) ||
539
705
  apiBackendPaths.length === 0
540
706
  ) {
541
707
  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'}.`,
708
+ `[ultramodern-release-envelope] ${target} MicroVertical has no actual ${target === 'node' ? 'compiled Node Effect API artifact' : 'Effect API/BFF worker artifact'}.`,
543
709
  );
544
710
  }
545
711
 
546
- const clientExecutionPaths = await manifestReferencedClientModules(
547
- distDirectory,
548
- files,
549
- );
550
- if (clientExecutionPaths.length === 0) {
712
+ const clientExecutionPaths = apiOnly
713
+ ? []
714
+ : await manifestReferencedClientModules(distDirectory, files);
715
+ if (!apiOnly && clientExecutionPaths.length === 0) {
551
716
  throw new Error(
552
717
  '[ultramodern-release-envelope] UI/client manifest references no compiled execution module.',
553
718
  );
554
719
  }
555
- const ssrExecutionPaths = await routeReferencedSsrModules(
556
- distDirectory,
557
- files,
558
- target,
559
- );
560
- if (ssrExecutionPaths.length === 0) {
720
+ const ssrExecutionPaths = apiOnly
721
+ ? []
722
+ : await routeReferencedSsrModules(distDirectory, files, target);
723
+ if (!apiOnly && ssrExecutionPaths.length === 0) {
561
724
  throw new Error(
562
725
  `[ultramodern-release-envelope] route manifest references no emitted ${target === 'node' ? 'Node' : 'Cloudflare'} SSR execution module.`,
563
726
  );
@@ -576,6 +739,11 @@ const createReleaseArtifactInputs = async (
576
739
  }
577
740
  };
578
741
  add(uiClientPaths, 'browser');
742
+ add(
743
+ files.filter(logicalPath => logicalPath === CRAWLER_POLICY_PATH),
744
+ 'crawler-policy',
745
+ );
746
+ add(files.filter(isPublicMetadataPath), 'public-metadata');
579
747
  add(ssrPaths, target === 'node' ? 'nodejs' : 'workerd');
580
748
  add(apiBackendPaths, target === 'node' ? 'nodejs' : 'workerd-effect');
581
749
  add([BACKEND_FEDERATION_MANIFEST_FILE], 'module-federation-manifest');
@@ -635,15 +803,14 @@ const pathExists = async (filePath: string) => {
635
803
  export const emitFrameworkMicroVerticalReleaseEnvelope = async ({
636
804
  apiOnly,
637
805
  distDirectory,
806
+ requirePromotable = true,
638
807
  target,
639
808
  }: {
640
809
  apiOnly: boolean;
641
810
  distDirectory: string;
811
+ requirePromotable?: boolean;
642
812
  target: MicroVerticalReleaseTarget;
643
813
  }): Promise<MicroVerticalReleaseEnvelope | undefined> => {
644
- if (apiOnly) {
645
- return undefined;
646
- }
647
814
  const backendManifestPath = path.join(
648
815
  distDirectory,
649
816
  BACKEND_FEDERATION_MANIFEST_FILE,
@@ -656,16 +823,22 @@ export const emitFrameworkMicroVerticalReleaseEnvelope = async ({
656
823
  pathExists(backendManifestPath),
657
824
  pathExists(backendContainerPath),
658
825
  ]);
659
- if (!hasBackendManifest && !hasBackendContainer) {
826
+ if (!hasBackendManifest && !hasBackendContainer && !apiOnly) {
660
827
  return undefined;
661
828
  }
662
829
  if (!hasBackendManifest || !hasBackendContainer) {
663
830
  throw new Error(
664
- '[ultramodern-release-envelope] full-stack MicroVertical backend federation manifest and container must be emitted together.',
831
+ '[ultramodern-release-envelope] MicroVertical backend federation manifest and container must be emitted together.',
665
832
  );
666
833
  }
667
834
 
668
- const buildArtifact = await readBuildArtifact(distDirectory);
835
+ const buildArtifact = await readBuildArtifact(
836
+ distDirectory,
837
+ requirePromotable,
838
+ );
839
+ if (!buildArtifact) {
840
+ return undefined;
841
+ }
669
842
  const identity = {
670
843
  unitId: buildArtifact.deliveryUnit.unitId,
671
844
  buildMarker: buildArtifact.deliveryUnit.buildMarker,
@@ -677,6 +850,7 @@ export const emitFrameworkMicroVerticalReleaseEnvelope = async ({
677
850
  distDirectory,
678
851
  identity,
679
852
  target,
853
+ apiOnly,
680
854
  );
681
855
  const envelope = await createMicroVerticalReleaseEnvelope({
682
856
  artifactRoot: distDirectory,
@@ -723,21 +897,14 @@ export const verifyBuildOutputReleaseEnvelope = async (
723
897
  distDirectory: string,
724
898
  expectedTarget?: MicroVerticalReleaseTarget,
725
899
  ) => {
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);
900
+ const [hasEnvelope, hasBuildArtifact] = await Promise.all([
901
+ pathExists(path.join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH)),
902
+ pathExists(path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE)),
903
+ ]);
737
904
  return readFrameworkMicroVerticalReleaseEnvelope({
738
905
  artifactRoot: distDirectory,
739
906
  ...(expectedTarget ? { expectedTarget } : {}),
740
- required: hasEnvelope || isFullStackMicroVertical,
907
+ required: hasEnvelope || hasBuildArtifact,
741
908
  });
742
909
  };
743
910
 
@@ -756,8 +923,15 @@ export const verifyNodeReleaseEnvelopeStaging = async ({
756
923
  const createNodeStagedReleaseArtifactInputs = async (
757
924
  outputDirectory: string,
758
925
  identity: MicroVerticalReleaseIdentity,
926
+ apiOnly: boolean,
927
+ declaredPublicAssets: readonly string[],
759
928
  ) => {
760
929
  const files = await collectFiles(outputDirectory);
930
+ const declaredPublicAssetPaths = resolveDeclaredPublicAssets(
931
+ files,
932
+ declaredPublicAssets,
933
+ 'Node',
934
+ );
761
935
  for (const requiredPath of [
762
936
  'index.js',
763
937
  'package.json',
@@ -780,7 +954,10 @@ const createNodeStagedReleaseArtifactInputs = async (
780
954
  return (
781
955
  topLevel === 'static' ||
782
956
  topLevel === 'html' ||
783
- topLevel === 'public' ||
957
+ (topLevel === 'public' &&
958
+ logicalPath !== CRAWLER_POLICY_PATH &&
959
+ !isPublicMetadataPath(logicalPath) &&
960
+ !declaredPublicAssetPaths.has(logicalPath)) ||
784
961
  [
785
962
  'index.html',
786
963
  'mf-manifest.json',
@@ -802,27 +979,35 @@ const createNodeStagedReleaseArtifactInputs = async (
802
979
  (logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) &&
803
980
  COMPILED_MODULE_PATTERN.test(logicalPath),
804
981
  );
982
+ if (
983
+ apiOnly &&
984
+ (uiClientPaths.length > 0 || ssrPaths.some(path => path !== 'index.js'))
985
+ ) {
986
+ throw new Error(
987
+ '[ultramodern-release-envelope] final API-only Node staging contains an undeclared UI/client or SSR surface.',
988
+ );
989
+ }
805
990
  for (const [surface, paths] of [
806
991
  ['UI/client', uiClientPaths],
807
- ['Node SSR', ssrPaths],
992
+ ['Node SSR', apiOnly ? [] : ssrPaths],
808
993
  ['Node API/backend', apiBackendPaths],
809
994
  ] as const) {
810
- if (paths.length === 0) {
995
+ if (paths.length === 0 && (!apiOnly || surface === 'Node API/backend')) {
811
996
  throw new Error(
812
997
  `[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`,
813
998
  );
814
999
  }
815
1000
  }
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) {
1001
+ const clientExecutionPaths = apiOnly
1002
+ ? []
1003
+ : await manifestReferencedClientModules(outputDirectory, files);
1004
+ const ssrExecutionPaths = apiOnly
1005
+ ? []
1006
+ : await routeReferencedSsrModules(outputDirectory, files, 'node');
1007
+ if (
1008
+ !apiOnly &&
1009
+ (clientExecutionPaths.length === 0 || ssrExecutionPaths.length === 0)
1010
+ ) {
826
1011
  throw new Error(
827
1012
  '[ultramodern-release-envelope] final Node manifests must reference emitted UI/client and SSR execution modules.',
828
1013
  );
@@ -836,7 +1021,16 @@ const createNodeStagedReleaseArtifactInputs = async (
836
1021
  for (const logicalPath of uiClientPaths) {
837
1022
  runtimeByPath.set(logicalPath, 'browser');
838
1023
  }
839
- for (const logicalPath of ssrPaths) {
1024
+ for (const logicalPath of declaredPublicAssetPaths) {
1025
+ runtimeByPath.set(logicalPath, DECLARED_PUBLIC_ASSET_RUNTIME);
1026
+ }
1027
+ if (files.includes(CRAWLER_POLICY_PATH)) {
1028
+ runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
1029
+ }
1030
+ for (const logicalPath of files.filter(isPublicMetadataPath)) {
1031
+ runtimeByPath.set(logicalPath, 'public-metadata');
1032
+ }
1033
+ for (const logicalPath of apiOnly ? [] : ssrPaths) {
840
1034
  runtimeByPath.set(logicalPath, 'nodejs');
841
1035
  }
842
1036
  for (const logicalPath of apiBackendPaths) {
@@ -857,12 +1051,14 @@ const createNodeStagedReleaseArtifactInputs = async (
857
1051
  BACKEND_FEDERATION_MANIFEST_FILE,
858
1052
  BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
859
1053
  ],
860
- ssr: [
861
- ...new Set([
862
- ...ssrExecutionPaths,
863
- ...ssrPaths.filter(logicalPath => logicalPath !== 'index.js'),
864
- ]),
865
- ],
1054
+ ssr: apiOnly
1055
+ ? []
1056
+ : [
1057
+ ...new Set([
1058
+ ...ssrExecutionPaths,
1059
+ ...ssrPaths.filter(logicalPath => logicalPath !== 'index.js'),
1060
+ ]),
1061
+ ],
866
1062
  uiClient: uiClientPaths.filter(logicalPath =>
867
1063
  COMPILED_MODULE_PATTERN.test(logicalPath),
868
1064
  ),
@@ -879,7 +1075,9 @@ const createNodeStagedReleaseArtifactInputs = async (
879
1075
  uiClient: [...uiClientPaths].sort((left, right) =>
880
1076
  left.localeCompare(right),
881
1077
  ),
882
- ssr: [...ssrPaths].sort((left, right) => left.localeCompare(right)),
1078
+ ssr: apiOnly
1079
+ ? []
1080
+ : [...ssrPaths].sort((left, right) => left.localeCompare(right)),
883
1081
  apiBackend: [...apiBackendPaths].sort((left, right) =>
884
1082
  left.localeCompare(right),
885
1083
  ),
@@ -892,9 +1090,12 @@ const createNodeStagedReleaseArtifactInputs = async (
892
1090
  };
893
1091
 
894
1092
  export const emitNodeStagedReleaseEnvelope = async ({
1093
+ declaredPublicAssets = [],
895
1094
  distDirectory,
896
1095
  outputDirectory,
897
1096
  }: {
1097
+ /** Staged `deploy.node.publicAssets` files, relative to `outputDirectory`. */
1098
+ declaredPublicAssets?: readonly string[];
898
1099
  distDirectory: string;
899
1100
  outputDirectory: string;
900
1101
  }) => {
@@ -905,6 +1106,8 @@ export const emitNodeStagedReleaseEnvelope = async ({
905
1106
  const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(
906
1107
  outputDirectory,
907
1108
  source.identity,
1109
+ source.surfaces.uiClient.length === 0,
1110
+ declaredPublicAssets,
908
1111
  );
909
1112
  const staged = await createMicroVerticalReleaseEnvelope({
910
1113
  artifactRoot: outputDirectory,
@@ -946,14 +1149,21 @@ export const stageCloudflareReleaseEnvelope = async ({
946
1149
  const createCloudflareStagedReleaseArtifactInputs = async (
947
1150
  outputDirectory: string,
948
1151
  identity: MicroVerticalReleaseIdentity,
1152
+ apiOnly: boolean,
1153
+ declaredPublicAssets: readonly string[],
949
1154
  ) => {
950
1155
  const files = await collectFiles(outputDirectory);
1156
+ const declaredPublicAssetPaths = resolveDeclaredPublicAssets(
1157
+ files,
1158
+ declaredPublicAssets,
1159
+ 'Cloudflare',
1160
+ );
951
1161
  for (const requiredPath of [
952
1162
  'server/index.mjs',
953
1163
  'server/modern-worker-manifest.json',
954
1164
  'wrangler.json',
955
1165
  'package.json',
956
- 'worker/package.json',
1166
+ ...(apiOnly ? [] : ['worker/package.json']),
957
1167
  ]) {
958
1168
  if (!files.includes(requiredPath)) {
959
1169
  throw new Error(
@@ -974,28 +1184,47 @@ const createCloudflareStagedReleaseArtifactInputs = async (
974
1184
  }
975
1185
  const backendManifestPath = `public/${BACKEND_FEDERATION_MANIFEST_FILE}`;
976
1186
  const backendContainerPath = `public/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`;
977
- const apiBackendPaths = files.filter(logicalPath =>
978
- EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
979
- );
1187
+ const apiBackendPaths = files.filter(isEffectBffWorkerArtifact);
1188
+ if (!files.some(logicalPath => EFFECT_BFF_WORKER_PATTERN.test(logicalPath))) {
1189
+ throw new Error(
1190
+ '[ultramodern-release-envelope] final Cloudflare staging has no actual Effect API/BFF worker artifact.',
1191
+ );
1192
+ }
980
1193
  const ssrPaths = files.filter(
981
1194
  logicalPath =>
982
1195
  (logicalPath.startsWith('server/') ||
983
1196
  logicalPath.startsWith('worker/')) &&
984
1197
  COMPILED_MODULE_PATTERN.test(logicalPath) &&
985
- !EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
1198
+ !isEffectBffWorkerArtifact(logicalPath),
986
1199
  );
987
1200
  const uiClientPaths = files.filter(
988
1201
  logicalPath =>
989
1202
  logicalPath.startsWith('public/') &&
1203
+ logicalPath !== CRAWLER_POLICY_PATH &&
1204
+ !isPublicMetadataPath(logicalPath) &&
1205
+ !declaredPublicAssetPaths.has(logicalPath) &&
1206
+ logicalPath !== PUBLIC_BUILD_ARTIFACT_PATH &&
990
1207
  logicalPath !== backendManifestPath &&
991
1208
  logicalPath !== backendContainerPath,
992
1209
  );
1210
+ if (
1211
+ apiOnly &&
1212
+ (uiClientPaths.length > 0 ||
1213
+ ssrPaths.some(path => path !== 'server/index.mjs'))
1214
+ ) {
1215
+ throw new Error(
1216
+ '[ultramodern-release-envelope] final API-only Cloudflare staging contains an undeclared UI/client or SSR surface.',
1217
+ );
1218
+ }
993
1219
  for (const [surface, paths] of [
994
1220
  ['UI/client', uiClientPaths],
995
- ['Cloudflare SSR', ssrPaths],
1221
+ ['Cloudflare SSR', apiOnly ? [] : ssrPaths],
996
1222
  ['Cloudflare API/backend', apiBackendPaths],
997
1223
  ] as const) {
998
- if (paths.length === 0) {
1224
+ if (
1225
+ paths.length === 0 &&
1226
+ (!apiOnly || surface === 'Cloudflare API/backend')
1227
+ ) {
999
1228
  throw new Error(
1000
1229
  `[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`,
1001
1230
  );
@@ -1009,18 +1238,25 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1009
1238
  }
1010
1239
  }
1011
1240
 
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) {
1241
+ const clientExecutionPaths = apiOnly
1242
+ ? []
1243
+ : await manifestReferencedClientModules(
1244
+ outputDirectory,
1245
+ files,
1246
+ 'public/mf-manifest.json',
1247
+ );
1248
+ const ssrExecutionPaths = apiOnly
1249
+ ? []
1250
+ : await routeReferencedSsrModules(
1251
+ outputDirectory,
1252
+ files,
1253
+ 'cloudflare',
1254
+ 'server/route.json',
1255
+ );
1256
+ if (
1257
+ !apiOnly &&
1258
+ (clientExecutionPaths.length === 0 || ssrExecutionPaths.length === 0)
1259
+ ) {
1024
1260
  throw new Error(
1025
1261
  '[ultramodern-release-envelope] final Cloudflare manifests must reference emitted UI/client and SSR execution modules.',
1026
1262
  );
@@ -1036,7 +1272,12 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1036
1272
  }
1037
1273
  };
1038
1274
  add(uiClientPaths, 'browser');
1039
- add(ssrPaths, 'workerd');
1275
+ add([...declaredPublicAssetPaths], DECLARED_PUBLIC_ASSET_RUNTIME);
1276
+ if (files.includes(CRAWLER_POLICY_PATH)) {
1277
+ runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
1278
+ }
1279
+ add(files.filter(isPublicMetadataPath), 'public-metadata');
1280
+ add(apiOnly ? [] : ssrPaths, 'workerd');
1040
1281
  add(apiBackendPaths, 'workerd-effect');
1041
1282
  add([backendManifestPath], 'module-federation-manifest');
1042
1283
  add([backendContainerPath], 'commonjs-module');
@@ -1047,12 +1288,16 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1047
1288
  {
1048
1289
  apiBackend: apiBackendPaths,
1049
1290
  backendFederation: [backendManifestPath, backendContainerPath],
1050
- ssr: [
1051
- ...new Set([
1052
- ...ssrExecutionPaths,
1053
- ...ssrPaths.filter(logicalPath => logicalPath.startsWith('worker/')),
1054
- ]),
1055
- ],
1291
+ ssr: apiOnly
1292
+ ? []
1293
+ : [
1294
+ ...new Set([
1295
+ ...ssrExecutionPaths,
1296
+ ...ssrPaths.filter(logicalPath =>
1297
+ logicalPath.startsWith('worker/'),
1298
+ ),
1299
+ ]),
1300
+ ],
1056
1301
  uiClient: uiClientPaths.filter(logicalPath =>
1057
1302
  COMPILED_MODULE_PATTERN.test(logicalPath),
1058
1303
  ),
@@ -1069,7 +1314,9 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1069
1314
  uiClient: [...uiClientPaths].sort((left, right) =>
1070
1315
  left.localeCompare(right),
1071
1316
  ),
1072
- ssr: [...ssrPaths].sort((left, right) => left.localeCompare(right)),
1317
+ ssr: apiOnly
1318
+ ? []
1319
+ : [...ssrPaths].sort((left, right) => left.localeCompare(right)),
1073
1320
  apiBackend: [...apiBackendPaths].sort((left, right) =>
1074
1321
  left.localeCompare(right),
1075
1322
  ),
@@ -1082,9 +1329,12 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1082
1329
  };
1083
1330
 
1084
1331
  export const emitCloudflareStagedReleaseEnvelope = async ({
1332
+ declaredPublicAssets = [],
1085
1333
  distDirectory,
1086
1334
  outputDirectory,
1087
1335
  }: {
1336
+ /** Staged `deploy.worker.publicAssets` files, relative to `outputDirectory`. */
1337
+ declaredPublicAssets?: readonly string[];
1088
1338
  distDirectory: string;
1089
1339
  outputDirectory: string;
1090
1340
  }) => {
@@ -1099,6 +1349,8 @@ export const emitCloudflareStagedReleaseEnvelope = async ({
1099
1349
  await createCloudflareStagedReleaseArtifactInputs(
1100
1350
  outputDirectory,
1101
1351
  source.identity,
1352
+ source.surfaces.uiClient.length === 0,
1353
+ declaredPublicAssets,
1102
1354
  );
1103
1355
  const staged = await createMicroVerticalReleaseEnvelope({
1104
1356
  artifactRoot: outputDirectory,