@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.12 → 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 (62) 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-builder.js +13 -3
  7. package/dist/cjs/cloudflare-output-contract.js +1 -0
  8. package/dist/cjs/cloudflare-output-verifier/identity.js +26 -24
  9. package/dist/cjs/cloudflare-output-verifier/index.js +10 -0
  10. package/dist/cjs/cloudflare-output-verifier/manifest.js +46 -0
  11. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +7 -1
  12. package/dist/cjs/deploy-output/npmAliases.js +28 -3
  13. package/dist/cjs/release-envelope/framework-output.js +62 -49
  14. package/dist/cjs/release-envelope/index.js +10 -4
  15. package/dist/esm/backend-federation-build/codegen.mjs +7 -2
  16. package/dist/esm/backend-federation-build/config.mjs +78 -31
  17. package/dist/esm/backend-federation-build/emit.mjs +18 -14
  18. package/dist/esm/cloudflare/delivery-unit.mjs +16 -26
  19. package/dist/esm/cloudflare/index.mjs +7 -1
  20. package/dist/esm/cloudflare-builder.mjs +13 -3
  21. package/dist/esm/cloudflare-output-contract.mjs +1 -0
  22. package/dist/esm/cloudflare-output-verifier/identity.mjs +26 -24
  23. package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
  24. package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
  25. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  26. package/dist/esm/deploy-output/npmAliases.mjs +28 -3
  27. package/dist/esm/release-envelope/framework-output.mjs +62 -49
  28. package/dist/esm/release-envelope/index.mjs +10 -4
  29. package/dist/esm-node/backend-federation-build/codegen.mjs +7 -2
  30. package/dist/esm-node/backend-federation-build/config.mjs +78 -31
  31. package/dist/esm-node/backend-federation-build/emit.mjs +18 -14
  32. package/dist/esm-node/cloudflare/delivery-unit.mjs +16 -26
  33. package/dist/esm-node/cloudflare/index.mjs +7 -1
  34. package/dist/esm-node/cloudflare-builder.mjs +13 -3
  35. package/dist/esm-node/cloudflare-output-contract.mjs +1 -0
  36. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +26 -24
  37. package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
  38. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
  39. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  40. package/dist/esm-node/deploy-output/npmAliases.mjs +28 -3
  41. package/dist/esm-node/release-envelope/framework-output.mjs +62 -49
  42. package/dist/esm-node/release-envelope/index.mjs +10 -4
  43. package/dist/types/backend-federation-build/codegen.d.ts +30 -0
  44. package/dist/types/backend-federation-build/config.d.ts +32 -13
  45. package/dist/types/cloudflare/delivery-unit.d.ts +1 -1
  46. package/dist/types/cloudflare-builder.d.ts +1 -0
  47. package/dist/types/cloudflare-output-contract.d.ts +1 -1
  48. package/package.json +20 -20
  49. package/src/backend-federation-build/codegen.ts +6 -2
  50. package/src/backend-federation-build/config.ts +207 -71
  51. package/src/backend-federation-build/emit.ts +33 -22
  52. package/src/cloudflare/delivery-unit.ts +39 -33
  53. package/src/cloudflare/index.ts +20 -6
  54. package/src/cloudflare-builder.ts +18 -4
  55. package/src/cloudflare-output-contract.ts +1 -0
  56. package/src/cloudflare-output-verifier/identity.ts +4 -0
  57. package/src/cloudflare-output-verifier/index.ts +20 -0
  58. package/src/cloudflare-output-verifier/manifest.ts +77 -0
  59. package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
  60. package/src/deploy-output/npmAliases.ts +52 -3
  61. package/src/release-envelope/framework-output.ts +163 -90
  62. package/src/release-envelope/index.ts +26 -5
@@ -9,6 +9,11 @@ const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_KIND = 'ultramodern-release-identi
9
9
  const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_SCHEMA_VERSION = 1;
10
10
  const COMPILED_MODULE_PATTERN = /\.(?:c|m)?js$/u;
11
11
  const EFFECT_BFF_WORKER_PATTERN = /^worker\/__modern_bff_effect\.(?:c|m)?js$/u;
