@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.11 → 3.9.0-ultramodern.13

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 (72) 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/build-config/remote-address.js +56 -0
  5. package/dist/cjs/cloudflare/delivery-unit.js +16 -26
  6. package/dist/cjs/cloudflare/index.js +7 -1
  7. package/dist/cjs/cloudflare-builder.js +13 -3
  8. package/dist/cjs/cloudflare-output-contract.js +1 -0
  9. package/dist/cjs/cloudflare-output-verifier/identity.js +26 -24
  10. package/dist/cjs/cloudflare-output-verifier/index.js +10 -0
  11. package/dist/cjs/cloudflare-output-verifier/manifest.js +46 -0
  12. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +7 -1
  13. package/dist/cjs/config.js +4 -0
  14. package/dist/cjs/deploy-output/npmAliases.js +28 -3
  15. package/dist/cjs/release-envelope/framework-output.js +62 -49
  16. package/dist/cjs/release-envelope/index.js +10 -4
  17. package/dist/esm/backend-federation-build/codegen.mjs +7 -2
  18. package/dist/esm/backend-federation-build/config.mjs +78 -31
  19. package/dist/esm/backend-federation-build/emit.mjs +18 -14
  20. package/dist/esm/build-config/remote-address.mjs +18 -0
  21. package/dist/esm/cloudflare/delivery-unit.mjs +16 -26
  22. package/dist/esm/cloudflare/index.mjs +7 -1
  23. package/dist/esm/cloudflare-builder.mjs +13 -3
  24. package/dist/esm/cloudflare-output-contract.mjs +1 -0
  25. package/dist/esm/cloudflare-output-verifier/identity.mjs +26 -24
  26. package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
  27. package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
  28. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  29. package/dist/esm/config.mjs +1 -0
  30. package/dist/esm/deploy-output/npmAliases.mjs +28 -3
  31. package/dist/esm/release-envelope/framework-output.mjs +62 -49
  32. package/dist/esm/release-envelope/index.mjs +10 -4
  33. package/dist/esm-node/backend-federation-build/codegen.mjs +7 -2
  34. package/dist/esm-node/backend-federation-build/config.mjs +78 -31
  35. package/dist/esm-node/backend-federation-build/emit.mjs +18 -14
  36. package/dist/esm-node/build-config/remote-address.mjs +19 -0
  37. package/dist/esm-node/cloudflare/delivery-unit.mjs +16 -26
  38. package/dist/esm-node/cloudflare/index.mjs +7 -1
  39. package/dist/esm-node/cloudflare-builder.mjs +13 -3
  40. package/dist/esm-node/cloudflare-output-contract.mjs +1 -0
  41. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +26 -24
  42. package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
  43. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
  44. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  45. package/dist/esm-node/config.mjs +1 -0
  46. package/dist/esm-node/deploy-output/npmAliases.mjs +28 -3
  47. package/dist/esm-node/release-envelope/framework-output.mjs +62 -49
  48. package/dist/esm-node/release-envelope/index.mjs +10 -4
  49. package/dist/types/backend-federation-build/codegen.d.ts +30 -0
  50. package/dist/types/backend-federation-build/config.d.ts +32 -13
  51. package/dist/types/build-config/remote-address.d.ts +3 -0
  52. package/dist/types/cloudflare/delivery-unit.d.ts +1 -1
  53. package/dist/types/cloudflare-builder.d.ts +1 -0
  54. package/dist/types/cloudflare-output-contract.d.ts +1 -1
  55. package/dist/types/config.d.ts +1 -0
  56. package/package.json +20 -19
  57. package/src/backend-federation-build/codegen.ts +6 -2
  58. package/src/backend-federation-build/config.ts +207 -71
  59. package/src/backend-federation-build/emit.ts +33 -22
  60. package/src/build-config/remote-address.ts +31 -0
  61. package/src/cloudflare/delivery-unit.ts +39 -33
  62. package/src/cloudflare/index.ts +20 -6
  63. package/src/cloudflare-builder.ts +18 -4
  64. package/src/cloudflare-output-contract.ts +1 -0
  65. package/src/cloudflare-output-verifier/identity.ts +4 -0
  66. package/src/cloudflare-output-verifier/index.ts +20 -0
  67. package/src/cloudflare-output-verifier/manifest.ts +77 -0
  68. package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
  69. package/src/config.ts +1 -0
  70. package/src/deploy-output/npmAliases.ts +52 -3
  71. package/src/release-envelope/framework-output.ts +163 -90
  72. package/src/release-envelope/index.ts +26 -5
