@bleedingdev/modern-js-app-tools 3.5.0-ultramodern.3 → 3.5.0-ultramodern.30

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 (32) hide show
  1. package/dist/cjs/builder/generator/getBuilderEnvironments.js +1 -0
  2. package/dist/cjs/index.js +50 -44
  3. package/dist/cjs/plugins/backendFederationBuild.js +347 -0
  4. package/dist/cjs/plugins/deploy/platforms/cloudflare-output-contract.js +91 -0
  5. package/dist/cjs/plugins/deploy/platforms/cloudflare-output-plan.js +93 -0
  6. package/dist/cjs/plugins/deploy/platforms/cloudflare-output-verifier.js +402 -0
  7. package/dist/cjs/plugins/deploy/platforms/cloudflare.js +272 -29
  8. package/dist/cjs/plugins/deploy/platforms/templates/cloudflare-entry.mjs +346 -43
  9. package/dist/esm/builder/generator/getBuilderEnvironments.mjs +1 -0
  10. package/dist/esm/index.mjs +2 -0
  11. package/dist/esm/plugins/backendFederationBuild.mjs +296 -0
  12. package/dist/esm/plugins/deploy/platforms/cloudflare-output-contract.mjs +17 -0
  13. package/dist/esm/plugins/deploy/platforms/cloudflare-output-plan.mjs +45 -0
  14. package/dist/esm/plugins/deploy/platforms/cloudflare-output-verifier.mjs +347 -0
  15. package/dist/esm/plugins/deploy/platforms/cloudflare.mjs +272 -29
  16. package/dist/esm/plugins/deploy/platforms/templates/cloudflare-entry.mjs +346 -43
  17. package/dist/esm-node/builder/generator/getBuilderEnvironments.mjs +1 -0
  18. package/dist/esm-node/index.mjs +2 -0
  19. package/dist/esm-node/plugins/backendFederationBuild.mjs +297 -0
  20. package/dist/esm-node/plugins/deploy/platforms/cloudflare-output-contract.mjs +18 -0
  21. package/dist/esm-node/plugins/deploy/platforms/cloudflare-output-plan.mjs +46 -0
  22. package/dist/esm-node/plugins/deploy/platforms/cloudflare-output-verifier.mjs +348 -0
  23. package/dist/esm-node/plugins/deploy/platforms/cloudflare.mjs +272 -29
  24. package/dist/esm-node/plugins/deploy/platforms/templates/cloudflare-entry.mjs +346 -43
  25. package/dist/types/plugins/backendFederationBuild.d.ts +11 -0
  26. package/dist/types/plugins/deploy/platforms/cloudflare-output-contract.d.ts +13 -0
  27. package/dist/types/plugins/deploy/platforms/cloudflare-output-plan.d.ts +33 -0
  28. package/dist/types/plugins/deploy/platforms/cloudflare-output-verifier.d.ts +32 -0
  29. package/dist/types/types/config/cloudflareDeploy.d.ts +22 -0
  30. package/dist/types/types/config/deploy.d.ts +15 -2
  31. package/lib/types.d.ts +2 -3
  32. package/package.json +17 -11
@@ -2,6 +2,8 @@ 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 effectBffDispatcherPromises = new Map();
6
+ let effectEdgeRuntimePromise;
5
7
  const remoteJsonPromises = new Map();
6
8
  const CORS_POLICY = MODERN_WORKER_MANIFEST.security?.cors || {};
7
9
  const ASSET_CORS_ENABLED = false !== CORS_POLICY.assets;
@@ -150,19 +152,10 @@ async function createCorsPreflightResponse(request, env) {
150
152
  status: 204
151
153
  });
152
154
  }
153
- if (!ASSET_CORS_ENABLED) return null;
154
- const assets = env?.[ASSETS_BINDING];
155
- if (!assets || 'function' != typeof assets.fetch) return null;
156
- const assetResponse = await assets.fetch(new Request(request.url, {
157
- method: 'HEAD'
158
- }));
159
- if (!assetResponse || 404 === assetResponse.status) return null;
160
- return new Response(null, {
161
- headers: ASSET_CORS_HEADERS,
162
- status: 204
163
- });
155
+ return null;
164
156
  }
