@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.12 → 3.9.0-ultramodern.14

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 (93) hide show
  1. package/dist/cjs/backend-federation-build/codegen.js +7 -2
  2. package/dist/cjs/backend-federation-build/config.js +86 -36
  3. package/dist/cjs/backend-federation-build/emit.js +17 -13
  4. package/dist/cjs/cloudflare/delivery-unit.js +16 -26
  5. package/dist/cjs/cloudflare/index.js +7 -1
  6. package/dist/cjs/cloudflare/worker-manifest.js +13 -4
  7. package/dist/cjs/cloudflare/wrangler-config.js +35 -7
  8. package/dist/cjs/cloudflare-builder.js +247 -16
  9. package/dist/cjs/cloudflare-output-contract.js +32 -2
  10. package/dist/cjs/cloudflare-output-verifier/identity.js +26 -24
  11. package/dist/cjs/cloudflare-output-verifier/index.js +10 -0
  12. package/dist/cjs/cloudflare-output-verifier/manifest.js +46 -0
  13. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +7 -1
  14. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +10 -5
  15. package/dist/cjs/deploy-output/npmAliases.js +28 -3
  16. package/dist/cjs/release-envelope/framework-output.js +62 -49
  17. package/dist/cjs/release-envelope/index.js +10 -4
  18. package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  19. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  20. package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  21. package/dist/cjs/templates/cloudflare-worker-route-data.mjs +1069 -0
  22. package/dist/esm/backend-federation-build/codegen.mjs +7 -2
  23. package/dist/esm/backend-federation-build/config.mjs +78 -31
  24. package/dist/esm/backend-federation-build/emit.mjs +18 -14
  25. package/dist/esm/cloudflare/delivery-unit.mjs +16 -26
  26. package/dist/esm/cloudflare/index.mjs +7 -1
  27. package/dist/esm/cloudflare/worker-manifest.mjs +13 -4
  28. package/dist/esm/cloudflare/wrangler-config.mjs +35 -7
  29. package/dist/esm/cloudflare-builder.mjs +241 -19
  30. package/dist/esm/cloudflare-output-contract.mjs +25 -1
  31. package/dist/esm/cloudflare-output-verifier/identity.mjs +26 -24
  32. package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
  33. package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
  34. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  35. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +11 -6
  36. package/dist/esm/deploy-output/npmAliases.mjs +28 -3
  37. package/dist/esm/release-envelope/framework-output.mjs +62 -49
  38. package/dist/esm/release-envelope/index.mjs +10 -4
  39. package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  40. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  41. package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  42. package/dist/esm/templates/cloudflare-worker-route-data.mjs +1069 -0
  43. package/dist/esm-node/backend-federation-build/codegen.mjs +7 -2
  44. package/dist/esm-node/backend-federation-build/config.mjs +78 -31
  45. package/dist/esm-node/backend-federation-build/emit.mjs +18 -14
  46. package/dist/esm-node/cloudflare/delivery-unit.mjs +16 -26
  47. package/dist/esm-node/cloudflare/index.mjs +7 -1
  48. package/dist/esm-node/cloudflare/worker-manifest.mjs +13 -4
  49. package/dist/esm-node/cloudflare/wrangler-config.mjs +35 -7
  50. package/dist/esm-node/cloudflare-builder.mjs +241 -19
  51. package/dist/esm-node/cloudflare-output-contract.mjs +25 -1
  52. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +26 -24
  53. package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
  54. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
  55. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  56. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +11 -6
  57. package/dist/esm-node/deploy-output/npmAliases.mjs +28 -3
  58. package/dist/esm-node/release-envelope/framework-output.mjs +62 -49
  59. package/dist/esm-node/release-envelope/index.mjs +10 -4
  60. package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  61. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  62. package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  63. package/dist/esm-node/templates/cloudflare-worker-route-data.mjs +1069 -0
  64. package/dist/types/backend-federation-build/codegen.d.ts +30 -0
  65. package/dist/types/backend-federation-build/config.d.ts +32 -13
  66. package/dist/types/cloudflare/delivery-unit.d.ts +1 -1
  67. package/dist/types/cloudflare/wrangler-config.d.ts +6 -0
  68. package/dist/types/cloudflare-builder.d.ts +45 -1
  69. package/dist/types/cloudflare-output-contract.d.ts +3 -1
  70. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
  71. package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
  72. package/package.json +25 -21
  73. package/src/backend-federation-build/codegen.ts +6 -2
  74. package/src/backend-federation-build/config.ts +207 -71
  75. package/src/backend-federation-build/emit.ts +33 -22
  76. package/src/cloudflare/delivery-unit.ts +39 -33
  77. package/src/cloudflare/index.ts +20 -6
  78. package/src/cloudflare/worker-manifest.ts +19 -3
  79. package/src/cloudflare/wrangler-config.ts +86 -10
  80. package/src/cloudflare-builder.ts +464 -15
  81. package/src/cloudflare-output-contract.ts +35 -2
  82. package/src/cloudflare-output-verifier/identity.ts +4 -0
  83. package/src/cloudflare-output-verifier/index.ts +20 -0
  84. package/src/cloudflare-output-verifier/manifest.ts +77 -0
  85. package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
  86. package/src/cloudflare-output-verifier/worker-bundles.ts +27 -11
  87. package/src/deploy-output/npmAliases.ts +52 -3
  88. package/src/release-envelope/framework-output.ts +163 -90
  89. package/src/release-envelope/index.ts +26 -5
  90. package/src/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  91. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  92. package/src/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  93. package/src/templates/cloudflare-worker-route-data.mjs +1069 -0
