@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,16 +1,55 @@
1
1
  import node_fs from "node:fs";
2
- import { createRequire } from "node:module";
2
+ import { createRequire, isBuiltin } from "node:module";
3
3
  import node_path from "node:path";
4
4
  import { SERVICE_WORKER_ENVIRONMENT_NAME } from "@modern-js/builder";
5
5
  import { isProd } from "@modern-js/utils";
6
6
  import { provider } from "std-env";
7
- import { CLOUDFLARE_WORKER_NODE_BUILTINS } from "./cloudflare-output-contract.mjs";
7
+ import { CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PLATFORM_MODULES, getCloudflareWorkerRouteDataEntryName } from "./cloudflare-output-contract.mjs";
8
8
  import { getTemplatePath } from "./read-template.mjs";
9
9
  const moduleRequire = createRequire(import.meta.url);
10
10
  const BFF_EFFECT_WORKER_ENTRY_NAME = '__modern_bff_effect';
11
11
  const BFF_EFFECT_WORKER_RUNTIME_QUERY = 'modern-bff-runtime';
12
+ const SERVER_LOADER_COMBINED_FILE = 'server-loader-combined.js';
13
+ const ROUTE_SERVER_LOADERS_FILE = 'route-server-loaders.js';
14
+ const WORKER_ROUTE_DATA_FILE = 'cloudflare-worker-route-data.js';
15
+ const WORKER_ROUTE_DATA_TEMPLATE = 'cloudflare-worker-route-data.mjs';
16
+ const DATA_LOADER_CLIENT_TRANSFORM = /plugin-data-loader[\\/](?:.+[\\/])?loader\.[cm]?js$/u;
12
17
  const MF_SSR_DATA_FETCH_RUNTIME_PLUGIN = '@module-federation/modern-js-v3/ssr-inject-data-fetch-function-plugin';
13
18
  const MF_SSR_DEV_RUNTIME_PLUGIN = '@module-federation/modern-js-v3/ssr-dev-plugin';