@@ -18,16 +18,23 @@ import {
18
18
  validateUltramodernBuildArtifact,
19
19
  } from '@modern-js/backend-federation-contracts';
20
20
 
21
- export const COMPACT_CONFIG_PATH = '.modernjs/ultramodern.json';
21
+ export const REFERENCE_TOPOLOGY_PATH = 'topology/reference-topology.json';
22
+ export const DEVELOPMENT_OVERLAY_PATH =
23
+ 'topology/local-overlays/development.json';
22
24
 
23
- export type CompactApp = {
25
+ export type TopologyApp = {
24
26
  id?: unknown;
27
+ domain?: unknown;
25
28
  kind?: unknown;
26
29
  path?: unknown;
27
30
  package?: unknown;
28
- port?: unknown;
31
+ portEnv?: unknown;
32
+ cloudflare?: { publicUrlEnv?: unknown };
29
33
  api?: {
30
- prefix?: unknown;
34
+ bff?: { prefix?: unknown };
35
+ protocol?: unknown;
36
+ rpcPath?: unknown;
37
+ rpcSerialization?: unknown;
31
38
  stem?: unknown;
32
39
  };
33
40
  moduleFederation?: {
@@ -57,20 +64,28 @@ export type CompactApp = {
57
64
  packageName?: unknown;
58
65
  version?: unknown;
59
66
  };
60
- serverExecution?: {
61
- node?: {
62
- remoteName?: unknown;
63
- manifestUrl?: unknown;
64
- containerEntry?: unknown;
65
- remoteType?: unknown;
66
- };
67
- };
68
67
  };
69
68
 
70
- export type CompactConfig = {
71
- topology?: {
72
- apps?: CompactApp[];
73
- };
69
+ export type DevelopmentOverlay = {
70
+ ports?: Record<string, unknown>;
71
+ manifests?: Record<string, unknown>;
72
+ serverExecution?: Record<
73
+ string,
74
+ {
75
+ node?: {
76
+ remoteName?: unknown;
77
+ manifestUrl?: unknown;
78
+ containerEntry?: unknown;
79
+ remoteType?: unknown;
80
+ };
81
+ }
82
+ >;
83
+ };
84
+
85
+ export type ReferenceTopology = {
86
+ shell?: TopologyApp;
87
+ shells?: TopologyApp[];
88
+ verticals?: TopologyApp[];
74
89
  };
75
90
 
76
91
  export type BackendFederationApp = {
@@ -84,13 +99,15 @@ export type BackendFederationApp = {
84
99
  deliveryUnit?: DeliveryUnitContractBlock;
85
100
  port: number;
86
101
  apiPrefix: string;
102
+ apiProtocol: 'rest' | 'rpc';
103
+ rpcPath?: string;
87
104
  apiStem: string;
88
105
  backendName: string;
89
106
  manifestUrl: string;
90
107
  containerEntry: string;
91
108
  remoteType: string;
92
109
  uiManifestUrl?: string;
93
- compactDeliveryUnit?: {
110
+ topologyDeliveryUnit?: {
94
111
  unitId?: string;
95
112
  buildMarker?: string;
96
113
  sourceRevision?: string;
@@ -169,11 +186,10 @@ const toPascalCase = (value: string) =>
169
186
  const stringValue = (value: unknown) =>
170
187
  typeof value === 'string' && value.length > 0 ? value : undefined;
171
188
 
172
- const createBackendName = (app: CompactApp, id: string) => {
189
+ const createBackendName = (app: TopologyApp, id: string) => {
173
190
  const configuredName =
174
191
  stringValue(app.backendFederation?.name) ??
175
- stringValue(app.backendFederation?.executionSurfaces?.node?.remoteName) ??
176
- stringValue(app.serverExecution?.node?.remoteName);
192
+ stringValue(app.backendFederation?.executionSurfaces?.node?.remoteName);
177
193
 
178
194
  if (configuredName) {
179
195
  return configuredName;
@@ -183,11 +199,34 @@ const createBackendName = (app: CompactApp, id: string) => {
183
199
  return mfName ? `${mfName}Backend` : `vertical${toPascalCase(id)}Backend`;
184
200
  };
185
201
 
202
+ const rebaseDefaultLocalUrl = (
203
+ value: string,
204
+ defaultPort: number,
205
+ publicOrigin: string,
206
+ ) => {
207
+ let url: URL;
208
+ try {
209
+ url = new URL(value);
210
+ } catch {
211
+ return value;
212
+ }
213
+ if (
214
+ url.protocol !== 'http:' ||
215
+ url.hostname !== 'localhost' ||
216
+ Number(url.port) !== defaultPort ||
217
+ url.username ||
218
+ url.password
219
+ ) {
220
+ return value;
221
+ }
222
+ return `${publicOrigin}${url.pathname}${url.search}${url.hash}`;
223
+ };
224
+
186
225
  export const findWorkspaceRoot = (appDirectory: string) => {
187
226
  let current = appDirectory;
188
227
 
189
228
  while (true) {
190
- if (existsSync(path.join(current, COMPACT_CONFIG_PATH))) {
229
+ if (existsSync(path.join(current, REFERENCE_TOPOLOGY_PATH))) {
191
230
  return current;
192
231
  }
193
232
 
@@ -240,50 +279,121 @@ export const createStampedDeliveryUnit = (input: {
240
279
  return deliveryUnitContractBlock(record as DeliveryUnitRecord);
241
280
  };
242
281
 
243
- export const createAppFromCompactMetadata = (
282
+ export const createAppFromTopology = (
244
283
  workspaceRoot: string,
245
284
  appDirectory: string,
246
- compactApp: CompactApp,
285
+ topologyApp: TopologyApp,
286
+ overlay: DevelopmentOverlay,
287
+ packageManifest: { name?: unknown; version?: unknown },
247
288
  ): BackendFederationApp | undefined => {
248
- const id = stringValue(compactApp.id);
249
- const appPath = stringValue(compactApp.path);
289
+ const id = stringValue(topologyApp.id);
290
+ const appPath = stringValue(topologyApp.path);
250
291
 
251
- if (!id || compactApp.kind !== 'vertical' || !isRecord(compactApp.api)) {
292
+ if (
293
+ !id ||
294
+ !appPath ||
295
+ topologyApp.kind !== 'vertical' ||
296
+ !isRecord(topologyApp.api) ||
297
+ !isRecord(topologyApp.backendFederation)
298
+ ) {
252
299
  return undefined;
253
300
  }
254
301
 
255
- const directory = appPath
256
- ? normalizeRelativePath(appPath)
257
- : `verticals/${id}`;
302
+ const directory = normalizeRelativePath(appPath);
258
303
  if (path.resolve(workspaceRoot, directory) !== path.resolve(appDirectory)) {
259
304
  return undefined;
260
305
  }
261
306
 
262
- const port =
263
- typeof compactApp.port === 'number' ? compactApp.port : undefined;
264
- if (port === undefined) {
265
- return undefined;
307
+ const defaultPort = overlay.ports?.[id];
308
+ if (
309
+ typeof defaultPort !== 'number' ||
310
+ !Number.isInteger(defaultPort) ||
311
+ defaultPort < 1 ||
312
+ defaultPort > 65535
313
+ ) {
314
+ throw new Error(
315
+ `[backend-federation-build] Invalid development port for ${id}.`,
316
+ );
317
+ }
318
+ const portEnv =
319
+ stringValue(topologyApp.portEnv) ??
320
+ `VERTICAL_${(stringValue(topologyApp.domain) ?? id)
321
+ .replace(/[^a-zA-Z0-9]/gu, '_')
322
+ .toUpperCase()}_PORT`;
323
+ const port = Number(process.env[portEnv] ?? defaultPort);
324
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
325
+ throw new Error(
326
+ `[backend-federation-build] Invalid ${portEnv} development port for ${id}.`,
327
+ );
328
+ }
329
+ const publicUrlEnv = stringValue(topologyApp.cloudflare?.publicUrlEnv);
330
+ const configuredPublicUrl = publicUrlEnv
331
+ ? process.env[publicUrlEnv]?.trim()
332
+ : undefined;
333
+ let publicOrigin = `http://localhost:${port}`;
334
+ if (configuredPublicUrl) {
335
+ let url: URL;
336
+ try {
337
+ url = new URL(configuredPublicUrl);
338
+ } catch {
339
+ throw new Error(
340
+ `[backend-federation-build] Invalid ${publicUrlEnv} public URL for ${id}.`,
341
+ );
342
+ }
343
+ if (
344
+ !['http:', 'https:'].includes(url.protocol) ||
345
+ url.username ||
346
+ url.password
347
+ ) {
348
+ throw new Error(
349
+ `[backend-federation-build] Invalid ${publicUrlEnv} public URL for ${id}.`,
350
+ );
351
+ }
352
+ publicOrigin = url.origin;
266
353
  }
267
354
 
268
- const apiPrefix = stringValue(compactApp.api.prefix) ?? `/${id}-api`;
269
- const apiStem = stringValue(compactApp.api.stem) ?? id;
270
- const backendName = createBackendName(compactApp, id);
271
- const manifestUrl =
272
- stringValue(
273
- compactApp.backendFederation?.executionSurfaces?.node?.manifestUrl,
274
- ) ??
275
- stringValue(compactApp.serverExecution?.node?.manifestUrl) ??
276
- `http://localhost:${port}/${BACKEND_MANIFEST_FILE}`;
277
- const containerEntry =
278
- stringValue(
279
- compactApp.backendFederation?.executionSurfaces?.node?.containerEntry,
280
- ) ??
281
- stringValue(compactApp.serverExecution?.node?.containerEntry) ??
282
- `http://localhost:${port}/${BACKEND_REMOTE_ENTRY_FILE}`;
355
+ const apiPrefix = stringValue(topologyApp.api.bff?.prefix) ?? `/${id}-api`;
356
+ const apiProtocol = topologyApp.api.protocol ?? 'rest';
357
+ if (apiProtocol !== 'rest' && apiProtocol !== 'rpc') {
358
+ throw new Error(
359
+ `[backend-federation-build] Unsupported API protocol for ${id}.`,
360
+ );
361
+ }
362
+ const rpcPath =
363
+ apiProtocol === 'rpc' ? stringValue(topologyApp.api.rpcPath) : undefined;
364
+ if (
365
+ apiProtocol === 'rpc' &&
366
+ (!rpcPath || topologyApp.api.rpcSerialization !== 'json')
367
+ ) {
368
+ throw new Error(
369
+ `[backend-federation-build] Missing JSON RPC route metadata for ${id}.`,
370
+ );
371
+ }
372
+ const apiStem = stringValue(topologyApp.api.stem) ?? id;
373
+ const backendName = createBackendName(topologyApp, id);
374
+ const nodeOverlay = overlay.serverExecution?.[id]?.node;
375
+ if (!isRecord(nodeOverlay)) {
376
+ throw new Error(
377
+ `[backend-federation-build] Missing declared Node server execution for ${id}.`,
378
+ );
379
+ }
380
+ const manifestUrl = rebaseDefaultLocalUrl(
381
+ stringValue(nodeOverlay?.manifestUrl) ??
382
+ `http://localhost:${defaultPort}/${BACKEND_MANIFEST_FILE}`,
383
+ defaultPort,
384
+ publicOrigin,
385
+ );
386
+ const containerEntry = rebaseDefaultLocalUrl(
387
+ stringValue(nodeOverlay?.containerEntry) ??
388
+ `http://localhost:${defaultPort}/${BACKEND_REMOTE_ENTRY_FILE}`,
389
+ defaultPort,
390
+ publicOrigin,
391
+ );
283
392
  const configuredRemoteType =
393
+ stringValue(nodeOverlay?.remoteType) ??
284
394
  stringValue(
285
- compactApp.backendFederation?.executionSurfaces?.node?.remoteType,
286
- ) ?? stringValue(compactApp.serverExecution?.node?.remoteType);
395
+ topologyApp.backendFederation?.executionSurfaces?.node?.remoteType,
396
+ );
287
397
  if (
288
398
  configuredRemoteType !== undefined &&
289
399
  configuredRemoteType !== 'commonjs-module'
@@ -300,18 +410,24 @@ export const createAppFromCompactMetadata = (
300
410
  );
301
411
  }
302
412
  const remoteType = 'commonjs-module';
303
- const packageName = stringValue(compactApp.package);
304
- const uiManifestUrl =
305
- stringValue(
306
- compactApp.backendFederation?.versionBoundary?.ui?.manifestUrl,
307
- ) ?? stringValue(compactApp.moduleFederation?.manifestUrl);
308
- const compactDeliveryUnit = isRecord(compactApp.deliveryUnit)
413
+ const packageName = stringValue(packageManifest.name);
414
+ const version = stringValue(packageManifest.version);
415
+ if (stringValue(topologyApp.package) !== packageName || !version) {
416
+ throw new Error(
417
+ `[backend-federation-build] Topology package identity must match package.json for ${id}.`,
418
+ );
419
+ }
420
+ const declaredUiManifestUrl = stringValue(overlay.manifests?.[id]);
421
+ const uiManifestUrl = declaredUiManifestUrl
422
+ ? rebaseDefaultLocalUrl(declaredUiManifestUrl, defaultPort, publicOrigin)
423
+ : undefined;
424
+ const topologyDeliveryUnit = isRecord(topologyApp.deliveryUnit)
309
425
  ? {
310
- unitId: stringValue(compactApp.deliveryUnit.unitId),
311
- buildMarker: stringValue(compactApp.deliveryUnit.buildMarker),
312
- sourceRevision: stringValue(compactApp.deliveryUnit.sourceRevision),
313
- packageName: stringValue(compactApp.deliveryUnit.packageName),
314
- version: stringValue(compactApp.deliveryUnit.version),
426
+ unitId: stringValue(topologyApp.deliveryUnit.unitId),
427
+ buildMarker: stringValue(topologyApp.deliveryUnit.buildMarker),
428
+ sourceRevision: stringValue(topologyApp.deliveryUnit.sourceRevision),
429
+ packageName: stringValue(topologyApp.deliveryUnit.packageName),
430
+ version: stringValue(topologyApp.deliveryUnit.version),
315
431
  }
316
432
  : undefined;
317
433
 
@@ -319,15 +435,18 @@ export const createAppFromCompactMetadata = (
319
435
  id,
320
436
  directory,
321
437
  packageName,
438
+ version,
322
439
  port,
323
440
  apiPrefix,
441
+ apiProtocol,
442
+ rpcPath,
324
443
  apiStem,
325
444
  backendName,
326
445
  manifestUrl,
327
446
  containerEntry,
328
447
  remoteType,
329
448
  uiManifestUrl,
330
- compactDeliveryUnit,
449
+ topologyDeliveryUnit,
331
450
  };
332
451
  };
333
452
 
@@ -335,22 +454,39 @@ export const findBackendFederationApp = async (
335
454
  workspaceRoot: string,
336
455
  appDirectory: string,
337
456
  ) => {
338
- const compactConfigPath = path.join(workspaceRoot, COMPACT_CONFIG_PATH);
339
- const compactConfig = await readJsonFile<CompactConfig>(compactConfigPath);
340
- const apps = Array.isArray(compactConfig.topology?.apps)
341
- ? compactConfig.topology.apps
342
- : [];
343
-
344
- for (const app of apps) {
345
- const resolved = createAppFromCompactMetadata(
457
+ const topologyPath = path.join(workspaceRoot, REFERENCE_TOPOLOGY_PATH);
458
+ const topology = await readJsonFile<ReferenceTopology>(topologyPath);
459
+ if (!isRecord(topology) || !Array.isArray(topology.verticals)) {
460
+ throw new Error(
461
+ `[backend-federation-build] Invalid declared topology at ${topologyPath}.`,
462
+ );
463
+ }
464
+ const overlayPath = path.join(workspaceRoot, DEVELOPMENT_OVERLAY_PATH);
465
+ const overlay = await readJsonFile<DevelopmentOverlay>(overlayPath);
466
+ if (!isRecord(overlay) || !isRecord(overlay.ports)) {
467
+ throw new Error(
468
+ `[backend-federation-build] Invalid development overlay at ${overlayPath}.`,
469
+ );
470
+ }
471
+ const packageManifest = await readJsonFile<{
472
+ name?: unknown;
473
+ version?: unknown;
474
+ }>(path.join(appDirectory, 'package.json'));
475
+
476
+ for (const app of topology.verticals) {
477
+ const resolved = createAppFromTopology(
346
478
  workspaceRoot,
347
479
  appDirectory,
348
480
  app,
481
+ overlay,
482
+ packageManifest,
349
483
  );
350
484
  if (resolved) {
351
485
  return resolved;
352
486
  }
353
487
  }
354
488
 
355
- return undefined;
489
+ throw new Error(
490
+ `[backend-federation-build] ${appDirectory} is missing from the declared reference topology.`,
491
+ );
356
492
  };
@@ -16,10 +16,10 @@ import {
16
16
  import {
17
17
  type BackendFederationApp,
18
18
  buildArtifactPathFor,
19
- COMPACT_CONFIG_PATH,
20
19
  createStampedDeliveryUnit,
21
20
  findBackendFederationApp,
22
21
  findWorkspaceRoot,
22
+ REFERENCE_TOPOLOGY_PATH,
23
23
  readBuildIdentity,
24
24
  resolveWorkspaceSourceRevision,
25
25
  } from './config';
@@ -58,13 +58,13 @@ export const emitBackendFederationArtifacts = async (
58
58
  return undefined;
59
59
  }
60
60
  const buildIdentity = await readBuildIdentity(appDirectory);
61
- const compactDeliveryUnit = app.compactDeliveryUnit;
62
- const hasCompactDeliveryUnit =
63
- compactDeliveryUnit !== undefined &&
64
- (compactDeliveryUnit.unitId !== undefined ||
65
- compactDeliveryUnit.buildMarker !== undefined ||
66
- compactDeliveryUnit.packageName !== undefined ||
67
- compactDeliveryUnit.version !== undefined);
61
+ const topologyDeliveryUnit = app.topologyDeliveryUnit;
62
+ const hasTopologyDeliveryUnit =
63
+ topologyDeliveryUnit !== undefined &&
64
+ (topologyDeliveryUnit.unitId !== undefined ||
65
+ topologyDeliveryUnit.buildMarker !== undefined ||
66
+ topologyDeliveryUnit.packageName !== undefined ||
67
+ topologyDeliveryUnit.version !== undefined);
68
68
  const hasBuildIdentity =
69
69
  buildIdentity.unitId !== undefined ||
70
70
  buildIdentity.buildVersion !== undefined ||
@@ -72,7 +72,7 @@ export const emitBackendFederationArtifacts = async (
72
72
  buildIdentity.version !== undefined;
73
73
 
74
74
  if (hasBuildIdentity) {
75
- const compactConfigPath = path.join(workspaceRoot, COMPACT_CONFIG_PATH);
75
+ const topologyPath = path.join(workspaceRoot, REFERENCE_TOPOLOGY_PATH);
76
76
  const buildIdentityPath = buildArtifactPathFor(appDirectory);
77
77
  const mismatches: string[] = [];
78
78
  const compare = (
@@ -89,31 +89,45 @@ export const emitBackendFederationArtifacts = async (
89
89
  };
90
90
  compare('appId', app.id, buildIdentity.appId, 'topology');
91
91
 
92
- if (hasCompactDeliveryUnit) {
93
- compare('unitId', compactDeliveryUnit?.unitId, buildIdentity.unitId);
92
+ if (hasTopologyDeliveryUnit) {
93
+ compare('unitId', topologyDeliveryUnit?.unitId, buildIdentity.unitId);
94
94
  compare(
95
95
  'buildMarker/build',
96
- compactDeliveryUnit?.buildMarker,
96
+ topologyDeliveryUnit?.buildMarker,
97
97
  buildIdentity.buildVersion,
98
98
  );
99
99
  compare(
100
100
  'packageName',
101
- compactDeliveryUnit?.packageName,
101
+ topologyDeliveryUnit?.packageName,
102
102
  buildIdentity.packageName,
103
103
  );
104
- compare('version', compactDeliveryUnit?.version, buildIdentity.version);
104
+ compare('version', topologyDeliveryUnit?.version, buildIdentity.version);
105
105
  }
106
+ compare(
107
+ 'packageName',
108
+ app.packageName,
109
+ buildIdentity.packageName,
110
+ 'package.json',
111
+ );
112
+ compare(
113
+ 'packageName',
114
+ topologyDeliveryUnit?.packageName,
115
+ app.packageName,
116
+ 'topology',
117
+ );
118
+ compare('version', app.version, buildIdentity.version, 'package.json');
119
+ compare('version', topologyDeliveryUnit?.version, app.version, 'topology');
106
120
 
107
121
  if (mismatches.length > 0) {
108
122
  throw new Error(
109
- `[backend-federation-build] Delivery-unit identity drift between ${compactConfigPath} (topology) and ${buildIdentityPath}: ${mismatches.join('; ')}`,
123
+ `[backend-federation-build] Delivery-unit identity drift between ${topologyPath}, package.json and ${buildIdentityPath}: ${mismatches.join('; ')}`,
110
124
  );
111
125
  }
112
126
  }
113
127
 
114
- const unitId = compactDeliveryUnit?.unitId ?? buildIdentity.unitId;
128
+ const unitId = topologyDeliveryUnit?.unitId ?? buildIdentity.unitId;
115
129
  const generationBuildMarker =
116
- compactDeliveryUnit?.buildMarker ?? buildIdentity.buildVersion;
130
+ topologyDeliveryUnit?.buildMarker ?? buildIdentity.buildVersion;
117
131
  const sourceRevision = await resolveWorkspaceSourceRevision(workspaceRoot);
118
132
  const buildVersion =
119
133
  generationBuildMarker && unitId
@@ -123,11 +137,8 @@ export const emitBackendFederationArtifacts = async (
123
137
  workspaceRoot,
124
138
  }).buildMarker
125
139
  : undefined;
126
- const packageName =
127
- compactDeliveryUnit?.packageName ??
128
- buildIdentity.packageName ??
129
- app.packageName;
130
- const version = compactDeliveryUnit?.version ?? buildIdentity.version;
140
+ const packageName = app.packageName;
141
+ const version = app.version;
131
142
  const deliveryUnit = createStampedDeliveryUnit({
132
143
  appId: app.id,
133
144
  unitId,
@@ -0,0 +1,31 @@
1
+ import {
2
+ type BrowserManifestAddressOptions,
3
+ resolveBrowserManifestAddress,
4
+ } from '@modern-js/surface-resolution';
5
+ import { getBuildConfigEnvironment } from './build-environment';
6
+
7
+ /** Adapt build environment leases and MF's name@URL syntax to discovery policy. */
8
+ export function createRemoteManifestUrl(
9
+ options: BrowserManifestAddressOptions,
10
+ ): string {
11
+ const env = Object.fromEntries(
12
+ [
13
+ options.manifestEnv,
14
+ options.publicUrlEnv,
15
+ 'MODERNJS_DEPLOY',
16
+ 'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN',
17
+ 'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS',
18
+ ].map(name => [name, getBuildConfigEnvironment(name)]),
19
+ );
20
+ const address = resolveBrowserManifestAddress(
21
+ env,
22
+ options,
23
+ getBuildConfigEnvironment('NODE_ENV') ?? 'production',
24
+ );
25
+ if (!address.ok)
26
+ throw new Error(`Remote ${options.mfName}: ${address.reason}`);
27
+ return (
28
+ env[options.manifestEnv]?.trim() ||
29
+ `${options.mfName}@${address.manifestUrl}`
30
+ );
31
+ }
@@ -10,7 +10,7 @@ import { fs as fse } from '@modern-js/utils';
10
10
  import { resolveUltramodernReleaseIdentity } from '../release-identity';
11
11
  import { isRecord } from './utils';
12
12
 
13
- const COMPACT_CONFIG_PATH = '.modernjs/ultramodern.json';
13
+ const TOPOLOGY_PATH = 'topology/reference-topology.json';
14
14
 
15
15
  export type DeliveryUnitStamp = DeliveryUnitIdentity & {
16
16
  surfaces: {
@@ -19,7 +19,7 @@ export type DeliveryUnitStamp = DeliveryUnitIdentity & {
19
19
  };
20
20
  };
21
21
 
22
- type CompactAppResolution = {
22
+ type TopologyAppResolution = {
23
23
  app?: Record<string, unknown>;
24
24
  workspaceRoot: string;
25
25
  };
@@ -30,7 +30,7 @@ const findWorkspaceRoot = async (
30
30
  let current = path.resolve(appDirectory);
31
31
 
32
32
  for (;;) {
33
- if (await fse.pathExists(path.join(current, COMPACT_CONFIG_PATH))) {
33
+ if (await fse.pathExists(path.join(current, TOPOLOGY_PATH))) {
34
34
  return current;
35
35
  }
36
36
 
@@ -60,34 +60,32 @@ const stampReleaseIdentity = (
60
60
  };
61
61
  };
62
62
 
63
- const resolveCompactApp = async (
63
+ const resolveTopologyApp = async (
64
64
  appDirectory: string,
65
- ): Promise<CompactAppResolution | undefined> => {
65
+ ): Promise<TopologyAppResolution | undefined> => {
66
66
  const workspaceRoot = await findWorkspaceRoot(appDirectory);
67
67
  if (!workspaceRoot) {
68
68
  return undefined;
69
69
  }
70
70
 
71
- let compactConfig: unknown;
72
- try {
73
- compactConfig = await fse.readJSON(
74
- path.join(workspaceRoot, COMPACT_CONFIG_PATH),
71
+ const topology: unknown = await fse.readJSON(
72
+ path.join(workspaceRoot, TOPOLOGY_PATH),
73
+ );
74
+ if (
75
+ !isRecord(topology) ||
76
+ !isRecord(topology.shell) ||
77
+ !Array.isArray(topology.verticals) ||
78
+ (topology.shells !== undefined && !Array.isArray(topology.shells))
79
+ ) {
80
+ throw new Error(
81
+ '[cloudflare-delivery-unit] Invalid declared reference topology.',
75
82
  );
76
- } catch {
77
- return { workspaceRoot };
78
- }
79
-
80
- if (!isRecord(compactConfig)) {
81
- return { workspaceRoot };
82
- }
83
-
84
- const topology = isRecord(compactConfig.topology)
85
- ? compactConfig.topology
86
- : undefined;
87
- const apps = Array.isArray(topology?.apps) ? topology.apps : undefined;
88
- if (!apps) {
89
- return { app: compactConfig, workspaceRoot };
90
83
  }
84
+ const apps = [
85
+ topology.shell,
86
+ ...topology.verticals,
87
+ ...(Array.isArray(topology.shells) ? topology.shells : []),
88
+ ];
91
89
 
92
90
  const resolvedAppDirectory = path.resolve(appDirectory);
93
91
  const app = apps.find(candidate => {
@@ -101,8 +99,13 @@ const resolveCompactApp = async (
101
99
  resolvedAppDirectory
102
100
  );
103
101
  });
102
+ if (!isRecord(app)) {
103
+ throw new Error(
104
+ `[cloudflare-delivery-unit] ${appDirectory} is missing from the declared reference topology.`,
105
+ );
106
+ }
104
107
  return {
105
- ...(isRecord(app) ? { app } : {}),
108
+ app,
106
109
  workspaceRoot,
107
110
  };
108
111
  };
@@ -126,23 +129,26 @@ const createDeliveryUnitStamp = (
126
129
 
127
130
  /**
128
131
  * Resolve the delivery-unit record declared for this app by the workspace
129
- * compact config (`.modernjs/ultramodern.json`). This is the topology source
132
+ * reference topology. This is the topology source
130
133
  * of truth the Cloudflare worker snapshot is verified against.
131
134
  */
132
135
  export const resolveTopologyDeliveryUnit = async (
133
136
  appDirectory: string,
134
137
  ): Promise<DeliveryUnitStamp | undefined> => {
135
- const resolved = await resolveCompactApp(appDirectory);
138
+ const resolved = await resolveTopologyApp(appDirectory);
136
139
  if (!resolved?.app) {
137
140
  return undefined;
138
141
  }
139
142
  const identity = toDeliveryUnitIdentity(resolved.app.deliveryUnit);
140
- return identity
141
- ? createDeliveryUnitStamp(
142
- stampReleaseIdentity(identity, resolved.workspaceRoot),
143
- resolved.app,
144
- )
145
- : undefined;
143
+ if (!identity) {
144
+ throw new Error(
145
+ '[cloudflare-delivery-unit] Declared app is missing a valid delivery-unit identity.',
146
+ );
147
+ }
148
+ return createDeliveryUnitStamp(
149
+ stampReleaseIdentity(identity, resolved.workspaceRoot),
150
+ resolved.app,
151
+ );
146
152
  };
147
153
 
148
154
  /**
@@ -170,7 +176,7 @@ export const resolveWorkerDeliveryUnitStamp = async (
170
176
  if (!identity) {
171
177
  return undefined;
172
178
  }
173
- const resolved = await resolveCompactApp(appDirectory);
179
+ const resolved = await resolveTopologyApp(appDirectory);
174
180
  if (resolved) {
175
181
  identity = stampReleaseIdentity(identity, resolved.workspaceRoot);
176
182
  }