@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
@@ -0,0 +1,45 @@
1
+ import { collectRuntimePackageModuleDirectories, createRuntimePackageResolutionPlugin } from "./runtime-package-resolution.mjs";
2
+ const RENDERER_EXTENSIONS_PACKAGE = '@modern-js/runtime-renderer-extensions';
3
+ const RENDERER_EXTENSIONS_PLUGIN_NAME = 'rendererHead';
4
+ const SERVER_EXTENSIONS_PLUGIN_NAME = '@modern-js/app-tools/server-plugin';
5
+ const POLICY_DEFAULTS_PLUGIN_NAME = '@modern-js/ultramodern-policy-defaults';
6
+ const applyPolicyDefaults = (api, options = {})=>{
7
+ if (false !== options.rendererExtensions) {
8
+ api._internalRuntimePlugins(({ entrypoint, plugins })=>{
9
+ if (!plugins.some((plugin)=>plugin.path === RENDERER_EXTENSIONS_PACKAGE)) plugins.push({
10
+ name: RENDERER_EXTENSIONS_PLUGIN_NAME,
11
+ path: RENDERER_EXTENSIONS_PACKAGE,
12
+ config: {}
13
+ });
14
+ return {
15
+ entrypoint,
16
+ plugins
17
+ };
18
+ });
19
+ const moduleDirectories = collectRuntimePackageModuleDirectories([
20
+ RENDERER_EXTENSIONS_PACKAGE
21
+ ], import.meta.url);
22
+ if (moduleDirectories.length > 0) api.modifyResolvedConfig((config)=>({
23
+ ...config,
24
+ builderPlugins: [
25
+ ...config.builderPlugins ?? [],
26
+ createRuntimePackageResolutionPlugin(moduleDirectories)
27
+ ]
28
+ }));
29
+ }
30
+ if (false !== options.serverExtensions) api._internalServerPlugins(({ plugins })=>{
31
+ if (!plugins.some((plugin)=>plugin.name === SERVER_EXTENSIONS_PLUGIN_NAME)) plugins.push({
32
+ name: SERVER_EXTENSIONS_PLUGIN_NAME
33
+ });
34
+ return {
35
+ plugins
36
+ };
37
+ });
38
+ };
39
+ const createPolicyDefaultsPlugin = (options = {})=>({
40
+ name: POLICY_DEFAULTS_PLUGIN_NAME,
41
+ setup (api) {
42
+ applyPolicyDefaults(api, options);
43
+ }
44
+ });
45
+ export { POLICY_DEFAULTS_PLUGIN_NAME, RENDERER_EXTENSIONS_PACKAGE, RENDERER_EXTENSIONS_PLUGIN_NAME, SERVER_EXTENSIONS_PLUGIN_NAME, applyPolicyDefaults, createPolicyDefaultsPlugin };
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import promises from "node:fs/promises";
3
3
  import node_path from "node:path";
4
- import { BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_REMOTE_ENTRY_FILE, ULTRAMODERN_BUILD_ARTIFACT_FILE, isUltramodernBuildArtifact } from "@modern-js/utils/universal";
4
+ import { BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_REMOTE_ENTRY_FILE, ULTRAMODERN_BUILD_ARTIFACT_FILE, isUltramodernBuildArtifact } from "@modern-js/backend-federation-contracts";
5
5
  import { canonicalSerializeMicroVerticalReleaseEnvelope, createMicroVerticalReleaseEnvelope, verifyMicroVerticalReleaseEnvelope } from "./index.mjs";
6
6
  const MICROVERTICAL_RELEASE_ENVELOPE_PATH = 'release/microvertical-release-envelope.json';
7
7
  const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_PATH = 'release/microvertical-release-identity-carriers.json';
@@ -9,6 +9,21 @@ const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_KIND = 'ultramodern-release-identi
9
9
  const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_SCHEMA_VERSION = 1;
10
10
  const COMPILED_MODULE_PATTERN = /\.(?:c|m)?js$/u;
11
11
  const EFFECT_BFF_WORKER_PATTERN = /^worker\/__modern_bff_effect\.(?:c|m)?js$/u;
12
+ const EFFECT_BFF_WORKER_SUPPORT_PATTERN = /^worker\/__modern_worker_(?:runtime|shared)_*\.(?:c|m)?js$/u;
13
+ const isEffectBffWorkerArtifact = (logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath) || EFFECT_BFF_WORKER_SUPPORT_PATTERN.test(logicalPath);
14
+ const CRAWLER_POLICY_PATH = 'public/robots.txt';
15
+ const isPublicMetadataPath = (logicalPath)=>'public/_headers' === logicalPath || /^public\/\.well-known\/[^/]+\.json$/u.test(logicalPath);
16
+ const PUBLIC_BUILD_ARTIFACT_PATH = `public/${ULTRAMODERN_BUILD_ARTIFACT_FILE}`;
17
+ const DECLARED_PUBLIC_ASSET_RUNTIME = 'public-asset';
18
+ const resolveDeclaredPublicAssets = (files, declaredPublicAssets, target)=>{
19
+ const staged = new Set(files);
20
+ const declared = new Set();
21
+ for (const logicalPath of declaredPublicAssets){
22
+ if (!logicalPath.startsWith('public/') || !staged.has(logicalPath)) throw new Error(`[ultramodern-release-envelope] final ${target} staging has no declared public asset file ${JSON.stringify(logicalPath)}.`);
23
+ declared.add(logicalPath);
24
+ }
25
+ return declared;
26
+ };
12
27
  const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
