@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.13 → 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 (42) hide show
  1. package/dist/cjs/cloudflare/worker-manifest.js +13 -4
  2. package/dist/cjs/cloudflare/wrangler-config.js +35 -7
  3. package/dist/cjs/cloudflare-builder.js +235 -14
  4. package/dist/cjs/cloudflare-output-contract.js +31 -2
  5. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +10 -5
  6. package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  7. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  8. package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  9. package/dist/cjs/templates/cloudflare-worker-route-data.mjs +1069 -0
  10. package/dist/esm/cloudflare/worker-manifest.mjs +13 -4
  11. package/dist/esm/cloudflare/wrangler-config.mjs +35 -7
  12. package/dist/esm/cloudflare-builder.mjs +229 -17
  13. package/dist/esm/cloudflare-output-contract.mjs +24 -1
  14. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +11 -6
  15. package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  16. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  17. package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  18. package/dist/esm/templates/cloudflare-worker-route-data.mjs +1069 -0
  19. package/dist/esm-node/cloudflare/worker-manifest.mjs +13 -4
  20. package/dist/esm-node/cloudflare/wrangler-config.mjs +35 -7
  21. package/dist/esm-node/cloudflare-builder.mjs +229 -17
  22. package/dist/esm-node/cloudflare-output-contract.mjs +24 -1
  23. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +11 -6
  24. package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  25. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  26. package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  27. package/dist/esm-node/templates/cloudflare-worker-route-data.mjs +1069 -0
  28. package/dist/types/cloudflare/wrangler-config.d.ts +6 -0
  29. package/dist/types/cloudflare-builder.d.ts +44 -1
  30. package/dist/types/cloudflare-output-contract.d.ts +3 -1
  31. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
  32. package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
  33. package/package.json +17 -13
  34. package/src/cloudflare/worker-manifest.ts +19 -3
  35. package/src/cloudflare/wrangler-config.ts +86 -10
  36. package/src/cloudflare-builder.ts +447 -12
  37. package/src/cloudflare-output-contract.ts +34 -2
  38. package/src/cloudflare-output-verifier/worker-bundles.ts +27 -11
  39. package/src/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
  40. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
  41. package/src/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  42. package/src/templates/cloudflare-worker-route-data.mjs +1069 -0
@@ -1,6 +1,7 @@
1
1
  import "node:module";
2
2
  import node_path from "node:path";
3
3
  import { fs } from "@modern-js/utils";
4
+ import { getCloudflareWorkerRouteDataEntryName } from "../cloudflare-output-contract.mjs";
4
5
  import { readRouteSpec } from "./artifacts.mjs";
5
6
  import { ASSETS_BINDING, BFF_EFFECT_WORKER_DISPATCHER_EXPORT, BFF_EFFECT_WORKER_ENTRY, CLOUDFLARE_RUNTIME_TYPE, CLOUDFLARE_WORKER_BUNDLE_FORMAT, DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES, EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE, LOADABLE_STATS_FILE, PUBLIC_ASSETS_DIRECTORY, ROUTE_MANIFEST_FILE, ROUTE_SPEC_FILE, ROUTE_SPEC_OUTPUT, SERVER_BUNDLE_DIRECTORY, WORKER_BUNDLE_DIRECTORY, WORKER_ENTRY } from "./constants.mjs";
6
7
  import { createI18nWorkerManifest } from "./i18n-worker.mjs";
