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