@@ -171,8 +171,13 @@ const createBackendManifest = (workspaceRoot, distDirectory, app, entryIntegrity
171
171
  source: {
172
172
  module: sourceModule
173
173
  },
174
- readinessPath: `${app.apiPrefix}/${app.apiStem}/readiness`,
175
- openapiPath: `${app.apiPrefix}/openapi.json`,
174
+ ...'rpc' === app.apiProtocol ? {
175
+ rpcPath: app.rpcPath,
176
+ rpcSerialization: 'json'
177
+ } : {
178
+ readinessPath: `${app.apiPrefix}/${app.apiStem}/readiness`,
179
+ openapiPath: `${app.apiPrefix}/openapi.json`
180
+ },
176
181
  ...app.deliveryUnit ? {
177
182
  deliveryUnit: app.deliveryUnit
178
183
  } : {},
@@ -4,7 +4,8 @@ import promises from "node:fs/promises";
4
4
  import node_path from "node:path";
5
5
  import { resolveUltramodernSourceRevision } from "@modern-js/app-tools-extensions/release-identity";
6
6
  import { BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_REMOTE_ENTRY_FILE, DELIVERY_UNIT_DEPLOY_PROFILE, DELIVERY_UNIT_KIND, DELIVERY_UNIT_SCHEMA_VERSION, ULTRAMODERN_BUILD_ARTIFACT_PATH, deliveryUnitContractBlock, isUltramodernBuildArtifact, validateDeliveryUnitRecord, validateUltramodernBuildArtifact } from "@modern-js/backend-federation-contracts";
7
- const COMPACT_CONFIG_PATH = '.modernjs/ultramodern.json';
7
+ const REFERENCE_TOPOLOGY_PATH = 'topology/reference-topology.json';
8
+ const DEVELOPMENT_OVERLAY_PATH = 'topology/local-overlays/development.json';
8
9
  const normalizeRelativePath = (value)=>value.replace(/\\/gu, '/').replace(/^\.\/+/u, '');
9
10
  const isRecord = (value)=>'object' == typeof value && null !== value;
10
11
  const readJsonFile = async (filePath)=>JSON.parse(await promises.readFile(filePath, 'utf8'));
@@ -32,15 +33,25 @@ const readBuildIdentity = async (appDirectory)=>{
32
33
  const toPascalCase = (value)=>value.split(/[^a-zA-Z0-9]+/u).filter(Boolean).map((part)=>part.charAt(0).toUpperCase() + part.slice(1)).join('');
33
34
  const stringValue = (value)=>'string' == typeof value && value.length > 0 ? value : void 0;
34
35
  const createBackendName = (app, id)=>{
35
- const configuredName = stringValue(app.backendFederation?.name) ?? stringValue(app.backendFederation?.executionSurfaces?.node?.remoteName) ?? stringValue(app.serverExecution?.node?.remoteName);
36
+ const configuredName = stringValue(app.backendFederation?.name) ?? stringValue(app.backendFederation?.executionSurfaces?.node?.remoteName);
36
37
  if (configuredName) return configuredName;
37
38
  const mfName = stringValue(app.moduleFederation?.name);
38
39
  return mfName ? `${mfName}Backend` : `vertical${toPascalCase(id)}Backend`;
39
40
  };
41
+ const rebaseDefaultLocalUrl = (value, defaultPort, publicOrigin)=>{
42
+ let url;
43
+ try {
44
+ url = new URL(value);
45
+ } catch {
46
+ return value;
47
+ }
48
+ if ('http:' !== url.protocol || 'localhost' !== url.hostname || Number(url.port) !== defaultPort || url.username || url.password) return value;
49
+ return `${publicOrigin}${url.pathname}${url.search}${url.hash}`;
50
+ };
40
51
  const findWorkspaceRoot = (appDirectory)=>{
41
52
  let current = appDirectory;
42
53
  while(true){
43
- if (existsSync(node_path.join(current, COMPACT_CONFIG_PATH))) return current;
54
+ if (existsSync(node_path.join(current, REFERENCE_TOPOLOGY_PATH))) return current;
44
55
  const parent = node_path.dirname(current);
45
56
  if (parent === current) return;
46
57
  current = parent;
@@ -64,54 +75,90 @@ const createStampedDeliveryUnit = (input)=>{
64
75
  if (!validation.ok) return;
65
76
  return deliveryUnitContractBlock(record);
66
77
  };
67
- const createAppFromCompactMetadata = (workspaceRoot, appDirectory, compactApp)=>{
68
- const id = stringValue(compactApp.id);
69
- const appPath = stringValue(compactApp.path);
70
- if (!id || 'vertical' !== compactApp.kind || !isRecord(compactApp.api)) return;
71
- const directory = appPath ? normalizeRelativePath(appPath) : `verticals/${id}`;
78
+ const createAppFromTopology = (workspaceRoot, appDirectory, topologyApp, overlay, packageManifest)=>{
79
+ const id = stringValue(topologyApp.id);
80
+ const appPath = stringValue(topologyApp.path);
81
+ if (!id || !appPath || 'vertical' !== topologyApp.kind || !isRecord(topologyApp.api) || !isRecord(topologyApp.backendFederation)) return;
82
+ const directory = normalizeRelativePath(appPath);
72
83
  if (node_path.resolve(workspaceRoot, directory) !== node_path.resolve(appDirectory)) return;
73
- const port = 'number' == typeof compactApp.port ? compactApp.port : void 0;
74
- if (void 0 === port) return;
75
- const apiPrefix = stringValue(compactApp.api.prefix) ?? `/${id}-api`;
76
- const apiStem = stringValue(compactApp.api.stem) ?? id;
77
- const backendName = createBackendName(compactApp, id);
78
- const manifestUrl = stringValue(compactApp.backendFederation?.executionSurfaces?.node?.manifestUrl) ?? stringValue(compactApp.serverExecution?.node?.manifestUrl) ?? `http://localhost:${port}/${BACKEND_FEDERATION_MANIFEST_FILE}`;
79
- const containerEntry = stringValue(compactApp.backendFederation?.executionSurfaces?.node?.containerEntry) ?? stringValue(compactApp.serverExecution?.node?.containerEntry) ?? `http://localhost:${port}/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`;
80
- const configuredRemoteType = stringValue(compactApp.backendFederation?.executionSurfaces?.node?.remoteType) ?? stringValue(compactApp.serverExecution?.node?.remoteType);
84
+ const defaultPort = overlay.ports?.[id];
85
+ if ('number' != typeof defaultPort || !Number.isInteger(defaultPort) || defaultPort < 1 || defaultPort > 65535) throw new Error(`[backend-federation-build] Invalid development port for ${id}.`);
86
+ const portEnv = stringValue(topologyApp.portEnv) ?? `VERTICAL_${(stringValue(topologyApp.domain) ?? id).replace(/[^a-zA-Z0-9]/gu, '_').toUpperCase()}_PORT`;
87
+ const port = Number(process.env[portEnv] ?? defaultPort);
88
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`[backend-federation-build] Invalid ${portEnv} development port for ${id}.`);
89
+ const publicUrlEnv = stringValue(topologyApp.cloudflare?.publicUrlEnv);
90
+ const configuredPublicUrl = publicUrlEnv ? process.env[publicUrlEnv]?.trim() : void 0;
91
+ let publicOrigin = `http://localhost:${port}`;
92
+ if (configuredPublicUrl) {
93
+ let url;
94
+ try {
95
+ url = new URL(configuredPublicUrl);
96
+ } catch {
97
+ throw new Error(`[backend-federation-build] Invalid ${publicUrlEnv} public URL for ${id}.`);
98
+ }
99
+ if (![
100
+ 'http:',
101
+ 'https:'
102
+ ].includes(url.protocol) || url.username || url.password) throw new Error(`[backend-federation-build] Invalid ${publicUrlEnv} public URL for ${id}.`);
103
+ publicOrigin = url.origin;
104
+ }
105
+ const apiPrefix = stringValue(topologyApp.api.bff?.prefix) ?? `/${id}-api`;
106
+ const apiProtocol = topologyApp.api.protocol ?? 'rest';
107
+ if ('rest' !== apiProtocol && 'rpc' !== apiProtocol) throw new Error(`[backend-federation-build] Unsupported API protocol for ${id}.`);
108
+ const rpcPath = 'rpc' === apiProtocol ? stringValue(topologyApp.api.rpcPath) : void 0;
109
+ if ('rpc' === apiProtocol && (!rpcPath || 'json' !== topologyApp.api.rpcSerialization)) throw new Error(`[backend-federation-build] Missing JSON RPC route metadata for ${id}.`);
110
+ const apiStem = stringValue(topologyApp.api.stem) ?? id;
111
+ const backendName = createBackendName(topologyApp, id);
112
+ const nodeOverlay = overlay.serverExecution?.[id]?.node;
113
+ if (!isRecord(nodeOverlay)) throw new Error(`[backend-federation-build] Missing declared Node server execution for ${id}.`);
114
+ const manifestUrl = rebaseDefaultLocalUrl(stringValue(nodeOverlay?.manifestUrl) ?? `http://localhost:${defaultPort}/${BACKEND_FEDERATION_MANIFEST_FILE}`, defaultPort, publicOrigin);
115
+ const containerEntry = rebaseDefaultLocalUrl(stringValue(nodeOverlay?.containerEntry) ?? `http://localhost:${defaultPort}/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`, defaultPort, publicOrigin);
116
+ const configuredRemoteType = stringValue(nodeOverlay?.remoteType) ?? stringValue(topologyApp.backendFederation?.executionSurfaces?.node?.remoteType);
81
117
  if (void 0 !== configuredRemoteType && 'commonjs-module' !== configuredRemoteType) throw new Error(`[backend-federation-build] Node backend federation remoteType must be "commonjs-module"; received "${configuredRemoteType}" for ${id}.`);
82
118
  if (!new URL(containerEntry).pathname.endsWith(`/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`)) throw new Error(`[backend-federation-build] Node backend federation containerEntry must end with "/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}"; received "${containerEntry}" for ${id}.`);
83
119
  const remoteType = 'commonjs-module';
84
- const packageName = stringValue(compactApp.package);
85
- const uiManifestUrl = stringValue(compactApp.backendFederation?.versionBoundary?.ui?.manifestUrl) ?? stringValue(compactApp.moduleFederation?.manifestUrl);
86
- const compactDeliveryUnit = isRecord(compactApp.deliveryUnit) ? {
87
- unitId: stringValue(compactApp.deliveryUnit.unitId),
88
- buildMarker: stringValue(compactApp.deliveryUnit.buildMarker),
89
- sourceRevision: stringValue(compactApp.deliveryUnit.sourceRevision),
90
- packageName: stringValue(compactApp.deliveryUnit.packageName),
91
- version: stringValue(compactApp.deliveryUnit.version)
120
+ const packageName = stringValue(packageManifest.name);
121
+ const version = stringValue(packageManifest.version);
122
+ if (stringValue(topologyApp.package) !== packageName || !version) throw new Error(`[backend-federation-build] Topology package identity must match package.json for ${id}.`);
123
+ const declaredUiManifestUrl = stringValue(overlay.manifests?.[id]);
124
+ const uiManifestUrl = declaredUiManifestUrl ? rebaseDefaultLocalUrl(declaredUiManifestUrl, defaultPort, publicOrigin) : void 0;
125
+ const topologyDeliveryUnit = isRecord(topologyApp.deliveryUnit) ? {
126
+ unitId: stringValue(topologyApp.deliveryUnit.unitId),
127
+ buildMarker: stringValue(topologyApp.deliveryUnit.buildMarker),
128
+ sourceRevision: stringValue(topologyApp.deliveryUnit.sourceRevision),
129
+ packageName: stringValue(topologyApp.deliveryUnit.packageName),
130
+ version: stringValue(topologyApp.deliveryUnit.version)
92
131
  } : void 0;
93
132
  return {
94
133
  id,
95
134
  directory,
96
135
  packageName,
136
+ version,
97
137
  port,
98
138
  apiPrefix,
139
+ apiProtocol,
140
+ rpcPath,
99
141
  apiStem,
100
142
  backendName,
101
143
  manifestUrl,
102
144
  containerEntry,
103
145
  remoteType,
104
146
  uiManifestUrl,
105
- compactDeliveryUnit
147
+ topologyDeliveryUnit
106
148
  };
107
149
  };
108
150
  const findBackendFederationApp = async (workspaceRoot, appDirectory)=>{
109
- const compactConfigPath = node_path.join(workspaceRoot, COMPACT_CONFIG_PATH);
110
- const compactConfig = await readJsonFile(compactConfigPath);
111
- const apps = Array.isArray(compactConfig.topology?.apps) ? compactConfig.topology.apps : [];
112
- for (const app of apps){
113
- const resolved = createAppFromCompactMetadata(workspaceRoot, appDirectory, app);
151
+ const topologyPath = node_path.join(workspaceRoot, REFERENCE_TOPOLOGY_PATH);
152
+ const topology = await readJsonFile(topologyPath);
153
+ if (!isRecord(topology) || !Array.isArray(topology.verticals)) throw new Error(`[backend-federation-build] Invalid declared topology at ${topologyPath}.`);
154
+ const overlayPath = node_path.join(workspaceRoot, DEVELOPMENT_OVERLAY_PATH);
155
+ const overlay = await readJsonFile(overlayPath);
156
+ if (!isRecord(overlay) || !isRecord(overlay.ports)) throw new Error(`[backend-federation-build] Invalid development overlay at ${overlayPath}.`);
157
+ const packageManifest = await readJsonFile(node_path.join(appDirectory, 'package.json'));
158
+ for (const app of topology.verticals){
159
+ const resolved = createAppFromTopology(workspaceRoot, appDirectory, app, overlay, packageManifest);
114
160
  if (resolved) return resolved;
115
161
  }
162
+ throw new Error(`[backend-federation-build] ${appDirectory} is missing from the declared reference topology.`);
116
163
  };
117
- export { COMPACT_CONFIG_PATH, buildArtifactPathFor, createAppFromCompactMetadata, createStampedDeliveryUnit, findBackendFederationApp, findWorkspaceRoot, normalizeRelativePath, readBuildIdentity, resolveWorkspaceSourceRevision };
164
+ export { DEVELOPMENT_OVERLAY_PATH, REFERENCE_TOPOLOGY_PATH, buildArtifactPathFor, createAppFromTopology, createStampedDeliveryUnit, findBackendFederationApp, findWorkspaceRoot, normalizeRelativePath, readBuildIdentity, resolveWorkspaceSourceRevision };
@@ -6,7 +6,7 @@ import { resolveUltramodernReleaseIdentity } from "@modern-js/app-tools-extensio
6
6
  import { BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_REMOTE_ENTRY_FILE, ULTRAMODERN_BUILD_ARTIFACT_FILE, stampUltramodernBuildArtifactIdentity } from "@modern-js/backend-federation-contracts";
7
7
  import { createBackendFederationEntryIntegrity } from "@modern-js/server-runtime-extensions/backend-federation-security/node";
8
8
  import { createBackendManifest, createBackendRemoteEntrySource } from "./codegen.mjs";
9
- import { COMPACT_CONFIG_PATH, buildArtifactPathFor, createStampedDeliveryUnit, findBackendFederationApp, findWorkspaceRoot, readBuildIdentity, resolveWorkspaceSourceRevision } from "./config.mjs";
9
+ import { REFERENCE_TOPOLOGY_PATH, buildArtifactPathFor, createStampedDeliveryUnit, findBackendFederationApp, findWorkspaceRoot, readBuildIdentity, resolveWorkspaceSourceRevision } from "./config.mjs";
10
10
  const emitBackendFederationArtifacts = async (appDirectory, distDirectory)=>{
11
11
  const workspaceRoot = findWorkspaceRoot(appDirectory);
12
12
  if (!workspaceRoot) return;
@@ -16,35 +16,39 @@ const emitBackendFederationArtifacts = async (appDirectory, distDirectory)=>{
16
16
  const app = await findBackendFederationApp(workspaceRoot, appDirectory);
17
17
  if (!app) return;
18
18
  const buildIdentity = await readBuildIdentity(appDirectory);
19
- const compactDeliveryUnit = app.compactDeliveryUnit;
20
- const hasCompactDeliveryUnit = void 0 !== compactDeliveryUnit && (void 0 !== compactDeliveryUnit.unitId || void 0 !== compactDeliveryUnit.buildMarker || void 0 !== compactDeliveryUnit.packageName || void 0 !== compactDeliveryUnit.version);
19
+ const topologyDeliveryUnit = app.topologyDeliveryUnit;
20
+ const hasTopologyDeliveryUnit = void 0 !== topologyDeliveryUnit && (void 0 !== topologyDeliveryUnit.unitId || void 0 !== topologyDeliveryUnit.buildMarker || void 0 !== topologyDeliveryUnit.packageName || void 0 !== topologyDeliveryUnit.version);
21
21
  const hasBuildIdentity = void 0 !== buildIdentity.unitId || void 0 !== buildIdentity.buildVersion || void 0 !== buildIdentity.packageName || void 0 !== buildIdentity.version;
22
22
  if (hasBuildIdentity) {
23
- const compactConfigPath = node_path.join(workspaceRoot, COMPACT_CONFIG_PATH);
23
+ const topologyPath = node_path.join(workspaceRoot, REFERENCE_TOPOLOGY_PATH);
24
24
  const buildIdentityPath = buildArtifactPathFor(appDirectory);
25
25
  const mismatches = [];
26
26
  const compare = (label, a, b, leftLabel = 'deliveryUnit')=>{
27
27
  if (void 0 !== a && void 0 !== b && a !== b) mismatches.push(`${label}: ${leftLabel}=${a} vs ultramodern-build=${b}`);
28
28
  };
29
29
  compare('appId', app.id, buildIdentity.appId, 'topology');
30
- if (hasCompactDeliveryUnit) {
31
- compare('unitId', compactDeliveryUnit?.unitId, buildIdentity.unitId);
32
- compare('buildMarker/build', compactDeliveryUnit?.buildMarker, buildIdentity.buildVersion);
33
- compare('packageName', compactDeliveryUnit?.packageName, buildIdentity.packageName);
34
- compare('version', compactDeliveryUnit?.version, buildIdentity.version);
30
+ if (hasTopologyDeliveryUnit) {
31
+ compare('unitId', topologyDeliveryUnit?.unitId, buildIdentity.unitId);
32
+ compare('buildMarker/build', topologyDeliveryUnit?.buildMarker, buildIdentity.buildVersion);
33
+ compare('packageName', topologyDeliveryUnit?.packageName, buildIdentity.packageName);
34
+ compare('version', topologyDeliveryUnit?.version, buildIdentity.version);
35
35
  }
36
- if (mismatches.length > 0) throw new Error(`[backend-federation-build] Delivery-unit identity drift between ${compactConfigPath} (topology) and ${buildIdentityPath}: ${mismatches.join('; ')}`);
36
+ compare('packageName', app.packageName, buildIdentity.packageName, 'package.json');
37
+ compare('packageName', topologyDeliveryUnit?.packageName, app.packageName, 'topology');
38
+ compare('version', app.version, buildIdentity.version, 'package.json');
39
+ compare('version', topologyDeliveryUnit?.version, app.version, 'topology');
40
+ if (mismatches.length > 0) throw new Error(`[backend-federation-build] Delivery-unit identity drift between ${topologyPath}, package.json and ${buildIdentityPath}: ${mismatches.join('; ')}`);
37
41
  }
38
- const unitId = compactDeliveryUnit?.unitId ?? buildIdentity.unitId;
39
- const generationBuildMarker = compactDeliveryUnit?.buildMarker ?? buildIdentity.buildVersion;
42
+ const unitId = topologyDeliveryUnit?.unitId ?? buildIdentity.unitId;
43
+ const generationBuildMarker = topologyDeliveryUnit?.buildMarker ?? buildIdentity.buildVersion;
40
44
  const sourceRevision = await resolveWorkspaceSourceRevision(workspaceRoot);
41
45
  const buildVersion = generationBuildMarker && unitId ? resolveUltramodernReleaseIdentity({
42
46
  generationBuildMarker,
43
47
  unitId,
44
48
  workspaceRoot
45
49
  }).buildMarker : void 0;
46
- const packageName = compactDeliveryUnit?.packageName ?? buildIdentity.packageName ?? app.packageName;
47
- const version = compactDeliveryUnit?.version ?? buildIdentity.version;
50
+ const packageName = app.packageName;
51
+ const version = app.version;
48
52
  const deliveryUnit = createStampedDeliveryUnit({
49
53
  appId: app.id,
50
54
  unitId,
@@ -4,11 +4,11 @@ import { ULTRAMODERN_BUILD_ARTIFACT_PATH, isUltramodernBuildArtifact, nonEmptySt
4
4
  import { fs } from "@modern-js/utils";
5
5
  import { resolveUltramodernReleaseIdentity } from "../release-identity.mjs";
6
6
  import { isRecord } from "./utils.mjs";
7
- const COMPACT_CONFIG_PATH = '.modernjs/ultramodern.json';
7
+ const TOPOLOGY_PATH = 'topology/reference-topology.json';
8
8
  const findWorkspaceRoot = async (appDirectory)=>{
9
9
  let current = node_path.resolve(appDirectory);
10
10
  for(;;){
11
- if (await fs.pathExists(node_path.join(current, COMPACT_CONFIG_PATH))) return current;
11
+ if (await fs.pathExists(node_path.join(current, TOPOLOGY_PATH))) return current;
12
12
  const parent = node_path.dirname(current);
13
13
  if (parent === current) return;
14
14
  current = parent;
@@ -25,36 +25,25 @@ const stampReleaseIdentity = (identity, workspaceRoot)=>{
25
25
  })
26
26
  };
27
27
  };
28
- const resolveCompactApp = async (appDirectory)=>{
28
+ const resolveTopologyApp = async (appDirectory)=>{
29
29
  const workspaceRoot = await findWorkspaceRoot(appDirectory);
30
30
  if (!workspaceRoot) return;
31
- let compactConfig;
32
- try {
33
- compactConfig = await fs.readJSON(node_path.join(workspaceRoot, COMPACT_CONFIG_PATH));
34
- } catch {
35
- return {
36
- workspaceRoot
37
- };
38
- }
39
- if (!isRecord(compactConfig)) return {
40
- workspaceRoot
41
- };
42
- const topology = isRecord(compactConfig.topology) ? compactConfig.topology : void 0;
43
- const apps = Array.isArray(topology?.apps) ? topology.apps : void 0;
44
- if (!apps) return {
45
- app: compactConfig,
46
- workspaceRoot
47
- };
31
+ const topology = await fs.readJSON(node_path.join(workspaceRoot, TOPOLOGY_PATH));
32
+ if (!isRecord(topology) || !isRecord(topology.shell) || !Array.isArray(topology.verticals) || void 0 !== topology.shells && !Array.isArray(topology.shells)) throw new Error('[cloudflare-delivery-unit] Invalid declared reference topology.');
33
+ const apps = [
34
+ topology.shell,
35
+ ...topology.verticals,
36
+ ...Array.isArray(topology.shells) ? topology.shells : []
37
+ ];
48
38
  const resolvedAppDirectory = node_path.resolve(appDirectory);
49
39
  const app = apps.find((candidate)=>{
50
40
  if (!isRecord(candidate)) return false;
51
41
  const appPath = nonEmptyString(candidate.path);
52
42
  return void 0 !== appPath && node_path.resolve(workspaceRoot, appPath.replace(/^\.\/+/u, '')) === resolvedAppDirectory;
53
43
  });
44
+ if (!isRecord(app)) throw new Error(`[cloudflare-delivery-unit] ${appDirectory} is missing from the declared reference topology.`);
54
45
  return {
55
- ...isRecord(app) ? {
56
- app
57
- } : {},
46
+ app,
58
47
  workspaceRoot
59
48
  };
60
49
  };
@@ -81,10 +70,11 @@ const createDeliveryUnitStamp = (identity, app)=>{
81
70
  };
82
71
  };
83
72
  const resolveTopologyDeliveryUnit = async (appDirectory)=>{
84
- const resolved = await resolveCompactApp(appDirectory);
73
+ const resolved = await resolveTopologyApp(appDirectory);
85
74
  if (!resolved?.app) return;
86
75
  const identity = toDeliveryUnitIdentity(resolved.app.deliveryUnit);
87
- return identity ? createDeliveryUnitStamp(stampReleaseIdentity(identity, resolved.workspaceRoot), resolved.app) : void 0;
76
+ if (!identity) throw new Error('[cloudflare-delivery-unit] Declared app is missing a valid delivery-unit identity.');
77
+ return createDeliveryUnitStamp(stampReleaseIdentity(identity, resolved.workspaceRoot), resolved.app);
88
78
  };
89
79
  const resolveWorkerDeliveryUnitStamp = async (appDirectory)=>{
90
80
  const buildArtifactPath = node_path.join(appDirectory, ULTRAMODERN_BUILD_ARTIFACT_PATH);
@@ -95,7 +85,7 @@ const resolveWorkerDeliveryUnitStamp = async (appDirectory)=>{
95
85
  identity = toDeliveryUnitIdentity(artifact.deliveryUnit);
96
86
  }
97
87
  if (!identity) return;
98
- const resolved = await resolveCompactApp(appDirectory);
88
+ const resolved = await resolveTopologyApp(appDirectory);
99
89
  if (resolved) identity = stampReleaseIdentity(identity, resolved.workspaceRoot);
100
90
  return createDeliveryUnitStamp(identity, resolved?.app);
101
91
  };
@@ -1,5 +1,6 @@
1
1
  import "node:module";
2
2
  import node_path from "node:path";
3
+ import { BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_REMOTE_ENTRY_FILE, ULTRAMODERN_BUILD_ARTIFACT_FILE } from "@modern-js/backend-federation-contracts";
3
4
  import { fs } from "@modern-js/utils";
4
5
  import { createCloudflareOutputPlan } from "../cloudflare-output-plan.mjs";
5
6
  import { assertCloudflareOutput } from "../cloudflare-output-verifier/index.mjs";
@@ -37,7 +38,7 @@ const createCloudflarePreset = ({ api, appContext, modernConfig })=>{
37
38
  await fs.remove(outputDirectory);
38
39
  },
39
40
  async writeOutput () {
40
- if (releaseEnvelopeEnabled && await fs.pathExists(node_path.join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH))) await emitFrameworkMicroVerticalReleaseEnvelope({
41
+ if (releaseEnvelopeEnabled) await emitFrameworkMicroVerticalReleaseEnvelope({
41
42
  apiOnly,
42
43
  distDirectory,
43
44
  target: 'cloudflare'
@@ -46,6 +47,11 @@ const createCloudflarePreset = ({ api, appContext, modernConfig })=>{
46
47
  await fs.copy(distDirectory, publicDirectory, {
47
48
  filter: (src)=>{
48
49
  const relativePath = node_path.relative(distDirectory, src).replace(/\\/gu, '/');
50
+ if (apiOnly && '' !== relativePath && ![
51
+ BACKEND_FEDERATION_MANIFEST_FILE,
52
+ BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
53
+ ULTRAMODERN_BUILD_ARTIFACT_FILE
54
+ ].includes(relativePath)) return false;
49
55
  return 'release' !== relativePath && !relativePath.startsWith('release/') && 'public' !== relativePath && !relativePath.startsWith('public/') && shouldCopyToPublicAssets(src, distDirectory, publicAssetExcludes);
50
56
  }
51
57
  });
@@ -1,6 +1,7 @@
1
1
  import "node:module";
2
2
  import node_path from "node:path";
3
3
  import { fs } from "@modern-js/utils";
4
+ import { getCloudflareWorkerRouteDataEntryName } from "../cloudflare-output-contract.mjs";
4
5
  import { readRouteSpec } from "./artifacts.mjs";
5
6
  import { ASSETS_BINDING, BFF_EFFECT_WORKER_DISPATCHER_EXPORT, BFF_EFFECT_WORKER_ENTRY, CLOUDFLARE_RUNTIME_TYPE, CLOUDFLARE_WORKER_BUNDLE_FORMAT, DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES, EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE, LOADABLE_STATS_FILE, PUBLIC_ASSETS_DIRECTORY, ROUTE_MANIFEST_FILE, ROUTE_SPEC_FILE, ROUTE_SPEC_OUTPUT, SERVER_BUNDLE_DIRECTORY, WORKER_BUNDLE_DIRECTORY, WORKER_ENTRY } from "./constants.mjs";
6
7
  import { createI18nWorkerManifest } from "./i18n-worker.mjs";
@@ -66,13 +67,18 @@ const createWorkerManifest = async (outputDirectory, modernConfig, appContext, d
66
67
  const routeSpec = await readRouteSpec(outputDirectory);
67
68
  const routes = await Promise.all(routeSpec.routes.map(async (route)=>{
68
69
  const worker = 'string' == typeof route.worker ? route.worker : void 0;
70
+ const routeDataWorker = route.isSSR && 'string' == typeof route.entryName ? `${WORKER_BUNDLE_DIRECTORY}/${getCloudflareWorkerRouteDataEntryName(route.entryName)}.js` : void 0;
71
+ const hasRouteDataWorker = routeDataWorker ? await fs.pathExists(node_path.join(outputDirectory, routeDataWorker)) : false;
69
72
  return {
70
73
  urlPath: route.urlPath,
71
74
  entryName: route.entryName,
72
75
  entryPath: route.entryPath,
73
76
  isSSR: Boolean(route.isSSR),
74
77
  worker,
75
- workerExists: worker ? await fs.pathExists(node_path.join(outputDirectory, worker)) : false
78
+ workerExists: worker ? await fs.pathExists(node_path.join(outputDirectory, worker)) : false,
79
+ ...hasRouteDataWorker ? {
80
+ routeDataWorker
81
+ } : {}
76
82
  };
77
83
  }));
78
84
  const bffPrefix = modernConfig.bff?.prefix;
@@ -132,9 +138,12 @@ const createWorkerManifest = async (outputDirectory, modernConfig, appContext, d
132
138
  };
133
139
  const createWorkerModuleLoaders = (manifest)=>{
134
140
  const imports = new Map();
135
- for (const route of manifest.routeSpec.routes)if (route.worker && route.workerExists) {
136
- const importPath = `../${String(route.worker).replace(/^\/+/u, '')}`;
137
- imports.set(route.worker, `() => import(${JSON.stringify(importPath)})`);
141
+ for (const route of manifest.routeSpec.routes)for (const worker of [
142
+ route.workerExists ? route.worker : void 0,
143
+ route.routeDataWorker
144
+ ])if (worker) {
145
+ const importPath = `../${String(worker).replace(/^\/+/u, '')}`;
146
+ imports.set(worker, `() => import(${JSON.stringify(importPath)})`);
138
147
  }
139
148
  if (manifest.bff?.worker) {
140
149
  const importPath = `../${String(manifest.bff.worker).replace(/^\/+/u, '')}`;
@@ -2,12 +2,16 @@ import "node:module";
2
2
  import node_path from "node:path";
3
3
  import { ASSETS_BINDING, COMPATIBILITY_DATE_PATTERN, DEFAULT_COMPATIBILITY_DATE, PUBLIC_ASSETS_DIRECTORY, REQUIRED_COMPATIBILITY_FLAGS, WORKER_ENTRY } from "./constants.mjs";
4
4
  import { isJsonRecord, normalizeRelativePath } from "./utils.mjs";
5
- const getCompatibilityDate = (modernConfig)=>{
6
- const configuredDate = modernConfig.deploy?.worker?.compatibilityDate?.trim();
7
- const compatibilityDate = configuredDate || DEFAULT_COMPATIBILITY_DATE;
8
- if (!COMPATIBILITY_DATE_PATTERN.test(compatibilityDate)) throw new Error(`deploy.worker.compatibilityDate must use YYYY-MM-DD, received ${JSON.stringify(compatibilityDate)}.`);
5
+ const assertSupportedCompatibilityDate = (compatibilityDate, label)=>{
6
+ if ('string' != typeof compatibilityDate) throw new Error(`${label} must be a YYYY-MM-DD string.`);
7
+ if (!COMPATIBILITY_DATE_PATTERN.test(compatibilityDate)) throw new Error(`${label} must use YYYY-MM-DD, received ${JSON.stringify(compatibilityDate)}.`);
8
+ if (compatibilityDate < DEFAULT_COMPATIBILITY_DATE) throw new Error(`${label} must be ${DEFAULT_COMPATIBILITY_DATE} or later, the date the Cloudflare worker Node.js built-in contract is verified against; received ${JSON.stringify(compatibilityDate)}.`);
9
9
  return compatibilityDate;
10
10
  };
11
+ const getCompatibilityDate = (modernConfig, wranglerCompatibilityDate)=>{
12
+ if (void 0 !== wranglerCompatibilityDate) return assertSupportedCompatibilityDate(wranglerCompatibilityDate, 'deploy.worker.wrangler.compatibility_date');
13
+ return assertSupportedCompatibilityDate(modernConfig.deploy?.worker?.compatibilityDate?.trim() || DEFAULT_COMPATIBILITY_DATE, 'deploy.worker.compatibilityDate');
14
+ };
11
15
  const getWorkerName = (appDirectory)=>{
12
16
  const basename = node_path.basename(appDirectory);
13
17
  return basename.replace(/[^a-zA-Z0-9-_]/g, '-') || 'modern-cloudflare-worker';
@@ -22,11 +26,11 @@ const getConfiguredWrangler = (modernConfig)=>{
22
26
  if (!isJsonRecord(wrangler)) throw new Error('deploy.worker.wrangler must be a JSON object.');
23
27
  return wrangler;
24
28
  };
25
- const createWranglerCompatibilityFlags = (configuredFlags)=>{
29
+ const createWranglerCompatibilityFlags = (configuredFlags, label = 'deploy.worker.wrangler.compatibility_flags')=>{
26
30
  if (void 0 === configuredFlags) return [
27
31
  ...REQUIRED_COMPATIBILITY_FLAGS
28
32
  ];
29
- if (!Array.isArray(configuredFlags) || configuredFlags.some((flag)=>'string' != typeof flag)) throw new Error('deploy.worker.wrangler.compatibility_flags must be an array of strings.');
33
+ if (!Array.isArray(configuredFlags) || configuredFlags.some((flag)=>'string' != typeof flag)) throw new Error(`${label} must be an array of strings.`);
30
34
  return [
31
35
  ...new Set([
32
36
  ...configuredFlags,
@@ -34,6 +38,26 @@ const createWranglerCompatibilityFlags = (configuredFlags)=>{
34
38
  ])
35
39
  ];
36
40
  };
41
+ const createWranglerEnvironments = (wranglerEnv)=>{
42
+ if (void 0 === wranglerEnv) return;
43
+ if (!isJsonRecord(wranglerEnv)) throw new Error('deploy.worker.wrangler.env must be an object.');
44
+ return Object.fromEntries(Object.entries(wranglerEnv).map(([name, environment])=>{
45
+ const label = `deploy.worker.wrangler.env.${name}`;
46
+ if (!isJsonRecord(environment)) throw new Error(`${label} must be an object.`);
47
+ return [
48
+ name,
49
+ {
50
+ ...environment,
51
+ ...void 0 === environment.compatibility_date ? {} : {
52
+ compatibility_date: assertSupportedCompatibilityDate(environment.compatibility_date, `${label}.compatibility_date`)
53
+ },
54
+ ...void 0 === environment.compatibility_flags ? {} : {
55
+ compatibility_flags: createWranglerCompatibilityFlags(environment.compatibility_flags, `${label}.compatibility_flags`)
56
+ }
57
+ }
58
+ ];
59
+ }));
60
+ };
37
61
  const createWranglerAssetsConfig = (configuredAssets)=>{
38
62
  if (void 0 !== configuredAssets && !isJsonRecord(configuredAssets)) throw new Error('deploy.worker.wrangler.assets must be an object.');
39
63
  return {
@@ -130,17 +154,21 @@ const createWorkerManifestServiceBindings = (serviceBindings)=>{
130
154
  };
131
155
  const createWranglerConfig = (appDirectory, modernConfig)=>{
132
156
  const wrangler = getConfiguredWrangler(modernConfig);
157
+ const environments = createWranglerEnvironments(wrangler.env);
133
158
  const d1Databases = createWranglerD1Databases(modernConfig, wrangler.d1_databases);
134
159
  const serviceBindings = createWorkerServiceBindings(modernConfig, wrangler.services);
135
160
  const wranglerServices = createWranglerServices(serviceBindings);
136
161
  return {
137
162
  $schema: 'node_modules/wrangler/config-schema.json',
138
163
  name: getConfiguredWorkerName(appDirectory, modernConfig),
139
- compatibility_date: getCompatibilityDate(modernConfig),
140
164
  ...wrangler,
165
+ compatibility_date: getCompatibilityDate(modernConfig, wrangler.compatibility_date),
141
166
  main: WORKER_ENTRY,
142
167
  compatibility_flags: createWranglerCompatibilityFlags(wrangler.compatibility_flags),
143
168
  assets: createWranglerAssetsConfig(wrangler.assets),
169
+ ...void 0 === environments ? {} : {
170
+ env: environments
171
+ },
144
172
  ...void 0 === d1Databases ? {} : {
145
173
  d1_databases: d1Databases
146
174
  },