13
28
  const flattenStrings = (value)=>'string' == typeof value ? [
14
29
  value
@@ -21,6 +36,46 @@ const collectNestedStrings = (value, key)=>{
21
36
  ...collectNestedStrings(entryValue, key)
22
37
  ]);
23
38
  };
39
+ const emptyProducerRouteModules = async (distDirectory, files, manifest, manifestLogicalPath)=>{
40
+ if (!isRecord(manifest) || !Array.isArray(manifest.exposes) || 0 !== manifest.exposes.length || !Array.isArray(manifest.remotes) || 0 !== manifest.remotes.length || !isRecord(manifest.metaData) || !isRecord(manifest.metaData.remoteEntry) || '' !== manifest.metaData.remoteEntry.name || '' !== manifest.metaData.remoteEntry.path || 'global' !== manifest.metaData.remoteEntry.type) return [];
41
+ const publicPath = manifest.metaData.publicPath;
42
+ const safeReference = (value)=>'string' == typeof value && value.length > 0 && !/[\\\\%?#\s]/u.test(value) && !value.split('/').some((segment)=>'.' === segment || '..' === segment);
43
+ if (!safeReference(publicPath)) return [];
44
+ const absoluteBase = /^https?:\/\//u.test(publicPath);
45
+ if ('auto' !== publicPath && (!publicPath.endsWith('/') || !absoluteBase && (!publicPath.startsWith('/') || publicPath.startsWith('//')))) return [];
46
+ if (absoluteBase) {
47
+ const base = new URL(publicPath);
48
+ if (base.username || base.password || base.href !== publicPath) return [];
49
+ }
50
+ const directory = node_path.posix.dirname(manifestLogicalPath);
51
+ const routesManifest = await readJson(node_path.join(distDirectory, directory, 'routes-manifest.json'));
52
+ if (!isRecord(routesManifest) || !isRecord(routesManifest.routeAssets)) return [];
53
+ const modules = new Set();
54
+ for (const route of Object.values(routesManifest.routeAssets)){
55
+ if (!isRecord(route) || !Array.isArray(route.assets)) return [];
56
+ for (const reference of route.assets){
57
+ if (!safeReference(reference) || reference.startsWith('//')) return [];
58
+ let relative = reference;
59
+ if (/^https?:\/\//u.test(reference)) {
60
+ if (!absoluteBase || !reference.startsWith(publicPath)) return [];
61
+ relative = reference.slice(publicPath.length);
62
+ } else if (reference.startsWith('/')) {
63
+ const prefix = 'auto' === publicPath ? '/' : absoluteBase ? new URL(publicPath).pathname : publicPath;
64
+ if (!reference.startsWith(prefix)) return [];
65
+ relative = reference.slice(prefix.length);
66
+ }
67
+ if (!/^(?:static|html|public)\/[^:]+$/u.test(relative) || relative.includes('//')) return [];
68
+ const logicalPath = node_path.posix.join(directory, relative);
69
+ if (!files.includes(logicalPath)) return [];
70
+ const assetStat = await promises.lstat(node_path.join(distDirectory, logicalPath));
71
+ if (!assetStat.isFile()) return [];
72
+ if (COMPILED_MODULE_PATTERN.test(logicalPath)) modules.add(logicalPath);
73
+ }
74
+ }
75
+ return [
76
+ ...modules
77
+ ];
78
+ };
24
79
  const manifestReferencedClientModules = async (distDirectory, files, manifestLogicalPath = 'mf-manifest.json')=>{
25
80
  const manifest = await readJson(node_path.join(distDirectory, manifestLogicalPath));
26
81
  const references = [
@@ -30,9 +85,10 @@ const manifestReferencedClientModules = async (distDirectory, files, manifestLog
30
85
  ...collectNestedStrings(manifest, 'async')
31
86
  ];
32
87
  const normalizedReferences = new Set(references.filter((value)=>'string' == typeof value).map((value)=>value.replace(/^https?:\/\/[^/]+/u, '')).map((value)=>value.replace(/^\/+/u, '')));
33
- return files.filter((logicalPath)=>COMPILED_MODULE_PATTERN.test(logicalPath) && [
88
+ const modules = files.filter((logicalPath)=>COMPILED_MODULE_PATTERN.test(logicalPath) && [
34
89
  ...normalizedReferences
35
90
  ].some((reference)=>reference === logicalPath || reference.endsWith(`/${logicalPath}`) || logicalPath.endsWith(`/${reference}`)));
91
+ return modules.length > 0 ? modules : emptyProducerRouteModules(distDirectory, files, manifest, manifestLogicalPath);
36
92
  };
37
93
  const routeReferencedSsrModules = async (distDirectory, files, target, routeSpecLogicalPath = 'route.json')=>{
38
94
  const routeSpec = await readJson(node_path.join(distDirectory, routeSpecLogicalPath));
@@ -82,11 +138,14 @@ const writeReleaseEnvelope = async (artifactRoot, envelope)=>{
82
138
  if (!envelopePath) throw new Error('[ultramodern-release-envelope] could not resolve release envelope path.');
83
139
  await promises.writeFile(envelopePath, `${canonicalSerializeMicroVerticalReleaseEnvelope(envelope)}\n`);
84
140
  };
85
- const readBuildArtifact = async (distDirectory)=>{
141
+ const readBuildArtifact = async (distDirectory, requirePromotable = true)=>{
86
142
  const artifactPath = node_path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE);
87
143
  const artifact = await readJson(artifactPath);
88
144
  if (!isUltramodernBuildArtifact(artifact)) throw new Error(`[ultramodern-release-envelope] ${ULTRAMODERN_BUILD_ARTIFACT_FILE} is missing or invalid.`);
89
- if ('workspace' === artifact.deliveryUnit.sourceRevision) throw new Error('[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable full-stack envelope.');
145
+ if ('workspace' === artifact.deliveryUnit.sourceRevision) {
146
+ if (!requirePromotable) return;
147
+ throw new Error('[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable envelope.');
148
+ }
90
149
  return artifact;
91
150
  };
92
151
  const assertIdentityBlock = (value, identity, location, includeReleaseVersion)=>{
@@ -155,7 +214,7 @@ const releaseArtifact = (logicalPath, runtime)=>({
155
214
  const writeReleaseIdentityCarrierMetadata = async (artifactRoot, identity, carrierPaths)=>{
156
215
  const surfacesByPath = new Map();
157
216
  for (const [surface, logicalPaths] of Object.entries(carrierPaths)){
158
- if (0 === logicalPaths.length) throw new Error(`[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`);
217
+ if (0 === logicalPaths.length && 'ssr' !== surface && 'uiClient' !== surface) throw new Error(`[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`);
159
218
  for (const logicalPath of logicalPaths){
160
219
  const surfaces = surfacesByPath.get(logicalPath) ?? new Set();
161
220
  surfaces.add(surface);
@@ -195,12 +254,12 @@ const writeReleaseIdentityCarrierMetadata = async (artifactRoot, identity, carri
195
254
  }, null, 2)}\n`);
196
255
  return releaseArtifact(MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_PATH, 'release-identity-metadata');
197
256
  };
198
- const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
257
+ const createReleaseArtifactInputs = async (distDirectory, identity, target, apiOnly)=>{
199
258
  const files = await collectFiles(distDirectory);
200
- if ('node' === target && files.some((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath))) throw new Error('[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.');
259
+ if ('node' === target && files.some(isEffectBffWorkerArtifact)) throw new Error('[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.');
201
260
  const uiClientPaths = files.filter((logicalPath)=>{
202
261
  const topLevel = logicalPath.split('/')[0];
203
- return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel || [
262
+ return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) || [
204
263
  'index.html',
205
264
  'mf-manifest.json',
206
265
  'mf-stats.json',
@@ -209,16 +268,17 @@ const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
209
268
  'loadable-stats.json'
210
269
  ].includes(logicalPath);
211
270
  });
212
- const ssrPaths = 'node' === target ? files.filter((logicalPath)=>logicalPath.startsWith('bundles/') && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter((logicalPath)=>logicalPath.startsWith('worker/') && COMPILED_MODULE_PATTERN.test(logicalPath) && !EFFECT_BFF_WORKER_PATTERN.test(logicalPath));
271
+ const ssrPaths = 'node' === target ? files.filter((logicalPath)=>logicalPath.startsWith('bundles/') && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter((logicalPath)=>logicalPath.startsWith('worker/') && COMPILED_MODULE_PATTERN.test(logicalPath) && !isEffectBffWorkerArtifact(logicalPath));
213
272
  const nodeApiEntryPaths = files.filter((logicalPath)=>logicalPath.startsWith('api/') && COMPILED_MODULE_PATTERN.test(logicalPath));
214
- const apiBackendPaths = 'node' === target ? files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath));
215
- if (0 === uiClientPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no UI/client artifacts.`);
216
- if (0 === ssrPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no ${'node' === target ? 'Node' : 'workerd'} SSR artifacts.`);
217
- if ('node' === target && 0 === nodeApiEntryPaths.length || 0 === apiBackendPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no actual ${'node' === target ? 'compiled Node Effect API artifact' : 'Effect API/BFF worker artifact'}.`);
218
- const clientExecutionPaths = await manifestReferencedClientModules(distDirectory, files);
219
- if (0 === clientExecutionPaths.length) throw new Error('[ultramodern-release-envelope] UI/client manifest references no compiled execution module.');
220
- const ssrExecutionPaths = await routeReferencedSsrModules(distDirectory, files, target);
221
- if (0 === ssrExecutionPaths.length) throw new Error(`[ultramodern-release-envelope] route manifest references no emitted ${'node' === target ? 'Node' : 'Cloudflare'} SSR execution module.`);
273
+ const apiBackendPaths = 'node' === target ? files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter(isEffectBffWorkerArtifact);
274
+ if (apiOnly && (uiClientPaths.length > 0 || ssrPaths.length > 0)) throw new Error('[ultramodern-release-envelope] API-only MicroVertical emitted an undeclared UI/client or SSR surface.');
275
+ if (!apiOnly && 0 === uiClientPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no UI/client artifacts.`);
276
+ if (!apiOnly && 0 === ssrPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no ${'node' === target ? 'Node' : 'workerd'} SSR artifacts.`);
277
+ if ('node' === target && 0 === nodeApiEntryPaths.length || 'cloudflare' === target && !files.some((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath)) || 0 === apiBackendPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} MicroVertical has no actual ${'node' === target ? 'compiled Node Effect API artifact' : 'Effect API/BFF worker artifact'}.`);
278
+ const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(distDirectory, files);
279
+ if (!apiOnly && 0 === clientExecutionPaths.length) throw new Error('[ultramodern-release-envelope] UI/client manifest references no compiled execution module.');
280
+ const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(distDirectory, files, target);
281
+ if (!apiOnly && 0 === ssrExecutionPaths.length) throw new Error(`[ultramodern-release-envelope] route manifest references no emitted ${'node' === target ? 'Node' : 'Cloudflare'} SSR execution module.`);
222
282
  const runtimeByPath = new Map();
223
283
  const add = (paths, runtime)=>{
224
284
  for (const logicalPath of paths){
@@ -228,6 +288,8 @@ const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
228
288
  }
229
289
  };
230
290
  add(uiClientPaths, 'browser');
291
+ add(files.filter((logicalPath)=>logicalPath === CRAWLER_POLICY_PATH), 'crawler-policy');
292
+ add(files.filter(isPublicMetadataPath), 'public-metadata');
231
293
  add(ssrPaths, 'node' === target ? 'nodejs' : 'workerd');
232
294
  add(apiBackendPaths, 'node' === target ? 'nodejs' : 'workerd-effect');
233
295
  add([
@@ -284,17 +346,17 @@ const pathExists = async (filePath)=>{
284
346
  return false;
285
347
  }
286
348
  };
287
- const emitFrameworkMicroVerticalReleaseEnvelope = async ({ apiOnly, distDirectory, target })=>{
288
- if (apiOnly) return;
349
+ const emitFrameworkMicroVerticalReleaseEnvelope = async ({ apiOnly, distDirectory, requirePromotable = true, target })=>{
289
350
  const backendManifestPath = node_path.join(distDirectory, BACKEND_FEDERATION_MANIFEST_FILE);
290
351
  const backendContainerPath = node_path.join(distDirectory, BACKEND_FEDERATION_REMOTE_ENTRY_FILE);
291
352
  const [hasBackendManifest, hasBackendContainer] = await Promise.all([
292
353
  pathExists(backendManifestPath),
293
354
  pathExists(backendContainerPath)
294
355
  ]);
295
- if (!hasBackendManifest && !hasBackendContainer) return;
296
- if (!hasBackendManifest || !hasBackendContainer) throw new Error('[ultramodern-release-envelope] full-stack MicroVertical backend federation manifest and container must be emitted together.');
297
- const buildArtifact = await readBuildArtifact(distDirectory);
356
+ if (!hasBackendManifest && !hasBackendContainer && !apiOnly) return;
357
+ if (!hasBackendManifest || !hasBackendContainer) throw new Error('[ultramodern-release-envelope] MicroVertical backend federation manifest and container must be emitted together.');
358
+ const buildArtifact = await readBuildArtifact(distDirectory, requirePromotable);
359
+ if (!buildArtifact) return;
298
360
  const identity = {
299
361
  unitId: buildArtifact.deliveryUnit.unitId,
300
362
  buildMarker: buildArtifact.deliveryUnit.buildMarker,
@@ -302,7 +364,7 @@ const emitFrameworkMicroVerticalReleaseEnvelope = async ({ apiOnly, distDirector
302
364
  releaseVersion: buildArtifact.deliveryUnit.version
303
365
  };
304
366
  await readBackendManifest(distDirectory, identity);
305
- const { artifacts, surfaces } = await createReleaseArtifactInputs(distDirectory, identity, target);
367
+ const { artifacts, surfaces } = await createReleaseArtifactInputs(distDirectory, identity, target, apiOnly);
306
368
  const envelope = await createMicroVerticalReleaseEnvelope({
307
369
  artifactRoot: distDirectory,
308
370
  target,
@@ -333,19 +395,16 @@ const readFrameworkMicroVerticalReleaseEnvelope = async ({ artifactRoot, expecte
333
395
  });
334
396
  };
335
397
  const verifyBuildOutputReleaseEnvelope = async (distDirectory, expectedTarget)=>{
336
- const [hasEnvelope, hasBuildArtifact, hasBackendManifest, hasBackendEntry] = await Promise.all([
398
+ const [hasEnvelope, hasBuildArtifact] = await Promise.all([
337
399
  pathExists(node_path.join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH)),
338
- pathExists(node_path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE)),
339
- pathExists(node_path.join(distDirectory, BACKEND_FEDERATION_MANIFEST_FILE)),
340
- pathExists(node_path.join(distDirectory, BACKEND_FEDERATION_REMOTE_ENTRY_FILE))
400
+ pathExists(node_path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE))
341
401
  ]);
342
- const isFullStackMicroVertical = hasBuildArtifact && (hasBackendManifest || hasBackendEntry);
343
402
  return readFrameworkMicroVerticalReleaseEnvelope({
344
403
  artifactRoot: distDirectory,
345
404
  ...expectedTarget ? {
346
405
  expectedTarget
347
406
  } : {},
348
- required: hasEnvelope || isFullStackMicroVertical
407
+ required: hasEnvelope || hasBuildArtifact
349
408
  });
350
409
  };
351
410
  const verifyNodeReleaseEnvelopeStaging = async ({ outputDirectory })=>readFrameworkMicroVerticalReleaseEnvelope({
@@ -353,8 +412,9 @@ const verifyNodeReleaseEnvelopeStaging = async ({ outputDirectory })=>readFramew
353
412
  expectedTarget: 'node',
354
413
  required: true
355
414
  });
356
- const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=>{
415
+ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity, apiOnly, declaredPublicAssets)=>{
357
416
  const files = await collectFiles(outputDirectory);
417
+ const declaredPublicAssetPaths = resolveDeclaredPublicAssets(files, declaredPublicAssets, 'Node');
358
418
  for (const requiredPath of [
359
419
  'index.js',
360
420
  'package.json',
@@ -367,7 +427,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
367
427
  ]);
368
428
  const uiClientPaths = files.filter((logicalPath)=>{
369
429
  const topLevel = logicalPath.split('/')[0];
370
- return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel || [
430
+ return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) && !declaredPublicAssetPaths.has(logicalPath) || [
371
431
  'index.html',
372
432
  'mf-manifest.json',
373
433
  'mf-stats.json',
@@ -378,6 +438,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
378
438
  });
379
439
  const ssrPaths = files.filter((logicalPath)=>'index.js' === logicalPath || logicalPath.startsWith('bundles/') && COMPILED_MODULE_PATTERN.test(logicalPath));
380
440
  const apiBackendPaths = files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath));
441
+ if (apiOnly && (uiClientPaths.length > 0 || ssrPaths.some((path)=>'index.js' !== path))) throw new Error('[ultramodern-release-envelope] final API-only Node staging contains an undeclared UI/client or SSR surface.');
381
442
  for (const [surface, paths] of [
382
443
  [
383
444
  'UI/client',
@@ -385,22 +446,25 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
385
446
  ],
386
447
  [
387
448
  'Node SSR',
388
- ssrPaths
449
+ apiOnly ? [] : ssrPaths
389
450
  ],
390
451
  [
391
452
  'Node API/backend',
392
453
  apiBackendPaths
393
454
  ]
394
- ])if (0 === paths.length) throw new Error(`[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`);
395
- const clientExecutionPaths = await manifestReferencedClientModules(outputDirectory, files);
396
- const ssrExecutionPaths = await routeReferencedSsrModules(outputDirectory, files, 'node');
397
- if (0 === clientExecutionPaths.length || 0 === ssrExecutionPaths.length) throw new Error('[ultramodern-release-envelope] final Node manifests must reference emitted UI/client and SSR execution modules.');
455
+ ])if (0 === paths.length && (!apiOnly || 'Node API/backend' === surface)) throw new Error(`[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`);
456
+ const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(outputDirectory, files);
457
+ const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(outputDirectory, files, 'node');
458
+ if (!apiOnly && (0 === clientExecutionPaths.length || 0 === ssrExecutionPaths.length)) throw new Error('[ultramodern-release-envelope] final Node manifests must reference emitted UI/client and SSR execution modules.');
398
459
  const runtimeByPath = new Map(files.filter((logicalPath)=>!backendFederationPaths.has(logicalPath)).map((logicalPath)=>[
399
460
  logicalPath,
400
461
  'nodejs-deployment'
401
462
  ]));
402
463
  for (const logicalPath of uiClientPaths)runtimeByPath.set(logicalPath, 'browser');
403
- for (const logicalPath of ssrPaths)runtimeByPath.set(logicalPath, 'nodejs');
464
+ for (const logicalPath of declaredPublicAssetPaths)runtimeByPath.set(logicalPath, DECLARED_PUBLIC_ASSET_RUNTIME);
465
+ if (files.includes(CRAWLER_POLICY_PATH)) runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
466
+ for (const logicalPath of files.filter(isPublicMetadataPath))runtimeByPath.set(logicalPath, 'public-metadata');
467
+ for (const logicalPath of apiOnly ? [] : ssrPaths)runtimeByPath.set(logicalPath, 'nodejs');
404
468
  for (const logicalPath of apiBackendPaths)runtimeByPath.set(logicalPath, 'nodejs');
405
469
  runtimeByPath.set(BACKEND_FEDERATION_MANIFEST_FILE, 'module-federation-manifest');
406
470
  runtimeByPath.set(BACKEND_FEDERATION_REMOTE_ENTRY_FILE, 'nodejs');
@@ -410,7 +474,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
410
474
  BACKEND_FEDERATION_MANIFEST_FILE,
411
475
  BACKEND_FEDERATION_REMOTE_ENTRY_FILE
412
476
  ],
413
- ssr: [
477
+ ssr: apiOnly ? [] : [
414
478
  ...new Set([
415
479
  ...ssrExecutionPaths,
416
480
  ...ssrPaths.filter((logicalPath)=>'index.js' !== logicalPath)
@@ -429,7 +493,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
429
493
  uiClient: [
430
494
  ...uiClientPaths
431
495
  ].sort((left, right)=>left.localeCompare(right)),
432
- ssr: [
496
+ ssr: apiOnly ? [] : [
433
497
  ...ssrPaths
434
498
  ].sort((left, right)=>left.localeCompare(right)),
435
499
  apiBackend: [
@@ -442,10 +506,10 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
442
506
  }
443
507
  };
444
508
  };
445
- const emitNodeStagedReleaseEnvelope = async ({ distDirectory, outputDirectory })=>{
509
+ const emitNodeStagedReleaseEnvelope = async ({ declaredPublicAssets = [], distDirectory, outputDirectory })=>{
446
510
  const source = await verifyBuildOutputReleaseEnvelope(distDirectory, 'node');
447
511
  if (!source) return;
448
- const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(outputDirectory, source.identity);
512
+ const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(outputDirectory, source.identity, 0 === source.surfaces.uiClient.length, declaredPublicAssets);
449
513
  const staged = await createMicroVerticalReleaseEnvelope({
450
514
  artifactRoot: outputDirectory,
451
515
  target: 'node',
@@ -463,21 +527,26 @@ const stageCloudflareReleaseEnvelope = async ({ distDirectory, outputDirectory }
463
527
  if (await pathExists(stagedEnvelopePath)) throw new Error('[ultramodern-release-envelope] Cloudflare staging contains a stale envelope before final output generation.');
464
528
  return source;
465
529
  };
466
- const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, identity)=>{
530
+ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, identity, apiOnly, declaredPublicAssets)=>{
467
531
  const files = await collectFiles(outputDirectory);
532
+ const declaredPublicAssetPaths = resolveDeclaredPublicAssets(files, declaredPublicAssets, 'Cloudflare');
468
533
  for (const requiredPath of [
469
534
  'server/index.mjs',
470
535
  'server/modern-worker-manifest.json',
471
536
  'wrangler.json',
472
537
  'package.json',
473
- 'worker/package.json'
538
+ ...apiOnly ? [] : [
539
+ 'worker/package.json'
540
+ ]
474
541
  ])if (!files.includes(requiredPath)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging is missing ${requiredPath}.`);
475
542
  if (files.some((logicalPath)=>'public/release' === logicalPath || logicalPath.startsWith('public/release/'))) throw new Error('[ultramodern-release-envelope] final Cloudflare envelope must remain private and cannot be staged under public/release.');
476
543
  const backendManifestPath = `public/${BACKEND_FEDERATION_MANIFEST_FILE}`;
477
544
  const backendContainerPath = `public/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`;
478
- const apiBackendPaths = files.filter((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath));
479
- const ssrPaths = files.filter((logicalPath)=>(logicalPath.startsWith('server/') || logicalPath.startsWith('worker/')) && COMPILED_MODULE_PATTERN.test(logicalPath) && !EFFECT_BFF_WORKER_PATTERN.test(logicalPath));
480
- const uiClientPaths = files.filter((logicalPath)=>logicalPath.startsWith('public/') && logicalPath !== backendManifestPath && logicalPath !== backendContainerPath);
545
+ const apiBackendPaths = files.filter(isEffectBffWorkerArtifact);
546
+ if (!files.some((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath))) throw new Error('[ultramodern-release-envelope] final Cloudflare staging has no actual Effect API/BFF worker artifact.');
547
+ const ssrPaths = files.filter((logicalPath)=>(logicalPath.startsWith('server/') || logicalPath.startsWith('worker/')) && COMPILED_MODULE_PATTERN.test(logicalPath) && !isEffectBffWorkerArtifact(logicalPath));
548
+ const uiClientPaths = files.filter((logicalPath)=>logicalPath.startsWith('public/') && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) && !declaredPublicAssetPaths.has(logicalPath) && logicalPath !== PUBLIC_BUILD_ARTIFACT_PATH && logicalPath !== backendManifestPath && logicalPath !== backendContainerPath);
549
+ if (apiOnly && (uiClientPaths.length > 0 || ssrPaths.some((path)=>'server/index.mjs' !== path))) throw new Error('[ultramodern-release-envelope] final API-only Cloudflare staging contains an undeclared UI/client or SSR surface.');
481
550
  for (const [surface, paths] of [
482
551
  [
483
552
  'UI/client',
@@ -485,27 +554,32 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
485
554
  ],
486
555
  [
487
556
  'Cloudflare SSR',
488
- ssrPaths
557
+ apiOnly ? [] : ssrPaths
489
558
  ],
490
559
  [
491
560
  'Cloudflare API/backend',
492
561
  apiBackendPaths
493
562
  ]
494
- ])if (0 === paths.length) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`);
563
+ ])if (0 === paths.length && (!apiOnly || 'Cloudflare API/backend' === surface)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`);
495
564
  for (const logicalPath of [
496
565
  backendManifestPath,
497
566
  backendContainerPath
498
567
  ])if (!files.includes(logicalPath)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging is missing ${logicalPath}.`);
499
- const clientExecutionPaths = await manifestReferencedClientModules(outputDirectory, files, 'public/mf-manifest.json');
500
- const ssrExecutionPaths = await routeReferencedSsrModules(outputDirectory, files, 'cloudflare', 'server/route.json');
501
- if (0 === clientExecutionPaths.length || 0 === ssrExecutionPaths.length) throw new Error('[ultramodern-release-envelope] final Cloudflare manifests must reference emitted UI/client and SSR execution modules.');
568
+ const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(outputDirectory, files, 'public/mf-manifest.json');
569
+ const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(outputDirectory, files, 'cloudflare', 'server/route.json');
570
+ if (!apiOnly && (0 === clientExecutionPaths.length || 0 === ssrExecutionPaths.length)) throw new Error('[ultramodern-release-envelope] final Cloudflare manifests must reference emitted UI/client and SSR execution modules.');
502
571
  const runtimeByPath = new Map();
503
572
  for (const logicalPath of files)runtimeByPath.set(logicalPath, 'cloudflare-deployment');
504
573
  const add = (paths, runtime)=>{
505
574
  for (const logicalPath of paths)runtimeByPath.set(logicalPath, runtime);
506
575
  };
507
576
  add(uiClientPaths, 'browser');
508
- add(ssrPaths, 'workerd');
577
+ add([
578
+ ...declaredPublicAssetPaths
579
+ ], DECLARED_PUBLIC_ASSET_RUNTIME);
580
+ if (files.includes(CRAWLER_POLICY_PATH)) runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
581
+ add(files.filter(isPublicMetadataPath), 'public-metadata');
582
+ add(apiOnly ? [] : ssrPaths, 'workerd');
509
583
  add(apiBackendPaths, 'workerd-effect');
510
584
  add([
511
585
  backendManifestPath
@@ -519,7 +593,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
519
593
  backendManifestPath,
520
594
  backendContainerPath
521
595
  ],
522
- ssr: [
596
+ ssr: apiOnly ? [] : [
523
597
  ...new Set([
524
598
  ...ssrExecutionPaths,
525
599
  ...ssrPaths.filter((logicalPath)=>logicalPath.startsWith('worker/'))
@@ -538,7 +612,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
538
612
  uiClient: [
539
613
  ...uiClientPaths
540
614
  ].sort((left, right)=>left.localeCompare(right)),
541
- ssr: [
615
+ ssr: apiOnly ? [] : [
542
616
  ...ssrPaths
543
617
  ].sort((left, right)=>left.localeCompare(right)),
544
618
  apiBackend: [
@@ -551,10 +625,10 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
551
625
  }
552
626
  };
553
627
  };
554
- const emitCloudflareStagedReleaseEnvelope = async ({ distDirectory, outputDirectory })=>{
628
+ const emitCloudflareStagedReleaseEnvelope = async ({ declaredPublicAssets = [], distDirectory, outputDirectory })=>{
555
629
  const source = await verifyBuildOutputReleaseEnvelope(distDirectory, 'cloudflare');
556
630
  if (!source) return;
557
- const { artifacts, surfaces } = await createCloudflareStagedReleaseArtifactInputs(outputDirectory, source.identity);
631
+ const { artifacts, surfaces } = await createCloudflareStagedReleaseArtifactInputs(outputDirectory, source.identity, 0 === source.surfaces.uiClient.length, declaredPublicAssets);
558
632
  const staged = await createMicroVerticalReleaseEnvelope({
559
633
  artifactRoot: outputDirectory,
560
634
  target: 'cloudflare',
@@ -184,7 +184,7 @@ const assertUniqueSortedArtifacts = (artifacts, location)=>{
184
184
  if (paths.some((logicalPath, index)=>logicalPath !== sorted[index])) throw new Error(`${location} must be sorted by logicalPath.`);
185
185
  };
186
186
  const assertSurfacePaths = (value, location)=>{
187
- if (!Array.isArray(value) || 0 === value.length) throw new Error(`${location} must contain at least one artifact path.`);
187
+ if (!Array.isArray(value)) throw new Error(`${location} must be an array of artifact paths.`);
188
188
  const paths = value.map((item, index)=>assertNormalizedLogicalPath(item, `${location}[${index}]`));
189
189
  const sorted = [
190
190
  ...paths
@@ -206,10 +206,16 @@ const assertSurfaces = (value)=>{
206
206
  'manifest',
207
207
  'container'
208
208
  ], 'surfaces.backendFederation');
209
+ const uiClient = assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient');
210
+ const ssr = assertSurfacePaths(surfaces.ssr, 'surfaces.ssr');
211
+ const apiBackend = assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend');
212
+ if (uiClient.length > 0 && 0 === ssr.length) throw new Error('surfaces.ssr must contain at least one artifact path when UI/client is declared.');
213
+ if (ssr.length > 0 && 0 === uiClient.length) throw new Error('surfaces.uiClient must contain at least one artifact path when SSR is declared.');
214
+ if (0 === apiBackend.length) throw new Error('surfaces.apiBackend must contain at least one artifact path.');
209
215
  return {
210
- uiClient: assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient'),
211
- ssr: assertSurfacePaths(surfaces.ssr, 'surfaces.ssr'),
212
- apiBackend: assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend'),
216
+ uiClient,
217
+ ssr,
218
+ apiBackend,
213
219
  backendFederation: {
214
220
  manifest: assertNormalizedLogicalPath(backendFederation.manifest, 'surfaces.backendFederation.manifest'),
215
221
  container: assertNormalizedLogicalPath(backendFederation.container, 'surfaces.backendFederation.container')
@@ -1,47 +1,52 @@
1
1
  import node_path from "node:path";
2
+ import { NODE_PUBLIC_ASSET_SCOPE, normalizeDeclaredPublicAssets, resolveAddedDeclaredPublicAssetPaths } from "../deploy-output/public-assets.mjs";
3
+ import { getDeployingTarget } from "../deploy-output/target.mjs";
2
4
  import { emitFrameworkMicroVerticalReleaseEnvelope, emitNodeStagedReleaseEnvelope, verifyBuildOutputReleaseEnvelope, verifyNodeReleaseEnvelopeStaging } from "./framework-output.mjs";
3
- const resolveActiveDeployTarget = (api, resolveDeployTarget)=>{
4
- const { metaName } = api.getAppContext();
5
- const config = api.getNormalizedConfig();
6
- if ('modern-js' !== metaName && !config.deploy?.target && !process.env.MODERNJS_DEPLOY) return;
7
- return resolveDeployTarget(config);
8
- };
9
- const createUltramodernReleaseEnvelopePlugin = ({ resolveDeployTarget })=>({
5
+ const createUltramodernReleaseEnvelopePlugin = ()=>({
10
6
  name: '@modern-js/ultramodern-release-envelope',
11
7
  pre: [
12
8
  '@modern-js/backend-federation-build',
13
- '@modern-js/plugin-bff'
9
+ '@modern-js/plugin-bff',
10
+ '@modern-js/deploy-output-aliases',
11
+ '@modern-js/deploy-output-public-assets'
14
12
  ],
15
13
  post: [
16
14
  '@modern-js/plugin-deploy'
17
15
  ],
18
16
  setup (api) {
19
- const emitBuildEnvelope = async (target)=>{
17
+ const emitBuildEnvelope = async (target, requirePromotable = true)=>{
20
18
  const { apiOnly, distDirectory } = api.getAppContext();
21
19
  await emitFrameworkMicroVerticalReleaseEnvelope({
22
20
  apiOnly,
23
21
  distDirectory,
22
+ requirePromotable,
24
23
  target
25
24
  });
26
25
  };
27
26
  api.onAfterBuild(async ()=>{
28
- const configuredTarget = resolveDeployTarget(api.getNormalizedConfig());
27
+ const configuredTarget = api.getAppContext().deployTarget?.target;
29
28
  if ('node' !== configuredTarget && 'cloudflare' !== configuredTarget) return;
30
- await emitBuildEnvelope(configuredTarget);
29
+ await emitBuildEnvelope(configuredTarget, false);
31
30
  });
32
31
  api.onBeforeDeploy(async ()=>{
33
- const configuredTarget = resolveActiveDeployTarget(api, resolveDeployTarget);
32
+ const configuredTarget = getDeployingTarget(api.getAppContext());
34
33
  if ('node' !== configuredTarget) return;
35
34
  await emitBuildEnvelope(configuredTarget);
36
35
  const { distDirectory } = api.getAppContext();
37
36
  await verifyBuildOutputReleaseEnvelope(distDirectory, configuredTarget);
38
37
  });
39
38
  api.onAfterDeploy(async ()=>{
40
- const configuredTarget = resolveActiveDeployTarget(api, resolveDeployTarget);
39
+ const configuredTarget = getDeployingTarget(api.getAppContext());
41
40
  if ('node' !== configuredTarget) return;
42
41
  const { appDirectory, distDirectory } = api.getAppContext();
43
42
  const outputDirectory = node_path.join(appDirectory, '.output');
44
43
  const releaseEnvelope = await emitNodeStagedReleaseEnvelope({
44
+ declaredPublicAssets: await resolveAddedDeclaredPublicAssetPaths({
45
+ appDirectory,
46
+ assets: normalizeDeclaredPublicAssets(api.getNormalizedConfig().deploy?.node?.publicAssets, NODE_PUBLIC_ASSET_SCOPE),
47
+ generatedRoot: distDirectory,
48
+ scope: NODE_PUBLIC_ASSET_SCOPE
49
+ }),
45
50
  distDirectory,
46
51
  outputDirectory
47
52
  });
@@ -2,14 +2,13 @@ import { execFileSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  const BUILD_MARKER_NAMESPACE = 'ultramodern-delivery-unit-release-build-marker:v1';
4
4
  const PROMOTABLE_SOURCE_REVISION_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u;
5
+ const workspaceGitEnv = ()=>Object.fromEntries(Object.entries(process.env).filter(([name])=>!name.toUpperCase().startsWith('GIT_')));
5
6
  const gitOutput = (workspaceRoot, args)=>{
6
7
  try {
7
- return execFileSync('git', [
8
- '-C',
9
- workspaceRoot,
10
- ...args
11
- ], {
8
+ return execFileSync('git', args, {
9
+ cwd: workspaceRoot,
12
10
  encoding: 'utf8',
11
+ env: workspaceGitEnv(),
13
12
  stdio: [
14
13
  'ignore',
15
14
  'pipe',
@@ -43,7 +42,7 @@ const resolveUltramodernSourceRevision = (workspaceRoot = process.cwd(), configu
43
42
  ]);
44
43
  const status = gitOutput(gitRoot, [
45
44
  'status',
46
- '--porcelain=v1',
45
+ '--porcelain',
47
46
  '--untracked-files=all'
48
47
  ]);
49
48
  if (!revision || void 0 === status || status.length > 0) return 'workspace';