@@ -66,13 +67,18 @@ const createWorkerManifest = async (outputDirectory, modernConfig, appContext, d
66
67
  const routeSpec = await readRouteSpec(outputDirectory);
67
68
  const routes = await Promise.all(routeSpec.routes.map(async (route)=>{
68
69
  const worker = 'string' == typeof route.worker ? route.worker : void 0;
70
+ const routeDataWorker = route.isSSR && 'string' == typeof route.entryName ? `${WORKER_BUNDLE_DIRECTORY}/${getCloudflareWorkerRouteDataEntryName(route.entryName)}.js` : void 0;
71
+ const hasRouteDataWorker = routeDataWorker ? await fs.pathExists(node_path.join(outputDirectory, routeDataWorker)) : false;
69
72
  return {
70
73
  urlPath: route.urlPath,
71
74
  entryName: route.entryName,
72
75
  entryPath: route.entryPath,
73
76
  isSSR: Boolean(route.isSSR),
74
77
  worker,
75
- workerExists: worker ? await fs.pathExists(node_path.join(outputDirectory, worker)) : false
78
+ workerExists: worker ? await fs.pathExists(node_path.join(outputDirectory, worker)) : false,
79
+ ...hasRouteDataWorker ? {
80
+ routeDataWorker
81
+ } : {}
76
82
  };
77
83
  }));
78
84
  const bffPrefix = modernConfig.bff?.prefix;
@@ -132,9 +138,12 @@ const createWorkerManifest = async (outputDirectory, modernConfig, appContext, d
132
138
  };
133
139
  const createWorkerModuleLoaders = (manifest)=>{
134
140
  const imports = new Map();
135
- for (const route of manifest.routeSpec.routes)if (route.worker && route.workerExists) {
136
- const importPath = `../${String(route.worker).replace(/^\/+/u, '')}`;
137
- imports.set(route.worker, `() => import(${JSON.stringify(importPath)})`);
141
+ for (const route of manifest.routeSpec.routes)for (const worker of [
142
+ route.workerExists ? route.worker : void 0,
143
+ route.routeDataWorker
144
+ ])if (worker) {
145
+ const importPath = `../${String(worker).replace(/^\/+/u, '')}`;
146
+ imports.set(worker, `() => import(${JSON.stringify(importPath)})`);
138
147
  }
139
148
  if (manifest.bff?.worker) {
140
149
  const importPath = `../${String(manifest.bff.worker).replace(/^\/+/u, '')}`;
@@ -2,12 +2,16 @@ import "node:module";
2
2
  import node_path from "node:path";
3
3
  import { ASSETS_BINDING, COMPATIBILITY_DATE_PATTERN, DEFAULT_COMPATIBILITY_DATE, PUBLIC_ASSETS_DIRECTORY, REQUIRED_COMPATIBILITY_FLAGS, WORKER_ENTRY } from "./constants.mjs";
4
4
  import { isJsonRecord, normalizeRelativePath } from "./utils.mjs";
5
- const getCompatibilityDate = (modernConfig)=>{
6
- const configuredDate = modernConfig.deploy?.worker?.compatibilityDate?.trim();
7
- const compatibilityDate = configuredDate || DEFAULT_COMPATIBILITY_DATE;
8
- if (!COMPATIBILITY_DATE_PATTERN.test(compatibilityDate)) throw new Error(`deploy.worker.compatibilityDate must use YYYY-MM-DD, received ${JSON.stringify(compatibilityDate)}.`);
5
+ const assertSupportedCompatibilityDate = (compatibilityDate, label)=>{
6
+ if ('string' != typeof compatibilityDate) throw new Error(`${label} must be a YYYY-MM-DD string.`);
7
+ if (!COMPATIBILITY_DATE_PATTERN.test(compatibilityDate)) throw new Error(`${label} must use YYYY-MM-DD, received ${JSON.stringify(compatibilityDate)}.`);
8
+ if (compatibilityDate < DEFAULT_COMPATIBILITY_DATE) throw new Error(`${label} must be ${DEFAULT_COMPATIBILITY_DATE} or later, the date the Cloudflare worker Node.js built-in contract is verified against; received ${JSON.stringify(compatibilityDate)}.`);
9
9
  return compatibilityDate;
10
10
  };
11
+ const getCompatibilityDate = (modernConfig, wranglerCompatibilityDate)=>{
12
+ if (void 0 !== wranglerCompatibilityDate) return assertSupportedCompatibilityDate(wranglerCompatibilityDate, 'deploy.worker.wrangler.compatibility_date');
13
+ return assertSupportedCompatibilityDate(modernConfig.deploy?.worker?.compatibilityDate?.trim() || DEFAULT_COMPATIBILITY_DATE, 'deploy.worker.compatibilityDate');
14
+ };
11
15
  const getWorkerName = (appDirectory)=>{
12
16
  const basename = node_path.basename(appDirectory);
13
17
  return basename.replace(/[^a-zA-Z0-9-_]/g, '-') || 'modern-cloudflare-worker';
@@ -22,11 +26,11 @@ const getConfiguredWrangler = (modernConfig)=>{
22
26
  if (!isJsonRecord(wrangler)) throw new Error('deploy.worker.wrangler must be a JSON object.');
23
27
  return wrangler;
24
28
  };
25
- const createWranglerCompatibilityFlags = (configuredFlags)=>{
29
+ const createWranglerCompatibilityFlags = (configuredFlags, label = 'deploy.worker.wrangler.compatibility_flags')=>{
26
30
  if (void 0 === configuredFlags) return [
27
31
  ...REQUIRED_COMPATIBILITY_FLAGS
28
32
  ];
29
- if (!Array.isArray(configuredFlags) || configuredFlags.some((flag)=>'string' != typeof flag)) throw new Error('deploy.worker.wrangler.compatibility_flags must be an array of strings.');
33
+ if (!Array.isArray(configuredFlags) || configuredFlags.some((flag)=>'string' != typeof flag)) throw new Error(`${label} must be an array of strings.`);
30
34
  return [
31
35
  ...new Set([
32
36
  ...configuredFlags,
@@ -34,6 +38,26 @@ const createWranglerCompatibilityFlags = (configuredFlags)=>{
34
38
  ])
35
39
  ];
36
40
  };
41
+ const createWranglerEnvironments = (wranglerEnv)=>{
42
+ if (void 0 === wranglerEnv) return;
43
+ if (!isJsonRecord(wranglerEnv)) throw new Error('deploy.worker.wrangler.env must be an object.');
44
+ return Object.fromEntries(Object.entries(wranglerEnv).map(([name, environment])=>{
45
+ const label = `deploy.worker.wrangler.env.${name}`;
46
+ if (!isJsonRecord(environment)) throw new Error(`${label} must be an object.`);
47
+ return [
48
+ name,
49
+ {
50
+ ...environment,
51
+ ...void 0 === environment.compatibility_date ? {} : {
52
+ compatibility_date: assertSupportedCompatibilityDate(environment.compatibility_date, `${label}.compatibility_date`)
53
+ },
54
+ ...void 0 === environment.compatibility_flags ? {} : {
55
+ compatibility_flags: createWranglerCompatibilityFlags(environment.compatibility_flags, `${label}.compatibility_flags`)
56
+ }
57
+ }
58
+ ];
59
+ }));
60
+ };
37
61
  const createWranglerAssetsConfig = (configuredAssets)=>{
38
62
  if (void 0 !== configuredAssets && !isJsonRecord(configuredAssets)) throw new Error('deploy.worker.wrangler.assets must be an object.');
39
63
  return {
@@ -130,17 +154,21 @@ const createWorkerManifestServiceBindings = (serviceBindings)=>{
130
154
  };
131
155
  const createWranglerConfig = (appDirectory, modernConfig)=>{
132
156
  const wrangler = getConfiguredWrangler(modernConfig);
157
+ const environments = createWranglerEnvironments(wrangler.env);
133
158
  const d1Databases = createWranglerD1Databases(modernConfig, wrangler.d1_databases);
134
159
  const serviceBindings = createWorkerServiceBindings(modernConfig, wrangler.services);
135
160
  const wranglerServices = createWranglerServices(serviceBindings);
136
161
  return {
137
162
  $schema: 'node_modules/wrangler/config-schema.json',
138
163
  name: getConfiguredWorkerName(appDirectory, modernConfig),
139
- compatibility_date: getCompatibilityDate(modernConfig),
140
164
  ...wrangler,
165
+ compatibility_date: getCompatibilityDate(modernConfig, wrangler.compatibility_date),
141
166
  main: WORKER_ENTRY,
142
167
  compatibility_flags: createWranglerCompatibilityFlags(wrangler.compatibility_flags),
143
168
  assets: createWranglerAssetsConfig(wrangler.assets),
169
+ ...void 0 === environments ? {} : {
170
+ env: environments
171
+ },
144
172
  ...void 0 === d1Databases ? {} : {
145
173
  d1_databases: d1Databases
146
174
  },
@@ -1,17 +1,56 @@
1
1
  import "node:module";
2
2
  import node_fs from "node:fs";
3
- import { createRequire } from "node:module";
3
+ import { createRequire, isBuiltin } from "node:module";
4
4
  import node_path from "node:path";
5
5
  import { SERVICE_WORKER_ENVIRONMENT_NAME } from "@modern-js/builder";
6
6
  import { isProd } from "@modern-js/utils";
7
7
  import { provider } from "std-env";
8
- import { CLOUDFLARE_WORKER_NODE_BUILTINS } from "./cloudflare-output-contract.mjs";
8
+ import { CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PLATFORM_MODULES, getCloudflareWorkerRouteDataEntryName } from "./cloudflare-output-contract.mjs";
9
9
  import { getTemplatePath } from "./read-template.mjs";
10
10
  const moduleRequire = createRequire(import.meta.url);
11
11
  const BFF_EFFECT_WORKER_ENTRY_NAME = '__modern_bff_effect';
12
12
  const BFF_EFFECT_WORKER_RUNTIME_QUERY = 'modern-bff-runtime';
13
+ const SERVER_LOADER_COMBINED_FILE = 'server-loader-combined.js';
14
+ const ROUTE_SERVER_LOADERS_FILE = 'route-server-loaders.js';
15
+ const WORKER_ROUTE_DATA_FILE = 'cloudflare-worker-route-data.js';
16
+ const WORKER_ROUTE_DATA_TEMPLATE = 'cloudflare-worker-route-data.mjs';
17
+ const DATA_LOADER_CLIENT_TRANSFORM = /plugin-data-loader[\\/](?:.+[\\/])?loader\.[cm]?js$/u;
13
18
  const MF_SSR_DATA_FETCH_RUNTIME_PLUGIN = '@module-federation/modern-js-v3/ssr-inject-data-fetch-function-plugin';
14
19
  const MF_SSR_DEV_RUNTIME_PLUGIN = '@module-federation/modern-js-v3/ssr-dev-plugin';
20
+ const WORKER_DEPENDENCY_CONDITIONS = [
21
+ [
22
+ 'esm',
23
+ 'import'
24
+ ],
25
+ [
26
+ 'wasm',
27
+ 'import'
28
+ ],
29
+ [
30
+ 'loaderImport',
31
+ 'import'
32
+ ],
33
+ [
34
+ 'worker',
35
+ 'import'
36
+ ],
37
+ [
38
+ 'commonjs',
39
+ 'require'
40
+ ],
41
+ [
42
+ 'amd',
43
+ 'require'
44
+ ],
45
+ [
46
+ 'loader',
47
+ 'require'
48
+ ],
49
+ [
50
+ 'unknown',
51
+ 'require'
52
+ ]
53
+ ];
15
54
  const JS_OR_TS_EXTENSIONS = new Set([
16
55
  '.js',
17
56
  '.jsx',
@@ -71,19 +110,154 @@ const resolvePackageFile = (packageName, filePath, paths)=>{
71
110
  const setAliasIfPresent = (alias, name, value)=>{
72
111
  if (value) alias.set(name, value);
73
112
  };
74
- const getCloudflareWorkerNodeExternals = ()=>Object.fromEntries(CLOUDFLARE_WORKER_NODE_BUILTINS.flatMap((builtin)=>{
75
- const nodeBuiltin = `node:${builtin}`;
113
+ const getCloudflareWorkerNodeExternals = ()=>Object.fromEntries([
114
+ ...CLOUDFLARE_WORKER_NODE_BUILTINS.flatMap((builtin)=>{
115
+ const nodeBuiltin = `node:${builtin}`;
116
+ const external = `module-import ${nodeBuiltin}`;
117
+ return isBuiltin(builtin) ? [
118
+ [
119
+ builtin,
120
+ external
121
+ ],
122
+ [
123
+ nodeBuiltin,
124
+ external
125
+ ]
126
+ ] : [
127
+ [
128
+ nodeBuiltin,
129
+ external
130
+ ]
131
+ ];
132
+ }),
133
+ ...CLOUDFLARE_WORKER_PLATFORM_MODULES.map((platformModule)=>[
134
+ platformModule,
135
+ `module-import ${platformModule}`
136
+ ])
137
+ ]);
138
+ const getPackageNameFromRequest = (request)=>{
139
+ if (request.startsWith('.') || request.startsWith('/') || request.includes(':') || node_path.isAbsolute(request)) return;
140
+ const segments = request.split('/');
141
+ return request.startsWith('@') ? segments.length > 1 ? `${segments[0]}/${segments[1]}` : void 0 : segments[0];
142
+ };
143
+ const isRedirectedByAliasField = (manifest, aliasFields, request)=>aliasFields.some((field)=>{
144
+ let value = manifest;
145
+ for (const key of 'string' == typeof field ? [
146
+ field
147
+ ] : field)value = value && 'object' == typeof value ? value[key] : void 0;
148
+ return 'object' == typeof value && null !== value && Object.hasOwn(value, request);
149
+ });
150
+ const findOwningPackageManifest = (directory, cache)=>{
151
+ if (cache.has(directory)) return cache.get(directory);
152
+ let manifest;
153
+ const packageJsonPath = node_path.join(directory, 'package.json');
154
+ if (node_fs.existsSync(packageJsonPath)) try {
155
+ manifest = JSON.parse(node_fs.readFileSync(packageJsonPath, 'utf-8'));
156
+ } catch {
157
+ manifest = void 0;
158
+ }
159
+ else if (node_path.dirname(directory) !== directory) manifest = findOwningPackageManifest(node_path.dirname(directory), cache);
160
+ cache.set(directory, manifest);
161
+ return manifest;
162
+ };
163
+ const isPackageInstalled = (packageName, directory, moduleDirectories)=>moduleDirectories.some((moduleDirectory)=>{
164
+ if (node_path.isAbsolute(moduleDirectory)) return node_fs.existsSync(node_path.join(moduleDirectory, packageName));
165
+ let current = directory;
166
+ while(true){
167
+ if (node_fs.existsSync(node_path.join(current, moduleDirectory, packageName))) return true;
168
+ const parent = node_path.dirname(current);
169
+ if (parent === current) return false;
170
+ current = parent;
171
+ }
172
+ });
173
+ const createAbsentOptionalDependencyFilter = (isRedirected = ()=>false, moduleDirectories = [
174
+ 'node_modules'
175
+ ], aliasFields = [])=>{
176
+ const manifests = new Map();
177
+ return (request, context)=>{
178
+ const packageName = getPackageNameFromRequest(request);
179
+ if (!packageName || !context || isRedirected(request)) return false;
180
+ const manifest = findOwningPackageManifest(context, manifests);
181
+ if (!manifest) return false;
182
+ const optional = manifest.peerDependenciesMeta?.[packageName]?.optional === true || Object.hasOwn(manifest.optionalDependencies ?? {}, packageName);
183
+ return optional && !isRedirectedByAliasField(manifest, aliasFields, request) && !isPackageInstalled(packageName, context, moduleDirectories);
184
+ };
185
+ };
186
+ const getAliasNames = (alias)=>alias ? Array.isArray(alias) ? alias.map((entry)=>entry.onlyModule ? `${entry.name}$` : entry.name) : Object.keys(alias) : [];
187
+ const getExternalMatchers = (externals)=>{
188
+ if (Array.isArray(externals)) return externals.flatMap(getExternalMatchers);
189
+ if ('string' == typeof externals) return [
190
+ (request)=>request === externals
191
+ ];
192
+ if (externals instanceof RegExp) return [
193
+ (request)=>{
194
+ externals.lastIndex = 0;
195
+ return externals.test(request);
196
+ }
197
+ ];
198
+ if (externals && 'object' == typeof externals) {
199
+ const names = new Set(Object.entries(externals).filter(([, value])=>false !== value).map(([name])=>name));
76
200
  return [
77
- [
78
- builtin,
79
- `module-import ${nodeBuiltin}`
80
- ],
81
- [
82
- nodeBuiltin,
83
- `module-import ${nodeBuiltin}`
84
- ]
201
+ (request)=>names.has(request)
85
202
  ];
86
- }));
203
+ }
204
+ return [];
205
+ };
206
+ const createRequestRedirectMatcher = (options)=>{
207
+ const aliasNames = [
208
+ ...getAliasNames(options.resolve?.alias),
209
+ ...getAliasNames(options.resolve?.fallback)
210
+ ];
211
+ const externalMatchers = getExternalMatchers(options.externals);
212
+ return (request)=>externalMatchers.some((matches)=>matches(request)) || aliasNames.some((name)=>name.endsWith('$') ? request === name.slice(0, -1) : request === name || request.startsWith(`${name}/`));
213
+ };
214
+ const getFunctionExternals = (externals)=>Array.isArray(externals) ? externals.flatMap(getFunctionExternals) : 'function' == typeof externals ? [
215
+ externals
216
+ ] : [];
217
+ const callFunctionExternal = (external, data)=>new Promise((resolve, reject)=>{
218
+ const returned = external(data, (error, result)=>error ? reject(error) : resolve(result));
219
+ if (null !== returned && 'object' == typeof returned && 'function' == typeof returned.then) returned.then(resolve, reject);
220
+ else if (1 === external.length) resolve(returned);
221
+ });
222
+ const claimsByFunctionExternal = async (externals, data)=>{
223
+ for (const external of externals)try {
224
+ const result = await callFunctionExternal(external, data);
225
+ if (null != result && false !== result) return true;
226
+ } catch {
227
+ return true;
228
+ }
229
+ return false;
230
+ };
231
+ const PLUGIN_NAME = 'AbsentOptionalDependencyPlugin';
232
+ class AbsentOptionalDependencyPlugin {
233
+ apply(compiler) {
234
+ const isAbsent = createAbsentOptionalDependencyFilter(createRequestRedirectMatcher(compiler.options), compiler.options.resolve.modules, compiler.options.resolve.aliasFields);
235
+ const functionExternals = getFunctionExternals(compiler.options.externals);
236
+ compiler.hooks.normalModuleFactory.tap(PLUGIN_NAME, (factory)=>{
237
+ factory.hooks.beforeResolve.tapPromise(PLUGIN_NAME, async (data)=>{
238
+ if (!isAbsent(data.request, data.context)) return;
239
+ const claimed = await claimsByFunctionExternal(functionExternals, {
240
+ context: data.context,
241
+ contextInfo: {
242
+ issuer: data.contextInfo.issuer,
243
+ issuerLayer: data.contextInfo.issuerLayer ?? null
244
+ },
245
+ getResolve: (options)=>{
246
+ const resolver = factory.getResolver('normal', options ?? {});
247
+ return (context, request, callback)=>{
248
+ if (callback) return void resolver.resolve({}, context, request, {}, callback);
249
+ return new Promise((resolve, reject)=>{
250
+ resolver.resolve({}, context, request, {}, (error, result)=>error ? reject(error) : resolve(result || void 0));
251
+ });
252
+ };
253
+ },
254
+ request: data.request
255
+ });
256
+ return claimed ? void 0 : false;
257
+ });
258
+ });
259
+ }
260
+ }
87
261
  const normalizeWorkerOutputName = (name)=>node_path.posix.normalize(name.replace(/\\/gu, '/')).toLowerCase();
88
262
  const reserveWorkerChunkName = (baseName, reservedOutputNames)=>{
89
263
  let name = baseName;
@@ -133,6 +307,9 @@ function applyCloudflareWorkerRspackConfig(chain, workerEntryNames) {
133
307
  chain.optimization.runtimeChunk(config.optimization.runtimeChunk);
134
308
  chain.optimization.splitChunks(config.optimization.splitChunks);
135
309
  }
310
+ function removeCloudflareWorkerDataLoaderClientTransform(chain) {
311
+ for (const [name, rule] of Object.entries(chain.module.rules.entries() ?? {}))if (rule.uses.values().some((use)=>DATA_LOADER_CLIENT_TRANSFORM.test(String(use.get('loader'))))) chain.module.rules.delete(name);
312
+ }
136
313
  function applyCloudflareWorkerMfRuntimeBoundary(chain) {
137
314
  const runtimePlugin = getTemplatePath('cloudflare-worker-mf-ssr-runtime-plugin.mjs');
138
315
  chain.plugins.delete('plugin-module-federation');
@@ -204,11 +381,40 @@ const prependBundlerChain = (environment, handler)=>{
204
381
  }
205
382
  };
206
383
  };
384
+ const createWorkerRouteDataModule = ()=>[
385
+ `import { routes } from './${ROUTE_SERVER_LOADERS_FILE}';`,
386
+ `import { createRouteDataRequestHandler } from ${JSON.stringify(getTemplatePath(WORKER_ROUTE_DATA_TEMPLATE))};`,
387
+ '',
388
+ `export const ${CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT} =`,
389
+ ' createRouteDataRequestHandler(routes);',
390
+ ''
391
+ ].join('\n');
392
+ const getWorkerRouteDataEntries = (appContext)=>{
393
+ const { entrypoints, internalDirectory } = appContext;
394
+ if (!entrypoints || !internalDirectory) return {};
395
+ return Object.fromEntries(entrypoints.flatMap(({ entryName })=>{
396
+ const entryDirectory = node_path.join(internalDirectory, entryName);
397
+ if (!node_fs.existsSync(node_path.join(entryDirectory, SERVER_LOADER_COMBINED_FILE)) || !node_fs.existsSync(node_path.join(entryDirectory, ROUTE_SERVER_LOADERS_FILE))) return [];
398
+ const routeDataFile = node_path.join(entryDirectory, WORKER_ROUTE_DATA_FILE);
399
+ node_fs.writeFileSync(routeDataFile, createWorkerRouteDataModule());
400
+ return [
401
+ [
402
+ getCloudflareWorkerRouteDataEntryName(entryName),
403
+ [
404
+ routeDataFile
405
+ ]
406
+ ]
407
+ ];
408
+ }));
409
+ };
207
410
  const getWorkerEntries = (environment, normalizedConfig, appContext)=>{
208
411
  const configuredEntries = environment.source?.entry;
209
412
  const effectApiEntry = getEffectBffEntry(normalizedConfig, appContext);
210
413
  if (!configuredEntries && !effectApiEntry) return;
211
- const entries = configuredEntries ? rewriteWorkerEntries(configuredEntries) : {};
414
+ const entries = configuredEntries ? {
415
+ ...rewriteWorkerEntries(configuredEntries),
416
+ ...getWorkerRouteDataEntries(appContext)
417
+ } : {};
212
418
  return effectApiEntry ? {
213
419
  ...entries,
214
420
  [BFF_EFFECT_WORKER_ENTRY_NAME]: [
@@ -241,6 +447,7 @@ const createCloudflareBundlerChain = (appContext, workerEntryNames)=>{
241
447
  ];
242
448
  return (chain)=>{
243
449
  applyCloudflareWorkerRspackConfig(chain, entryNames);
450
+ removeCloudflareWorkerDataLoaderClientTransform(chain);
244
451
  chain.output.module(true).library({
245
452
  type: 'module'
246
453
  }).publicPath('/').chunkFormat('module').chunkLoading('import').workerChunkLoading('import');
@@ -249,10 +456,15 @@ const createCloudflareBundlerChain = (appContext, workerEntryNames)=>{
249
456
  'worker',
250
457
  'webpack',
251
458
  isProd() ? 'production' : 'development',
252
- 'import',
253
- 'require',
254
459
  'module'
255
460
  ])chain.resolve.conditionNames.add(condition);
461
+ for (const [dependencyType, condition] of WORKER_DEPENDENCY_CONDITIONS)chain.resolve.byDependency.set(dependencyType, {
462
+ conditionNames: [
463
+ condition,
464
+ '...'
465
+ ]
466
+ });
467
+ chain.plugin('cloudflare-worker-absent-optional-dependencies').use(AbsentOptionalDependencyPlugin);
256
468
  applyCloudflareWorkerMfRuntimeBoundary(chain);
257
469
  if (tanstackRouterSsrServerFile) {
258
470
  chain.resolve.alias.set('@tanstack/router-core/ssr/server$', tanstackRouterSsrServerFile);
@@ -335,4 +547,4 @@ const createCloudflareBuilderPlugin = (resolveDeployProvider = resolveStdEnvDepl
335
547
  }));
336
548
  }
337
549
  });
338
- export { applyCloudflareBuilderEnvironments, applyCloudflareWorkerMfRuntimeBoundary, applyCloudflareWorkerRspackConfig, createCloudflareBuilderPlugin, getCloudflareBuilderEnvironments, getCloudflareWorkerRspackConfig };
550
+ export { applyCloudflareBuilderEnvironments, applyCloudflareWorkerMfRuntimeBoundary, applyCloudflareWorkerRspackConfig, claimsByFunctionExternal, createAbsentOptionalDependencyFilter, createCloudflareBuilderPlugin, createRequestRedirectMatcher, getCloudflareBuilderEnvironments, getCloudflareWorkerRspackConfig };
@@ -11,15 +11,30 @@ const CLOUDFLARE_RUNTIME_TYPE = 'cloudflare-module-worker';
11
11
  const CLOUDFLARE_OUTPUT_PACKAGE_TYPE = 'module';
12
12
  const CLOUDFLARE_WORKER_PACKAGE_TYPE = 'commonjs';
13
13
  const CLOUDFLARE_WORKER_BUNDLE_FORMAT = 'commonjs';
14
+ const CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT = 'handleRouteDataRequest';
15
+ const getCloudflareWorkerRouteDataEntryName = (entryName)=>`${entryName}-server-loaders`;
14
16
  const CLOUDFLARE_WORKER_NODE_BUILTINS = [
17
+ '_http_agent',
18
+ '_http_client',
19
+ '_http_common',
20
+ '_http_incoming',
21
+ '_http_outgoing',
22
+ '_http_server',
23
+ '_tls_common',
24
+ '_tls_wrap',
15
25
  'assert',
16
26
  'assert/strict',
17
27
  'async_hooks',
18
28
  'buffer',
19
29
  'child_process',
30
+ 'cluster',
31
+ 'console',
32
+ 'constants',
20
33
  'crypto',
21
34
  'dgram',
35
+ 'diagnostics_channel',
22
36
  'dns',
37
+ 'dns/promises',
23
38
  'domain',
24
39
  'events',
25
40
  'fs',
@@ -28,11 +43,17 @@ const CLOUDFLARE_WORKER_NODE_BUILTINS = [
28
43
  'http2',
29
44
  'https',
30
45
  'inspector',
46
+ 'inspector/promises',
31
47
  'module',
32
48
  'net',
33
49
  'os',
34
50
  'path',
51
+ 'path/posix',
52
+ 'path/win32',
53
+ 'perf_hooks',
35
54
  'process',
55
+ 'punycode',
56
+ 'querystring',
36
57
  'readline',
37
58
  'readline/promises',
38
59
  'repl',
@@ -42,6 +63,8 @@ const CLOUDFLARE_WORKER_NODE_BUILTINS = [
42
63
  'stream/promises',
43
64
  'stream/web',
44
65
  'string_decoder',
66
+ 'sys',
67
+ 'test',
45
68
  'timers',
46
69
  'timers/promises',
47
70
  'tls',
@@ -63,4 +86,4 @@ const CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS = [
63
86
  'nodejs_compat',
64
87
  'global_fetch_strictly_public'
65
88
  ];
66
- export { CLOUDFLARE_ASSETS_BINDING, CLOUDFLARE_OUTPUT_PACKAGE_FILE, CLOUDFLARE_OUTPUT_PACKAGE_TYPE, CLOUDFLARE_PUBLIC_ASSETS_DIRECTORY, CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS, CLOUDFLARE_RUNTIME_TYPE, CLOUDFLARE_WORKER_BUNDLE_DIRECTORY, CLOUDFLARE_WORKER_BUNDLE_FORMAT, CLOUDFLARE_WORKER_ENTRY, CLOUDFLARE_WORKER_MANIFEST, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PACKAGE_FILE, CLOUDFLARE_WORKER_PACKAGE_TYPE, CLOUDFLARE_WORKER_PLATFORM_MODULES, CLOUDFLARE_WRANGLER_CONFIG_FILE };
89
+ export { CLOUDFLARE_ASSETS_BINDING, CLOUDFLARE_OUTPUT_PACKAGE_FILE, CLOUDFLARE_OUTPUT_PACKAGE_TYPE, CLOUDFLARE_PUBLIC_ASSETS_DIRECTORY, CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS, CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT, CLOUDFLARE_RUNTIME_TYPE, CLOUDFLARE_WORKER_BUNDLE_DIRECTORY, CLOUDFLARE_WORKER_BUNDLE_FORMAT, CLOUDFLARE_WORKER_ENTRY, CLOUDFLARE_WORKER_MANIFEST, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PACKAGE_FILE, CLOUDFLARE_WORKER_PACKAGE_TYPE, CLOUDFLARE_WORKER_PLATFORM_MODULES, CLOUDFLARE_WRANGLER_CONFIG_FILE, getCloudflareWorkerRouteDataEntryName };
@@ -5,9 +5,9 @@ import { pathToFileURL } from "node:url";
5
5
  import { isDeepStrictEqual } from "node:util";
6
6
  import { parse } from "@babel/parser";
7
7
  import traverse from "@babel/traverse";
8
- import { CLOUDFLARE_WORKER_BUNDLE_DIRECTORY, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PLATFORM_MODULES } from "../cloudflare-output-contract.mjs";
8
+ import { CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT, CLOUDFLARE_WORKER_BUNDLE_DIRECTORY, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PLATFORM_MODULES } from "../cloudflare-output-contract.mjs";
9
9
  import { addIssue } from "./issues.mjs";
10
- const getReferencedRouteWorkers = (manifest)=>Array.isArray(manifest?.routeSpec?.routes) ? manifest.routeSpec.routes.map((route)=>'string' == typeof route?.worker && route.worker.length > 0 ? route.worker : void 0).filter((worker)=>'string' == typeof worker) : [];
10
+ const getReferencedRouteWorkers = (manifest, field)=>Array.isArray(manifest?.routeSpec?.routes) ? manifest.routeSpec.routes.map((route)=>'string' == typeof route?.[field] && route[field].length > 0 ? route[field] : void 0).filter((worker)=>'string' == typeof worker) : [];
11
11
  const getEffectBffWorker = (manifest)=>manifest?.bff?.runtimeFramework === 'effect' && 'string' == typeof manifest.bff.worker ? manifest.bff.worker : void 0;
12
12
  const getWorkerBundleReferences = (manifest)=>{
13
13
  const effectBffWorker = getEffectBffWorker(manifest);
@@ -19,9 +19,14 @@ const getWorkerBundleReferences = (manifest)=>{
19
19
  reference: effectBffWorker
20
20
  }
21
21
  ] : [],
22
- ...getReferencedRouteWorkers(manifest).map((reference)=>({
22
+ ...getReferencedRouteWorkers(manifest, 'worker').map((reference)=>({
23
23
  kind: 'route',
24
24
  reference
25
+ })),
26
+ ...getReferencedRouteWorkers(manifest, 'routeDataWorker').map((reference)=>({
27
+ dispatcherExport: CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT,
28
+ kind: 'route-data',
29
+ reference
25
30
  }))
26
31
  ];
27
32
  };
@@ -42,7 +47,7 @@ const resolveWorkerBundleReference = (issues, outputDirectory, reference, manife
42
47
  path: workerPath
43
48
  };
44
49
  };
45
- const missingWorkerBundleMessage = (reference)=>'effect-bff' === reference.kind ? 'Cloudflare Effect BFF manifest points to a missing worker bundle.' : 'Cloudflare route worker manifest points to a missing worker bundle.';
50
+ const missingWorkerBundleMessage = (reference)=>'effect-bff' === reference.kind ? 'Cloudflare Effect BFF manifest points to a missing worker bundle.' : 'route-data' === reference.kind ? 'Cloudflare route data worker manifest points to a missing worker bundle.' : 'Cloudflare route worker manifest points to a missing worker bundle.';
46
51
  const isAstNode = (value)=>null !== value && 'object' == typeof value && 'type' in value && 'string' == typeof value.type;
47
52
  const getNodeName = (node)=>{
48
53
  if (!isAstNode(node)) return;
@@ -286,9 +291,9 @@ const verifyWorkerModuleClosure = async (issues, entryPath, entrySource, contrac
286
291
  };
287
292
  const verifyWorkerBundleReferences = async (issues, worker, source, contract)=>{
288
293
  const entryAnalysis = await verifyWorkerModuleClosure(issues, worker.path, source, contract);
289
- if ('effect-bff' === worker.kind && 'string' == typeof worker.dispatcherExport && !entryAnalysis?.exports.has(worker.dispatcherExport)) addIssue(issues, {
294
+ if ('string' == typeof worker.dispatcherExport && !entryAnalysis?.exports.has(worker.dispatcherExport)) addIssue(issues, {
290
295
  code: 'invalid-worker-bundle',
291
- message: 'Cloudflare Effect BFF worker bundle must expose its manifest dispatcherExport.',
296
+ message: 'route-data' === worker.kind ? `Cloudflare route data worker bundle must expose ${worker.dispatcherExport}.` : 'Cloudflare Effect BFF worker bundle must expose its manifest dispatcherExport.',
292
297
  path: worker.path
293
298
  });
294
299
  };
@@ -2,9 +2,13 @@ function createNoopMonitors() {
2
2
  const noop = () => {};
3
3
 
4
4
  return {
5
+ counter: noop,
5
6
  debug: noop,
6
7
  error: noop,
7
8
  info: noop,
9
+ push: noop,
10
+ timing: noop,
11
+ trace: noop,
8
12
  warn: noop,
9
13
  };
10
14
  }
@@ -52,6 +52,52 @@ async function getRequestHandler(workerModule) {
52
52
  );
53
53
  }
54
54
 
55
+ const ROUTE_DATA_REQUEST_PARAM = '__loader';
56
+
57
+ // Route data requests (`?__loader=`) are answered by the entry's route data
58
+ // worker, like the Node server's data handler. Without a match the request
59
+ // falls through to page rendering, as it does on Node.
60
+ async function dispatchRouteDataRequest(route, request) {
61
+ const routeDataWorkerPath = route.routeDataWorker;
62
+ if (
63
+ !routeDataWorkerPath ||
64
+ !new URL(request.url).searchParams.has(ROUTE_DATA_REQUEST_PARAM)
65
+ ) {
66
+ return undefined;
67
+ }
68
+
69
+ const routeDataWorkerModule = await loadWorkerModule(routeDataWorkerPath);
70
+ const handleRouteDataRequest = routeDataWorkerModule
71
+ ? getRuntimeModule(routeDataWorkerModule).handleRouteDataRequest
72
+ : undefined;
73
+
74
+ if (typeof handleRouteDataRequest !== 'function') {
75
+ return new Response(
76
+ `Route data worker bundle has no handleRouteDataRequest export: ${routeDataWorkerPath}`,
77
+ {
78
+ status: 500,
79
+ headers: {
80
+ 'content-type': 'text/plain; charset=utf-8',
81
+ 'x-modern-js-route-data-worker': routeDataWorkerPath,
82
+ },
83
+ },
84
+ );
85
+ }
86
+
87
+ return handleRouteDataRequest({
88
+ request,
89
+ serverRoutes: MODERN_WORKER_MANIFEST.routeSpec.routes,
90
+ context: {
91
+ loaderContext: new Map(),
92
+ monitors: createNoopMonitors(),
93
+ reporter: {
94
+ reportTiming: () => {},
95
+ },
96
+ },
97
+ onTiming() {},
98
+ });
99
+ }
100
+
55
101
  async function dispatchRouteWorker(route, request, env, ctx) {
56
102
  const workerPath = route.worker;
57
103
  if (!workerPath) {
@@ -62,11 +62,18 @@ export default {
62
62
  }
63
63
 
64
64
  if (route?.worker) {
65
- const renderableRequest = createRenderableRequest(request);
65
+ // Route data requests keep their method, as on the Node server.
66
+ const routeDataResponse = await dispatchRouteDataRequest(route, request);
66
67
 
67
68
  return finalizeResponseForRequest(
68
69
  withAppCorsHeaders(
69
- await dispatchRouteWorker(route, renderableRequest, env, ctx),
70
+ routeDataResponse ??
71
+ (await dispatchRouteWorker(
72
+ route,
73
+ createRenderableRequest(request),
74
+ env,
75
+ ctx,
76
+ )),
70
77
  request,
71
78
  ),
72
79
  request,