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