165
157
  async function fetchAsset(request, env) {
158
+ if ('GET' !== request.method && 'HEAD' !== request.method) return null;
166
159
  const assets = env?.[ASSETS_BINDING];
167
160
  if (!assets || 'function' != typeof assets.fetch) return null;
168
161
  const response = await assets.fetch(request);
@@ -211,7 +204,7 @@ function getPathExtension(pathname) {
211
204
  }
212
205
  function isAssetLikePathname(pathname) {
213
206
  const extension = getPathExtension(pathname);
214
- return '' !== extension && '.html' !== extension && '.htm' !== extension;
207
+ return '' !== extension;
215
208
  }
216
209
  function routeMatchesExactly(route, pathname) {
217
210
  if ('string' != typeof route?.urlPath) return false;
@@ -223,6 +216,255 @@ function routeMatches(route, pathname) {
223
216
  const requestPath = normalizeRoutePath(pathname);
224
217
  return routePath === requestPath || '/' === routePath && route.isSSR || requestPath.startsWith(`${routePath}/`);
225
218
  }
219
+ function getRouteI18nEntry(route) {
220
+ if (!route || 'string' != typeof route.entryName) return;
221
+ return MODERN_WORKER_MANIFEST.i18n?.entries?.[route.entryName];
222
+ }
223
+ function getRouteBasePath(route) {
224
+ const routePath = 'string' == typeof route?.urlPath ? normalizeRoutePath(route.urlPath) : '/';
225
+ return routePath || '/';
226
+ }
227
+ function getRouteRemainingPathname(route, pathname) {
228
+ const basePath = getRouteBasePath(route);
229
+ if ('/' === basePath) return pathname || '/';
230
+ if (!matchesPrefix(pathname, basePath)) return pathname || '/';
231
+ const remaining = pathname.slice(basePath.length);
232
+ return remaining ? remaining.startsWith('/') ? remaining : `/${remaining}` : '/';
233
+ }
234
+ function normalizeLanguage(value, languages) {
235
+ if ('string' != typeof value || '' === value.trim()) return null;
236
+ const normalized = value.trim().toLowerCase();
237
+ const exact = languages.find((language)=>language.toLowerCase() === normalized);
238
+ if (exact) return exact;
239
+ const baseLanguage = normalized.split(/[-_]/u)[0];
240
+ return languages.find((language)=>language.toLowerCase() === baseLanguage) || null;
241
+ }
242
+ function getLanguageFromPath(route, pathname, languages) {
243
+ const remainingPathname = getRouteRemainingPathname(route, pathname);
244
+ const firstSegment = remainingPathname.split('/').filter(Boolean)[0];
245
+ return normalizeLanguage(firstSegment, languages);
246
+ }
247
+ function safeDecodeURIComponent(value) {
248
+ try {
249
+ return decodeURIComponent(value);
250
+ } catch {
251
+ return value;
252
+ }
253
+ }
254
+ function detectLanguageFromCookie(cookieHeader, lookupCookie, languages) {
255
+ if (!cookieHeader) return null;
256
+ for (const cookiePart of cookieHeader.split(';')){
257
+ const [rawName, ...rawValue] = cookiePart.trim().split('=');
258
+ if (rawName === lookupCookie && 0 !== rawValue.length) return normalizeLanguage(safeDecodeURIComponent(rawValue.join('=')), languages);
259
+ }
260
+ return null;
261
+ }
262
+ function detectLanguageFromAcceptLanguage(acceptLanguage, languages) {
263
+ if (!acceptLanguage) return null;
264
+ return acceptLanguage.split(',').map((entry, index)=>{
265
+ const [rawRange, ...rawParameters] = entry.trim().split(';');
266
+ const qualityParameter = rawParameters.find((parameter)=>parameter.trim().startsWith('q='));
267
+ const quality = qualityParameter ? Number.parseFloat(qualityParameter.split('=')[1] || '') : 1;
268
+ return {
269
+ index,
270
+ language: normalizeLanguage(rawRange, languages),
271
+ quality: Number.isFinite(quality) ? quality : 1
272
+ };
273
+ }).filter((candidate)=>candidate.language && candidate.quality > 0).sort((left, right)=>right.quality - left.quality || left.index - right.index)[0]?.language || null;
274
+ }
275
+ function detectRedirectLanguage(request, entry) {
276
+ if (!entry.i18nextDetector) return null;
277
+ const url = new URL(request.url);
278
+ const detection = entry.detection || {};
279
+ const order = Array.isArray(detection.order) ? detection.order : [
280
+ 'querystring',
281
+ 'cookie',
282
+ 'header'
283
+ ];
284
+ for (const method of order){
285
+ const language = 'querystring' === method ? normalizeLanguage(url.searchParams.get(detection.lookupQuerystring || 'lng'), entry.languages) : 'cookie' === method ? detectLanguageFromCookie(request.headers.get('cookie'), detection.lookupCookie || 'i18next', entry.languages) : 'header' === method ? detectLanguageFromAcceptLanguage(request.headers.get(detection.lookupHeader || 'accept-language'), entry.languages) : null;
286
+ if (language) return language;
287
+ }
288
+ return null;
289
+ }
290
+ function normaliseSlashes(pathname) {
291
+ const withoutDuplicateSlashes = pathname.replace(/\/+/g, '/');
292
+ const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
293
+ return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/u, '') : withLeadingSlash;
294
+ }
295
+ function normalisePathPattern(pathname) {
296
+ return normaliseSlashes(pathname).replace(/\[(.+?)\]/g, ':$1');
297
+ }
298
+ function stripLanguagePrefix(pathname, languages) {
299
+ const segments = pathname.split('/').filter(Boolean);
300
+ const firstSegment = segments[0];
301
+ if (firstSegment && normalizeLanguage(firstSegment, languages)) return `/${segments.slice(1).join('/')}` || '/';
302
+ return pathname || '/';
303
+ }
304
+ function escapeRegExp(value) {
305
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
306
+ }
307
+ function getPatternParamName(segment) {
308
+ return segment.slice(1).replace(/\?$/, '');
309
+ }
310
+ function compilePathPattern(pattern) {
311
+ const names = [];
312
+ const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
313
+ const source = segments.map((segment)=>{
314
+ if (segment.startsWith(':')) {
315
+ names.push(getPatternParamName(segment));
316
+ return segment.endsWith('?') ? '(?:/([^/]+))?' : '/([^/]+)';
317
+ }
318
+ if ('*' === segment) {
319
+ names.push('*');
320
+ return '/(.*)';
321
+ }
322
+ return `/${escapeRegExp(segment)}`;
323
+ }).join('');
324
+ return {
325
+ names,
326
+ regexp: new RegExp(`^${source || '/'}$`)
327
+ };
328
+ }
329
+ function matchPathPattern(pathname, pattern) {
330
+ const { names, regexp } = compilePathPattern(pattern);
331
+ const match = regexp.exec(normaliseSlashes(pathname));
332
+ if (!match) return null;
333
+ const params = {};
334
+ for(let index = 0; index < names.length; index++)try {
335
+ params[names[index]] = decodeURIComponent(match[index + 1] || '');
336
+ } catch {
337
+ return null;
338
+ }
339
+ return params;
340
+ }
341
+ function buildPathFromPattern(pattern, params) {
342
+ const path = normalisePathPattern(pattern).split('/').filter(Boolean).map((segment)=>{
343
+ if (segment.startsWith(':')) {
344
+ const param = params[getPatternParamName(segment)];
345
+ return param ? encodeURIComponent(param) : '';
346
+ }
347
+ if ('*' === segment) return params['*'] || '';
348
+ return segment;
349
+ }).filter(Boolean).join('/');
350
+ return `/${path}`;
351
+ }
352
+ function patternSpecificity(pattern) {
353
+ const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
354
+ let staticSegments = 0;
355
+ let dynamicSegments = 0;
356
+ let splatSegments = 0;
357
+ for (const segment of segments)if ('*' === segment) splatSegments++;
358
+ else if (segment.startsWith(':')) dynamicSegments++;
359
+ else staticSegments++;
360
+ return {
361
+ dynamicSegments,
362
+ splatSegments,
363
+ staticSegments,
364
+ totalSegments: segments.length
365
+ };
366
+ }
367
+ function comparePatternSpecificity(left, right) {
368
+ const leftSpecificity = patternSpecificity(left);
369
+ const rightSpecificity = patternSpecificity(right);
370
+ return rightSpecificity.staticSegments - leftSpecificity.staticSegments || rightSpecificity.totalSegments - leftSpecificity.totalSegments || leftSpecificity.splatSegments - rightSpecificity.splatSegments || leftSpecificity.dynamicSegments - rightSpecificity.dynamicSegments;
371
+ }
372
+ function sortLocalisedPatterns(patterns) {
373
+ return patterns.map((pattern, index)=>({
374
+ pattern,
375
+ index
376
+ })).sort((left, right)=>comparePatternSpecificity(left.pattern.pattern, right.pattern.pattern) || left.index - right.index).map(({ pattern })=>pattern);
377
+ }
378
+ function resolveLocalisedPath(pathname, targetLanguage, languages, localisedUrls) {
379
+ const normalizedPathname = normaliseSlashes(pathname);
380
+ const canonicalCandidates = sortLocalisedPatterns(Object.entries(localisedUrls || {}).map(([canonicalPattern, localisedUrlEntry])=>({
381
+ canonicalPattern,
382
+ localisedUrlEntry,
383
+ pattern: canonicalPattern
384
+ })));
385
+ for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
386
+ const targetPattern = localisedUrlEntry[targetLanguage];
387
+ const params = targetPattern ? matchPathPattern(normalizedPathname, canonicalPattern) : null;
388
+ if (params) return buildPathFromPattern(targetPattern, params);
389
+ }
390
+ const localisedCandidates = sortLocalisedPatterns(Object.values(localisedUrls || {}).flatMap((localisedUrlEntry)=>{
391
+ const targetPattern = localisedUrlEntry[targetLanguage];
392
+ if (!targetPattern) return [];
393
+ return languages.map((language)=>localisedUrlEntry[language]).filter(Boolean).map((sourcePattern)=>({
394
+ pattern: sourcePattern,
395
+ sourcePattern,
396
+ targetPattern
397
+ }));
398
+ }));
399
+ for (const { sourcePattern, targetPattern } of localisedCandidates){
400
+ const params = matchPathPattern(normalizedPathname, sourcePattern);
401
+ if (params) return buildPathFromPattern(targetPattern, params);
402
+ }
403
+ return normalizedPathname;
404
+ }
405
+ function localiseTargetPathname(pathname, language, languages, localisedUrls) {
406
+ const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
407
+ const resolvedPath = localisedUrls ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrls) : pathWithoutLanguage;
408
+ const resolvedSegments = resolvedPath.split('/').filter(Boolean);
409
+ return `/${[
410
+ language,
411
+ ...resolvedSegments
412
+ ].join('/')}`;
413
+ }
414
+ function shouldIgnoreLocaleRedirect(pathname, route, entry) {
415
+ const remainingPathname = getRouteRemainingPathname(route, pathname);
416
+ const normalizedPathname = remainingPathname.startsWith('/') ? remainingPathname : `/${remainingPathname}`;
417
+ const ignoredPrefixes = [
418
+ ...entry.ignoreRedirectRoutes || [],
419
+ ...entry.staticRoutePrefixes || [],
420
+ '/static',
421
+ '/upload'
422
+ ];
423
+ return ignoredPrefixes.some((prefix)=>{
424
+ const normalizedPrefix = normaliseSlashes(prefix);
425
+ return normalizedPathname === normalizedPrefix || normalizedPathname.startsWith(`${normalizedPrefix}/`);
426
+ });
427
+ }
428
+ function buildLocalizedUrl(route, request, language, entry) {
429
+ const url = new URL(request.url);
430
+ const basePath = getRouteBasePath(route);
431
+ const remainingPathname = getRouteRemainingPathname(route, url.pathname);
432
+ const localizedPathname = localiseTargetPathname(remainingPathname, language, entry.languages, entry.localisedUrls);
433
+ const nextPathname = '/' === basePath ? localizedPathname : normaliseSlashes(`${basePath}/${localizedPathname}`);
434
+ return `${nextPathname}${url.search}`;
435
+ }
436
+ function createLocaleRedirectResponse(location, entry) {
437
+ const headers = new Headers({
438
+ 'cache-control': 'private, no-store',
439
+ location
440
+ });
441
+ const vary = [];
442
+ const detectionOrder = entry.i18nextDetector ? entry.detection?.order || [] : [];
443
+ if (detectionOrder.includes('header')) vary.push('Accept-Language');
444
+ if (detectionOrder.includes('cookie')) vary.push('Cookie');
445
+ if (vary.length > 0) headers.set('vary', vary.join(', '));
446
+ return new Response(null, {
447
+ headers,
448
+ status: 302
449
+ });
450
+ }
451
+ function createLocaleRedirectResponseForRequest(route, request) {
452
+ const entry = getRouteI18nEntry(route);
453
+ if (!entry || !Array.isArray(entry.languages) || 0 === entry.languages.length) return null;
454
+ const { pathname, search } = new URL(request.url);
455
+ if (isAssetLikePathname(pathname)) return null;
456
+ if (shouldIgnoreLocaleRedirect(pathname, route, entry)) return null;
457
+ const language = getLanguageFromPath(route, pathname, entry.languages);
458
+ if (!language) {
459
+ const targetLanguage = detectRedirectLanguage(request, entry) || entry.fallbackLanguage;
460
+ return createLocaleRedirectResponse(buildLocalizedUrl(route, request, targetLanguage, entry), entry);
461
+ }
462
+ if (entry.localisedUrls) {
463
+ const expectedUrl = buildLocalizedUrl(route, request, language, entry);
464
+ if (expectedUrl !== `${pathname}${search}`) return createLocaleRedirectResponse(expectedUrl, entry);
465
+ }
466
+ return null;
467
+ }
226
468
  function findRoute(request) {
227
469
  const { pathname } = new URL(request.url);
228
470
  const routes = MODERN_WORKER_MANIFEST.routeSpec.routes;
@@ -399,21 +641,40 @@ async function collectRenderedRemoteCssHrefs(html, request, env) {
399
641
  function escapeAttribute(value) {
400
642
  return String(value).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
401
643
  }
644
+ function isAbsoluteUrl(value) {
645
+ return /^[a-z][a-z\d+.-]*:/iu.test(value);
646
+ }
647
+ function toRouteCssHtmlHref(asset) {
648
+ if (isAbsoluteUrl(asset) || asset.startsWith('/')) return asset;
649
+ return `/${asset.replace(/^\/+/u, '')}`;
650
+ }
402
651
  async function withRouteCssLinks(response, route, routeManifest, request, env) {
403
652
  const contentType = response.headers.get('content-type') || '';
404
653
  if (!contentType.includes('text/html')) return response;
405
654
  const html = await response.text();
406
- const cssHrefs = [
407
- ...collectRouteCssAssets(route, routeManifest).map((asset)=>new URL(asset, request.url).toString()),
408
- ...await collectRenderedRemoteCssHrefs(html, request, env)
409
- ];
410
- if (0 === cssHrefs.length) return response;
411
- const uniqueCssHrefs = [
412
- ...new Set(cssHrefs)
655
+ const cssEntries = [
656
+ ...collectRouteCssAssets(route, routeManifest).map((asset)=>{
657
+ const href = toRouteCssHtmlHref(asset);
658
+ return {
659
+ href,
660
+ preloadHref: new URL(href, request.url).toString()
661
+ };
662
+ }),
663
+ ...(await collectRenderedRemoteCssHrefs(html, request, env)).map((href)=>({
664
+ href,
665
+ preloadHref: new URL(href, request.url).toString()
666
+ }))
413
667
  ];
668
+ if (0 === cssEntries.length) return response;
669
+ const seenCssHrefs = new Set();
670
+ const uniqueCssEntries = cssEntries.filter((entry)=>{
671
+ if (seenCssHrefs.has(entry.preloadHref)) return false;
672
+ seenCssHrefs.add(entry.preloadHref);
673
+ return true;
674
+ });
414
675
  const headers = new Headers(response.headers);
415
- for (const href of uniqueCssHrefs)headers.append('link', `<${href}>; rel=preload; as=style`);
416
- const links = uniqueCssHrefs.filter((href)=>!html.includes(href)).map((href)=>`<link rel="stylesheet" href="${escapeAttribute(href)}">`);
676
+ for (const { preloadHref } of uniqueCssEntries)headers.append('link', `<${preloadHref}>; rel=preload; as=style`);
677
+ const links = uniqueCssEntries.filter(({ href, preloadHref })=>!html.includes(href) && !html.includes(preloadHref)).map(({ href })=>`<link rel="stylesheet" href="${escapeAttribute(href)}">`);
417
678
  if (0 === links.length || !html.includes('</head>')) return new Response(html, {
418
679
  headers,
419
680
  status: response.status,
@@ -444,6 +705,10 @@ async function loadWorkerModule(workerPath) {
444
705
  if (!workerModulePromises.has(workerPath)) workerModulePromises.set(workerPath, loader());
445
706
  return workerModulePromises.get(workerPath);
446
707
  }
708
+ async function loadEffectEdgeRuntime() {
709
+ effectEdgeRuntimePromise ??= import('@modern-js/plugin-bff/effect-edge');
710
+ return effectEdgeRuntimePromise;
711
+ }
447
712
  function getRuntimeModule(workerModule) {
448
713
  const defaultExport = workerModule.default;
449
714
  const nestedDefaultExport = defaultExport && 'object' == typeof defaultExport ? defaultExport.default : void 0;
@@ -508,21 +773,6 @@ function createRequestForMountedPrefix(request, prefix) {
508
773
  url.pathname = nextPath.startsWith('/') ? nextPath : `/${nextPath}`;
509
774
  return new Request(url, request);
510
775
  }
511
- function createEffectContext(originalRequest, mountedRequest, env) {
512
- const url = new URL(originalRequest.url);
513
- return {
514
- request: mountedRequest,
515
- env: env || {},
516
- path: url.pathname,
517
- method: originalRequest.method,
518
- operationContext: {
519
- request: mountedRequest,
520
- env: env || {},
521
- path: url.pathname,
522
- method: originalRequest.method
523
- }
524
- };
525
- }
526
776
  async function dispatchBffRequest(request, env) {
527
777
  const bff = MODERN_WORKER_MANIFEST.bff;
528
778
  if (!bff?.worker || !matchesPrefix(new URL(request.url).pathname, bff.prefix)) return null;
@@ -535,9 +785,23 @@ async function dispatchBffRequest(request, env) {
535
785
  }
536
786
  });
537
787
  const mountedRequest = createRequestForMountedPrefix(request, bff.prefix);
538
- const effectContext = createEffectContext(request, mountedRequest, env);
539
788
  const defaultExport = workerModule.default;
540
789
  const runtime = getRuntimeModule(workerModule);
790
+ if ('effect' === bff.runtimeFramework) {
791
+ let effectDispatcherPromise = effectBffDispatcherPromises.get(bff.worker);
792
+ if (!effectDispatcherPromise) {
793
+ const { createEffectBffEdgeDispatcher } = await loadEffectEdgeRuntime();
794
+ effectDispatcherPromise = createEffectBffEdgeDispatcher({
795
+ module: runtime,
796
+ prefix: bff.prefix
797
+ });
798
+ effectBffDispatcherPromises.set(bff.worker, effectDispatcherPromise);
799
+ }
800
+ const effectDispatcher = await effectDispatcherPromise;
801
+ return effectDispatcher.dispatch(request, {
802
+ env
803
+ });
804
+ }
541
805
  const directHandler = 'function' == typeof runtime.handler && runtime.handler || 'function' == typeof defaultExport && defaultExport;
542
806
  const createdHandler = 'function' == typeof runtime.createHandler ? runtime.createHandler().handler : void 0;
543
807
  const handler = directHandler || createdHandler;
@@ -548,27 +812,66 @@ async function dispatchBffRequest(request, env) {
548
812
  'x-modern-js-bff-worker': bff.worker
549
813
  }
550
814
  });
551
- return handler.length > 1 ? handler(mountedRequest, effectContext) : handler(mountedRequest);
815
+ const effectContext = {
816
+ request: mountedRequest,
817
+ env: env || {},
818
+ path: new URL(request.url).pathname,
819
+ method: request.method,
820
+ operationContext: {
821
+ request: mountedRequest,
822
+ env: env || {},
823
+ path: new URL(request.url).pathname,
824
+ method: request.method
825
+ }
826
+ };
827
+ return handler(mountedRequest, effectContext);
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;
552
847
  }
553
848
  export default {
554
849
  async fetch (request, env, ctx) {
555
850
  const corsPreflightResponse = await createCorsPreflightResponse(request, env);
556
851
  if (corsPreflightResponse) return finalizeResponseForRequest(corsPreflightResponse, request);
557
- const assetResponse = await fetchAsset(request, env);
558
- if (assetResponse) return finalizeResponseForRequest(assetResponse, request);
559
852
  const bffResponse = await dispatchBffRequest(request, env);
560
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);
561
856
  const route = findRoute(request);
562
857
  const { pathname } = new URL(request.url);
563
- if (isAssetLikePathname(pathname) && !routeMatchesExactly(route, pathname)) return finalizeResponseForRequest(withAppCorsHeaders(new Response('Not found', {
564
- status: 404
565
- }), request), request);
858
+ if (isAssetLikePathname(pathname) && !routeMatchesExactly(route, pathname)) {
859
+ const assetResponse = await fetchAsset(request, env);
860
+ if (assetResponse) return finalizeResponseForRequest(assetResponse, request);
861
+ return finalizeResponseForRequest(withAppCorsHeaders(new Response('Not found', {
862
+ status: 404
863
+ }), request), request);
864
+ }
865
+ const localeRedirectResponse = createLocaleRedirectResponseForRequest(route, request);
866
+ if (localeRedirectResponse) return finalizeResponseForRequest(withAppCorsHeaders(localeRedirectResponse, request), request);
566
867
  if (route?.worker) {
567
868
  const renderableRequest = createRenderableRequest(request);
568
869
  return finalizeResponseForRequest(withAppCorsHeaders(await dispatchRouteWorker(route, renderableRequest, env, ctx), request), request);
569
870
  }
570
871
  const htmlResponse = await fetchRouteHtml(route, request, env);
571
872
  if (htmlResponse) return finalizeResponseForRequest(htmlResponse, request);
873
+ const assetResponse = await fetchAsset(request, env);
874
+ if (assetResponse) return finalizeResponseForRequest(assetResponse, request);
572
875
  return finalizeResponseForRequest(withAppCorsHeaders(new Response('Not found', {
573
876
  status: 404
574
877
  }), request), request);
@@ -0,0 +1,11 @@
1
+ import type { AppTools, CliPlugin } from '../types';
2
+ export type BackendFederationArtifactResult = {
3
+ appId: string;
4
+ manifestPath: string;
5
+ containerPath: string;
6
+ remoteName: string;
7
+ remoteType: string;
8
+ };
9
+ export declare const emitBackendFederationArtifacts: (appDirectory: string, distDirectory: string) => Promise<BackendFederationArtifactResult | undefined>;
10
+ export default _default;
11
+ declare function _default(): CliPlugin<AppTools>;
@@ -0,0 +1,13 @@
1
+ export declare const CLOUDFLARE_WORKER_ENTRY = "server/index.mjs";
2
+ export declare const CLOUDFLARE_WORKER_MANIFEST = "server/modern-worker-manifest.json";
3
+ export declare const CLOUDFLARE_WRANGLER_CONFIG_FILE = "wrangler.json";
4
+ export declare const CLOUDFLARE_OUTPUT_PACKAGE_FILE = "package.json";
5
+ export declare const CLOUDFLARE_WORKER_PACKAGE_FILE = "worker/package.json";
6
+ export declare const CLOUDFLARE_ASSETS_BINDING = "ASSETS";
7
+ export declare const CLOUDFLARE_PUBLIC_ASSETS_DIRECTORY = "public";
8
+ export declare const CLOUDFLARE_WORKER_BUNDLE_DIRECTORY = "worker";
9
+ export declare const CLOUDFLARE_RUNTIME_TYPE = "cloudflare-module-worker";
10
+ export declare const CLOUDFLARE_OUTPUT_PACKAGE_TYPE = "module";
11
+ export declare const CLOUDFLARE_WORKER_PACKAGE_TYPE = "commonjs";
12
+ export declare const CLOUDFLARE_WORKER_BUNDLE_FORMAT = "commonjs";
13
+ export declare const CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS: readonly ['nodejs_compat', 'global_fetch_strictly_public'];
@@ -0,0 +1,33 @@
1
+ import { CLOUDFLARE_ASSETS_BINDING, CLOUDFLARE_OUTPUT_PACKAGE_TYPE, CLOUDFLARE_PUBLIC_ASSETS_DIRECTORY, CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS, CLOUDFLARE_WORKER_ENTRY, CLOUDFLARE_WORKER_PACKAGE_TYPE } from './cloudflare-output-contract';
2
+ export type CloudflareOutputPlan = {
3
+ outputDirectory: string;
4
+ requiredFiles: string[];
5
+ publicLeakDirectories: string[];
6
+ paths: {
7
+ workerEntry: string;
8
+ workerManifest: string;
9
+ wranglerConfig: string;
10
+ outputPackage: string;
11
+ workerPackage: string;
12
+ publicAssets: string;
13
+ workerBundle: string;
14
+ };
15
+ packages: {
16
+ output: {
17
+ type: typeof CLOUDFLARE_OUTPUT_PACKAGE_TYPE;
18
+ };
19
+ worker: {
20
+ type: typeof CLOUDFLARE_WORKER_PACKAGE_TYPE;
21
+ };
22
+ };
23
+ wrangler: {
24
+ main: typeof CLOUDFLARE_WORKER_ENTRY;
25
+ assets: {
26
+ binding: typeof CLOUDFLARE_ASSETS_BINDING;
27
+ directory: `./${typeof CLOUDFLARE_PUBLIC_ASSETS_DIRECTORY}`;
28
+ run_worker_first: true;
29
+ };
30
+ requiredCompatibilityFlags: typeof CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS;
31
+ };
32
+ };
33
+ export declare function createCloudflareOutputPlan(outputDirectory: string): CloudflareOutputPlan;
@@ -0,0 +1,32 @@
1
+ export type CloudflareOutputVerifierIssueCode = 'missing-file' | 'invalid-manifest' | 'invalid-wrangler' | 'invalid-package' | 'public-output-leak' | 'missing-worker-bundle' | 'invalid-worker-bundle' | 'worker-import-failed' | 'delivery-unit-drift' | 'missing-delivery-unit' | 'forbidden-mutation-pattern';
2
+ export interface CloudflareOutputVerifierIssue {
3
+ code: CloudflareOutputVerifierIssueCode;
4
+ message: string;
5
+ path?: string;
6
+ }
7
+ export interface CloudflareDeliveryUnitIdentity {
8
+ unitId: string;
9
+ buildMarker: string;
10
+ sourceRevision: string;
11
+ }
12
+ export interface VerifyCloudflareOutputOptions {
13
+ outputDirectory: string;
14
+ importWorker?: boolean;
15
+ /**
16
+ * Topology-declared delivery-unit record (from the workspace compact config).
17
+ * When provided, the Cloudflare worker manifest must carry a matching
18
+ * `deliveryUnit` stamp so the deployed worker snapshot is proven to derive
19
+ * from the same delivery unit as the Node/API surfaces (ADR-0019 lane D).
20
+ */
21
+ deliveryUnit?: CloudflareDeliveryUnitIdentity;
22
+ }
23
+ export interface VerifyCloudflareOutputMutationPolicyOptions {
24
+ scanRoots: string[];
25
+ }
26
+ export interface CloudflareOutputVerifierResult {
27
+ ok: boolean;
28
+ issues: CloudflareOutputVerifierIssue[];
29
+ }
30
+ export declare const verifyCloudflareOutputMutationPolicy: (options: VerifyCloudflareOutputMutationPolicyOptions) => Promise<CloudflareOutputVerifierResult>;
31
+ export declare const verifyCloudflareOutput: (options: VerifyCloudflareOutputOptions) => Promise<CloudflareOutputVerifierResult>;
32
+ export declare const assertCloudflareOutput: (options: VerifyCloudflareOutputOptions) => Promise<void>;
@@ -14,6 +14,17 @@ export interface CloudflareWorkerArtifactConfig {
14
14
  */
15
15
  to: string;
16
16
  }
17
+ export interface CloudflareWorkerPublicAssetConfig {
18
+ /**
19
+ * Source file or directory, relative to the app root, to copy into
20
+ * Cloudflare Worker Static Assets.
21
+ */
22
+ from: string;
23
+ /**
24
+ * Destination path relative to `.output/public`.
25
+ */
26
+ to: string;
27
+ }
17
28
  export interface CloudflareWorkerD1DatabaseConfig {
18
29
  /**
19
30
  * Worker binding name, for example `DB`.
@@ -41,6 +52,17 @@ export interface CloudflareWorkerD1DatabaseConfig {
41
52
  */
42
53
  remote?: boolean;
43
54
  }
55
+ export interface CloudflareWorkerServiceBindingConfig {
56
+ /** Worker binding name exposed on the module worker `env` object. */
57
+ binding: string;
58
+ /** Target Cloudflare Worker service name. */
59
+ service: string;
60
+ /**
61
+ * Optional application path prefix that Modern.js should dispatch to this
62
+ * service binding with `env[binding].fetch(request)`.
63
+ */
64
+ prefix?: string;
65
+ }
44
66
  export interface CloudflareWorkerSecurityCspConfig {
45
67
  mode?: CloudflareWorkerSecurityCspMode;
46
68
  directives?: Record<string, string[] | string | false>;
@@ -1,5 +1,5 @@
1
- import type { CloudflareWorkerArtifactConfig, CloudflareWorkerD1DatabaseConfig, CloudflareWorkerSecurityConfig, DeployTarget, JsonValue } from './cloudflareDeploy';
2
- export type { CloudflareWorkerArtifactConfig, CloudflareWorkerD1DatabaseConfig, CloudflareWorkerSecurityConfig, CloudflareWorkerSecurityCorsConfig, CloudflareWorkerSecurityCspConfig, CloudflareWorkerSecurityCspMode, CloudflareWorkerSecurityNoindexConfig, DeployTarget, JsonValue, } from './cloudflareDeploy';
1
+ import type { CloudflareWorkerArtifactConfig, CloudflareWorkerD1DatabaseConfig, CloudflareWorkerPublicAssetConfig, CloudflareWorkerSecurityConfig, CloudflareWorkerServiceBindingConfig, DeployTarget, JsonValue } from './cloudflareDeploy';
2
+ export type { CloudflareWorkerArtifactConfig, CloudflareWorkerD1DatabaseConfig, CloudflareWorkerPublicAssetConfig, CloudflareWorkerSecurityConfig, CloudflareWorkerSecurityCorsConfig, CloudflareWorkerSecurityCspConfig, CloudflareWorkerSecurityCspMode, CloudflareWorkerSecurityNoindexConfig, CloudflareWorkerServiceBindingConfig, DeployTarget, JsonValue, } from './cloudflareDeploy';
3
3
  export interface MicroFrontend {
4
4
  /**
5
5
  * Specifies whether to enable the HTML entry.
@@ -47,12 +47,25 @@ export interface DeployUserConfig {
47
47
  * Use this for provider resources such as migrations or generated config.
48
48
  */
49
49
  artifacts?: CloudflareWorkerArtifactConfig[];
50
+ /**
51
+ * Additional app-root files or directories to serve as Cloudflare Worker
52
+ * Static Assets under `.output/public`. Use `to: '.'` to copy a
53
+ * source directory's contents into the public asset root.
54
+ */
55
+ publicAssets?: CloudflareWorkerPublicAssetConfig[];
50
56
  /**
51
57
  * First-class Cloudflare D1 bindings. Modern.js writes these to
52
58
  * `wrangler.json` as `d1_databases` and stages configured migration
53
59
  * directories into `.output`.
54
60
  */
55
61
  d1Databases?: CloudflareWorkerD1DatabaseConfig[];
62
+ /**
63
+ * First-class Cloudflare service bindings. Modern.js writes these to
64
+ * `wrangler.json` as `services`; when a binding also has `prefix`,
65
+ * the generated Worker dispatches matching requests through
66
+ * `env[binding].fetch(request)`.
67
+ */
68
+ services?: CloudflareWorkerServiceBindingConfig[];
56
69
  /**
57
70
  * Dist output paths that must not be copied into Cloudflare public assets.
58
71
  * Entries are slash-normalized path prefixes relative to the app dist root.
package/lib/types.d.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="react" />
3
3
  /// <reference types="react-dom" />
4
- /// <reference types="@rsbuild/core/types" />
5
- /// <reference path="../dist/types/index.d.ts" />
6
4
 
7
5
  declare namespace NodeJS {
8
6
  interface ProcessEnv {
@@ -12,7 +10,8 @@ declare namespace NodeJS {
12
10
  }
13
11
  }
14
12
 
15
- // reference @rsbuild/core/types, but there are some differences, such as svg
13
+ // Keep this app-environment surface independent from framework configuration
14
+ // declarations so strict app typecheck does not enter toolchain internals.
16
15
  /**
17
16
  * Image assets
18
17
  */