19
+ const WORKER_DEPENDENCY_CONDITIONS = [
20
+ [
21
+ 'esm',
22
+ 'import'
23
+ ],
24
+ [
25
+ 'wasm',
26
+ 'import'
27
+ ],
28
+ [
29
+ 'loaderImport',
30
+ 'import'
31
+ ],
32
+ [
33
+ 'worker',
34
+ 'import'
35
+ ],
36
+ [
37
+ 'commonjs',
38
+ 'require'
39
+ ],
40
+ [
41
+ 'amd',
42
+ 'require'
43
+ ],
44
+ [
45
+ 'loader',
46
+ 'require'
47
+ ],
48
+ [
49
+ 'unknown',
50
+ 'require'
51
+ ]
52
+ ];
14
53
  const JS_OR_TS_EXTENSIONS = new Set([
15
54
  '.js',
16
55
  '.jsx',
@@ -70,19 +109,154 @@ const resolvePackageFile = (packageName, filePath, paths)=>{
70
109
  const setAliasIfPresent = (alias, name, value)=>{
71
110
  if (value) alias.set(name, value);
72
111
  };
73
- const getCloudflareWorkerNodeExternals = ()=>Object.fromEntries(CLOUDFLARE_WORKER_NODE_BUILTINS.flatMap((builtin)=>{
74
- const nodeBuiltin = `node:${builtin}`;
112
+ const getCloudflareWorkerNodeExternals = ()=>Object.fromEntries([
113
+ ...CLOUDFLARE_WORKER_NODE_BUILTINS.flatMap((builtin)=>{
114
+ const nodeBuiltin = `node:${builtin}`;
115
+ const external = `module-import ${nodeBuiltin}`;
116
+ return isBuiltin(builtin) ? [
117
+ [
118
+ builtin,
119
+ external
120
+ ],
121
+ [
122
+ nodeBuiltin,
123
+ external
124
+ ]
125
+ ] : [
126
+ [
127
+ nodeBuiltin,
128
+ external
129
+ ]
130
+ ];
131
+ }),
132
+ ...CLOUDFLARE_WORKER_PLATFORM_MODULES.map((platformModule)=>[
133
+ platformModule,
134
+ `module-import ${platformModule}`
135
+ ])
136
+ ]);
137
+ const getPackageNameFromRequest = (request)=>{
138
+ if (request.startsWith('.') || request.startsWith('/') || request.includes(':') || node_path.isAbsolute(request)) return;
139
+ const segments = request.split('/');
140
+ return request.startsWith('@') ? segments.length > 1 ? `${segments[0]}/${segments[1]}` : void 0 : segments[0];
141
+ };
142
+ const isRedirectedByAliasField = (manifest, aliasFields, request)=>aliasFields.some((field)=>{
143
+ let value = manifest;
144
+ for (const key of 'string' == typeof field ? [
145
+ field
146
+ ] : field)value = value && 'object' == typeof value ? value[key] : void 0;
147
+ return 'object' == typeof value && null !== value && Object.hasOwn(value, request);
148
+ });
149
+ const findOwningPackageManifest = (directory, cache)=>{
150
+ if (cache.has(directory)) return cache.get(directory);
151
+ let manifest;
152
+ const packageJsonPath = node_path.join(directory, 'package.json');
153
+ if (node_fs.existsSync(packageJsonPath)) try {
154
+ manifest = JSON.parse(node_fs.readFileSync(packageJsonPath, 'utf-8'));
155
+ } catch {
156
+ manifest = void 0;
157
+ }
158
+ else if (node_path.dirname(directory) !== directory) manifest = findOwningPackageManifest(node_path.dirname(directory), cache);
159
+ cache.set(directory, manifest);
160
+ return manifest;
161
+ };
162
+ const isPackageInstalled = (packageName, directory, moduleDirectories)=>moduleDirectories.some((moduleDirectory)=>{
163
+ if (node_path.isAbsolute(moduleDirectory)) return node_fs.existsSync(node_path.join(moduleDirectory, packageName));
164
+ let current = directory;
165
+ while(true){
166
+ if (node_fs.existsSync(node_path.join(current, moduleDirectory, packageName))) return true;
167
+ const parent = node_path.dirname(current);
168
+ if (parent === current) return false;
169
+ current = parent;
170
+ }
171
+ });
172
+ const createAbsentOptionalDependencyFilter = (isRedirected = ()=>false, moduleDirectories = [
173
+ 'node_modules'
174
+ ], aliasFields = [])=>{
175
+ const manifests = new Map();
176
+ return (request, context)=>{
177
+ const packageName = getPackageNameFromRequest(request);
178
+ if (!packageName || !context || isRedirected(request)) return false;
179
+ const manifest = findOwningPackageManifest(context, manifests);
180
+ if (!manifest) return false;
181
+ const optional = manifest.peerDependenciesMeta?.[packageName]?.optional === true || Object.hasOwn(manifest.optionalDependencies ?? {}, packageName);
182
+ return optional && !isRedirectedByAliasField(manifest, aliasFields, request) && !isPackageInstalled(packageName, context, moduleDirectories);
183
+ };
184
+ };
185
+ const getAliasNames = (alias)=>alias ? Array.isArray(alias) ? alias.map((entry)=>entry.onlyModule ? `${entry.name}$` : entry.name) : Object.keys(alias) : [];
186
+ const getExternalMatchers = (externals)=>{
187
+ if (Array.isArray(externals)) return externals.flatMap(getExternalMatchers);
188
+ if ('string' == typeof externals) return [
189
+ (request)=>request === externals
190
+ ];
191
+ if (externals instanceof RegExp) return [
192
+ (request)=>{
193
+ externals.lastIndex = 0;
194
+ return externals.test(request);
195
+ }
196
+ ];
197
+ if (externals && 'object' == typeof externals) {
198
+ const names = new Set(Object.entries(externals).filter(([, value])=>false !== value).map(([name])=>name));
75
199
  return [
76
- [
77
- builtin,
78
- `module-import ${nodeBuiltin}`
79
- ],
80
- [
81
- nodeBuiltin,
82
- `module-import ${nodeBuiltin}`
83
- ]
200
+ (request)=>names.has(request)
84
201
  ];
85
- }));
202
+ }
203
+ return [];
204
+ };
205
+ const createRequestRedirectMatcher = (options)=>{
206
+ const aliasNames = [
207
+ ...getAliasNames(options.resolve?.alias),
208
+ ...getAliasNames(options.resolve?.fallback)
209
+ ];
210
+ const externalMatchers = getExternalMatchers(options.externals);
211
+ return (request)=>externalMatchers.some((matches)=>matches(request)) || aliasNames.some((name)=>name.endsWith('$') ? request === name.slice(0, -1) : request === name || request.startsWith(`${name}/`));
212
+ };
213
+ const getFunctionExternals = (externals)=>Array.isArray(externals) ? externals.flatMap(getFunctionExternals) : 'function' == typeof externals ? [
214
+ externals
215
+ ] : [];
216
+ const callFunctionExternal = (external, data)=>new Promise((resolve, reject)=>{
217
+ const returned = external(data, (error, result)=>error ? reject(error) : resolve(result));
218
+ if (null !== returned && 'object' == typeof returned && 'function' == typeof returned.then) returned.then(resolve, reject);
219
+ else if (1 === external.length) resolve(returned);
220
+ });
221
+ const claimsByFunctionExternal = async (externals, data)=>{
222
+ for (const external of externals)try {
223
+ const result = await callFunctionExternal(external, data);
224
+ if (null != result && false !== result) return true;
225
+ } catch {
226
+ return true;
227
+ }
228
+ return false;
229
+ };
230
+ const PLUGIN_NAME = 'AbsentOptionalDependencyPlugin';
231
+ class AbsentOptionalDependencyPlugin {
232
+ apply(compiler) {
233
+ const isAbsent = createAbsentOptionalDependencyFilter(createRequestRedirectMatcher(compiler.options), compiler.options.resolve.modules, compiler.options.resolve.aliasFields);
234
+ const functionExternals = getFunctionExternals(compiler.options.externals);
235
+ compiler.hooks.normalModuleFactory.tap(PLUGIN_NAME, (factory)=>{
236
+ factory.hooks.beforeResolve.tapPromise(PLUGIN_NAME, async (data)=>{
237
+ if (!isAbsent(data.request, data.context)) return;
238
+ const claimed = await claimsByFunctionExternal(functionExternals, {
239
+ context: data.context,
240
+ contextInfo: {
241
+ issuer: data.contextInfo.issuer,
242
+ issuerLayer: data.contextInfo.issuerLayer ?? null
243
+ },
244
+ getResolve: (options)=>{
245
+ const resolver = factory.getResolver('normal', options ?? {});
246
+ return (context, request, callback)=>{
247
+ if (callback) return void resolver.resolve({}, context, request, {}, callback);
248
+ return new Promise((resolve, reject)=>{
249
+ resolver.resolve({}, context, request, {}, (error, result)=>error ? reject(error) : resolve(result || void 0));
250
+ });
251
+ };
252
+ },
253
+ request: data.request
254
+ });
255
+ return claimed ? void 0 : false;
256
+ });
257
+ });
258
+ }
259
+ }
86
260
  const normalizeWorkerOutputName = (name)=>node_path.posix.normalize(name.replace(/\\/gu, '/')).toLowerCase();
87
261
  const reserveWorkerChunkName = (baseName, reservedOutputNames)=>{
88
262
  let name = baseName;
@@ -132,6 +306,9 @@ function applyCloudflareWorkerRspackConfig(chain, workerEntryNames) {
132
306
  chain.optimization.runtimeChunk(config.optimization.runtimeChunk);
133
307
  chain.optimization.splitChunks(config.optimization.splitChunks);
134
308
  }
309
+ function removeCloudflareWorkerDataLoaderClientTransform(chain) {
310
+ 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);
311
+ }
135
312
  function applyCloudflareWorkerMfRuntimeBoundary(chain) {
136
313
  const runtimePlugin = getTemplatePath('cloudflare-worker-mf-ssr-runtime-plugin.mjs');
137
314
  chain.plugins.delete('plugin-module-federation');
@@ -203,11 +380,40 @@ const prependBundlerChain = (environment, handler)=>{
203
380
  }
204
381
  };
205
382
  };
383
+ const createWorkerRouteDataModule = ()=>[
384
+ `import { routes } from './${ROUTE_SERVER_LOADERS_FILE}';`,
385
+ `import { createRouteDataRequestHandler } from ${JSON.stringify(getTemplatePath(WORKER_ROUTE_DATA_TEMPLATE))};`,
386
+ '',
387
+ `export const ${CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT} =`,
388
+ ' createRouteDataRequestHandler(routes);',
389
+ ''
390
+ ].join('\n');
391
+ const getWorkerRouteDataEntries = (appContext)=>{
392
+ const { entrypoints, internalDirectory } = appContext;
393
+ if (!entrypoints || !internalDirectory) return {};
394
+ return Object.fromEntries(entrypoints.flatMap(({ entryName })=>{
395
+ const entryDirectory = node_path.join(internalDirectory, entryName);
396
+ if (!node_fs.existsSync(node_path.join(entryDirectory, SERVER_LOADER_COMBINED_FILE)) || !node_fs.existsSync(node_path.join(entryDirectory, ROUTE_SERVER_LOADERS_FILE))) return [];
397
+ const routeDataFile = node_path.join(entryDirectory, WORKER_ROUTE_DATA_FILE);
398
+ node_fs.writeFileSync(routeDataFile, createWorkerRouteDataModule());
399
+ return [
400
+ [
401
+ getCloudflareWorkerRouteDataEntryName(entryName),
402
+ [
403
+ routeDataFile
404
+ ]
405
+ ]
406
+ ];
407
+ }));
408
+ };
206
409
  const getWorkerEntries = (environment, normalizedConfig, appContext)=>{
207
410
  const configuredEntries = environment.source?.entry;
208
- if (!configuredEntries) return;
209
- const entries = rewriteWorkerEntries(configuredEntries);
210
411
  const effectApiEntry = getEffectBffEntry(normalizedConfig, appContext);
412
+ if (!configuredEntries && !effectApiEntry) return;
413
+ const entries = configuredEntries ? {
414
+ ...rewriteWorkerEntries(configuredEntries),
415
+ ...getWorkerRouteDataEntries(appContext)
416
+ } : {};
211
417
  return effectApiEntry ? {
212
418
  ...entries,
213
419
  [BFF_EFFECT_WORKER_ENTRY_NAME]: [
@@ -240,6 +446,7 @@ const createCloudflareBundlerChain = (appContext, workerEntryNames)=>{
240
446
  ];
241
447
  return (chain)=>{
242
448
  applyCloudflareWorkerRspackConfig(chain, entryNames);
449
+ removeCloudflareWorkerDataLoaderClientTransform(chain);
243
450
  chain.output.module(true).library({
244
451
  type: 'module'
245
452
  }).publicPath('/').chunkFormat('module').chunkLoading('import').workerChunkLoading('import');
@@ -248,10 +455,15 @@ const createCloudflareBundlerChain = (appContext, workerEntryNames)=>{
248
455
  'worker',
249
456
  'webpack',
250
457
  isProd() ? 'production' : 'development',
251
- 'import',
252
- 'require',
253
458
  'module'
254
459
  ])chain.resolve.conditionNames.add(condition);
460
+ for (const [dependencyType, condition] of WORKER_DEPENDENCY_CONDITIONS)chain.resolve.byDependency.set(dependencyType, {
461
+ conditionNames: [
462
+ condition,
463
+ '...'
464
+ ]
465
+ });
466
+ chain.plugin('cloudflare-worker-absent-optional-dependencies').use(AbsentOptionalDependencyPlugin);
255
467
  applyCloudflareWorkerMfRuntimeBoundary(chain);
256
468
  if (tanstackRouterSsrServerFile) {
257
469
  chain.resolve.alias.set('@tanstack/router-core/ssr/server$', tanstackRouterSsrServerFile);
@@ -287,7 +499,14 @@ const createCloudflareBundlerChain = (appContext, workerEntryNames)=>{
287
499
  };
288
500
  function getCloudflareBuilderEnvironments({ appContext, environments, normalizedConfig, resolveDeployProvider = resolveStdEnvDeployProvider }) {
289
501
  if (!isCloudflareWorkerDeploy(normalizedConfig, resolveDeployProvider)) return environments;
290
- const workerEnvironment = environments[SERVICE_WORKER_ENVIRONMENT_NAME];
502
+ const workerEnvironment = environments[SERVICE_WORKER_ENVIRONMENT_NAME] ?? (appContext.apiOnly ? {
503
+ output: {
504
+ target: 'web-worker'
505
+ },
506
+ source: {
507
+ entry: {}
508
+ }
509
+ } : void 0);
291
510
  if (!workerEnvironment) return environments;
292
511
  const workerEntries = getWorkerEntries(workerEnvironment, normalizedConfig, appContext);
293
512
  if (!workerEntries) return environments;
@@ -303,6 +522,9 @@ function getCloudflareBuilderEnvironments({ appContext, environments, normalized
303
522
  entry: workerEntries
304
523
  }
305
524
  }, createCloudflareBundlerChain(appContext, Object.keys(workerEntries)));
525
+ if (appContext.apiOnly) return {
526
+ [SERVICE_WORKER_ENVIRONMENT_NAME]: cloudflareWorkerEnvironment
527
+ };
306
528
  return {
307
529
  ...environments,
308
530
  [SERVICE_WORKER_ENVIRONMENT_NAME]: cloudflareWorkerEnvironment
@@ -324,4 +546,4 @@ const createCloudflareBuilderPlugin = (resolveDeployProvider = resolveStdEnvDepl
324
546
  }));
325
547
  }
326
548
  });
327
- export { applyCloudflareBuilderEnvironments, applyCloudflareWorkerMfRuntimeBoundary, applyCloudflareWorkerRspackConfig, createCloudflareBuilderPlugin, getCloudflareBuilderEnvironments, getCloudflareWorkerRspackConfig };
549
+ export { applyCloudflareBuilderEnvironments, applyCloudflareWorkerMfRuntimeBoundary, applyCloudflareWorkerRspackConfig, claimsByFunctionExternal, createAbsentOptionalDependencyFilter, createCloudflareBuilderPlugin, createRequestRedirectMatcher, getCloudflareBuilderEnvironments, getCloudflareWorkerRspackConfig };
@@ -10,27 +10,49 @@ const CLOUDFLARE_RUNTIME_TYPE = 'cloudflare-module-worker';
10
10
  const CLOUDFLARE_OUTPUT_PACKAGE_TYPE = 'module';
11
11
  const CLOUDFLARE_WORKER_PACKAGE_TYPE = 'commonjs';
12
12
  const CLOUDFLARE_WORKER_BUNDLE_FORMAT = 'commonjs';
13
+ const CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT = 'handleRouteDataRequest';
14
+ const getCloudflareWorkerRouteDataEntryName = (entryName)=>`${entryName}-server-loaders`;
13
15
  const CLOUDFLARE_WORKER_NODE_BUILTINS = [
16
+ '_http_agent',
17
+ '_http_client',
18
+ '_http_common',
19
+ '_http_incoming',
20
+ '_http_outgoing',
21
+ '_http_server',
22
+ '_tls_common',
23
+ '_tls_wrap',
14
24
  'assert',
15
25
  'assert/strict',
16
26
  'async_hooks',
17
27
  'buffer',
18
28
  'child_process',
29
+ 'cluster',
30
+ 'console',
31
+ 'constants',
19
32
  'crypto',
20
33
  'dgram',
34
+ 'diagnostics_channel',
21
35
  'dns',
36
+ 'dns/promises',
22
37
  'domain',
23
38
  'events',
24
39
  'fs',
25
40
  'fs/promises',
26
41
  'http',
27
42
  'http2',
43
+ 'https',
28
44
  'inspector',
45
+ 'inspector/promises',
29
46
  'module',
30
47
  'net',
31
48
  'os',
32
49
  'path',
50
+ 'path/posix',
51
+ 'path/win32',
52
+ 'perf_hooks',
33
53
  'process',
54
+ 'punycode',
55
+ 'querystring',
34
56
  'readline',
35
57
  'readline/promises',
36
58
  'repl',
@@ -40,6 +62,8 @@ const CLOUDFLARE_WORKER_NODE_BUILTINS = [
40
62
  'stream/promises',
41
63
  'stream/web',
42
64
  'string_decoder',
65
+ 'sys',
66
+ 'test',
43
67
  'timers',
44
68
  'timers/promises',
45
69
  'tls',
@@ -61,4 +85,4 @@ const CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS = [
61
85
  'nodejs_compat',
62
86
  'global_fetch_strictly_public'
63
87
  ];
64
- 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 };
88
+ 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 };
@@ -24,30 +24,32 @@ const verifyDeliveryUnitIdentity = (issues, manifest, manifestPath, declared)=>{
24
24
  ]){
25
25
  const marker = stamped.surfaces[surface];
26
26
  const expected = declared?.surfaces?.[surface];
27
- if (declared?.surfaces && !expected) {
28
- if (marker) addIssue(issues, {
29
- code: 'delivery-unit-drift',
30
- message: `Cloudflare worker manifest declares an unexpected ${surface} delivery-unit surface for this topology profile.`,
31
- path: manifestPath
32
- });
33
- continue;
34
- }
35
- if (!marker || 'object' != typeof marker) {
36
- addIssue(issues, {
37
- code: 'missing-delivery-unit',
38
- message: `Cloudflare worker manifest is missing the ${surface} delivery-unit surface marker.`,
39
- path: manifestPath
40
- });
41
- continue;
42
- }
43
- for (const field of DELIVERY_UNIT_IDENTITY_FIELDS){
44
- const markerValue = deliveryUnitIdentityFieldValue(marker, field);
45
- const stampedValue = deliveryUnitIdentityFieldValue(stamped, field);
46
- assertEqual(issues, markerValue, stampedValue, {
47
- code: 'delivery-unit-drift',
48
- message: `Cloudflare worker manifest ${surface} surface deliveryUnit.${field} must derive from one delivery-unit record (expected ${stampedValue ?? 'undefined'}, received ${markerValue ?? 'undefined'}).`,
49
- path: manifestPath
50
- });
27
+ if (expected || marker) {
28
+ if (declared?.surfaces && !expected) {
29
+ if (marker) addIssue(issues, {
30
+ code: 'delivery-unit-drift',
31
+ message: `Cloudflare worker manifest declares an unexpected ${surface} delivery-unit surface for this topology profile.`,
32
+ path: manifestPath
33
+ });
34
+ continue;
35
+ }
36
+ if (!marker || 'object' != typeof marker) {
37
+ addIssue(issues, {
38
+ code: 'missing-delivery-unit',
39
+ message: `Cloudflare worker manifest is missing the ${surface} delivery-unit surface marker.`,
40
+ path: manifestPath
41
+ });
42
+ continue;
43
+ }
44
+ for (const field of DELIVERY_UNIT_IDENTITY_FIELDS){
45
+ const markerValue = deliveryUnitIdentityFieldValue(marker, field);
46
+ const stampedValue = deliveryUnitIdentityFieldValue(stamped, field);
47
+ assertEqual(issues, markerValue, stampedValue, {
48
+ code: 'delivery-unit-drift',
49
+ message: `Cloudflare worker manifest ${surface} surface deliveryUnit.${field} must derive from one delivery-unit record (expected ${stampedValue ?? 'undefined'}, received ${markerValue ?? 'undefined'}).`,
50
+ path: manifestPath
51
+ });
52
+ }
51
53
  }
52
54
  }
53
55
  };
@@ -72,6 +72,16 @@ const verifyCloudflareOutput = async (options)=>{
72
72
  }
73
73
  }
