@bleedingdev/modern-js-app-tools 3.5.0-ultramodern.28 → 3.5.0-ultramodern.29

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 (28) hide show
  1. package/dist/cjs/index.js +50 -44
  2. package/dist/cjs/plugins/backendFederationBuild.js +347 -0
  3. package/dist/cjs/plugins/deploy/platforms/cloudflare-output-contract.js +91 -0
  4. package/dist/cjs/plugins/deploy/platforms/cloudflare-output-plan.js +93 -0
  5. package/dist/cjs/plugins/deploy/platforms/cloudflare-output-verifier.js +402 -0
  6. package/dist/cjs/plugins/deploy/platforms/cloudflare.js +152 -27
  7. package/dist/cjs/plugins/deploy/platforms/templates/cloudflare-entry.mjs +30 -19
  8. package/dist/esm/index.mjs +2 -0
  9. package/dist/esm/plugins/backendFederationBuild.mjs +296 -0
  10. package/dist/esm/plugins/deploy/platforms/cloudflare-output-contract.mjs +17 -0
  11. package/dist/esm/plugins/deploy/platforms/cloudflare-output-plan.mjs +45 -0
  12. package/dist/esm/plugins/deploy/platforms/cloudflare-output-verifier.mjs +347 -0
  13. package/dist/esm/plugins/deploy/platforms/cloudflare.mjs +152 -27
  14. package/dist/esm/plugins/deploy/platforms/templates/cloudflare-entry.mjs +30 -19
  15. package/dist/esm-node/index.mjs +2 -0
  16. package/dist/esm-node/plugins/backendFederationBuild.mjs +297 -0
  17. package/dist/esm-node/plugins/deploy/platforms/cloudflare-output-contract.mjs +18 -0
  18. package/dist/esm-node/plugins/deploy/platforms/cloudflare-output-plan.mjs +46 -0
  19. package/dist/esm-node/plugins/deploy/platforms/cloudflare-output-verifier.mjs +348 -0
  20. package/dist/esm-node/plugins/deploy/platforms/cloudflare.mjs +152 -27
  21. package/dist/esm-node/plugins/deploy/platforms/templates/cloudflare-entry.mjs +30 -19
  22. package/dist/types/plugins/backendFederationBuild.d.ts +11 -0
  23. package/dist/types/plugins/deploy/platforms/cloudflare-output-contract.d.ts +13 -0
  24. package/dist/types/plugins/deploy/platforms/cloudflare-output-plan.d.ts +33 -0
  25. package/dist/types/plugins/deploy/platforms/cloudflare-output-verifier.d.ts +32 -0
  26. package/dist/types/types/config/cloudflareDeploy.d.ts +11 -0
  27. package/dist/types/types/config/deploy.d.ts +9 -2
  28. package/package.json +17 -11
@@ -43,17 +43,19 @@ const external_node_path_namespaceObject = require("node:path");
43
43
  var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
44
44
  const utils_namespaceObject = require("@modern-js/utils");
45
45
  const index_js_namespaceObject = require("../utils/index.js");
46
- const WORKER_ENTRY = 'server/index.mjs';
47
- const WORKER_MANIFEST = 'server/modern-worker-manifest.json';
48
- const WRANGLER_CONFIG_FILE = 'wrangler.json';
49
- const OUTPUT_PACKAGE_FILE = 'package.json';
50
- const ASSETS_BINDING = 'ASSETS';
46
+ const external_cloudflare_output_contract_js_namespaceObject = require("./cloudflare-output-contract.js");
47
+ const external_cloudflare_output_plan_js_namespaceObject = require("./cloudflare-output-plan.js");
48
+ const external_cloudflare_output_verifier_js_namespaceObject = require("./cloudflare-output-verifier.js");
49
+ const WORKER_ENTRY = external_cloudflare_output_contract_js_namespaceObject.CLOUDFLARE_WORKER_ENTRY;
50
+ const WRANGLER_CONFIG_FILE = external_cloudflare_output_contract_js_namespaceObject.CLOUDFLARE_WRANGLER_CONFIG_FILE;
51
+ const OUTPUT_PACKAGE_FILE = external_cloudflare_output_contract_js_namespaceObject.CLOUDFLARE_OUTPUT_PACKAGE_FILE;
52
+ const ASSETS_BINDING = external_cloudflare_output_contract_js_namespaceObject.CLOUDFLARE_ASSETS_BINDING;
51
53
  const ROUTE_SPEC_FILE = 'route.json';
