@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.13 → 3.9.0-ultramodern.15
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.
- package/dist/cjs/cloudflare/worker-manifest.js +13 -4
- package/dist/cjs/cloudflare/wrangler-config.js +35 -7
- package/dist/cjs/cloudflare-builder.js +235 -14
- package/dist/cjs/cloudflare-output-contract.js +31 -2
- package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +10 -5
- package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
- package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
- package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
- package/dist/cjs/templates/cloudflare-worker-route-data.mjs +1069 -0
- package/dist/esm/cloudflare/worker-manifest.mjs +13 -4
- package/dist/esm/cloudflare/wrangler-config.mjs +35 -7
- package/dist/esm/cloudflare-builder.mjs +229 -17
- package/dist/esm/cloudflare-output-contract.mjs +24 -1
- package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +11 -6
- package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
- package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
- package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
- package/dist/esm/templates/cloudflare-worker-route-data.mjs +1069 -0
- package/dist/esm-node/cloudflare/worker-manifest.mjs +13 -4
- package/dist/esm-node/cloudflare/wrangler-config.mjs +35 -7
- package/dist/esm-node/cloudflare-builder.mjs +229 -17
- package/dist/esm-node/cloudflare-output-contract.mjs +24 -1
- package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +11 -6
- package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
- package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
- package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
- package/dist/esm-node/templates/cloudflare-worker-route-data.mjs +1069 -0
- package/dist/types/cloudflare/wrangler-config.d.ts +6 -0
- package/dist/types/cloudflare-builder.d.ts +44 -1
- package/dist/types/cloudflare-output-contract.d.ts +3 -1
- package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
- package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
- package/package.json +17 -13
- package/src/cloudflare/worker-manifest.ts +19 -3
- package/src/cloudflare/wrangler-config.ts +86 -10
- package/src/cloudflare-builder.ts +447 -12
- package/src/cloudflare-output-contract.ts +34 -2
- package/src/cloudflare-output-verifier/worker-bundles.ts +27 -11
- package/src/templates/cloudflare-entry.004-rendering-css.mjs +4 -0
- package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +46 -0
- package/src/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
- 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 {
|
|
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',
|
|
@@ -42,6 +74,8 @@ export interface CloudflareBuilderAppContext {
|
|
|
42
74
|
apiOnly?: boolean;
|
|
43
75
|
apiDirectory: string;
|
|
44
76
|
appDirectory: string;
|
|
77
|
+
entrypoints?: ReadonlyArray<{ entryName: string }>;
|
|
78
|
+
internalDirectory?: string;
|
|
45
79
|
}
|
|
46
80
|
|
|
47
81
|
export interface CloudflareBuilderEnvironmentsOptions {
|
|
@@ -191,16 +225,329 @@ const setAliasIfPresent = (
|
|
|
191
225
|
}
|
|
192
226
|
};
|
|
193
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:`).
|
|
194
230
|
const getCloudflareWorkerNodeExternals = () =>
|
|
195
|
-
Object.fromEntries(
|
|
196
|
-
CLOUDFLARE_WORKER_NODE_BUILTINS.flatMap(builtin => {
|
|
231
|
+
Object.fromEntries([
|
|
232
|
+
...CLOUDFLARE_WORKER_NODE_BUILTINS.flatMap(builtin => {
|
|
197
233
|
const nodeBuiltin = `node:${builtin}`;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
[
|
|
201
|
-
|
|
234
|
+
const external = `module-import ${nodeBuiltin}`;
|
|
235
|
+
return isBuiltin(builtin)
|
|
236
|
+
? [
|
|
237
|
+
[builtin, external],
|
|
238
|
+
[nodeBuiltin, external],
|
|
239
|
+
]
|
|
240
|
+
: [[nodeBuiltin, external]];
|
|
202
241
|
}),
|
|
203
|
-
|
|
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
|
+
}
|
|
204
551
|
|
|
205
552
|
const normalizeWorkerOutputName = (name: string) =>
|
|
206
553
|
path.posix.normalize(name.replace(/\\/gu, '/')).toLowerCase();
|
|
@@ -283,6 +630,30 @@ export function applyCloudflareWorkerRspackConfig(
|
|
|
283
630
|
chain.optimization.splitChunks(config.optimization.splitChunks);
|
|
284
631
|
}
|
|
285
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
|
+
|
|
286
657
|
export function applyCloudflareWorkerMfRuntimeBoundary(
|
|
287
658
|
chain: Parameters<ModifyBundlerChainFn>[0],
|
|
288
659
|
) {
|
|
@@ -398,6 +769,55 @@ const prependBundlerChain = (
|
|
|
398
769
|
};
|
|
399
770
|
};
|
|
400
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
|
+
|
|
401
821
|
const getWorkerEntries = (
|
|
402
822
|
environment: EnvironmentConfig,
|
|
403
823
|
normalizedConfig: CloudflareBuilderNormalizedConfig,
|
|
@@ -410,7 +830,10 @@ const getWorkerEntries = (
|
|
|
410
830
|
}
|
|
411
831
|
|
|
412
832
|
const entries = configuredEntries
|
|
413
|
-
?
|
|
833
|
+
? {
|
|
834
|
+
...rewriteWorkerEntries(configuredEntries),
|
|
835
|
+
...getWorkerRouteDataEntries(appContext),
|
|
836
|
+
}
|
|
414
837
|
: {};
|
|
415
838
|
return effectApiEntry
|
|
416
839
|
? {
|
|
@@ -479,6 +902,7 @@ const createCloudflareBundlerChain = (
|
|
|
479
902
|
|
|
480
903
|
return chain => {
|
|
481
904
|
applyCloudflareWorkerRspackConfig(chain, entryNames);
|
|
905
|
+
removeCloudflareWorkerDataLoaderClientTransform(chain);
|
|
482
906
|
chain.output
|
|
483
907
|
.module(true)
|
|
484
908
|
.library({ type: 'module' })
|
|
@@ -492,12 +916,23 @@ const createCloudflareBundlerChain = (
|
|
|
492
916
|
'worker',
|
|
493
917
|
'webpack',
|
|
494
918
|
isProd() ? 'production' : 'development',
|
|
495
|
-
'import',
|
|
496
|
-
'require',
|
|
497
919
|
'module',
|
|
498
920
|
]) {
|
|
499
921
|
chain.resolve.conditionNames.add(condition);
|
|
500
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);
|
|
501
936
|
|
|
502
937
|
applyCloudflareWorkerMfRuntimeBoundary(chain);
|
|
503
938
|
if (tanstackRouterSsrServerFile) {
|
|
@@ -10,17 +10,41 @@ 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
|
-
//
|
|
14
|
-
//
|
|
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',
|
|
@@ -29,11 +53,17 @@ export const CLOUDFLARE_WORKER_NODE_BUILTINS = [
|
|
|
29
53
|
'http2',
|
|
30
54
|
'https',
|
|
31
55
|
'inspector',
|
|
56
|
+
'inspector/promises',
|
|
32
57
|
'module',
|
|
33
58
|
'net',
|
|
34
59
|
'os',
|
|
35
60
|
'path',
|
|
61
|
+
'path/posix',
|
|
62
|
+
'path/win32',
|
|
63
|
+
'perf_hooks',
|
|
36
64
|
'process',
|
|
65
|
+
'punycode',
|
|
66
|
+
'querystring',
|
|
37
67
|
'readline',
|
|
38
68
|
'readline/promises',
|
|
39
69
|
'repl',
|
|
@@ -43,6 +73,8 @@ export const CLOUDFLARE_WORKER_NODE_BUILTINS = [
|
|
|
43
73
|
'stream/promises',
|
|
44
74
|
'stream/web',
|
|
45
75
|
'string_decoder',
|
|
76
|
+
'sys',
|
|
77
|
+
'test',
|
|
46
78
|
'timers',
|
|
47
79
|
'timers/promises',
|
|
48
80
|
'tls',
|
|
@@ -5,6 +5,7 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
5
5
|
import { parse } from '@babel/parser';
|
|
6
6
|
import traverse, { type NodePath } from '@babel/traverse';
|
|
7
7
|
import {
|
|
8
|
+
CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT,
|
|
8
9
|
CLOUDFLARE_WORKER_BUNDLE_DIRECTORY,
|
|
9
10
|
CLOUDFLARE_WORKER_NODE_BUILTINS,
|
|
10
11
|
CLOUDFLARE_WORKER_PLATFORM_MODULES,
|
|
@@ -14,7 +15,7 @@ import { addIssue } from './issues';
|
|
|
14
15
|
|
|
15
16
|
interface WorkerBundleReference {
|
|
16
17
|
dispatcherExport?: string;
|
|
17
|
-
kind: 'effect-bff' | 'route';
|
|
18
|
+
kind: 'effect-bff' | 'route' | 'route-data';
|
|
18
19
|
reference: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -22,12 +23,15 @@ interface ResolvedWorkerBundleReference extends WorkerBundleReference {
|
|
|
22
23
|
path: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
const getReferencedRouteWorkers = (
|
|
26
|
+
const getReferencedRouteWorkers = (
|
|
27
|
+
manifest: JsonObject,
|
|
28
|
+
field: 'routeDataWorker' | 'worker',
|
|
29
|
+
) =>
|
|
26
30
|
Array.isArray(manifest?.routeSpec?.routes)
|
|
27
31
|
? manifest.routeSpec.routes
|
|
28
32
|
.map((route: any) =>
|
|
29
|
-
typeof route?.
|
|
30
|
-
? route
|
|
33
|
+
typeof route?.[field] === 'string' && route[field].length > 0
|
|
34
|
+
? route[field]
|
|
31
35
|
: undefined,
|
|
32
36
|
)
|
|
33
37
|
.filter(
|
|
@@ -58,10 +62,19 @@ export const getWorkerBundleReferences = (
|
|
|
58
62
|
},
|
|
59
63
|
]
|
|
60
64
|
: []),
|
|
61
|
-
...getReferencedRouteWorkers(manifest).map(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
...getReferencedRouteWorkers(manifest, 'worker').map(
|
|
66
|
+
(reference: string) => ({
|
|
67
|
+
kind: 'route' as const,
|
|
68
|
+
reference,
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
...getReferencedRouteWorkers(manifest, 'routeDataWorker').map(
|
|
72
|
+
(reference: string) => ({
|
|
73
|
+
dispatcherExport: CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT,
|
|
74
|
+
kind: 'route-data' as const,
|
|
75
|
+
reference,
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
65
78
|
];
|
|
66
79
|
};
|
|
67
80
|
|
|
@@ -101,7 +114,9 @@ export const resolveWorkerBundleReference = (
|
|
|
101
114
|
export const missingWorkerBundleMessage = (reference: WorkerBundleReference) =>
|
|
102
115
|
reference.kind === 'effect-bff'
|
|
103
116
|
? 'Cloudflare Effect BFF manifest points to a missing worker bundle.'
|
|
104
|
-
:
|
|
117
|
+
: reference.kind === 'route-data'
|
|
118
|
+
? 'Cloudflare route data worker manifest points to a missing worker bundle.'
|
|
119
|
+
: 'Cloudflare route worker manifest points to a missing worker bundle.';
|
|
105
120
|
|
|
106
121
|
interface AstNode {
|
|
107
122
|
type: string;
|
|
@@ -611,14 +626,15 @@ export const verifyWorkerBundleReferences = async (
|
|
|
611
626
|
contract,
|
|
612
627
|
);
|
|
613
628
|
if (
|
|
614
|
-
worker.kind === 'effect-bff' &&
|
|
615
629
|
typeof worker.dispatcherExport === 'string' &&
|
|
616
630
|
!entryAnalysis?.exports.has(worker.dispatcherExport)
|
|
617
631
|
) {
|
|
618
632
|
addIssue(issues, {
|
|
619
633
|
code: 'invalid-worker-bundle',
|
|
620
634
|
message:
|
|
621
|
-
|
|
635
|
+
worker.kind === 'route-data'
|
|
636
|
+
? `Cloudflare route data worker bundle must expose ${worker.dispatcherExport}.`
|
|
637
|
+
: 'Cloudflare Effect BFF worker bundle must expose its manifest dispatcherExport.',
|
|
622
638
|
path: worker.path,
|
|
623
639
|
});
|
|
624
640
|
}
|