@bleedingdev/modern-js-app-tools 3.5.0-ultramodern.27 → 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 +158 -30
  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 +158 -30
  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 +158 -30
  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 +11 -3
  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
@@ -261,11 +260,12 @@ const getConfiguredWorkerName = (appDirectory, modernConfig)=>{
261
260
  const configuredName = modernConfig.deploy?.worker?.name?.trim();
262
261
  return configuredName || getWorkerName(appDirectory);
263
262
  };
264
- const normalizeRelativePath = (value, label, scope = 'app output')=>{
263
+ const normalizeRelativePath = (value, label, scope = 'app output', options = {})=>{
265
264
  if ('string' != typeof value) throw new Error(`${label} must be a relative path inside the ${scope}.`);
266
265
  const normalized = value.trim().replace(/\\/g, '/').replace(/^\.\/+/u, '').replace(/\/+$/u, '');
267
266
  const segments = normalized.split('/');
268
- if (!normalized || external_node_path_default().isAbsolute(normalized) || '.' === normalized || segments.includes('..')) throw new Error(`${label} must be a relative path inside the ${scope}.`);
267
+ const isRootDestination = '.' === normalized;
268
+ if (!normalized || external_node_path_default().isAbsolute(normalized) || !options.allowRoot && isRootDestination || segments.includes('..')) throw new Error(`${label} must be a relative path inside the ${scope}.`);
269
269
  return normalized;
270
270
  };
271
271
  const normalizeDirectiveValues = (value)=>{
@@ -420,9 +420,48 @@ const createWranglerD1Databases = (modernConfig, configuredWranglerD1)=>{
420
420
  if (!Array.isArray(d1Databases)) throw new Error('deploy.worker.d1Databases must be an array.');
421
421
  return d1Databases.map(normalizeD1Database);
422
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
+ };
423
460
  const createWranglerConfig = (appDirectory, modernConfig)=>{
424
461
  const wrangler = getConfiguredWrangler(modernConfig);
425
462
  const d1Databases = createWranglerD1Databases(modernConfig, wrangler.d1_databases);
463
+ const serviceBindings = createWorkerServiceBindings(modernConfig, wrangler.services);
464
+ const wranglerServices = createWranglerServices(serviceBindings);
426
465
  return {
427
466
  $schema: 'node_modules/wrangler/config-schema.json',
428
467
  name: getConfiguredWorkerName(appDirectory, modernConfig),
@@ -433,6 +472,9 @@ const createWranglerConfig = (appDirectory, modernConfig)=>{
433
472
  assets: createWranglerAssetsConfig(wrangler.assets),
434
473
  ...void 0 === d1Databases ? {} : {
435
474
  d1_databases: d1Databases
475
+ },
476
+ ...void 0 === wranglerServices ? {} : {
477
+ services: wranglerServices
436
478
  }
437
479
  };
438
480
  };
@@ -451,7 +493,9 @@ const normalizeCloudflareArtifact = (artifact, index)=>{
451
493
  const getCloudflareArtifacts = (modernConfig)=>(modernConfig.deploy?.worker?.artifacts ?? []).map(normalizeCloudflareArtifact);
452
494
  const normalizeCloudflarePublicAsset = (asset, index)=>{
453
495
  const from = normalizeRelativePath(asset.from, `deploy.worker.publicAssets[${index}].from`, 'app root');
454
- const to = normalizeRelativePath(asset.to, `deploy.worker.publicAssets[${index}].to`, 'Cloudflare public output');
496
+ const to = normalizeRelativePath(asset.to, `deploy.worker.publicAssets[${index}].to`, 'Cloudflare public output', {
497
+ allowRoot: true
498
+ });
455
499
  return {
456
500
  from,
457
501
  to,
@@ -493,8 +537,79 @@ const readRouteSpec = async (outputDirectory)=>{
493
537
  routes: Array.isArray(routeSpec.routes) ? routeSpec.routes : []
494
538
  };
495
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
+ };
496
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}`);
497
- const createWorkerManifest = async (outputDirectory, modernConfig, appContext)=>{
612
+ const createWorkerManifest = async (outputDirectory, modernConfig, appContext, deliveryUnitStamp)=>{
498
613
  const routeSpec = await readRouteSpec(outputDirectory);
499
614
  const routes = await Promise.all(routeSpec.routes.map(async (route)=>{
500
615
  const worker = 'string' == typeof route.worker ? route.worker : void 0;
@@ -511,11 +626,12 @@ const createWorkerManifest = async (outputDirectory, modernConfig, appContext)=>
511
626
  const primaryBffPrefix = Array.isArray(bffPrefix) ? bffPrefix[0] : bffPrefix;
512
627
  const isEffectApi = Boolean(modernConfig.bff) && modernConfig.bff?.runtimeFramework !== 'hono';
513
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));
514
630
  if (isEffectApi && primaryBffPrefix && !effectApiWorkerExists) throw createMissingEffectBffWorkerError(outputDirectory, BFF_EFFECT_WORKER_ENTRY);
515
631
  return {
516
632
  version: 1,
517
633
  runtime: {
518
- type: 'cloudflare-module-worker',
634
+ type: external_cloudflare_output_contract_js_namespaceObject.CLOUDFLARE_RUNTIME_TYPE,
519
635
  entry: WORKER_ENTRY,
520
636
  fetchExport: true,
521
637
  nodeListen: false
@@ -531,7 +647,7 @@ const createWorkerManifest = async (outputDirectory, modernConfig, appContext)=>
531
647
  },
532
648
  workerBundles: {
533
649
  directory: WORKER_BUNDLE_DIRECTORY,
534
- format: 'commonjs',
650
+ format: external_cloudflare_output_contract_js_namespaceObject.CLOUDFLARE_WORKER_BUNDLE_FORMAT,
535
651
  importableFromModuleWorker: true,
536
652
  requestHandlerExport: 'requestHandler'
537
653
  },
@@ -540,12 +656,18 @@ const createWorkerManifest = async (outputDirectory, modernConfig, appContext)=>
540
656
  routeManifest: ROUTE_MANIFEST_FILE
541
657
  },
542
658
  security: createCloudflareWorkerSecurityPolicy(modernConfig),
659
+ ...deliveryUnitStamp ? {
660
+ deliveryUnit: deliveryUnitStamp
661
+ } : {},
543
662
  i18n: createI18nWorkerManifest(routeSpec, appContext),
544
663
  bff: isEffectApi && primaryBffPrefix && effectApiWorkerExists ? {
545
664
  runtimeFramework: 'effect',
546
665
  prefix: primaryBffPrefix,
547
666
  worker: BFF_EFFECT_WORKER_ENTRY
548
- } : void 0
667
+ } : void 0,
668
+ ...void 0 === serviceBindings ? {} : {
669
+ serviceBindings
670
+ }
549
671
  };
550
672
  };
551
673
  const createWorkerModuleLoaders = (manifest)=>{
@@ -597,11 +719,12 @@ const shouldCopyToWorkerBundle = (src, workerBundleDirectory)=>{
597
719
  const createCloudflarePreset = ({ appContext, modernConfig })=>{
598
720
  const { appDirectory, distDirectory } = appContext;
599
721
  const outputDirectory = external_node_path_default().join(appDirectory, '.output');
600
- const publicDirectory = external_node_path_default().join(outputDirectory, PUBLIC_ASSETS_DIRECTORY);
601
- const workerEntryPath = external_node_path_default().join(outputDirectory, WORKER_ENTRY);
602
- 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;
603
726
  const routeSpecOutputPath = external_node_path_default().join(outputDirectory, ROUTE_SPEC_OUTPUT);
604
- const wranglerConfigPath = external_node_path_default().join(outputDirectory, WRANGLER_CONFIG_FILE);
727
+ const wranglerConfigPath = outputPlan.paths.wranglerConfig;
605
728
  const cloudflareArtifacts = getCloudflareArtifacts(modernConfig);
606
729
  const publicAssetExcludes = getPublicAssetExcludes(appDirectory, modernConfig);
607
730
  return {
@@ -617,14 +740,12 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
617
740
  const routeSpecSourcePath = external_node_path_default().join(distDirectory, ROUTE_SPEC_FILE);
618
741
  if (await utils_namespaceObject.fs.pathExists(routeSpecSourcePath)) await utils_namespaceObject.fs.copy(routeSpecSourcePath, routeSpecOutputPath);
619
742
  const workerBundleSourceDirectory = external_node_path_default().join(distDirectory, WORKER_BUNDLE_DIRECTORY);
620
- const workerBundleOutputDirectory = external_node_path_default().join(outputDirectory, WORKER_BUNDLE_DIRECTORY);
743
+ const workerBundleOutputDirectory = outputPlan.paths.workerBundle;
621
744
  if (await utils_namespaceObject.fs.pathExists(workerBundleSourceDirectory)) {
622
745
  await utils_namespaceObject.fs.copy(workerBundleSourceDirectory, workerBundleOutputDirectory, {
623
746
  filter: (src)=>shouldCopyToWorkerBundle(src, workerBundleSourceDirectory)
624
747
  });
625
- await utils_namespaceObject.fs.writeJSON(external_node_path_default().join(workerBundleOutputDirectory, 'package.json'), {
626
- type: 'commonjs'
627
- });
748
+ await utils_namespaceObject.fs.writeJSON(outputPlan.paths.workerPackage, outputPlan.packages.worker);
628
749
  }
629
750
  await copyCloudflareArtifacts(appDirectory, outputDirectory, cloudflareArtifacts);
630
751
  await copyCloudflarePublicAssets(appDirectory, publicDirectory, getCloudflarePublicAssets(modernConfig));
@@ -632,17 +753,24 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
632
753
  await utils_namespaceObject.fs.writeJSON(wranglerConfigPath, createWranglerConfig(appDirectory, modernConfig), {
633
754
  spaces: 2
634
755
  });
635
- 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), {
636
758
  spaces: 2
637
759
  });
638
- await utils_namespaceObject.fs.writeJSON(external_node_path_default().join(outputDirectory, OUTPUT_PACKAGE_FILE), {
639
- type: 'module'
640
- });
760
+ await utils_namespaceObject.fs.writeJSON(outputPlan.paths.outputPackage, outputPlan.packages.output);
641
761
  },
642
762
  async genEntry () {
643
763
  const template = await (0, index_js_namespaceObject.readTemplate)('cloudflare-entry.mjs');
644
764
  const manifest = await utils_namespaceObject.fs.readJSON(workerManifestPath);
645
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
+ });
646
774
  }
647
775
  };
648
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: [