74
74
  if (wrangler) {
75
+ if ('string' != typeof wrangler.name || 0 === wrangler.name.trim().length) addIssue(issues, {
76
+ code: 'invalid-wrangler',
77
+ message: 'wrangler.json name must be a non-empty worker name.',
78
+ path: wranglerPath
79
+ });
80
+ if ('string' != typeof wrangler.compatibility_date || !/^\d{4}-\d{2}-\d{2}$/u.test(wrangler.compatibility_date)) addIssue(issues, {
81
+ code: 'invalid-wrangler',
82
+ message: 'wrangler.json compatibility_date must use YYYY-MM-DD.',
83
+ path: wranglerPath
84
+ });
75
85
  assertEqual(issues, wrangler.main, outputPlan.wrangler.main, {
76
86
  code: 'invalid-wrangler',
77
87
  message: `wrangler.json main must be ${outputPlan.wrangler.main}.`,
@@ -42,6 +42,52 @@ const verifyManifestShape = (issues, manifest, manifestPath)=>{
42
42
  message: 'Cloudflare output manifest routeSpec.routes must be array.',
43
43
  path: manifestPath
44
44
  });
45
+ const security = manifest.security;
46
+ if (security && 'object' == typeof security && 'boolean' == typeof security.enabled) {
47
+ const cors = security.cors;
48
+ if (!cors || 'object' != typeof cors || 'boolean' != typeof cors.assets || !Array.isArray(cors.allowedOrigins) || !Array.isArray(cors.allowedMethods) || !Array.isArray(cors.allowedHeaders)) addIssue(issues, {
49
+ code: 'invalid-manifest',
50
+ message: 'Cloudflare output manifest security.cors is incomplete.',
51
+ path: manifestPath
52
+ });
53
+ if (security.enabled) {
54
+ const headers = security.headers;
55
+ const csp = security.contentSecurityPolicy;
56
+ const noindex = security.noindex;
57
+ if (!headers || 'object' != typeof headers || ![
58
+ 'string',
59
+ 'boolean'
60
+ ].includes(typeof headers.referrerPolicy) || ![
61
+ 'string',
62
+ 'boolean'
63
+ ].includes(typeof headers.contentTypeOptions) || ![
64
+ 'string',
65
+ 'boolean'
66
+ ].includes(typeof headers.permissionsPolicy)) addIssue(issues, {
67
+ code: 'invalid-manifest',
68
+ message: 'Cloudflare output manifest security.headers is incomplete.',
69
+ path: manifestPath
70
+ });
71
+ if (!csp || ![
72
+ 'off',
73
+ 'report-only',
74
+ 'enforce'
75
+ ].includes(csp.mode) || !csp.directives || 'object' != typeof csp.directives || Array.isArray(csp.directives) || Object.values(csp.directives).some((value)=>!Array.isArray(value) || value.some((entry)=>'string' != typeof entry))) addIssue(issues, {
76
+ code: 'invalid-manifest',
77
+ message: 'Cloudflare output manifest security.contentSecurityPolicy is invalid.',
78
+ path: manifestPath
79
+ });
80
+ if (!noindex || 'boolean' != typeof noindex.workersDev || 'boolean' != typeof noindex.localhost || !Array.isArray(noindex.previewHostnames)) addIssue(issues, {
81
+ code: 'invalid-manifest',
82
+ message: 'Cloudflare output manifest security.noindex is incomplete.',
83
+ path: manifestPath
84
+ });
85
+ }
86
+ } else addIssue(issues, {
87
+ code: 'invalid-manifest',
88
+ message: 'Cloudflare output manifest security.enabled must be a boolean.',
89
+ path: manifestPath
90
+ });
45
91
  if (manifest.bff?.runtimeFramework === 'effect') assertEqual(issues, manifest.bff.dispatcherExport, BFF_EFFECT_WORKER_DISPATCHER_EXPORT, {
46
92
  code: 'invalid-manifest',
47
93
  message: `Cloudflare Effect BFF manifest dispatcherExport must be ${BFF_EFFECT_WORKER_DISPATCHER_EXPORT}.`,
@@ -84,7 +84,13 @@ const verifyCloudflareOutputMutationPolicy = async (options)=>{
84
84
  const files = (await Promise.all(options.scanRoots.map((scanRoot)=>walkFiles(node_path.resolve(scanRoot))))).flat();
85
85
  for (const file of files){
86
86
  if (excludedFiles.has(file)) continue;
87
- const source = await promises.readFile(file, 'utf-8');
87
+ let source;
88
+ try {
89
+ source = await promises.readFile(file, 'utf-8');
90
+ } catch (error) {
91
+ if ('ENOENT' === error.code) continue;
92
+ throw error;
93
+ }
88
94
  for (const forbidden of FORBIDDEN_MUTATION_PATTERNS)if (forbidden.pattern.test(source)) addIssue(issues, {
89
95
  code: forbidden.code,
90
96
  message: forbidden.message,
@@ -4,9 +4,9 @@ import { pathToFileURL } from "node:url";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { parse } from "@babel/parser";
6
6
  import traverse from "@babel/traverse";
7
- import { CLOUDFLARE_WORKER_BUNDLE_DIRECTORY, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PLATFORM_MODULES } from "../cloudflare-output-contract.mjs";
7
+ import { CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT, CLOUDFLARE_WORKER_BUNDLE_DIRECTORY, CLOUDFLARE_WORKER_NODE_BUILTINS, CLOUDFLARE_WORKER_PLATFORM_MODULES } from "../cloudflare-output-contract.mjs";
8
8
  import { addIssue } from "./issues.mjs";
9
- 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) : [];
9
+ 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) : [];
10
10
  const getEffectBffWorker = (manifest)=>manifest?.bff?.runtimeFramework === 'effect' && 'string' == typeof manifest.bff.worker ? manifest.bff.worker : void 0;
11
11
  const getWorkerBundleReferences = (manifest)=>{
12
12
  const effectBffWorker = getEffectBffWorker(manifest);
@@ -18,9 +18,14 @@ const getWorkerBundleReferences = (manifest)=>{
18
18
  reference: effectBffWorker
19
19
  }
20
20
  ] : [],
21
- ...getReferencedRouteWorkers(manifest).map((reference)=>({
21
+ ...getReferencedRouteWorkers(manifest, 'worker').map((reference)=>({
22
22
  kind: 'route',
23
23
  reference
24
+ })),
25
+ ...getReferencedRouteWorkers(manifest, 'routeDataWorker').map((reference)=>({
26
+ dispatcherExport: CLOUDFLARE_ROUTE_DATA_HANDLER_EXPORT,
27
+ kind: 'route-data',
28
+ reference
24
29
  }))
25
30
  ];
26
31
  };
@@ -41,7 +46,7 @@ const resolveWorkerBundleReference = (issues, outputDirectory, reference, manife
41
46
  path: workerPath
42
47
  };
43
48
  };
44
- 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.';
49
+ 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.';
45
50
  const isAstNode = (value)=>null !== value && 'object' == typeof value && 'type' in value && 'string' == typeof value.type;
46
51
  const getNodeName = (node)=>{
47
52
  if (!isAstNode(node)) return;
@@ -285,9 +290,9 @@ const verifyWorkerModuleClosure = async (issues, entryPath, entrySource, contrac
285
290
  };
286
291
  const verifyWorkerBundleReferences = async (issues, worker, source, contract)=>{
287
292
  const entryAnalysis = await verifyWorkerModuleClosure(issues, worker.path, source, contract);
288
- if ('effect-bff' === worker.kind && 'string' == typeof worker.dispatcherExport && !entryAnalysis?.exports.has(worker.dispatcherExport)) addIssue(issues, {
293
+ if ('string' == typeof worker.dispatcherExport && !entryAnalysis?.exports.has(worker.dispatcherExport)) addIssue(issues, {
289
294
  code: 'invalid-worker-bundle',
290
- message: 'Cloudflare Effect BFF worker bundle must expose its manifest dispatcherExport.',
295
+ message: 'route-data' === worker.kind ? `Cloudflare route data worker bundle must expose ${worker.dispatcherExport}.` : 'Cloudflare Effect BFF worker bundle must expose its manifest dispatcherExport.',
291
296
  path: worker.path
292
297
  });
293
298
  };
@@ -4,6 +4,10 @@ const dependencyKeys = [
4
4
  'dependencies',
5
5
  'optionalDependencies'
6
6
  ];
7
+ const rootDependencyKeys = [
8
+ ...dependencyKeys,
9
+ 'devDependencies'
10
+ ];
7
11
  const exactVersionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
8
12
  async function readPackageJson(filePath) {
9
13
  return JSON.parse(await promises.readFile(filePath, 'utf8'));
@@ -19,14 +23,34 @@ function parseNpmAlias(aliasName, specifier) {
19
23
  targetVersion: target.slice(versionSeparator + 1)
20
24
  };
21
25
  }
22
- function aliasesFromPackageJson(packageJson) {
26
+ function aliasesFromPackageJson(packageJson, keys = dependencyKeys) {
23
27
  const aliases = [];
24
- for (const dependencyKey of dependencyKeys)for (const [aliasName, specifier] of Object.entries(packageJson[dependencyKey] ?? {})){
28
+ for (const dependencyKey of keys)for (const [aliasName, specifier] of Object.entries(packageJson[dependencyKey] ?? {})){
25
29
  const alias = parseNpmAlias(aliasName, specifier);
26
30
  if (alias) aliases.push(alias);
27
31
  }
28
32
  return aliases;
29
33
  }
34
+ async function catalogAliasesFromInstalledPackages(appDirectory, packageJson) {
35
+ const aliases = [];
36
+ for (const dependencyKey of rootDependencyKeys)for (const [aliasName, specifier] of Object.entries(packageJson[dependencyKey] ?? {})){
37
+ if (!specifier.startsWith('catalog:')) continue;
38
+ let installedPackage;
39
+ try {
40
+ installedPackage = await readPackageJson(node_path.join(appDirectory, 'node_modules', aliasName, 'package.json'));
41
+ } catch (error) {
42
+ if ('ENOENT' === error.code) continue;
43
+ throw error;
44
+ }
45
+ if (!installedPackage.name || !installedPackage.version) throw new Error(`Installed catalog package ${aliasName} has no identity`);
46
+ if (installedPackage.name !== aliasName) aliases.push({
47
+ aliasName,
48
+ targetName: installedPackage.name,
49
+ targetVersion: installedPackage.version
50
+ });
51
+ }
52
+ return aliases;
53
+ }
30
54
  async function collectPackageRecords(nodeModulesDirectory) {
31
55
  const records = [];
32
56
  const visitedNodeModules = new Set();
@@ -148,7 +172,8 @@ async function preserveNpmAliases({ appDirectory, outputDirectory, implicitAlias
148
172
  const records = await collectPackageRecords(node_path.join(outputDirectory, 'node_modules'));
149
173
  const appPackageJson = await readPackageJson(node_path.join(appDirectory, 'package.json'));
150
174
  const rootAliases = [
151
- ...aliasesFromPackageJson(appPackageJson),
175
+ ...aliasesFromPackageJson(appPackageJson, rootDependencyKeys),
176
+ ...await catalogAliasesFromInstalledPackages(appDirectory, appPackageJson),
152
177
  ...implicitAliases
153
178
  ].filter((alias)=>alias.aliasName !== alias.targetName);
154
179
  const emittedRootAliases = [];