@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.12 → 3.9.0-ultramodern.14

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 (93) hide show
  1. package/dist/cjs/backend-federation-build/codegen.js +7 -2
  2. package/dist/cjs/backend-federation-build/config.js +86 -36
  3. package/dist/cjs/backend-federation-build/emit.js +17 -13
  4. package/dist/cjs/cloudflare/delivery-unit.js +16 -26
  5. package/dist/cjs/cloudflare/index.js +7 -1
  6. package/dist/cjs/cloudflare/worker-manifest.js +13 -4
  7. package/dist/cjs/cloudflare/wrangler-config.js +35 -7
  8. package/dist/cjs/cloudflare-builder.js +247 -16
  9. package/dist/cjs/cloudflare-output-contract.js +32 -2
  10. package/dist/cjs/cloudflare-output-verifier/identity.js +26 -24
  11. package/dist/cjs/cloudflare-output-verifier/index.js +10 -0
  12. package/dist/cjs/cloudflare-output-verifier/manifest.js +46 -0
  13. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +7 -1
  14. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +10 -5
  15. package/dist/cjs/deploy-output/npmAliases.js +28 -3
  16. package/dist/cjs/release-envelope/framework-output.js +62 -49
  17. package/dist/cjs/release-envelope/index.js +10 -4
  18. package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  19. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  20. package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  21. package/dist/cjs/templates/cloudflare-worker-route-data.mjs +1069 -0
  22. package/dist/esm/backend-federation-build/codegen.mjs +7 -2
  23. package/dist/esm/backend-federation-build/config.mjs +78 -31
  24. package/dist/esm/backend-federation-build/emit.mjs +18 -14
  25. package/dist/esm/cloudflare/delivery-unit.mjs +16 -26
  26. package/dist/esm/cloudflare/index.mjs +7 -1
  27. package/dist/esm/cloudflare/worker-manifest.mjs +13 -4
  28. package/dist/esm/cloudflare/wrangler-config.mjs +35 -7
  29. package/dist/esm/cloudflare-builder.mjs +241 -19
  30. package/dist/esm/cloudflare-output-contract.mjs +25 -1
  31. package/dist/esm/cloudflare-output-verifier/identity.mjs +26 -24
  32. package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
  33. package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
  34. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  35. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +11 -6
  36. package/dist/esm/deploy-output/npmAliases.mjs +28 -3
  37. package/dist/esm/release-envelope/framework-output.mjs +62 -49
  38. package/dist/esm/release-envelope/index.mjs +10 -4
  39. package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  40. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  41. package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  42. package/dist/esm/templates/cloudflare-worker-route-data.mjs +1069 -0
  43. package/dist/esm-node/backend-federation-build/codegen.mjs +7 -2
  44. package/dist/esm-node/backend-federation-build/config.mjs +78 -31
  45. package/dist/esm-node/backend-federation-build/emit.mjs +18 -14
  46. package/dist/esm-node/cloudflare/delivery-unit.mjs +16 -26
  47. package/dist/esm-node/cloudflare/index.mjs +7 -1
  48. package/dist/esm-node/cloudflare/worker-manifest.mjs +13 -4
  49. package/dist/esm-node/cloudflare/wrangler-config.mjs +35 -7
  50. package/dist/esm-node/cloudflare-builder.mjs +241 -19
  51. package/dist/esm-node/cloudflare-output-contract.mjs +25 -1
  52. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +26 -24
  53. package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
  54. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
  55. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  56. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +11 -6
  57. package/dist/esm-node/deploy-output/npmAliases.mjs +28 -3
  58. package/dist/esm-node/release-envelope/framework-output.mjs +62 -49
  59. package/dist/esm-node/release-envelope/index.mjs +10 -4
  60. package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  61. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  62. package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  63. package/dist/esm-node/templates/cloudflare-worker-route-data.mjs +1069 -0
  64. package/dist/types/backend-federation-build/codegen.d.ts +30 -0
  65. package/dist/types/backend-federation-build/config.d.ts +32 -13
  66. package/dist/types/cloudflare/delivery-unit.d.ts +1 -1
  67. package/dist/types/cloudflare/wrangler-config.d.ts +6 -0
  68. package/dist/types/cloudflare-builder.d.ts +45 -1
  69. package/dist/types/cloudflare-output-contract.d.ts +3 -1
  70. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
  71. package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
  72. package/package.json +25 -21
  73. package/src/backend-federation-build/codegen.ts +6 -2
  74. package/src/backend-federation-build/config.ts +207 -71
  75. package/src/backend-federation-build/emit.ts +33 -22
  76. package/src/cloudflare/delivery-unit.ts +39 -33
  77. package/src/cloudflare/index.ts +20 -6
  78. package/src/cloudflare/worker-manifest.ts +19 -3
  79. package/src/cloudflare/wrangler-config.ts +86 -10
  80. package/src/cloudflare-builder.ts +464 -15
  81. package/src/cloudflare-output-contract.ts +35 -2
  82. package/src/cloudflare-output-verifier/identity.ts +4 -0
  83. package/src/cloudflare-output-verifier/index.ts +20 -0
  84. package/src/cloudflare-output-verifier/manifest.ts +77 -0
  85. package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
  86. package/src/cloudflare-output-verifier/worker-bundles.ts +27 -11
  87. package/src/deploy-output/npmAliases.ts +52 -3
  88. package/src/release-envelope/framework-output.ts +163 -90
  89. package/src/release-envelope/index.ts +26 -5
  90. package/src/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  91. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  92. package/src/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  93. package/src/templates/cloudflare-worker-route-data.mjs +1069 -0
