@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
@@ -28,15 +28,19 @@ var __webpack_require__ = {};
28
28
  var __webpack_exports__ = {};
29
29
  __webpack_require__.r(__webpack_exports__);
30
30
  __webpack_require__.d(__webpack_exports__, {
31
+ createRemoteManifestUrl: ()=>remote_address_js_namespaceObject.createRemoteManifestUrl,
31
32
  getBuildConfigEnvironment: ()=>public_js_namespaceObject.getBuildConfigEnvironment,
32
33
  resolveEffectTsgoCompiler: ()=>public_js_namespaceObject.resolveEffectTsgoCompiler,
33
34
  withBuildConfigEnvironment: ()=>public_js_namespaceObject.withBuildConfigEnvironment
34
35
  });
35
36
  const public_js_namespaceObject = require("./build-config/public.js");
37
+ const remote_address_js_namespaceObject = require("./build-config/remote-address.js");
38
+ exports.createRemoteManifestUrl = __webpack_exports__.createRemoteManifestUrl;
36
39
  exports.getBuildConfigEnvironment = __webpack_exports__.getBuildConfigEnvironment;
37
40
  exports.resolveEffectTsgoCompiler = __webpack_exports__.resolveEffectTsgoCompiler;
38
41
  exports.withBuildConfigEnvironment = __webpack_exports__.withBuildConfigEnvironment;