12
+ const EFFECT_BFF_WORKER_SUPPORT_PATTERN = /^worker\/__modern_worker_(?:runtime|shared)_*\.(?:c|m)?js$/u;
13
+ const isEffectBffWorkerArtifact = (logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath) || EFFECT_BFF_WORKER_SUPPORT_PATTERN.test(logicalPath);
14
+ const CRAWLER_POLICY_PATH = 'public/robots.txt';
15
+ const isPublicMetadataPath = (logicalPath)=>'public/_headers' === logicalPath || /^public\/\.well-known\/[^/]+\.json$/u.test(logicalPath);
16
+ const PUBLIC_BUILD_ARTIFACT_PATH = `public/${ULTRAMODERN_BUILD_ARTIFACT_FILE}`;
12
17
  const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
13
18
  const flattenStrings = (value)=>'string' == typeof value ? [
14
19
  value
@@ -129,7 +134,7 @@ const readBuildArtifact = async (distDirectory, requirePromotable = true)=>{
129
134
  if (!isUltramodernBuildArtifact(artifact)) throw new Error(`[ultramodern-release-envelope] ${ULTRAMODERN_BUILD_ARTIFACT_FILE} is missing or invalid.`);
130
135
  if ('workspace' === artifact.deliveryUnit.sourceRevision) {
131
136
  if (!requirePromotable) return;
132
- throw new Error('[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable full-stack envelope.');
137
+ throw new Error('[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable envelope.');
133
138
  }
134
139
  return artifact;
135
140
  };
@@ -199,7 +204,7 @@ const releaseArtifact = (logicalPath, runtime)=>({
199
204
  const writeReleaseIdentityCarrierMetadata = async (artifactRoot, identity, carrierPaths)=>{
200
205
  const surfacesByPath = new Map();
201
206
  for (const [surface, logicalPaths] of Object.entries(carrierPaths)){
202
- if (0 === logicalPaths.length) throw new Error(`[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`);
207
+ if (0 === logicalPaths.length && 'ssr' !== surface && 'uiClient' !== surface) throw new Error(`[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`);
203
208
  for (const logicalPath of logicalPaths){
204
209
  const surfaces = surfacesByPath.get(logicalPath) ?? new Set();
205
210
  surfaces.add(surface);
@@ -239,12 +244,12 @@ const writeReleaseIdentityCarrierMetadata = async (artifactRoot, identity, carri
239
244
  }, null, 2)}\n`);
240
245
  return releaseArtifact(MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_PATH, 'release-identity-metadata');
241
246
  };
242
- const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
247
+ const createReleaseArtifactInputs = async (distDirectory, identity, target, apiOnly)=>{
243
248
  const files = await collectFiles(distDirectory);
244
- if ('node' === target && files.some((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath))) throw new Error('[ultramodern-release-envelope] Node target conflicts with a Cloudflare Effect API/BFF worker artifact; resolve the canonical deploy target instead of emitting a Node envelope for Cloudflare output.');
249
+ if ('node' === target && files.some(isEffectBffWorkerArtifact)) throw new Error('[ultramodern-release-envelope] Node target conflicts with a Cloudflare Effect API/BFF worker artifact; resolve the canonical deploy target instead of emitting a Node envelope for Cloudflare output.');
245
250
  const uiClientPaths = files.filter((logicalPath)=>{
246
251
  const topLevel = logicalPath.split('/')[0];
247
- return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel || [
252
+ return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) || [
248
253
  'index.html',
249
254
  'mf-manifest.json',
250
255
  'mf-stats.json',
@@ -253,16 +258,17 @@ const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
253
258
  'loadable-stats.json'
254
259
  ].includes(logicalPath);
255
260
  });
256
- const ssrPaths = 'node' === target ? files.filter((logicalPath)=>logicalPath.startsWith('bundles/') && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter((logicalPath)=>logicalPath.startsWith('worker/') && COMPILED_MODULE_PATTERN.test(logicalPath) && !EFFECT_BFF_WORKER_PATTERN.test(logicalPath));
261
+ const ssrPaths = 'node' === target ? files.filter((logicalPath)=>logicalPath.startsWith('bundles/') && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter((logicalPath)=>logicalPath.startsWith('worker/') && COMPILED_MODULE_PATTERN.test(logicalPath) && !isEffectBffWorkerArtifact(logicalPath));
257
262
  const nodeApiEntryPaths = files.filter((logicalPath)=>logicalPath.startsWith('api/') && COMPILED_MODULE_PATTERN.test(logicalPath));
258
- const apiBackendPaths = 'node' === target ? files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath));
259
- if (0 === uiClientPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no UI/client artifacts.`);
260
- if (0 === ssrPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no ${'node' === target ? 'Node' : 'workerd'} SSR artifacts.`);
261
- if ('node' === target && 0 === nodeApiEntryPaths.length || 0 === apiBackendPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no actual ${'node' === target ? 'compiled Node Effect API artifact' : 'Effect API/BFF worker artifact'}.`);
262
- const clientExecutionPaths = await manifestReferencedClientModules(distDirectory, files);
263
- if (0 === clientExecutionPaths.length) throw new Error('[ultramodern-release-envelope] UI/client manifest references no compiled execution module.');
264
- const ssrExecutionPaths = await routeReferencedSsrModules(distDirectory, files, target);
265
- if (0 === ssrExecutionPaths.length) throw new Error(`[ultramodern-release-envelope] route manifest references no emitted ${'node' === target ? 'Node' : 'Cloudflare'} SSR execution module.`);
263
+ const apiBackendPaths = 'node' === target ? files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter(isEffectBffWorkerArtifact);
264
+ if (apiOnly && (uiClientPaths.length > 0 || ssrPaths.length > 0)) throw new Error('[ultramodern-release-envelope] API-only MicroVertical emitted an undeclared UI/client or SSR surface.');
265
+ if (!apiOnly && 0 === uiClientPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no UI/client artifacts.`);
266
+ if (!apiOnly && 0 === ssrPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no ${'node' === target ? 'Node' : 'workerd'} SSR artifacts.`);
267
+ if ('node' === target && 0 === nodeApiEntryPaths.length || 'cloudflare' === target && !files.some((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath)) || 0 === apiBackendPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} MicroVertical has no actual ${'node' === target ? 'compiled Node Effect API artifact' : 'Effect API/BFF worker artifact'}.`);
268
+ const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(distDirectory, files);
269
+ if (!apiOnly && 0 === clientExecutionPaths.length) throw new Error('[ultramodern-release-envelope] UI/client manifest references no compiled execution module.');
270
+ const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(distDirectory, files, target);
271
+ if (!apiOnly && 0 === ssrExecutionPaths.length) throw new Error(`[ultramodern-release-envelope] route manifest references no emitted ${'node' === target ? 'Node' : 'Cloudflare'} SSR execution module.`);
266
272
  const runtimeByPath = new Map();
267
273
  const add = (paths, runtime)=>{
268
274
  for (const logicalPath of paths){
@@ -272,6 +278,8 @@ const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
272
278
  }
273
279
  };
274
280
  add(uiClientPaths, 'browser');
281
+ add(files.filter((logicalPath)=>logicalPath === CRAWLER_POLICY_PATH), 'crawler-policy');
282
+ add(files.filter(isPublicMetadataPath), 'public-metadata');
275
283
  add(ssrPaths, 'node' === target ? 'nodejs' : 'workerd');
276
284
  add(apiBackendPaths, 'node' === target ? 'nodejs' : 'workerd-effect');
277
285
  add([
@@ -329,15 +337,14 @@ const pathExists = async (filePath)=>{
329
337
  }
330
338
  };
331
339
  const emitFrameworkMicroVerticalReleaseEnvelope = async ({ apiOnly, distDirectory, requirePromotable = true, target })=>{
332
- if (apiOnly) return;
333
340
  const backendManifestPath = node_path.join(distDirectory, BACKEND_FEDERATION_MANIFEST_FILE);
334
341
  const backendContainerPath = node_path.join(distDirectory, BACKEND_FEDERATION_REMOTE_ENTRY_FILE);
335
342
  const [hasBackendManifest, hasBackendContainer] = await Promise.all([
336
343
  pathExists(backendManifestPath),
337
344
  pathExists(backendContainerPath)
338
345
  ]);
339
- if (!hasBackendManifest && !hasBackendContainer) return;
340
- if (!hasBackendManifest || !hasBackendContainer) throw new Error('[ultramodern-release-envelope] full-stack MicroVertical backend federation manifest and container must be emitted together.');
346
+ if (!hasBackendManifest && !hasBackendContainer && !apiOnly) return;
347
+ if (!hasBackendManifest || !hasBackendContainer) throw new Error('[ultramodern-release-envelope] MicroVertical backend federation manifest and container must be emitted together.');
341
348
  const buildArtifact = await readBuildArtifact(distDirectory, requirePromotable);
342
349
  if (!buildArtifact) return;
343
350
  const identity = {
@@ -347,7 +354,7 @@ const emitFrameworkMicroVerticalReleaseEnvelope = async ({ apiOnly, distDirector
347
354
  releaseVersion: buildArtifact.deliveryUnit.version
348
355
  };
349
356
  await readBackendManifest(distDirectory, identity);
350
- const { artifacts, surfaces } = await createReleaseArtifactInputs(distDirectory, identity, target);
357
+ const { artifacts, surfaces } = await createReleaseArtifactInputs(distDirectory, identity, target, apiOnly);
351
358
  const envelope = await createMicroVerticalReleaseEnvelope({
352
359
  artifactRoot: distDirectory,
353
360
  target,
@@ -378,19 +385,16 @@ const readFrameworkMicroVerticalReleaseEnvelope = async ({ artifactRoot, expecte
378
385
  });
379
386
  };
380
387
  const verifyBuildOutputReleaseEnvelope = async (distDirectory, expectedTarget)=>{
381
- const [hasEnvelope, hasBuildArtifact, hasBackendManifest, hasBackendEntry] = await Promise.all([
388
+ const [hasEnvelope, hasBuildArtifact] = await Promise.all([
382
389
  pathExists(node_path.join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH)),
383
- pathExists(node_path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE)),
384
- pathExists(node_path.join(distDirectory, BACKEND_FEDERATION_MANIFEST_FILE)),
385
- pathExists(node_path.join(distDirectory, BACKEND_FEDERATION_REMOTE_ENTRY_FILE))
390
+ pathExists(node_path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE))
386
391
  ]);
387
- const isFullStackMicroVertical = hasBuildArtifact && (hasBackendManifest || hasBackendEntry);
388
392
  return readFrameworkMicroVerticalReleaseEnvelope({
389
393
  artifactRoot: distDirectory,
390
394
  ...expectedTarget ? {
391
395
  expectedTarget
392
396
  } : {},
393
- required: hasEnvelope || isFullStackMicroVertical
397
+ required: hasEnvelope || hasBuildArtifact
394
398
  });
395
399
  };
396
400
  const verifyNodeReleaseEnvelopeStaging = async ({ outputDirectory })=>readFrameworkMicroVerticalReleaseEnvelope({
@@ -398,7 +402,7 @@ const verifyNodeReleaseEnvelopeStaging = async ({ outputDirectory })=>readFramew
398
402
  expectedTarget: 'node',
399
403
  required: true
400
404
  });
401
- const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=>{
405
+ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity, apiOnly)=>{
402
406
  const files = await collectFiles(outputDirectory);
403
407
  for (const requiredPath of [
404
408
  'index.js',
@@ -412,7 +416,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
412
416
  ]);
413
417
  const uiClientPaths = files.filter((logicalPath)=>{
414
418
  const topLevel = logicalPath.split('/')[0];
415
- return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel || [
419
+ return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) || [
416
420
  'index.html',
417
421
  'mf-manifest.json',
418
422
  'mf-stats.json',
@@ -423,6 +427,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
423
427
  });
424
428
  const ssrPaths = files.filter((logicalPath)=>'index.js' === logicalPath || logicalPath.startsWith('bundles/') && COMPILED_MODULE_PATTERN.test(logicalPath));
425
429
  const apiBackendPaths = files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath));
430
+ if (apiOnly && (uiClientPaths.length > 0 || ssrPaths.some((path)=>'index.js' !== path))) throw new Error('[ultramodern-release-envelope] final API-only Node staging contains an undeclared UI/client or SSR surface.');
426
431
  for (const [surface, paths] of [
427
432
  [
428
433
  'UI/client',
@@ -430,22 +435,24 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
430
435
  ],
431
436
  [
432
437
  'Node SSR',
433
- ssrPaths
438
+ apiOnly ? [] : ssrPaths
434
439
  ],
435
440
  [
436
441
  'Node API/backend',
437
442
  apiBackendPaths
438
443
  ]
439
- ])if (0 === paths.length) throw new Error(`[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`);
440
- const clientExecutionPaths = await manifestReferencedClientModules(outputDirectory, files);
441
- const ssrExecutionPaths = await routeReferencedSsrModules(outputDirectory, files, 'node');
442
- if (0 === clientExecutionPaths.length || 0 === ssrExecutionPaths.length) throw new Error('[ultramodern-release-envelope] final Node manifests must reference emitted UI/client and SSR execution modules.');
444
+ ])if (0 === paths.length && (!apiOnly || 'Node API/backend' === surface)) throw new Error(`[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`);
445
+ const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(outputDirectory, files);
446
+ const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(outputDirectory, files, 'node');
447
+ if (!apiOnly && (0 === clientExecutionPaths.length || 0 === ssrExecutionPaths.length)) throw new Error('[ultramodern-release-envelope] final Node manifests must reference emitted UI/client and SSR execution modules.');
443
448
  const runtimeByPath = new Map(files.filter((logicalPath)=>!backendFederationPaths.has(logicalPath)).map((logicalPath)=>[
444
449
  logicalPath,
445
450
  'nodejs-deployment'
446
451
  ]));
447
452
  for (const logicalPath of uiClientPaths)runtimeByPath.set(logicalPath, 'browser');
448
- for (const logicalPath of ssrPaths)runtimeByPath.set(logicalPath, 'nodejs');
453
+ if (files.includes(CRAWLER_POLICY_PATH)) runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
454
+ for (const logicalPath of files.filter(isPublicMetadataPath))runtimeByPath.set(logicalPath, 'public-metadata');
455
+ for (const logicalPath of apiOnly ? [] : ssrPaths)runtimeByPath.set(logicalPath, 'nodejs');
449
456
  for (const logicalPath of apiBackendPaths)runtimeByPath.set(logicalPath, 'nodejs');
450
457
  runtimeByPath.set(BACKEND_FEDERATION_MANIFEST_FILE, 'module-federation-manifest');
451
458
  runtimeByPath.set(BACKEND_FEDERATION_REMOTE_ENTRY_FILE, 'nodejs');
@@ -455,7 +462,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
455
462
  BACKEND_FEDERATION_MANIFEST_FILE,
456
463
  BACKEND_FEDERATION_REMOTE_ENTRY_FILE
457
464
  ],
458
- ssr: [
465
+ ssr: apiOnly ? [] : [
459
466
  ...new Set([
460
467
  ...ssrExecutionPaths,
461
468
  ...ssrPaths.filter((logicalPath)=>'index.js' !== logicalPath)
@@ -474,7 +481,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
474
481
  uiClient: [
475
482
  ...uiClientPaths
476
483
  ].sort((left, right)=>left.localeCompare(right)),
477
- ssr: [
484
+ ssr: apiOnly ? [] : [
478
485
  ...ssrPaths
479
486
  ].sort((left, right)=>left.localeCompare(right)),
480
487
  apiBackend: [
@@ -490,7 +497,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
490
497
  const emitNodeStagedReleaseEnvelope = async ({ distDirectory, outputDirectory })=>{
491
498
  const source = await verifyBuildOutputReleaseEnvelope(distDirectory, 'node');
492
499
  if (!source) return;
493
- const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(outputDirectory, source.identity);
500
+ const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(outputDirectory, source.identity, 0 === source.surfaces.uiClient.length);
494
501
  const staged = await createMicroVerticalReleaseEnvelope({
495
502
  artifactRoot: outputDirectory,
496
503
  target: 'node',
@@ -508,21 +515,25 @@ const stageCloudflareReleaseEnvelope = async ({ distDirectory, outputDirectory }
508
515
  if (await pathExists(stagedEnvelopePath)) throw new Error('[ultramodern-release-envelope] Cloudflare staging contains a stale envelope before final output generation.');
509
516
  return source;
510
517
  };
511
- const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, identity)=>{
518
+ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, identity, apiOnly)=>{
512
519
  const files = await collectFiles(outputDirectory);
513
520
  for (const requiredPath of [
514
521
  'server/index.mjs',
515
522
  'server/modern-worker-manifest.json',
516
523
  'wrangler.json',
517
524
  'package.json',
518
- 'worker/package.json'
525
+ ...apiOnly ? [] : [
526
+ 'worker/package.json'
527
+ ]
519
528
  ])if (!files.includes(requiredPath)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging is missing ${requiredPath}.`);
520
529
  if (files.some((logicalPath)=>'public/release' === logicalPath || logicalPath.startsWith('public/release/'))) throw new Error('[ultramodern-release-envelope] final Cloudflare envelope must remain private and cannot be staged under public/release.');
521
530
  const backendManifestPath = `public/${BACKEND_FEDERATION_MANIFEST_FILE}`;
522
531
  const backendContainerPath = `public/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`;
523
- const apiBackendPaths = files.filter((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath));
524
- const ssrPaths = files.filter((logicalPath)=>(logicalPath.startsWith('server/') || logicalPath.startsWith('worker/')) && COMPILED_MODULE_PATTERN.test(logicalPath) && !EFFECT_BFF_WORKER_PATTERN.test(logicalPath));
525
- const uiClientPaths = files.filter((logicalPath)=>logicalPath.startsWith('public/') && logicalPath !== backendManifestPath && logicalPath !== backendContainerPath);
532
+ const apiBackendPaths = files.filter(isEffectBffWorkerArtifact);
533
+ if (!files.some((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath))) throw new Error('[ultramodern-release-envelope] final Cloudflare staging has no actual Effect API/BFF worker artifact.');
534
+ const ssrPaths = files.filter((logicalPath)=>(logicalPath.startsWith('server/') || logicalPath.startsWith('worker/')) && COMPILED_MODULE_PATTERN.test(logicalPath) && !isEffectBffWorkerArtifact(logicalPath));
535
+ const uiClientPaths = files.filter((logicalPath)=>logicalPath.startsWith('public/') && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) && logicalPath !== PUBLIC_BUILD_ARTIFACT_PATH && logicalPath !== backendManifestPath && logicalPath !== backendContainerPath);
536
+ if (apiOnly && (uiClientPaths.length > 0 || ssrPaths.some((path)=>'server/index.mjs' !== path))) throw new Error('[ultramodern-release-envelope] final API-only Cloudflare staging contains an undeclared UI/client or SSR surface.');
526
537
  for (const [surface, paths] of [
527
538
  [
528
539
  'UI/client',
@@ -530,27 +541,29 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
530
541
  ],
531
542
  [
532
543
  'Cloudflare SSR',
533
- ssrPaths
544
+ apiOnly ? [] : ssrPaths
534
545
  ],
535
546
  [
536
547
  'Cloudflare API/backend',
537
548
  apiBackendPaths
538
549
  ]
539
- ])if (0 === paths.length) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`);
550
+ ])if (0 === paths.length && (!apiOnly || 'Cloudflare API/backend' === surface)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`);
540
551
  for (const logicalPath of [
541
552
  backendManifestPath,
542
553
  backendContainerPath
543
554
  ])if (!files.includes(logicalPath)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging is missing ${logicalPath}.`);
544
- const clientExecutionPaths = await manifestReferencedClientModules(outputDirectory, files, 'public/mf-manifest.json');
545
- const ssrExecutionPaths = await routeReferencedSsrModules(outputDirectory, files, 'cloudflare', 'server/route.json');
546
- if (0 === clientExecutionPaths.length || 0 === ssrExecutionPaths.length) throw new Error('[ultramodern-release-envelope] final Cloudflare manifests must reference emitted UI/client and SSR execution modules.');
555
+ const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(outputDirectory, files, 'public/mf-manifest.json');
556
+ const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(outputDirectory, files, 'cloudflare', 'server/route.json');
557
+ if (!apiOnly && (0 === clientExecutionPaths.length || 0 === ssrExecutionPaths.length)) throw new Error('[ultramodern-release-envelope] final Cloudflare manifests must reference emitted UI/client and SSR execution modules.');
547
558
  const runtimeByPath = new Map();
548
559
  for (const logicalPath of files)runtimeByPath.set(logicalPath, 'cloudflare-deployment');
549
560
  const add = (paths, runtime)=>{
550
561
  for (const logicalPath of paths)runtimeByPath.set(logicalPath, runtime);
551
562
  };
552
563
  add(uiClientPaths, 'browser');
553
- add(ssrPaths, 'workerd');
564
+ if (files.includes(CRAWLER_POLICY_PATH)) runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
565
+ add(files.filter(isPublicMetadataPath), 'public-metadata');
566
+ add(apiOnly ? [] : ssrPaths, 'workerd');
554
567
  add(apiBackendPaths, 'workerd-effect');
555
568
  add([
556
569
  backendManifestPath
@@ -564,7 +577,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
564
577
  backendManifestPath,
565
578
  backendContainerPath
566
579
  ],
567
- ssr: [
580
+ ssr: apiOnly ? [] : [
568
581
  ...new Set([
569
582
  ...ssrExecutionPaths,
570
583
  ...ssrPaths.filter((logicalPath)=>logicalPath.startsWith('worker/'))
@@ -583,7 +596,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
583
596
  uiClient: [
584
597
  ...uiClientPaths
585
598
  ].sort((left, right)=>left.localeCompare(right)),
586
- ssr: [
599
+ ssr: apiOnly ? [] : [
587
600
  ...ssrPaths
588
601
  ].sort((left, right)=>left.localeCompare(right)),
589
602
  apiBackend: [
@@ -599,7 +612,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
599
612
  const emitCloudflareStagedReleaseEnvelope = async ({ distDirectory, outputDirectory })=>{
600
613
  const source = await verifyBuildOutputReleaseEnvelope(distDirectory, 'cloudflare');
601
614
  if (!source) return;
602
- const { artifacts, surfaces } = await createCloudflareStagedReleaseArtifactInputs(outputDirectory, source.identity);
615
+ const { artifacts, surfaces } = await createCloudflareStagedReleaseArtifactInputs(outputDirectory, source.identity, 0 === source.surfaces.uiClient.length);
603
616
  const staged = await createMicroVerticalReleaseEnvelope({
604
617
  artifactRoot: outputDirectory,
605
618
  target: 'cloudflare',
@@ -184,7 +184,7 @@ const assertUniqueSortedArtifacts = (artifacts, location)=>{
184
184
  if (paths.some((logicalPath, index)=>logicalPath !== sorted[index])) throw new Error(`${location} must be sorted by logicalPath.`);
185
185
  };
186
186
  const assertSurfacePaths = (value, location)=>{
187
- if (!Array.isArray(value) || 0 === value.length) throw new Error(`${location} must contain at least one artifact path.`);
187
+ if (!Array.isArray(value)) throw new Error(`${location} must be an array of artifact paths.`);
188
188
  const paths = value.map((item, index)=>assertNormalizedLogicalPath(item, `${location}[${index}]`));
189
189
  const sorted = [
190
190
  ...paths
@@ -206,10 +206,16 @@ const assertSurfaces = (value)=>{
206
206
  'manifest',
207
207
  'container'
208
208
  ], 'surfaces.backendFederation');
209
+ const uiClient = assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient');
210
+ const ssr = assertSurfacePaths(surfaces.ssr, 'surfaces.ssr');
211
+ const apiBackend = assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend');
212
+ if (uiClient.length > 0 && 0 === ssr.length) throw new Error('surfaces.ssr must contain at least one artifact path when UI/client is declared.');
213
+ if (ssr.length > 0 && 0 === uiClient.length) throw new Error('surfaces.uiClient must contain at least one artifact path when SSR is declared.');
214
+ if (0 === apiBackend.length) throw new Error('surfaces.apiBackend must contain at least one artifact path.');
209
215
  return {
210
- uiClient: assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient'),
211
- ssr: assertSurfacePaths(surfaces.ssr, 'surfaces.ssr'),
212
- apiBackend: assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend'),
216
+ uiClient,
217
+ ssr,
218
+ apiBackend,
213
219
  backendFederation: {
214
220
  manifest: assertNormalizedLogicalPath(backendFederation.manifest, 'surfaces.backendFederation.manifest'),
215
221
  container: assertNormalizedLogicalPath(backendFederation.container, 'surfaces.backendFederation.container')
@@ -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,