@@ -1,5 +1,5 @@
1
1
  import fs from 'node:fs';
2
- import { createRequire } from 'node:module';
2
+ import { createRequire, isBuiltin } from 'node:module';
3
3
  import path from 'node:path';
4
4
  import { SERVICE_WORKER_ENVIRONMENT_NAME } from '@modern-js/builder';
5
5
  import type { BffUserConfig } from '@modern-js/server-core';
@@ -10,18 +10,50 @@ import type {
10
10
  ModifyBundlerChainFn,
11
11
  RsbuildEntry,
12
12
  RsbuildEntryDescription,
13
+ Rspack,
13
14
  } from '@rsbuild/core';
14
15
  import { provider } from 'std-env';
15
- import { CLOUDFLARE_WORKER_NODE_BUILTINS } from './cloudflare-output-contract';
16
+ import {
17
+ CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT,
18
+ CLOUDFLARE_WORKER_NODE_BUILTINS,
19
+ CLOUDFLARE_WORKER_PLATFORM_MODULES,
20
+ getCloudflareWorkerRouteDataEntryName,
21
+ } from './cloudflare-output-contract';
16
22
  import { getTemplatePath } from './read-template';
17
23
 
18
24
  const moduleRequire = createRequire(import.meta.url);
19
25
  const BFF_EFFECT_WORKER_ENTRY_NAME = '__modern_bff_effect';
20
26
  const BFF_EFFECT_WORKER_RUNTIME_QUERY = 'modern-bff-runtime';
27
+ // Generated by the router code generator for SSR entries with nested route
28
+ // data loaders; the Node server loads this module as its loader bundle. It
29
+ // re-exports the data loader runtime next to the entry's route loaders.
30
+ const SERVER_LOADER_COMBINED_FILE = 'server-loader-combined.js';
31
+ // The entry's route loaders (`routes`), which the loader bundle re-exports.
32
+ const ROUTE_SERVER_LOADERS_FILE = 'route-server-loaders.js';
33
+ const WORKER_ROUTE_DATA_FILE = 'cloudflare-worker-route-data.js';
34
+ const WORKER_ROUTE_DATA_TEMPLATE = 'cloudflare-worker-route-data.mjs';
35
+ // Client builds rewrite `*.data.*` modules into `?__loader=` fetch clients
36
+ // through the data loader plugin's webpack loader. The worker is a server
37
+ // runtime and must execute route loaders in-process. The rule is matched by
38
+ // that loader module, not by the rule name the SSR adapter registers, in both
39
+ // the workspace package and the published `@bleedingdev/modern-js-*` name.
40
+ const DATA_LOADER_CLIENT_TRANSFORM =
41
+ /plugin-data-loader[\\/](?:.+[\\/])?loader\.[cm]?js$/u;
21
42
  const MF_SSR_DATA_FETCH_RUNTIME_PLUGIN =
22
43
  '@module-federation/modern-js-v3/ssr-inject-data-fetch-function-plugin';
23
44
  const MF_SSR_DEV_RUNTIME_PLUGIN =
24
45
  '@module-federation/modern-js-v3/ssr-dev-plugin';