39
42
  for(var __rspack_i in __webpack_exports__)if (-1 === [
43
+ "createRemoteManifestUrl",
40
44
  "getBuildConfigEnvironment",
41
45
  "resolveEffectTsgoCompiler",
42
46
  "withBuildConfigEnvironment"
@@ -48,6 +48,10 @@ const dependencyKeys = [
48
48
  'dependencies',
49
49
  'optionalDependencies'
50
50
  ];
51
+ const rootDependencyKeys = [
52
+ ...dependencyKeys,
53
+ 'devDependencies'
54
+ ];
51
55
  const exactVersionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
52
56
  async function readPackageJson(filePath) {
53
57
  return JSON.parse(await promises_default().readFile(filePath, 'utf8'));
@@ -63,14 +67,34 @@ function parseNpmAlias(aliasName, specifier) {
63
67
  targetVersion: target.slice(versionSeparator + 1)
64
68
  };
65
69
  }
66
- function aliasesFromPackageJson(packageJson) {
70
+ function aliasesFromPackageJson(packageJson, keys = dependencyKeys) {
67
71
  const aliases = [];
68
- for (const dependencyKey of dependencyKeys)for (const [aliasName, specifier] of Object.entries(packageJson[dependencyKey] ?? {})){
72
+ for (const dependencyKey of keys)for (const [aliasName, specifier] of Object.entries(packageJson[dependencyKey] ?? {})){
69
73
  const alias = parseNpmAlias(aliasName, specifier);
70
74
  if (alias) aliases.push(alias);
71
75
  }
72
76
  return aliases;
73
77
  }
78
+ async function catalogAliasesFromInstalledPackages(appDirectory, packageJson) {
79
+ const aliases = [];
80
+ for (const dependencyKey of rootDependencyKeys)for (const [aliasName, specifier] of Object.entries(packageJson[dependencyKey] ?? {})){
81
+ if (!specifier.startsWith('catalog:')) continue;
82
+ let installedPackage;
83
+ try {
84
+ installedPackage = await readPackageJson(external_node_path_default().join(appDirectory, 'node_modules', aliasName, 'package.json'));
85
+ } catch (error) {
86
+ if ('ENOENT' === error.code) continue;
87
+ throw error;
88
+ }
89
+ if (!installedPackage.name || !installedPackage.version) throw new Error(`Installed catalog package ${aliasName} has no identity`);
90
+ if (installedPackage.name !== aliasName) aliases.push({
91
+ aliasName,
92
+ targetName: installedPackage.name,
93
+ targetVersion: installedPackage.version
94
+ });
95
+ }
96
+ return aliases;
97
+ }
74
98
  async function collectPackageRecords(nodeModulesDirectory) {
75
99
  const records = [];
76
100
  const visitedNodeModules = new Set();
@@ -192,7 +216,8 @@ async function preserveNpmAliases({ appDirectory, outputDirectory, implicitAlias
192
216
  const records = await collectPackageRecords(external_node_path_default().join(outputDirectory, 'node_modules'));
193
217
  const appPackageJson = await readPackageJson(external_node_path_default().join(appDirectory, 'package.json'));
194
218
  const rootAliases = [
195
- ...aliasesFromPackageJson(appPackageJson),
219
+ ...aliasesFromPackageJson(appPackageJson, rootDependencyKeys),
220
+ ...await catalogAliasesFromInstalledPackages(appDirectory, appPackageJson),
196
221
  ...implicitAliases
197
222
  ].filter((alias)=>alias.aliasName !== alias.targetName);
198
223
  const emittedRootAliases = [];
@@ -61,6 +61,11 @@ const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_KIND = 'ultramodern-release-identi
61
61
  const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_SCHEMA_VERSION = 1;
62
62
  const COMPILED_MODULE_PATTERN = /\.(?:c|m)?js$/u;
63
63
  const EFFECT_BFF_WORKER_PATTERN = /^worker\/__modern_bff_effect\.(?:c|m)?js$/u;
64
+ const EFFECT_BFF_WORKER_SUPPORT_PATTERN = /^worker\/__modern_worker_(?:runtime|shared)_*\.(?:c|m)?js$/u;
65
+ const isEffectBffWorkerArtifact = (logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath) || EFFECT_BFF_WORKER_SUPPORT_PATTERN.test(logicalPath);
66
+ const CRAWLER_POLICY_PATH = 'public/robots.txt';
67
+ const isPublicMetadataPath = (logicalPath)=>'public/_headers' === logicalPath || /^public\/\.well-known\/[^/]+\.json$/u.test(logicalPath);
68
+ const PUBLIC_BUILD_ARTIFACT_PATH = `public/${backend_federation_contracts_namespaceObject.ULTRAMODERN_BUILD_ARTIFACT_FILE}`;
64
69
  const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
65
70
  const flattenStrings = (value)=>'string' == typeof value ? [
66
71
  value
@@ -181,7 +186,7 @@ const readBuildArtifact = async (distDirectory, requirePromotable = true)=>{
181
186
  if (!(0, backend_federation_contracts_namespaceObject.isUltramodernBuildArtifact)(artifact)) throw new Error(`[ultramodern-release-envelope] ${backend_federation_contracts_namespaceObject.ULTRAMODERN_BUILD_ARTIFACT_FILE} is missing or invalid.`);
182
187
  if ('workspace' === artifact.deliveryUnit.sourceRevision) {
183
188
  if (!requirePromotable) return;
184
- throw new Error('[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable full-stack envelope.');
189
+ throw new Error('[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable envelope.');
185
190
  }
186
191
  return artifact;
187
192
  };
@@ -251,7 +256,7 @@ const releaseArtifact = (logicalPath, runtime)=>({
251
256
  const writeReleaseIdentityCarrierMetadata = async (artifactRoot, identity, carrierPaths)=>{
252
257
  const surfacesByPath = new Map();
253
258
  for (const [surface, logicalPaths] of Object.entries(carrierPaths)){
254
- if (0 === logicalPaths.length) throw new Error(`[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`);
259
+ if (0 === logicalPaths.length && 'ssr' !== surface && 'uiClient' !== surface) throw new Error(`[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`);
255
260
  for (const logicalPath of logicalPaths){
256
261
  const surfaces = surfacesByPath.get(logicalPath) ?? new Set();
257
262
  surfaces.add(surface);
@@ -291,12 +296,12 @@ const writeReleaseIdentityCarrierMetadata = async (artifactRoot, identity, carri
291
296
  }, null, 2)}\n`);
292
297
  return releaseArtifact(MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_PATH, 'release-identity-metadata');
293
298
  };
294
- const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
299
+ const createReleaseArtifactInputs = async (distDirectory, identity, target, apiOnly)=>{
295
300
  const files = await collectFiles(distDirectory);
296
- 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.');
301
+ 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.');
297
302
  const uiClientPaths = files.filter((logicalPath)=>{
298
303
  const topLevel = logicalPath.split('/')[0];
299
- return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel || [
304
+ return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) || [
300
305
  'index.html',
301
306
  'mf-manifest.json',
302
307
  'mf-stats.json',
@@ -305,16 +310,17 @@ const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
305
310
  'loadable-stats.json'
306
311
  ].includes(logicalPath);
307
312
  });
308
- 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));
313
+ 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));
309
314
  const nodeApiEntryPaths = files.filter((logicalPath)=>logicalPath.startsWith('api/') && COMPILED_MODULE_PATTERN.test(logicalPath));
310
- 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));
311
- if (0 === uiClientPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no UI/client artifacts.`);
312
- if (0 === ssrPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no ${'node' === target ? 'Node' : 'workerd'} SSR artifacts.`);
313
- 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'}.`);
314
- const clientExecutionPaths = await manifestReferencedClientModules(distDirectory, files);
315
- if (0 === clientExecutionPaths.length) throw new Error('[ultramodern-release-envelope] UI/client manifest references no compiled execution module.');
316
- const ssrExecutionPaths = await routeReferencedSsrModules(distDirectory, files, target);
317
- if (0 === ssrExecutionPaths.length) throw new Error(`[ultramodern-release-envelope] route manifest references no emitted ${'node' === target ? 'Node' : 'Cloudflare'} SSR execution module.`);
315
+ const apiBackendPaths = 'node' === target ? files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter(isEffectBffWorkerArtifact);
316
+ 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.');
317
+ if (!apiOnly && 0 === uiClientPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no UI/client artifacts.`);
318
+ if (!apiOnly && 0 === ssrPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no ${'node' === target ? 'Node' : 'workerd'} SSR artifacts.`);
319
+ 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'}.`);
320
+ const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(distDirectory, files);
321
+ if (!apiOnly && 0 === clientExecutionPaths.length) throw new Error('[ultramodern-release-envelope] UI/client manifest references no compiled execution module.');
322
+ const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(distDirectory, files, target);
323
+ if (!apiOnly && 0 === ssrExecutionPaths.length) throw new Error(`[ultramodern-release-envelope] route manifest references no emitted ${'node' === target ? 'Node' : 'Cloudflare'} SSR execution module.`);
318
324
  const runtimeByPath = new Map();
319
325
  const add = (paths, runtime)=>{
320
326
  for (const logicalPath of paths){
@@ -324,6 +330,8 @@ const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
324
330
  }
325
331
  };
326
332
  add(uiClientPaths, 'browser');
333
+ add(files.filter((logicalPath)=>logicalPath === CRAWLER_POLICY_PATH), 'crawler-policy');
334
+ add(files.filter(isPublicMetadataPath), 'public-metadata');
327
335
  add(ssrPaths, 'node' === target ? 'nodejs' : 'workerd');
328
336
  add(apiBackendPaths, 'node' === target ? 'nodejs' : 'workerd-effect');
329
337
  add([
@@ -381,15 +389,14 @@ const pathExists = async (filePath)=>{
381
389
  }
382
390
  };
383
391
  const emitFrameworkMicroVerticalReleaseEnvelope = async ({ apiOnly, distDirectory, requirePromotable = true, target })=>{
384
- if (apiOnly) return;
385
392
  const backendManifestPath = external_node_path_default().join(distDirectory, backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_MANIFEST_FILE);
386
393
  const backendContainerPath = external_node_path_default().join(distDirectory, backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_REMOTE_ENTRY_FILE);
387
394
  const [hasBackendManifest, hasBackendContainer] = await Promise.all([
388
395
  pathExists(backendManifestPath),
389
396
  pathExists(backendContainerPath)
390
397
  ]);
391
- if (!hasBackendManifest && !hasBackendContainer) return;
392
- if (!hasBackendManifest || !hasBackendContainer) throw new Error('[ultramodern-release-envelope] full-stack MicroVertical backend federation manifest and container must be emitted together.');
398
+ if (!hasBackendManifest && !hasBackendContainer && !apiOnly) return;
399
+ if (!hasBackendManifest || !hasBackendContainer) throw new Error('[ultramodern-release-envelope] MicroVertical backend federation manifest and container must be emitted together.');
393
400
  const buildArtifact = await readBuildArtifact(distDirectory, requirePromotable);
394
401
  if (!buildArtifact) return;
395
402
  const identity = {
@@ -399,7 +406,7 @@ const emitFrameworkMicroVerticalReleaseEnvelope = async ({ apiOnly, distDirector
399
406
  releaseVersion: buildArtifact.deliveryUnit.version
400
407
  };
401
408
  await readBackendManifest(distDirectory, identity);
402
- const { artifacts, surfaces } = await createReleaseArtifactInputs(distDirectory, identity, target);
409
+ const { artifacts, surfaces } = await createReleaseArtifactInputs(distDirectory, identity, target, apiOnly);
403
410
  const envelope = await (0, external_index_js_namespaceObject.createMicroVerticalReleaseEnvelope)({
404
411
  artifactRoot: distDirectory,
405
412
  target,
@@ -430,19 +437,16 @@ const readFrameworkMicroVerticalReleaseEnvelope = async ({ artifactRoot, expecte
430
437
  });
431
438
  };
432
439
  const verifyBuildOutputReleaseEnvelope = async (distDirectory, expectedTarget)=>{
433
- const [hasEnvelope, hasBuildArtifact, hasBackendManifest, hasBackendEntry] = await Promise.all([
440
+ const [hasEnvelope, hasBuildArtifact] = await Promise.all([
434
441
  pathExists(external_node_path_default().join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH)),
435
- pathExists(external_node_path_default().join(distDirectory, backend_federation_contracts_namespaceObject.ULTRAMODERN_BUILD_ARTIFACT_FILE)),
436
- pathExists(external_node_path_default().join(distDirectory, backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_MANIFEST_FILE)),
437
- pathExists(external_node_path_default().join(distDirectory, backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_REMOTE_ENTRY_FILE))
442
+ pathExists(external_node_path_default().join(distDirectory, backend_federation_contracts_namespaceObject.ULTRAMODERN_BUILD_ARTIFACT_FILE))
438
443
  ]);
439
- const isFullStackMicroVertical = hasBuildArtifact && (hasBackendManifest || hasBackendEntry);
440
444
  return readFrameworkMicroVerticalReleaseEnvelope({
441
445
  artifactRoot: distDirectory,
442
446
  ...expectedTarget ? {
443
447
  expectedTarget
444
448
  } : {},
445
- required: hasEnvelope || isFullStackMicroVertical
449
+ required: hasEnvelope || hasBuildArtifact
446
450
  });
447
451
  };
448
452
  const verifyNodeReleaseEnvelopeStaging = async ({ outputDirectory })=>readFrameworkMicroVerticalReleaseEnvelope({
@@ -450,7 +454,7 @@ const verifyNodeReleaseEnvelopeStaging = async ({ outputDirectory })=>readFramew
450
454
  expectedTarget: 'node',
451
455
  required: true
452
456
  });
453
- const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=>{
457
+ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity, apiOnly)=>{
454
458
  const files = await collectFiles(outputDirectory);
455
459
  for (const requiredPath of [
456
460
  'index.js',
@@ -464,7 +468,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
464
468
  ]);
465
469
  const uiClientPaths = files.filter((logicalPath)=>{
466
470
  const topLevel = logicalPath.split('/')[0];
467
- return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel || [
471
+ return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) || [
468
472
  'index.html',
469
473
  'mf-manifest.json',
470
474
  'mf-stats.json',
@@ -475,6 +479,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
475
479
  });
476
480
  const ssrPaths = files.filter((logicalPath)=>'index.js' === logicalPath || logicalPath.startsWith('bundles/') && COMPILED_MODULE_PATTERN.test(logicalPath));
477
481
  const apiBackendPaths = files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath));
482
+ 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.');
478
483
  for (const [surface, paths] of [
479
484
  [
480
485
  'UI/client',
@@ -482,22 +487,24 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
482
487
  ],
483
488
  [
484
489
  'Node SSR',
485
- ssrPaths
490
+ apiOnly ? [] : ssrPaths
486
491
  ],
487
492
  [
488
493
  'Node API/backend',
489
494
  apiBackendPaths
490
495
  ]
491
- ])if (0 === paths.length) throw new Error(`[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`);
492
- const clientExecutionPaths = await manifestReferencedClientModules(outputDirectory, files);
493
- const ssrExecutionPaths = await routeReferencedSsrModules(outputDirectory, files, 'node');
494
- 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.');
496
+ ])if (0 === paths.length && (!apiOnly || 'Node API/backend' === surface)) throw new Error(`[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`);
497
+ const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(outputDirectory, files);
498
+ const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(outputDirectory, files, 'node');
499
+ 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.');
495
500
  const runtimeByPath = new Map(files.filter((logicalPath)=>!backendFederationPaths.has(logicalPath)).map((logicalPath)=>[
496
501
  logicalPath,
497
502
  'nodejs-deployment'
498
503
  ]));
499
504
  for (const logicalPath of uiClientPaths)runtimeByPath.set(logicalPath, 'browser');
500
- for (const logicalPath of ssrPaths)runtimeByPath.set(logicalPath, 'nodejs');
505
+ if (files.includes(CRAWLER_POLICY_PATH)) runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
506
+ for (const logicalPath of files.filter(isPublicMetadataPath))runtimeByPath.set(logicalPath, 'public-metadata');
507
+ for (const logicalPath of apiOnly ? [] : ssrPaths)runtimeByPath.set(logicalPath, 'nodejs');
501
508
  for (const logicalPath of apiBackendPaths)runtimeByPath.set(logicalPath, 'nodejs');
502
509
  runtimeByPath.set(backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_MANIFEST_FILE, 'module-federation-manifest');
503
510
  runtimeByPath.set(backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_REMOTE_ENTRY_FILE, 'nodejs');
@@ -507,7 +514,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
507
514
  backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_MANIFEST_FILE,
508
515
  backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_REMOTE_ENTRY_FILE
509
516
  ],
510
- ssr: [
517
+ ssr: apiOnly ? [] : [
511
518
  ...new Set([
512
519
  ...ssrExecutionPaths,
513
520
  ...ssrPaths.filter((logicalPath)=>'index.js' !== logicalPath)
@@ -526,7 +533,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
526
533
  uiClient: [
527
534
  ...uiClientPaths
528
535
  ].sort((left, right)=>left.localeCompare(right)),
529
- ssr: [
536
+ ssr: apiOnly ? [] : [
530
537
  ...ssrPaths
531
538
  ].sort((left, right)=>left.localeCompare(right)),
532
539
  apiBackend: [
@@ -542,7 +549,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
542
549
  const emitNodeStagedReleaseEnvelope = async ({ distDirectory, outputDirectory })=>{
543
550
  const source = await verifyBuildOutputReleaseEnvelope(distDirectory, 'node');
544
551
  if (!source) return;
545
- const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(outputDirectory, source.identity);
552
+ const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(outputDirectory, source.identity, 0 === source.surfaces.uiClient.length);
546
553
  const staged = await (0, external_index_js_namespaceObject.createMicroVerticalReleaseEnvelope)({
547
554
  artifactRoot: outputDirectory,
548
555
  target: 'node',
@@ -560,21 +567,25 @@ const stageCloudflareReleaseEnvelope = async ({ distDirectory, outputDirectory }
560
567
  if (await pathExists(stagedEnvelopePath)) throw new Error('[ultramodern-release-envelope] Cloudflare staging contains a stale envelope before final output generation.');
561
568
  return source;
562
569
  };
563
- const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, identity)=>{
570
+ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, identity, apiOnly)=>{
564
571
  const files = await collectFiles(outputDirectory);
565
572
  for (const requiredPath of [
566
573
  'server/index.mjs',
567
574
  'server/modern-worker-manifest.json',
568
575
  'wrangler.json',
569
576
  'package.json',
570
- 'worker/package.json'
577
+ ...apiOnly ? [] : [
578
+ 'worker/package.json'
579
+ ]
571
580
  ])if (!files.includes(requiredPath)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging is missing ${requiredPath}.`);
572
581
  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.');
573
582
  const backendManifestPath = `public/${backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_MANIFEST_FILE}`;
574
583
  const backendContainerPath = `public/${backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`;
575
- const apiBackendPaths = files.filter((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath));
576
- const ssrPaths = files.filter((logicalPath)=>(logicalPath.startsWith('server/') || logicalPath.startsWith('worker/')) && COMPILED_MODULE_PATTERN.test(logicalPath) && !EFFECT_BFF_WORKER_PATTERN.test(logicalPath));
577
- const uiClientPaths = files.filter((logicalPath)=>logicalPath.startsWith('public/') && logicalPath !== backendManifestPath && logicalPath !== backendContainerPath);
584
+ const apiBackendPaths = files.filter(isEffectBffWorkerArtifact);
585
+ 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.');
586
+ const ssrPaths = files.filter((logicalPath)=>(logicalPath.startsWith('server/') || logicalPath.startsWith('worker/')) && COMPILED_MODULE_PATTERN.test(logicalPath) && !isEffectBffWorkerArtifact(logicalPath));
587
+ const uiClientPaths = files.filter((logicalPath)=>logicalPath.startsWith('public/') && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) && logicalPath !== PUBLIC_BUILD_ARTIFACT_PATH && logicalPath !== backendManifestPath && logicalPath !== backendContainerPath);
588
+ 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.');
578
589
  for (const [surface, paths] of [
579
590
  [
580
591
  'UI/client',
@@ -582,27 +593,29 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
582
593
  ],
583
594
  [
584
595
  'Cloudflare SSR',
585
- ssrPaths
596
+ apiOnly ? [] : ssrPaths
586
597
  ],
587
598
  [
588
599
  'Cloudflare API/backend',
589
600
  apiBackendPaths
590
601
  ]
591
- ])if (0 === paths.length) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`);
602
+ ])if (0 === paths.length && (!apiOnly || 'Cloudflare API/backend' === surface)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`);
592
603
  for (const logicalPath of [
593
604
  backendManifestPath,
594
605
  backendContainerPath
595
606
  ])if (!files.includes(logicalPath)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging is missing ${logicalPath}.`);
596
- const clientExecutionPaths = await manifestReferencedClientModules(outputDirectory, files, 'public/mf-manifest.json');
597
- const ssrExecutionPaths = await routeReferencedSsrModules(outputDirectory, files, 'cloudflare', 'server/route.json');
598
- 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.');
607
+ const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(outputDirectory, files, 'public/mf-manifest.json');
608
+ const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(outputDirectory, files, 'cloudflare', 'server/route.json');
609
+ 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.');
599
610
  const runtimeByPath = new Map();
600
611
  for (const logicalPath of files)runtimeByPath.set(logicalPath, 'cloudflare-deployment');
601
612
  const add = (paths, runtime)=>{
602
613
  for (const logicalPath of paths)runtimeByPath.set(logicalPath, runtime);
603
614
  };
604
615
  add(uiClientPaths, 'browser');
605
- add(ssrPaths, 'workerd');
616
+ if (files.includes(CRAWLER_POLICY_PATH)) runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
617
+ add(files.filter(isPublicMetadataPath), 'public-metadata');
618
+ add(apiOnly ? [] : ssrPaths, 'workerd');
606
619
  add(apiBackendPaths, 'workerd-effect');
607
620
  add([
608
621
  backendManifestPath
@@ -616,7 +629,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
616
629
  backendManifestPath,
617
630
  backendContainerPath
618
631
  ],
619
- ssr: [
632
+ ssr: apiOnly ? [] : [
620
633
  ...new Set([
621
634
  ...ssrExecutionPaths,
622
635
  ...ssrPaths.filter((logicalPath)=>logicalPath.startsWith('worker/'))
@@ -635,7 +648,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
635
648
  uiClient: [
636
649
  ...uiClientPaths
637
650
  ].sort((left, right)=>left.localeCompare(right)),
638
- ssr: [
651
+ ssr: apiOnly ? [] : [
639
652
  ...ssrPaths
640
653
  ].sort((left, right)=>left.localeCompare(right)),
641
654
  apiBackend: [
@@ -651,7 +664,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
651
664
  const emitCloudflareStagedReleaseEnvelope = async ({ distDirectory, outputDirectory })=>{
652
665
  const source = await verifyBuildOutputReleaseEnvelope(distDirectory, 'cloudflare');
653
666
  if (!source) return;
654
- const { artifacts, surfaces } = await createCloudflareStagedReleaseArtifactInputs(outputDirectory, source.identity);
667
+ const { artifacts, surfaces } = await createCloudflareStagedReleaseArtifactInputs(outputDirectory, source.identity, 0 === source.surfaces.uiClient.length);
655
668
  const staged = await (0, external_index_js_namespaceObject.createMicroVerticalReleaseEnvelope)({
656
669
  artifactRoot: outputDirectory,
657
670
  target: 'cloudflare',
@@ -232,7 +232,7 @@ const assertUniqueSortedArtifacts = (artifacts, location)=>{
232
232
  if (paths.some((logicalPath, index)=>logicalPath !== sorted[index])) throw new Error(`${location} must be sorted by logicalPath.`);
233
233
  };
234
234
  const assertSurfacePaths = (value, location)=>{
235
- if (!Array.isArray(value) || 0 === value.length) throw new Error(`${location} must contain at least one artifact path.`);
235
+ if (!Array.isArray(value)) throw new Error(`${location} must be an array of artifact paths.`);
236
236
  const paths = value.map((item, index)=>assertNormalizedLogicalPath(item, `${location}[${index}]`));
237
237
  const sorted = [
238
238
  ...paths
@@ -254,10 +254,16 @@ const assertSurfaces = (value)=>{
254
254
  'manifest',
255
255
  'container'
256
256
  ], 'surfaces.backendFederation');
257
+ const uiClient = assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient');
258
+ const ssr = assertSurfacePaths(surfaces.ssr, 'surfaces.ssr');
259
+ const apiBackend = assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend');
260
+ if (uiClient.length > 0 && 0 === ssr.length) throw new Error('surfaces.ssr must contain at least one artifact path when UI/client is declared.');
261
+ if (ssr.length > 0 && 0 === uiClient.length) throw new Error('surfaces.uiClient must contain at least one artifact path when SSR is declared.');
262
+ if (0 === apiBackend.length) throw new Error('surfaces.apiBackend must contain at least one artifact path.');
257
263
  return {
258
- uiClient: assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient'),
259
- ssr: assertSurfacePaths(surfaces.ssr, 'surfaces.ssr'),
260
- apiBackend: assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend'),
264
+ uiClient,
265
+ ssr,
266
+ apiBackend,
261
267
  backendFederation: {
262
268
  manifest: assertNormalizedLogicalPath(backendFederation.manifest, 'surfaces.backendFederation.manifest'),
263
269
  container: assertNormalizedLogicalPath(backendFederation.container, 'surfaces.backendFederation.container')
@@ -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 };