52
54
  const ROUTE_SPEC_OUTPUT = `server/${ROUTE_SPEC_FILE}`;
53
55
  const LOADABLE_STATS_FILE = 'loadable-stats.json';
54
56
  const ROUTE_MANIFEST_FILE = 'routes-manifest.json';
55
- const PUBLIC_ASSETS_DIRECTORY = 'public';
56
- const WORKER_BUNDLE_DIRECTORY = 'worker';
57
+ const PUBLIC_ASSETS_DIRECTORY = external_cloudflare_output_contract_js_namespaceObject.CLOUDFLARE_PUBLIC_ASSETS_DIRECTORY;
58
+ const WORKER_BUNDLE_DIRECTORY = external_cloudflare_output_contract_js_namespaceObject.CLOUDFLARE_WORKER_BUNDLE_DIRECTORY;
57
59
  const SERVER_BUNDLE_DIRECTORY = 'bundles';
58
60
  const SERVER_OUTPUT_DIRECTORY = 'server';
59
61
  const DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES = [
@@ -64,10 +66,7 @@ const BFF_EFFECT_WORKER_ENTRY = `${WORKER_BUNDLE_DIRECTORY}/__modern_bff_effect.
64
66
  const EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE = 'Ensure the Effect API entry exists at api/index.ts or bff.effect.entry, and import Cloudflare edge handlers from @modern-js/plugin-bff/effect-edge instead of lambda/Hono server helpers.';
65
67
  const DEFAULT_COMPATIBILITY_DATE = '2026-06-02';
66
68
  const COMPATIBILITY_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
67
- const REQUIRED_COMPATIBILITY_FLAGS = [
68
- 'nodejs_compat',
69
- 'global_fetch_strictly_public'
70
- ];
69
+ const REQUIRED_COMPATIBILITY_FLAGS = external_cloudflare_output_contract_js_namespaceObject.CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS;
71
70
  const RESERVED_ARTIFACT_DESTINATION_FILES = new Set([
72
71
  WRANGLER_CONFIG_FILE,
73
72
  OUTPUT_PACKAGE_FILE
@@ -421,9 +420,48 @@ const createWranglerD1Databases = (modernConfig, configuredWranglerD1)=>{
421
420
  if (!Array.isArray(d1Databases)) throw new Error('deploy.worker.d1Databases must be an array.');
422
421
  return d1Databases.map(normalizeD1Database);
423
422
  };
423
+ const normalizeServiceBinding = (service, index)=>{
424
+ const binding = assertNonEmptyString(service.binding, `deploy.worker.services[${index}].binding`);
425
+ const serviceName = assertNonEmptyString(service.service, `deploy.worker.services[${index}].service`);
426
+ const prefix = void 0 === service.prefix ? void 0 : assertNonEmptyString(service.prefix, `deploy.worker.services[${index}].prefix`);
427
+ return {
428
+ binding,
429
+ service: serviceName,
430
+ ...void 0 === prefix ? {} : {
431
+ prefix
432
+ }
433
+ };
434
+ };
435
+ const createWorkerServiceBindings = (modernConfig, configuredWranglerServices)=>{
436
+ const services = modernConfig.deploy?.worker?.services;
437
+ if (void 0 === services) return configuredWranglerServices;
438
+ if (void 0 !== configuredWranglerServices) throw new Error('Use deploy.worker.services or deploy.worker.wrangler.services, not both.');
439
+ if (!Array.isArray(services)) throw new Error('deploy.worker.services must be an array.');
440
+ return services.map(normalizeServiceBinding);
441
+ };
442
+ const createWranglerServices = (serviceBindings)=>{
443
+ if (!Array.isArray(serviceBindings)) return serviceBindings;
444
+ if (!serviceBindings.every(isJsonRecord)) return serviceBindings;
445
+ return serviceBindings.map((service)=>{
446
+ const { prefix, ...wranglerService } = service;
447
+ return wranglerService;
448
+ });
449
+ };
450
+ const createWorkerManifestServiceBindings = (serviceBindings)=>{
451
+ if (!Array.isArray(serviceBindings)) return;
452
+ const dispatchBindings = serviceBindings.filter((service)=>isJsonRecord(service) && 'string' == typeof service.binding && 'string' == typeof service.service && 'string' == typeof service.prefix).map((service)=>({
453
+ binding: service.binding,
454
+ service: service.service,
455
+ prefix: service.prefix,
456
+ interface: 'fetch'
457
+ }));
458
+ return dispatchBindings.length > 0 ? dispatchBindings : void 0;
459
+ };
424
460
  const createWranglerConfig = (appDirectory, modernConfig)=>{
425
461
  const wrangler = getConfiguredWrangler(modernConfig);
426
462
  const d1Databases = createWranglerD1Databases(modernConfig, wrangler.d1_databases);
463
+ const serviceBindings = createWorkerServiceBindings(modernConfig, wrangler.services);
464
+ const wranglerServices = createWranglerServices(serviceBindings);
427
465
  return {
428
466
  $schema: 'node_modules/wrangler/config-schema.json',
429
467
  name: getConfiguredWorkerName(appDirectory, modernConfig),
@@ -434,6 +472,9 @@ const createWranglerConfig = (appDirectory, modernConfig)=>{
434
472
  assets: createWranglerAssetsConfig(wrangler.assets),
435
473
  ...void 0 === d1Databases ? {} : {
436
474
  d1_databases: d1Databases
475
+ },
476
+ ...void 0 === wranglerServices ? {} : {
477
+ services: wranglerServices
437
478
  }
438
479
  };
439
480
  };
@@ -496,8 +537,79 @@ const readRouteSpec = async (outputDirectory)=>{
496
537
  routes: Array.isArray(routeSpec.routes) ? routeSpec.routes : []
497
538
  };
498
539
  };
540
+ const COMPACT_CONFIG_PATH = '.modernjs/ultramodern.json';
541
+ const ULTRAMODERN_BUILD_MODULE = 'shared/ultramodern-build.ts';
542
+ const nonEmptyString = (value)=>'string' == typeof value && value.length > 0 ? value : void 0;
543
+ const toDeliveryUnitIdentity = (value)=>{
544
+ if (!isRecord(value)) return;
545
+ const unitId = nonEmptyString(value.unitId);
546
+ const buildMarker = nonEmptyString(value.buildMarker);
547
+ const sourceRevision = nonEmptyString(value.sourceRevision);
548
+ if (!unitId || !buildMarker || !sourceRevision) return;
549
+ return {
550
+ unitId,
551
+ buildMarker,
552
+ sourceRevision
553
+ };
554
+ };
555
+ const findWorkspaceRoot = async (appDirectory)=>{
556
+ let current = external_node_path_default().resolve(appDirectory);
557
+ for(;;){
558
+ if (await utils_namespaceObject.fs.pathExists(external_node_path_default().join(current, COMPACT_CONFIG_PATH))) return current;
559
+ const parent = external_node_path_default().dirname(current);
560
+ if (parent === current) return;
561
+ current = parent;
562
+ }
563
+ };
564
+ const resolveTopologyDeliveryUnit = async (appDirectory)=>{
565
+ const workspaceRoot = await findWorkspaceRoot(appDirectory);
566
+ if (!workspaceRoot) return;
567
+ let compactConfig;
568
+ try {
569
+ compactConfig = await utils_namespaceObject.fs.readJSON(external_node_path_default().join(workspaceRoot, COMPACT_CONFIG_PATH));
570
+ } catch {
571
+ return;
572
+ }
573
+ if (!isRecord(compactConfig)) return;
574
+ const topology = isRecord(compactConfig.topology) ? compactConfig.topology : void 0;
575
+ const apps = Array.isArray(topology?.apps) ? topology.apps : [];
576
+ const resolvedAppDirectory = external_node_path_default().resolve(appDirectory);
577
+ for (const app of apps){
578
+ if (!isRecord(app)) continue;
579
+ const appPath = nonEmptyString(app.path);
580
+ if (appPath && external_node_path_default().resolve(workspaceRoot, appPath.replace(/^\.\/+/u, '')) === resolvedAppDirectory) return toDeliveryUnitIdentity(app.deliveryUnit);
581
+ }
582
+ return toDeliveryUnitIdentity(compactConfig.deliveryUnit);
583
+ };
584
+ const resolveWorkerDeliveryUnitStamp = async (appDirectory)=>{
585
+ const buildModulePath = external_node_path_default().join(appDirectory, ULTRAMODERN_BUILD_MODULE);
586
+ if (!await utils_namespaceObject.fs.pathExists(buildModulePath)) return;
587
+ const source = await utils_namespaceObject.fs.readFile(buildModulePath, 'utf8');
588
+ const buildMarker = source.match(/\bbuild:\s*['"]([^'"]+)['"]/u)?.[1];
589
+ const unitId = source.match(/\bunitId:\s*['"]([^'"]+)['"]/u)?.[1];
590
+ const sourceRevision = source.match(/\bsourceRevision:\s*['"]([^'"]+)['"]/u)?.[1];
591
+ if (!buildMarker || !unitId || !sourceRevision) return;
592
+ const identity = {
593
+ unitId,
594
+ buildMarker,
595
+ sourceRevision
596
+ };
597
+ return {
598
+ ...identity,
599
+ surfaces: {
600
+ ui: {
601
+ ...identity,
602
+ surface: 'ui'
603
+ },
604
+ api: {
605
+ ...identity,
606
+ surface: 'api'
607
+ }
608
+ }
609
+ };
610
+ };
499
611
  const createMissingEffectBffWorkerError = (outputDirectory, worker)=>new Error(`Cloudflare Effect API runtime is configured, but the BFF worker bundle is missing: ${external_node_path_default().join(outputDirectory, worker)}. ${EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE}`);
500
- const createWorkerManifest = async (outputDirectory, modernConfig, appContext)=>{
612
+ const createWorkerManifest = async (outputDirectory, modernConfig, appContext, deliveryUnitStamp)=>{
501
613
  const routeSpec = await readRouteSpec(outputDirectory);
502
614
  const routes = await Promise.all(routeSpec.routes.map(async (route)=>{
503
615
  const worker = 'string' == typeof route.worker ? route.worker : void 0;
@@ -514,11 +626,12 @@ const createWorkerManifest = async (outputDirectory, modernConfig, appContext)=>
514
626
  const primaryBffPrefix = Array.isArray(bffPrefix) ? bffPrefix[0] : bffPrefix;
515
627
  const isEffectApi = Boolean(modernConfig.bff) && modernConfig.bff?.runtimeFramework !== 'hono';
516
628
  const effectApiWorkerExists = await utils_namespaceObject.fs.pathExists(external_node_path_default().join(outputDirectory, BFF_EFFECT_WORKER_ENTRY));
629
+ const serviceBindings = createWorkerManifestServiceBindings(createWorkerServiceBindings(modernConfig, void 0));
517
630
  if (isEffectApi && primaryBffPrefix && !effectApiWorkerExists) throw createMissingEffectBffWorkerError(outputDirectory, BFF_EFFECT_WORKER_ENTRY);
518
631
  return {
519
632
  version: 1,
520
633
  runtime: {
521
- type: 'cloudflare-module-worker',
634
+ type: external_cloudflare_output_contract_js_namespaceObject.CLOUDFLARE_RUNTIME_TYPE,
522
635
  entry: WORKER_ENTRY,
523
636
  fetchExport: true,
524
637
  nodeListen: false
@@ -534,7 +647,7 @@ const createWorkerManifest = async (outputDirectory, modernConfig, appContext)=>
534
647
  },
535
648
  workerBundles: {
536
649
  directory: WORKER_BUNDLE_DIRECTORY,
537
- format: 'commonjs',
650
+ format: external_cloudflare_output_contract_js_namespaceObject.CLOUDFLARE_WORKER_BUNDLE_FORMAT,
538
651
  importableFromModuleWorker: true,
539
652
  requestHandlerExport: 'requestHandler'
540
653
  },
@@ -543,12 +656,18 @@ const createWorkerManifest = async (outputDirectory, modernConfig, appContext)=>
543
656
  routeManifest: ROUTE_MANIFEST_FILE
544
657
  },
545
658
  security: createCloudflareWorkerSecurityPolicy(modernConfig),
659
+ ...deliveryUnitStamp ? {
660
+ deliveryUnit: deliveryUnitStamp
661
+ } : {},
546
662
  i18n: createI18nWorkerManifest(routeSpec, appContext),
547
663
  bff: isEffectApi && primaryBffPrefix && effectApiWorkerExists ? {
548
664
  runtimeFramework: 'effect',
549
665
  prefix: primaryBffPrefix,
550
666
  worker: BFF_EFFECT_WORKER_ENTRY
551
- } : void 0
667
+ } : void 0,
668
+ ...void 0 === serviceBindings ? {} : {
669
+ serviceBindings
670
+ }
552
671
  };
553
672
  };
554
673
  const createWorkerModuleLoaders = (manifest)=>{
@@ -600,11 +719,12 @@ const shouldCopyToWorkerBundle = (src, workerBundleDirectory)=>{
600
719
  const createCloudflarePreset = ({ appContext, modernConfig })=>{
601
720
  const { appDirectory, distDirectory } = appContext;
602
721
  const outputDirectory = external_node_path_default().join(appDirectory, '.output');
603
- const publicDirectory = external_node_path_default().join(outputDirectory, PUBLIC_ASSETS_DIRECTORY);
604
- const workerEntryPath = external_node_path_default().join(outputDirectory, WORKER_ENTRY);
605
- const workerManifestPath = external_node_path_default().join(outputDirectory, WORKER_MANIFEST);
722
+ const outputPlan = (0, external_cloudflare_output_plan_js_namespaceObject.createCloudflareOutputPlan)(outputDirectory);
723
+ const publicDirectory = outputPlan.paths.publicAssets;
724
+ const workerEntryPath = outputPlan.paths.workerEntry;
725
+ const workerManifestPath = outputPlan.paths.workerManifest;
606
726
  const routeSpecOutputPath = external_node_path_default().join(outputDirectory, ROUTE_SPEC_OUTPUT);
607
- const wranglerConfigPath = external_node_path_default().join(outputDirectory, WRANGLER_CONFIG_FILE);
727
+ const wranglerConfigPath = outputPlan.paths.wranglerConfig;
608
728
  const cloudflareArtifacts = getCloudflareArtifacts(modernConfig);
609
729
  const publicAssetExcludes = getPublicAssetExcludes(appDirectory, modernConfig);
610
730
  return {
@@ -620,14 +740,12 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
620
740
  const routeSpecSourcePath = external_node_path_default().join(distDirectory, ROUTE_SPEC_FILE);
621
741
  if (await utils_namespaceObject.fs.pathExists(routeSpecSourcePath)) await utils_namespaceObject.fs.copy(routeSpecSourcePath, routeSpecOutputPath);
622
742
  const workerBundleSourceDirectory = external_node_path_default().join(distDirectory, WORKER_BUNDLE_DIRECTORY);
623
- const workerBundleOutputDirectory = external_node_path_default().join(outputDirectory, WORKER_BUNDLE_DIRECTORY);
743
+ const workerBundleOutputDirectory = outputPlan.paths.workerBundle;
624
744
  if (await utils_namespaceObject.fs.pathExists(workerBundleSourceDirectory)) {
625
745
  await utils_namespaceObject.fs.copy(workerBundleSourceDirectory, workerBundleOutputDirectory, {
626
746
  filter: (src)=>shouldCopyToWorkerBundle(src, workerBundleSourceDirectory)
627
747
  });
628
- await utils_namespaceObject.fs.writeJSON(external_node_path_default().join(workerBundleOutputDirectory, 'package.json'), {
629
- type: 'commonjs'
630
- });
748
+ await utils_namespaceObject.fs.writeJSON(outputPlan.paths.workerPackage, outputPlan.packages.worker);
631
749
  }
632
750
  await copyCloudflareArtifacts(appDirectory, outputDirectory, cloudflareArtifacts);
633
751
  await copyCloudflarePublicAssets(appDirectory, publicDirectory, getCloudflarePublicAssets(modernConfig));
@@ -635,17 +753,24 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
635
753
  await utils_namespaceObject.fs.writeJSON(wranglerConfigPath, createWranglerConfig(appDirectory, modernConfig), {
636
754
  spaces: 2
637
755
  });
638
- await utils_namespaceObject.fs.writeJSON(workerManifestPath, await createWorkerManifest(outputDirectory, modernConfig, appContext), {
756
+ const deliveryUnitStamp = await resolveWorkerDeliveryUnitStamp(appDirectory);
757
+ await utils_namespaceObject.fs.writeJSON(workerManifestPath, await createWorkerManifest(outputDirectory, modernConfig, appContext, deliveryUnitStamp), {
639
758
  spaces: 2
640
759
  });
641
- await utils_namespaceObject.fs.writeJSON(external_node_path_default().join(outputDirectory, OUTPUT_PACKAGE_FILE), {
642
- type: 'module'
643
- });
760
+ await utils_namespaceObject.fs.writeJSON(outputPlan.paths.outputPackage, outputPlan.packages.output);
644
761
  },
645
762
  async genEntry () {
646
763
  const template = await (0, index_js_namespaceObject.readTemplate)('cloudflare-entry.mjs');
647
764
  const manifest = await utils_namespaceObject.fs.readJSON(workerManifestPath);
648
765
  await utils_namespaceObject.fs.writeFile(workerEntryPath, template.replace('p_workerManifest', JSON.stringify(manifest, null, 2)).replace('p_workerModuleLoaders', createWorkerModuleLoaders(manifest)));
766
+ const topologyDeliveryUnit = await resolveTopologyDeliveryUnit(appDirectory);
767
+ await (0, external_cloudflare_output_verifier_js_namespaceObject.assertCloudflareOutput)({
768
+ outputDirectory,
769
+ importWorker: false,
770
+ ...topologyDeliveryUnit ? {
771
+ deliveryUnit: topologyDeliveryUnit
772
+ } : {}
773
+ });
649
774
  }
650
775
  };
651
776
  };
@@ -2,7 +2,7 @@ const ASSETS_BINDING = 'ASSETS';
2
2
  const MODERN_WORKER_MANIFEST = p_workerManifest;
3
3
  const WORKER_MODULE_LOADERS = p_workerModuleLoaders;
4
4
  const workerModulePromises = new Map();
5
- const effectBffHandlerPromises = new Map();
5
+ const effectBffDispatcherPromises = new Map();
6
6
  let effectEdgeRuntimePromise;
7
7
  const remoteJsonPromises = new Map();
8
8
  const CORS_POLICY = MODERN_WORKER_MANIFEST.security?.cors || {};
@@ -152,17 +152,7 @@ async function createCorsPreflightResponse(request, env) {
152
152
  status: 204
153
153
  });
154
154
  }
155
- if (!ASSET_CORS_ENABLED) return null;
156
- const assets = env?.[ASSETS_BINDING];
157
- if (!assets || 'function' != typeof assets.fetch) return null;
158
- const assetResponse = await assets.fetch(new Request(request.url, {
159
- method: 'HEAD'
160
- }));
161
- if (!assetResponse || 404 === assetResponse.status) return null;
162
- return new Response(null, {
163
- headers: ASSET_CORS_HEADERS,
164
- status: 204
165
- });
155
+ return null;
166
156
  }
167
157
  async function fetchAsset(request, env) {
168
158
  if ('GET' !== request.method && 'HEAD' !== request.method) return null;
@@ -798,17 +788,17 @@ async function dispatchBffRequest(request, env) {
798
788
  const defaultExport = workerModule.default;
799
789
  const runtime = getRuntimeModule(workerModule);
800
790
  if ('effect' === bff.runtimeFramework) {
801
- let effectHandlerPromise = effectBffHandlerPromises.get(bff.worker);
802
- if (!effectHandlerPromise) {
803
- const { createEffectBffEdgeHandler } = await loadEffectEdgeRuntime();
804
- effectHandlerPromise = createEffectBffEdgeHandler({
791
+ let effectDispatcherPromise = effectBffDispatcherPromises.get(bff.worker);
792
+ if (!effectDispatcherPromise) {
793
+ const { createEffectBffEdgeDispatcher } = await loadEffectEdgeRuntime();
794
+ effectDispatcherPromise = createEffectBffEdgeDispatcher({
805
795
  module: runtime,
806
796
  prefix: bff.prefix
807
797
  });
808
- effectBffHandlerPromises.set(bff.worker, effectHandlerPromise);
798
+ effectBffDispatcherPromises.set(bff.worker, effectDispatcherPromise);
809
799
  }
810
- const effectHandler = await effectHandlerPromise;
811
- return effectHandler.handler(request, {
800
+ const effectDispatcher = await effectDispatcherPromise;
801
+ return effectDispatcher.dispatch(request, {
812
802
  env
813
803
  });
814
804
  }
@@ -836,12 +826,33 @@ async function dispatchBffRequest(request, env) {
836
826
  };
837
827
  return handler(mountedRequest, effectContext);
838
828
  }
829
+ async function dispatchServiceBindingRequest(request, env) {
830
+ const serviceBindings = MODERN_WORKER_MANIFEST.serviceBindings;
831
+ if (!Array.isArray(serviceBindings) || 0 === serviceBindings.length) return null;
832
+ const pathname = new URL(request.url).pathname;
833
+ for (const binding of serviceBindings){
834
+ if (!binding?.binding || !binding?.prefix) continue;
835
+ if (!matchesPrefix(pathname, binding.prefix)) continue;
836
+ const service = env?.[binding.binding];
837
+ if (!service || 'function' != typeof service.fetch) return new Response(`Cloudflare service binding not available: ${binding.binding}`, {
838
+ status: 502,
839
+ headers: {
840
+ 'content-type': 'text/plain; charset=utf-8',
841
+ 'x-modern-js-service-binding': binding.binding
842
+ }
843
+ });
844
+ return service.fetch(request);
845
+ }
846
+ return null;
847
+ }
839
848
  export default {
840
849
  async fetch (request, env, ctx) {
841
850
  const corsPreflightResponse = await createCorsPreflightResponse(request, env);
842
851
  if (corsPreflightResponse) return finalizeResponseForRequest(corsPreflightResponse, request);
843
852
  const bffResponse = await dispatchBffRequest(request, env);
844
853
  if (bffResponse) return finalizeResponseForRequest(withAppCorsHeaders(bffResponse, request), request);
854
+ const serviceBindingResponse = await dispatchServiceBindingRequest(request, env);
855
+ if (serviceBindingResponse) return finalizeResponseForRequest(withAppCorsHeaders(serviceBindingResponse, request), request);
845
856
  const route = findRoute(request);
846
857
  const { pathname } = new URL(request.url);
847
858
  if (isAssetLikePathname(pathname) && !routeMatchesExactly(route, pathname)) {
@@ -8,6 +8,7 @@ import { compatPlugin } from "./compat/index.mjs";
8
8
  import { DEFAULT_RUNTIME_CONFIG_FILE } from "./constants.mjs";
9
9
  import { i18n } from "./locale/index.mjs";
10
10
  import analyze from "./plugins/analyze/index.mjs";
11
+ import backendFederationBuild from "./plugins/backendFederationBuild.mjs";
11
12
  import deploy from "./plugins/deploy/index.mjs";
12
13
  import initialize from "./plugins/initialize/index.mjs";
13
14
  import serverBuild from "./plugins/serverBuild.mjs";
@@ -28,6 +29,7 @@ const appTools = ()=>({
28
29
  initialize(),
29
30
  analyze(),
30
31
  serverBuild(),
32
+ backendFederationBuild(),
31
33
  deploy()
32
34
  ],
33
35
  post: [