46
+ // Rspack's default dependency-type conditions (`resolve.byDependency`).
47
+ const WORKER_DEPENDENCY_CONDITIONS = [
48
+ ['esm', 'import'],
49
+ ['wasm', 'import'],
50
+ ['loaderImport', 'import'],
51
+ ['worker', 'import'],
52
+ ['commonjs', 'require'],
53
+ ['amd', 'require'],
54
+ ['loader', 'require'],
55
+ ['unknown', 'require'],
56
+ ] as const;
25
57
  const JS_OR_TS_EXTENSIONS = new Set([
26
58
  '.js',
27
59
  '.jsx',
@@ -39,8 +71,11 @@ export interface CloudflareBuilderNormalizedConfig {
39
71
  }
40
72
 
41
73
  export interface CloudflareBuilderAppContext {
74
+ apiOnly?: boolean;
42
75
  apiDirectory: string;
43
76
  appDirectory: string;
77
+ entrypoints?: ReadonlyArray<{ entryName: string }>;
78
+ internalDirectory?: string;
44
79
  }
45
80
 
46
81
  export interface CloudflareBuilderEnvironmentsOptions {
@@ -190,16 +225,329 @@ const setAliasIfPresent = (
190
225
  }
191
226
  };
192
227
 
228
+ // Node accepts a bare specifier only for built-ins that are not prefix-only
229
+ // (`sqlite` and `test` are ordinary npm package names without `node:`).
193
230
  const getCloudflareWorkerNodeExternals = () =>
194
- Object.fromEntries(
195
- CLOUDFLARE_WORKER_NODE_BUILTINS.flatMap(builtin => {
231
+ Object.fromEntries([
232
+ ...CLOUDFLARE_WORKER_NODE_BUILTINS.flatMap(builtin => {
196
233
  const nodeBuiltin = `node:${builtin}`;
197
- return [
198
- [builtin, `module-import ${nodeBuiltin}`],
199
- [nodeBuiltin, `module-import ${nodeBuiltin}`],
200
- ];
234
+ const external = `module-import ${nodeBuiltin}`;
235
+ return isBuiltin(builtin)
236
+ ? [
237
+ [builtin, external],
238
+ [nodeBuiltin, external],
239
+ ]
240
+ : [[nodeBuiltin, external]];
201
241
  }),
202
- );
242
+ ...CLOUDFLARE_WORKER_PLATFORM_MODULES.map(platformModule => [
243
+ platformModule,
244
+ `module-import ${platformModule}`,
245
+ ]),
246
+ ]);
247
+
248
+ const getPackageNameFromRequest = (request: string) => {
249
+ if (
250
+ request.startsWith('.') ||
251
+ request.startsWith('/') ||
252
+ request.includes(':') ||
253
+ path.isAbsolute(request)
254
+ ) {
255
+ return undefined;
256
+ }
257
+ const segments = request.split('/');
258
+ return request.startsWith('@')
259
+ ? segments.length > 1
260
+ ? `${segments[0]}/${segments[1]}`
261
+ : undefined
262
+ : segments[0];
263
+ };
264
+
265
+ interface OptionalDependencyManifest {
266
+ optionalDependencies?: Record<string, string>;
267
+ peerDependenciesMeta?: Record<string, { optional?: boolean }>;
268
+ [field: string]: unknown;
269
+ }
270
+
271
+ type AliasField = string | readonly string[];
272
+
273
+ // Mirrors the resolver's `resolve.aliasFields` lookup: a field is a key or a
274
+ // nested key path into the importing package's manifest whose object maps
275
+ // exact requests to replacements (or to `false` for an empty module); an
276
+ // entry for a package name does not cover its subpaths.
277
+ const isRedirectedByAliasField = (
278
+ manifest: OptionalDependencyManifest,
279
+ aliasFields: readonly AliasField[],
280
+ request: string,
281
+ ) =>
282
+ aliasFields.some(field => {
283
+ let value: unknown = manifest;
284
+ for (const key of typeof field === 'string' ? [field] : field) {
285
+ value =
286
+ value && typeof value === 'object'
287
+ ? (value as Record<string, unknown>)[key]
288
+ : undefined;
289
+ }
290
+ return (
291
+ typeof value === 'object' &&
292
+ value !== null &&
293
+ Object.hasOwn(value, request)
294
+ );
295
+ });
296
+
297
+ const findOwningPackageManifest = (
298
+ directory: string,
299
+ cache: Map<string, OptionalDependencyManifest | undefined>,
300
+ ): OptionalDependencyManifest | undefined => {
301
+ if (cache.has(directory)) {
302
+ return cache.get(directory);
303
+ }
304
+ let manifest: OptionalDependencyManifest | undefined;
305
+ const packageJsonPath = path.join(directory, 'package.json');
306
+ if (fs.existsSync(packageJsonPath)) {
307
+ try {
308
+ manifest = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
309
+ } catch {
310
+ manifest = undefined;
311
+ }
312
+ } else if (path.dirname(directory) !== directory) {
313
+ manifest = findOwningPackageManifest(path.dirname(directory), cache);
314
+ }
315
+ cache.set(directory, manifest);
316
+ return manifest;
317
+ };
318
+
319
+ // Mirrors the resolver's `resolve.modules` lookup: absolute entries are
320
+ // searched directly, named entries in the context directory and each ancestor.
321
+ const isPackageInstalled = (
322
+ packageName: string,
323
+ directory: string,
324
+ moduleDirectories: readonly string[],
325
+ ) =>
326
+ moduleDirectories.some(moduleDirectory => {
327
+ if (path.isAbsolute(moduleDirectory)) {
328
+ return fs.existsSync(path.join(moduleDirectory, packageName));
329
+ }
330
+ let current = directory;
331
+ while (true) {
332
+ if (fs.existsSync(path.join(current, moduleDirectory, packageName))) {
333
+ return true;
334
+ }
335
+ const parent = path.dirname(current);
336
+ if (parent === current) {
337
+ return false;
338
+ }
339
+ current = parent;
340
+ }
341
+ });
342
+
343
+ /**
344
+ * Leaves an absent optional dependency missing at runtime, exactly as Node
345
+ * does, instead of failing the worker build. It applies only when the
346
+ * importing package itself declares the request as an optional peer
347
+ * (`peerDependenciesMeta.<name>.optional`) or an `optionalDependencies`
348
+ * entry, the package is not found in any `resolve.modules` directory, and
349
+ * neither the app (`resolve.alias`, `resolve.fallback`, `externals`) nor the
350
+ * importing package (an enabled `resolve.aliasFields` entry such as
351
+ * `browser`) redirects the request. The import
352
+ * then rejects with "Cannot find module" so the library's own fallback
353
+ * handles it (for example `@redis/client` guards `import('@node-rs/xxhash')`).
354
+ */
355
+ export const createAbsentOptionalDependencyFilter = (
356
+ isRedirected: (request: string) => boolean = () => false,
357
+ moduleDirectories: readonly string[] = ['node_modules'],
358
+ aliasFields: readonly AliasField[] = [],
359
+ ) => {
360
+ const manifests = new Map<string, OptionalDependencyManifest | undefined>();
361
+ return (request: string, context: string) => {
362
+ const packageName = getPackageNameFromRequest(request);
363
+ if (!packageName || !context || isRedirected(request)) {
364
+ return false;
365
+ }
366
+ const manifest = findOwningPackageManifest(context, manifests);
367
+ if (!manifest) {
368
+ return false;
369
+ }
370
+ const optional =
371
+ manifest.peerDependenciesMeta?.[packageName]?.optional === true ||
372
+ Object.hasOwn(manifest.optionalDependencies ?? {}, packageName);
373
+ return (
374
+ optional &&
375
+ !isRedirectedByAliasField(manifest, aliasFields, request) &&
376
+ !isPackageInstalled(packageName, context, moduleDirectories)
377
+ );
378
+ };
379
+ };
380
+
381
+ type AliasOption =
382
+ | Record<string, unknown>
383
+ | { name: string; onlyModule?: boolean }[]
384
+ | false
385
+ | undefined;
386
+
387
+ const getAliasNames = (alias: AliasOption) =>
388
+ !alias
389
+ ? []
390
+ : Array.isArray(alias)
391
+ ? alias.map(entry => (entry.onlyModule ? `${entry.name}$` : entry.name))
392
+ : Object.keys(alias);
393
+
394
+ type ExternalMatcher = (request: string) => boolean;
395
+
396
+ // String, RegExp and object-key externals are matched synchronously; function
397
+ // externals are consulted per request by `claimsByFunctionExternal`.
398
+ // An object entry mapped to `false` keeps the request bundled, so it is not
399
+ // a redirect.
400
+ const getExternalMatchers = (externals: unknown): ExternalMatcher[] => {
401
+ if (Array.isArray(externals)) {
402
+ return externals.flatMap(getExternalMatchers);
403
+ }
404
+ if (typeof externals === 'string') {
405
+ return [request => request === externals];
406
+ }
407
+ if (externals instanceof RegExp) {
408
+ return [
409
+ request => {
410
+ externals.lastIndex = 0;
411
+ return externals.test(request);
412
+ },
413
+ ];
414
+ }
415
+ if (externals && typeof externals === 'object') {
416
+ const names = new Set(
417
+ Object.entries(externals)
418
+ .filter(([, value]) => value !== false)
419
+ .map(([name]) => name),
420
+ );
421
+ return [request => names.has(request)];
422
+ }
423
+ return [];
424
+ };
425
+
426
+ /**
427
+ * Reads the final resolved `resolve.alias`, `resolve.fallback` and
428
+ * `externals` so an app's own
429
+ * replacement for an optional package still wins over the absent-module
430
+ * fallback (the fallback runs before resolution).
431
+ */
432
+ export const createRequestRedirectMatcher = (options: {
433
+ externals?: unknown;
434
+ resolve?: { alias?: AliasOption; fallback?: AliasOption };
435
+ }) => {
436
+ const aliasNames = [
437
+ ...getAliasNames(options.resolve?.alias),
438
+ ...getAliasNames(options.resolve?.fallback),
439
+ ];
440
+ const externalMatchers = getExternalMatchers(options.externals);
441
+ return (request: string) =>
442
+ externalMatchers.some(matches => matches(request)) ||
443
+ aliasNames.some(name =>
444
+ name.endsWith('$')
445
+ ? request === name.slice(0, -1)
446
+ : request === name || request.startsWith(`${name}/`),
447
+ );
448
+ };
449
+
450
+ type FunctionExternal = (
451
+ data: Rspack.ExternalItemFunctionData,
452
+ callback: (error?: Error | null, result?: unknown) => void,
453
+ ) => unknown;
454
+
455
+ const getFunctionExternals = (externals: unknown): FunctionExternal[] =>
456
+ Array.isArray(externals)
457
+ ? externals.flatMap(getFunctionExternals)
458
+ : typeof externals === 'function'
459
+ ? [externals as FunctionExternal]
460
+ : [];
461
+
462
+ // Calls a function external with the same callback, promise and synchronous
463
+ // return forms Rspack accepts. `undefined`, `null` and `false` decline.
464
+ const callFunctionExternal = (
465
+ external: FunctionExternal,
466
+ data: Rspack.ExternalItemFunctionData,
467
+ ) =>
468
+ new Promise<unknown>((resolve, reject) => {
469
+ const returned = external(data, (error, result) =>
470
+ error ? reject(error) : resolve(result),
471
+ );
472
+ if (
473
+ returned !== null &&
474
+ typeof returned === 'object' &&
475
+ typeof (returned as PromiseLike<unknown>).then === 'function'
476
+ ) {
477
+ (returned as PromiseLike<unknown>).then(resolve, reject);
478
+ } else if (external.length === 1) {
479
+ resolve(returned);
480
+ }
481
+ });
482
+
483
+ /**
484
+ * Reports whether any function external claims the request. A function that
485
+ * throws is treated as claiming it, so the request goes through ordinary
486
+ * external processing and surfaces that error instead of being ignored.
487
+ */
488
+ export const claimsByFunctionExternal = async (
489
+ externals: readonly FunctionExternal[],
490
+ data: Rspack.ExternalItemFunctionData,
491
+ ) => {
492
+ for (const external of externals) {
493
+ try {
494
+ const result = await callFunctionExternal(external, data);
495
+ if (result !== undefined && result !== null && result !== false) {
496
+ return true;
497
+ }
498
+ } catch {
499
+ return true;
500
+ }
501
+ }
502
+ return false;
503
+ };
504
+
505
+ const PLUGIN_NAME = 'AbsentOptionalDependencyPlugin';
506
+
507
+ class AbsentOptionalDependencyPlugin {
508
+ apply(compiler: Rspack.Compiler) {
509
+ const isAbsent = createAbsentOptionalDependencyFilter(
510
+ createRequestRedirectMatcher(compiler.options),
511
+ compiler.options.resolve.modules,
512
+ compiler.options.resolve.aliasFields,
513
+ );
514
+ const functionExternals = getFunctionExternals(compiler.options.externals);
515
+ compiler.hooks.normalModuleFactory.tap(PLUGIN_NAME, factory => {
516
+ factory.hooks.beforeResolve.tapPromise(PLUGIN_NAME, async data => {
517
+ if (!isAbsent(data.request, data.context)) {
518
+ return undefined;
519
+ }
520
+ const claimed = await claimsByFunctionExternal(functionExternals, {
521
+ context: data.context,
522
+ contextInfo: {
523
+ issuer: data.contextInfo.issuer,
524
+ issuerLayer: data.contextInfo.issuerLayer ?? null,
525
+ },
526
+ getResolve: options => {
527
+ const resolver = factory.getResolver('normal', options ?? {});
528
+ return (
529
+ context: string,
530
+ request: string,
531
+ callback?: Parameters<typeof resolver.resolve>[4],
532
+ ) => {
533
+ if (callback) {
534
+ resolver.resolve({}, context, request, {}, callback);
535
+ return undefined;
536
+ }
537
+ return new Promise<string | undefined>((resolve, reject) => {
538
+ resolver.resolve({}, context, request, {}, (error, result) =>
539
+ error ? reject(error) : resolve(result || undefined),
540
+ );
541
+ });
542
+ };
543
+ },
544
+ request: data.request,
545
+ });
546
+ return claimed ? undefined : false;
547
+ });
548
+ });
549
+ }
550
+ }
203
551
 
204
552
  const normalizeWorkerOutputName = (name: string) =>
205
553
  path.posix.normalize(name.replace(/\\/gu, '/')).toLowerCase();
@@ -282,6 +630,30 @@ export function applyCloudflareWorkerRspackConfig(
282
630
  chain.optimization.splitChunks(config.optimization.splitChunks);
283
631
  }
284
632
 
633
+ /**
634
+ * The worker environment uses Rspack's web target for ESM output, which the
635
+ * data loader transform treats as a browser bundle. Remove every module rule
636
+ * that applies it, so SSR runs route loaders in-process as the Node server
637
+ * does instead of fetching the worker's own `?__loader=` URL.
638
+ */
639
+ function removeCloudflareWorkerDataLoaderClientTransform(
640
+ chain: Parameters<ModifyBundlerChainFn>[0],
641
+ ) {
642
+ for (const [name, rule] of Object.entries(
643
+ chain.module.rules.entries() ?? {},
644
+ )) {
645
+ if (
646
+ rule.uses
647
+ .values()
648
+ .some(use =>
649
+ DATA_LOADER_CLIENT_TRANSFORM.test(String(use.get('loader'))),
650
+ )
651
+ ) {
652
+ chain.module.rules.delete(name);
653
+ }
654
+ }
655
+ }
656
+
285
657
  export function applyCloudflareWorkerMfRuntimeBoundary(
286
658
  chain: Parameters<ModifyBundlerChainFn>[0],
287
659
  ) {
@@ -397,18 +769,72 @@ const prependBundlerChain = (
397
769
  };
398
770
  };
399
771
 
772
+ const createWorkerRouteDataModule = () =>
773
+ [
774
+ `import { routes } from './${ROUTE_SERVER_LOADERS_FILE}';`,
775
+ `import { createRouteDataRequestHandler } from ${JSON.stringify(
776
+ getTemplatePath(WORKER_ROUTE_DATA_TEMPLATE),
777
+ )};`,
778
+ '',
779
+ `export const ${CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT} =`,
780
+ ' createRouteDataRequestHandler(routes);',
781
+ '',
782
+ ].join('\n');
783
+
784
+ /**
785
+ * Route data requests reach the worker whenever the browser navigates to a
786
+ * route with a data loader. Bundle the route loaders of each entry that has a
787
+ * Node server loader bundle behind the worker route data handler, so the
788
+ * worker answers them instead of rendering the page. The worker imports the
789
+ * route loaders without the loader bundle's data loader runtime, which is
790
+ * built on react-router.
791
+ */
792
+ const getWorkerRouteDataEntries = (
793
+ appContext: CloudflareBuilderAppContext,
794
+ ): Record<string, string[]> => {
795
+ const { entrypoints, internalDirectory } = appContext;
796
+ if (!entrypoints || !internalDirectory) {
797
+ return {};
798
+ }
799
+
800
+ return Object.fromEntries(
801
+ entrypoints.flatMap(({ entryName }) => {
802
+ const entryDirectory = path.join(internalDirectory, entryName);
803
+ if (
804
+ !fs.existsSync(
805
+ path.join(entryDirectory, SERVER_LOADER_COMBINED_FILE),
806
+ ) ||
807
+ !fs.existsSync(path.join(entryDirectory, ROUTE_SERVER_LOADERS_FILE))
808
+ ) {
809
+ return [];
810
+ }
811
+
812
+ const routeDataFile = path.join(entryDirectory, WORKER_ROUTE_DATA_FILE);
813
+ fs.writeFileSync(routeDataFile, createWorkerRouteDataModule());
814
+ return [
815
+ [getCloudflareWorkerRouteDataEntryName(entryName), [routeDataFile]],
816
+ ];
817
+ }),
818
+ );
819
+ };
820
+
400
821
  const getWorkerEntries = (
401
822
  environment: EnvironmentConfig,
402
823
  normalizedConfig: CloudflareBuilderNormalizedConfig,
403
824
  appContext: CloudflareBuilderAppContext,
404
825
  ) => {
405
826
  const configuredEntries = environment.source?.entry;
406
- if (!configuredEntries) {
827
+ const effectApiEntry = getEffectBffEntry(normalizedConfig, appContext);
828
+ if (!configuredEntries && !effectApiEntry) {
407
829
  return undefined;
408
830
  }
409
831
 
410
- const entries = rewriteWorkerEntries(configuredEntries);
411
- const effectApiEntry = getEffectBffEntry(normalizedConfig, appContext);
832
+ const entries = configuredEntries
833
+ ? {
834
+ ...rewriteWorkerEntries(configuredEntries),
835
+ ...getWorkerRouteDataEntries(appContext),
836
+ }
837
+ : {};
412
838
  return effectApiEntry
413
839
  ? {
414
840
  ...entries,
@@ -476,6 +902,7 @@ const createCloudflareBundlerChain = (
476
902
 
477
903
  return chain => {
478
904
  applyCloudflareWorkerRspackConfig(chain, entryNames);
905
+ removeCloudflareWorkerDataLoaderClientTransform(chain);
479
906
  chain.output
480
907
  .module(true)
481
908
  .library({ type: 'module' })
@@ -489,12 +916,23 @@ const createCloudflareBundlerChain = (
489
916
  'worker',
490
917
  'webpack',
491
918
  isProd() ? 'production' : 'development',
492
- 'import',
493
- 'require',
494
919
  'module',
495
920
  ]) {
496
921
  chain.resolve.conditionNames.add(condition);
497
922
  }
923
+ // Setting base conditions drops Rspack's per-dependency `import` /
924
+ // `require` defaults, and adding both globally lets a dual package's
925
+ // first-listed `import` branch win for `require()` (so `pg` received an
926
+ // ES module namespace for `pg-pool` and missed `pg-cloudflare`'s
927
+ // `workerd.require` entry). Restore Rspack's own per-dependency split.
928
+ for (const [dependencyType, condition] of WORKER_DEPENDENCY_CONDITIONS) {
929
+ chain.resolve.byDependency.set(dependencyType, {
930
+ conditionNames: [condition, '...'],
931
+ });
932
+ }
933
+ chain
934
+ .plugin('cloudflare-worker-absent-optional-dependencies')
935
+ .use(AbsentOptionalDependencyPlugin);
498
936
 
499
937
  applyCloudflareWorkerMfRuntimeBoundary(chain);
500
938
  if (tanstackRouterSsrServerFile) {
@@ -595,7 +1033,14 @@ export function getCloudflareBuilderEnvironments({
595
1033
  return environments;
596
1034
  }
597
1035
 
598
- const workerEnvironment = environments[SERVICE_WORKER_ENVIRONMENT_NAME];
1036
+ const workerEnvironment =
1037
+ environments[SERVICE_WORKER_ENVIRONMENT_NAME] ??
1038
+ (appContext.apiOnly
1039
+ ? {
1040
+ output: { target: 'web-worker' as const },
1041
+ source: { entry: {} },
1042
+ }
1043
+ : undefined);
599
1044
  if (!workerEnvironment) {
600
1045
  return environments;
601
1046
  }
@@ -625,6 +1070,10 @@ export function getCloudflareBuilderEnvironments({
625
1070
  createCloudflareBundlerChain(appContext, Object.keys(workerEntries)),
626
1071
  );
627
1072
 
1073
+ if (appContext.apiOnly) {
1074
+ return { [SERVICE_WORKER_ENVIRONMENT_NAME]: cloudflareWorkerEnvironment };
1075
+ }
1076
+
628
1077
  return {
629
1078
  ...environments,
630
1079
  [SERVICE_WORKER_ENVIRONMENT_NAME]: cloudflareWorkerEnvironment,
@@ -10,29 +10,60 @@ export const CLOUDFLARE_RUNTIME_TYPE = 'cloudflare-module-worker';
10
10
  export const CLOUDFLARE_OUTPUT_PACKAGE_TYPE = 'module';
11
11
  export const CLOUDFLARE_WORKER_PACKAGE_TYPE = 'commonjs';
12
12
  export const CLOUDFLARE_WORKER_BUNDLE_FORMAT = 'commonjs';
13
- // Import availability for the default 2026-06-02 nodejs_compat target.
14
- // Some modules are runtime-provided stubs whose unsupported operations throw.
13
+ // Each SSR entry with route data loaders gets a worker bundle that answers
14
+ // `?__loader=` route data requests through this export.
15
+ export const CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT = 'handleRouteDataRequest';
16
+ export const getCloudflareWorkerRouteDataEntryName = (entryName: string) =>
17
+ `${entryName}-server-loaders`;
18
+ // Every Node.js built-in that workerd resolves for the default 2026-06-02
19
+ // nodejs_compat target (tests/cloudflare-worker-node-builtins.test.ts probes
20
+ // workerd against Node's own `builtinModules`). Names are listed without the
21
+ // `node:` scheme; bare-name imports are only valid where Node itself accepts
22
+ // them (`isBuiltin`), so prefix-only modules such as `sqlite` and `test` are
23
+ // externalized solely as `node:sqlite` and `node:test`. Some modules are
24
+ // runtime-provided stubs whose unsupported operations throw. Earlier
25
+ // compatibility dates are rejected when the Wrangler config is generated.
15
26
  export const CLOUDFLARE_WORKER_NODE_BUILTINS = [
27
+ '_http_agent',
28
+ '_http_client',
29
+ '_http_common',
30
+ '_http_incoming',
31
+ '_http_outgoing',
32
+ '_http_server',
33
+ '_tls_common',
34
+ '_tls_wrap',
16
35
  'assert',
17
36
  'assert/strict',
18
37
  'async_hooks',
19
38
  'buffer',
20
39
  'child_process',
40
+ 'cluster',
41
+ 'console',
42
+ 'constants',
21
43
  'crypto',
22
44
  'dgram',
45
+ 'diagnostics_channel',
23
46
  'dns',
47
+ 'dns/promises',
24
48
  'domain',
25
49
  'events',
26
50
  'fs',
27
51
  'fs/promises',
28
52
  'http',
29
53
  'http2',
54
+ 'https',
30
55
  'inspector',
56
+ 'inspector/promises',
31
57
  'module',
32
58
  'net',
33
59
  'os',
34
60
  'path',
61
+ 'path/posix',
62
+ 'path/win32',
63
+ 'perf_hooks',
35
64
  'process',
65
+ 'punycode',
66
+ 'querystring',
36
67
  'readline',
37
68
  'readline/promises',
38
69
  'repl',
@@ -42,6 +73,8 @@ export const CLOUDFLARE_WORKER_NODE_BUILTINS = [
42
73
  'stream/promises',
43
74
  'stream/web',
44
75
  'string_decoder',
76
+ 'sys',
77
+ 'test',
45
78
  'timers',
46
79
  'timers/promises',
47
80
  'tls',
@@ -51,6 +51,10 @@ export const verifyDeliveryUnitIdentity = (
51
51
  const marker = stamped.surfaces[surface];
52
52
  const expected = declared?.surfaces?.[surface];
53
53
 
54
+ if (!expected && !marker) {
55
+ continue;
56
+ }
57
+
54
58
  if (declared?.surfaces && !expected) {
55
59
  if (marker) {
56
60
  addIssue(issues, {
@@ -174,6 +174,26 @@ export const verifyCloudflareOutput = async (
174
174
  }
175
175
 
176
176
  if (wrangler) {
177
+ if (
178
+ typeof wrangler.name !== 'string' ||
179
+ wrangler.name.trim().length === 0
180
+ ) {
181
+ addIssue(issues, {
182
+ code: 'invalid-wrangler',
183
+ message: 'wrangler.json name must be a non-empty worker name.',
184
+ path: wranglerPath,
185
+ });
186
+ }
187
+ if (
188
+ typeof wrangler.compatibility_date !== 'string' ||
189
+ !/^\d{4}-\d{2}-\d{2}$/u.test(wrangler.compatibility_date)
190
+ ) {
191
+ addIssue(issues, {
192
+ code: 'invalid-wrangler',
193
+ message: 'wrangler.json compatibility_date must use YYYY-MM-DD.',
194
+ path: wranglerPath,
195
+ });
196
+ }
177
197
  assertEqual(issues, wrangler.main, outputPlan.wrangler.main, {
178
198
  code: 'invalid-wrangler',
179
199
  message: `wrangler.json main must be ${outputPlan.wrangler.main}.`,