@bleedingdev/modern-js-app-tools 3.4.0-ultramodern.9 → 3.5.0-ultramodern.0

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.
@@ -137,8 +137,11 @@ function getCloudflareWorkerNodeExternals() {
137
137
  function getEffectBffEntry(normalizedConfig, appContext) {
138
138
  if (!normalizedConfig.bff || 'hono' === normalizedConfig.bff.runtimeFramework) return;
139
139
  const configuredEntry = normalizedConfig.bff.effect?.entry;
140
- const entryWithoutOrWithExtension = configuredEntry ? external_node_path_default().isAbsolute(configuredEntry) ? configuredEntry : external_node_path_default().resolve(appContext.appDirectory, configuredEntry) : external_node_path_default().resolve(appContext.apiDirectory, 'effect', 'index');
141
- return resolveJsOrTsEntry(entryWithoutOrWithExtension);
140
+ if (configuredEntry) {
141
+ const entryWithoutOrWithExtension = external_node_path_default().isAbsolute(configuredEntry) ? configuredEntry : external_node_path_default().resolve(appContext.appDirectory, configuredEntry);
142
+ return resolveJsOrTsEntry(entryWithoutOrWithExtension);
143
+ }
144
+ return resolveJsOrTsEntry(external_node_path_default().resolve(appContext.apiDirectory, 'index'));
142
145
  }
143
146
  function isCloudflareWorkerDeploy(normalizedConfig) {
144
147
  return normalizedConfig.deploy?.target === 'cloudflare' || 'cloudflare' === process.env.MODERNJS_DEPLOY;
@@ -227,7 +230,7 @@ function getBuilderEnvironments(normalizedConfig, appContext, tempBuilderConfig)
227
230
  const useWorkerTarget = (0, utils_namespaceObject.isServiceWorker)(normalizedConfig);
228
231
  if (useWorkerTarget) {
229
232
  const useCloudflareModuleWorker = isCloudflareWorkerDeploy(normalizedConfig);
230
- const effectBffEntry = useCloudflareModuleWorker ? getEffectBffEntry(normalizedConfig, appContext) : void 0;
233
+ const effectApiEntry = useCloudflareModuleWorker ? getEffectBffEntry(normalizedConfig, appContext) : void 0;
231
234
  const tanstackRouterSsrServerFile = useCloudflareModuleWorker ? resolvePackageFile('@tanstack/router-core', 'dist/esm/ssr/ssr-server.js', [
232
235
  appContext.appDirectory,
233
236
  process.cwd()
@@ -269,10 +272,10 @@ function getBuilderEnvironments(normalizedConfig, appContext, tempBuilderConfig)
269
272
  const fsPromisesWorkerFile = useCloudflareModuleWorker ? getCloudflareWorkerCompatFile('cloudflare-worker-fs-promises.mjs') : void 0;
270
273
  const pathWorkerFile = useCloudflareModuleWorker ? getCloudflareWorkerCompatFile('cloudflare-worker-path.mjs') : void 0;
271
274
  const baseWorkerEntries = useCloudflareModuleWorker ? cloudflareWorkerServerEntries : serverEntries;
272
- const workerEntries = effectBffEntry ? {
275
+ const workerEntries = effectApiEntry ? {
273
276
  ...baseWorkerEntries,
274
277
  [BFF_EFFECT_WORKER_ENTRY_NAME]: [
275
- `${effectBffEntry}?${BFF_EFFECT_WORKER_RUNTIME_QUERY}`
278
+ `${effectApiEntry}?${BFF_EFFECT_WORKER_RUNTIME_QUERY}`
276
279
  ]
277
280
  } : baseWorkerEntries;
278
281
  environments[builder_namespaceObject.SERVICE_WORKER_ENVIRONMENT_NAME] = {
@@ -45,6 +45,8 @@ const utils_namespaceObject = require("@modern-js/utils");
45
45
  const index_js_namespaceObject = require("../utils/index.js");
46
46
  const WORKER_ENTRY = 'server/index.mjs';
47
47
  const WORKER_MANIFEST = 'server/modern-worker-manifest.json';
48
+ const WRANGLER_CONFIG_FILE = 'wrangler.json';
49
+ const OUTPUT_PACKAGE_FILE = 'package.json';
48
50
  const ASSETS_BINDING = 'ASSETS';
49
51
  const ROUTE_SPEC_FILE = 'route.json';
50
52
  const ROUTE_SPEC_OUTPUT = `server/${ROUTE_SPEC_FILE}`;
@@ -53,10 +55,28 @@ const ROUTE_MANIFEST_FILE = 'routes-manifest.json';
53
55
  const PUBLIC_ASSETS_DIRECTORY = 'public';
54
56
  const WORKER_BUNDLE_DIRECTORY = 'worker';
55
57
  const SERVER_BUNDLE_DIRECTORY = 'bundles';
58
+ const SERVER_OUTPUT_DIRECTORY = 'server';
59
+ const DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES = [
60
+ 'api',
61
+ 'shared'
62
+ ];
56
63
  const BFF_EFFECT_WORKER_ENTRY = `${WORKER_BUNDLE_DIRECTORY}/__modern_bff_effect.js`;
57
- const EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE = 'Ensure the Effect BFF entry exists at api/effect/index.ts or bff.effect.entry, and import Cloudflare edge handlers from @modern-js/plugin-bff/effect-edge instead of lambda/Hono server helpers.';
64
+ const EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE = 'Ensure the Effect API entry exists at api/index.ts or bff.effect.entry, and import Cloudflare edge handlers from @modern-js/plugin-bff/effect-edge instead of lambda/Hono server helpers.';
58
65
  const DEFAULT_COMPATIBILITY_DATE = '2026-06-02';
59
66
  const COMPATIBILITY_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
67
+ const REQUIRED_COMPATIBILITY_FLAGS = [
68
+ 'nodejs_compat',
69
+ 'global_fetch_strictly_public'
70
+ ];
71
+ const RESERVED_ARTIFACT_DESTINATION_FILES = new Set([
72
+ WRANGLER_CONFIG_FILE,
73
+ OUTPUT_PACKAGE_FILE
74
+ ]);
75
+ const RESERVED_ARTIFACT_DESTINATION_DIRECTORIES = new Set([
76
+ PUBLIC_ASSETS_DIRECTORY,
77
+ SERVER_OUTPUT_DIRECTORY,
78
+ WORKER_BUNDLE_DIRECTORY
79
+ ]);
60
80
  const DEFAULT_SECURITY_HEADERS = {
61
81
  referrerPolicy: 'strict-origin-when-cross-origin',
62
82
  contentTypeOptions: 'nosniff',
@@ -145,6 +165,13 @@ const getConfiguredWorkerName = (appDirectory, modernConfig)=>{
145
165
  const configuredName = modernConfig.deploy?.worker?.name?.trim();
146
166
  return configuredName || getWorkerName(appDirectory);
147
167
  };
168
+ const normalizeRelativePath = (value, label, scope = 'app output')=>{
169
+ if ('string' != typeof value) throw new Error(`${label} must be a relative path inside the ${scope}.`);
170
+ const normalized = value.trim().replace(/\\/g, '/').replace(/^\.\/+/u, '').replace(/\/+$/u, '');
171
+ const segments = normalized.split('/');
172
+ if (!normalized || external_node_path_default().isAbsolute(normalized) || '.' === normalized || segments.includes('..')) throw new Error(`${label} must be a relative path inside the ${scope}.`);
173
+ return normalized;
174
+ };
148
175
  const normalizeDirectiveValues = (value)=>{
149
176
  const values = Array.isArray(value) ? value : [
150
177
  value
@@ -237,6 +264,66 @@ const createCloudflareWorkerSecurityPolicy = (modernConfig)=>{
237
264
  reason: security?.reason
238
265
  };
239
266
  };
267
+ const isJsonRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
268
+ const getConfiguredWrangler = (modernConfig)=>{
269
+ const wrangler = modernConfig.deploy?.worker?.wrangler;
270
+ if (void 0 === wrangler) return {};
271
+ if (!isJsonRecord(wrangler)) throw new Error('deploy.worker.wrangler must be a JSON object.');
272
+ return wrangler;
273
+ };
274
+ const createWranglerCompatibilityFlags = (configuredFlags)=>{
275
+ if (void 0 === configuredFlags) return [
276
+ ...REQUIRED_COMPATIBILITY_FLAGS
277
+ ];
278
+ if (!Array.isArray(configuredFlags) || configuredFlags.some((flag)=>'string' != typeof flag)) throw new Error('deploy.worker.wrangler.compatibility_flags must be an array of strings.');
279
+ return [
280
+ ...new Set([
281
+ ...configuredFlags,
282
+ ...REQUIRED_COMPATIBILITY_FLAGS
283
+ ])
284
+ ];
285
+ };
286
+ const createWranglerAssetsConfig = (configuredAssets)=>{
287
+ if (void 0 !== configuredAssets && !isJsonRecord(configuredAssets)) throw new Error('deploy.worker.wrangler.assets must be an object.');
288
+ return {
289
+ ...isJsonRecord(configuredAssets) ? configuredAssets : {},
290
+ directory: `./${PUBLIC_ASSETS_DIRECTORY}`,
291
+ binding: ASSETS_BINDING,
292
+ run_worker_first: true
293
+ };
294
+ };
295
+ const createWranglerConfig = (appDirectory, modernConfig)=>{
296
+ const wrangler = getConfiguredWrangler(modernConfig);
297
+ return {
298
+ $schema: 'node_modules/wrangler/config-schema.json',
299
+ name: getConfiguredWorkerName(appDirectory, modernConfig),
300
+ compatibility_date: getCompatibilityDate(modernConfig),
301
+ ...wrangler,
302
+ main: WORKER_ENTRY,
303
+ compatibility_flags: createWranglerCompatibilityFlags(wrangler.compatibility_flags),
304
+ assets: createWranglerAssetsConfig(wrangler.assets)
305
+ };
306
+ };
307
+ const normalizeCloudflareArtifact = (artifact, index)=>{
308
+ const from = normalizeRelativePath(artifact.from, `deploy.worker.artifacts[${index}].from`, 'app root');
309
+ const to = normalizeRelativePath(artifact.to, `deploy.worker.artifacts[${index}].to`, 'Cloudflare output');
310
+ const [topLevelDestination] = to.split('/');
311
+ const reservedDestination = RESERVED_ARTIFACT_DESTINATION_FILES.has(to) ? to : topLevelDestination;
312
+ if (RESERVED_ARTIFACT_DESTINATION_FILES.has(to) || RESERVED_ARTIFACT_DESTINATION_DIRECTORIES.has(topLevelDestination)) throw new Error(`deploy.worker.artifacts[${index}].to must not target generated Cloudflare output path ${JSON.stringify(reservedDestination)}.`);
313
+ return {
314
+ from,
315
+ to,
316
+ index
317
+ };
318
+ };
319
+ const getCloudflareArtifacts = (modernConfig)=>(modernConfig.deploy?.worker?.artifacts ?? []).map(normalizeCloudflareArtifact);
320
+ const copyCloudflareArtifacts = async (appDirectory, outputDirectory, artifacts)=>{
321
+ for (const artifact of artifacts){
322
+ const sourcePath = external_node_path_default().join(appDirectory, artifact.from);
323
+ if (!await utils_namespaceObject.fs.pathExists(sourcePath)) throw new Error(`deploy.worker.artifacts[${artifact.index}].from does not exist: ${artifact.from}`);
324
+ await utils_namespaceObject.fs.copy(sourcePath, external_node_path_default().join(outputDirectory, artifact.to));
325
+ }
326
+ };
240
327
  const readRouteSpec = async (outputDirectory)=>{
241
328
  const routeSpecPath = external_node_path_default().join(outputDirectory, ROUTE_SPEC_OUTPUT);
242
329
  if (!await utils_namespaceObject.fs.pathExists(routeSpecPath)) return {
@@ -248,7 +335,7 @@ const readRouteSpec = async (outputDirectory)=>{
248
335
  routes: Array.isArray(routeSpec.routes) ? routeSpec.routes : []
249
336
  };
250
337
  };
251
- const createMissingEffectBffWorkerError = (outputDirectory, worker)=>new Error(`Cloudflare Effect BFF is configured, but the BFF worker bundle is missing: ${external_node_path_default().join(outputDirectory, worker)}. ${EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE}`);
338
+ const createMissingEffectBffWorkerError = (outputDirectory, worker)=>new Error(`Cloudflare Effect API runtime is configured, but the BFF worker bundle is missing: ${external_node_path_default().join(outputDirectory, worker)}. ${EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE}`);
252
339
  const createWorkerManifest = async (outputDirectory, modernConfig)=>{
253
340
  const routeSpec = await readRouteSpec(outputDirectory);
254
341
  const routes = await Promise.all(routeSpec.routes.map(async (route)=>{
@@ -264,9 +351,9 @@ const createWorkerManifest = async (outputDirectory, modernConfig)=>{
264
351
  }));
265
352
  const bffPrefix = modernConfig.bff?.prefix;
266
353
  const primaryBffPrefix = Array.isArray(bffPrefix) ? bffPrefix[0] : bffPrefix;
267
- const isEffectBff = Boolean(modernConfig.bff) && modernConfig.bff?.runtimeFramework !== 'hono';
268
- const effectBffWorkerExists = await utils_namespaceObject.fs.pathExists(external_node_path_default().join(outputDirectory, BFF_EFFECT_WORKER_ENTRY));
269
- if (isEffectBff && primaryBffPrefix && !effectBffWorkerExists) throw createMissingEffectBffWorkerError(outputDirectory, BFF_EFFECT_WORKER_ENTRY);
354
+ const isEffectApi = Boolean(modernConfig.bff) && modernConfig.bff?.runtimeFramework !== 'hono';
355
+ const effectApiWorkerExists = await utils_namespaceObject.fs.pathExists(external_node_path_default().join(outputDirectory, BFF_EFFECT_WORKER_ENTRY));
356
+ if (isEffectApi && primaryBffPrefix && !effectApiWorkerExists) throw createMissingEffectBffWorkerError(outputDirectory, BFF_EFFECT_WORKER_ENTRY);
270
357
  return {
271
358
  version: 1,
272
359
  runtime: {
@@ -295,7 +382,7 @@ const createWorkerManifest = async (outputDirectory, modernConfig)=>{
295
382
  routeManifest: ROUTE_MANIFEST_FILE
296
383
  },
297
384
  security: createCloudflareWorkerSecurityPolicy(modernConfig),
298
- bff: isEffectBff && primaryBffPrefix && effectBffWorkerExists ? {
385
+ bff: isEffectApi && primaryBffPrefix && effectApiWorkerExists ? {
299
386
  runtimeFramework: 'effect',
300
387
  prefix: primaryBffPrefix,
301
388
  worker: BFF_EFFECT_WORKER_ENTRY
@@ -318,12 +405,22 @@ const createWorkerModuleLoaders = (manifest)=>{
318
405
  ].map(([worker, loader])=>`${JSON.stringify(worker)}: ${loader}`);
319
406
  return `{\n${loaders.join(',\n')}\n}`;
320
407
  };
321
- const shouldCopyToPublicAssets = (src, distDirectory)=>{
408
+ const getPublicAssetExcludes = (appDirectory, modernConfig)=>[
409
+ ...DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES.filter((directory)=>{
410
+ try {
411
+ return utils_namespaceObject.fs.statSync(external_node_path_default().join(appDirectory, directory)).isDirectory();
412
+ } catch {
413
+ return false;
414
+ }
415
+ }),
416
+ ...modernConfig.deploy?.worker?.publicAssetExcludes ?? []
417
+ ].map((entry)=>normalizeRelativePath(entry, 'deploy.worker.publicAssetExcludes'));
418
+ const shouldCopyToPublicAssets = (src, distDirectory, publicAssetExcludes)=>{
322
419
  const relativePath = external_node_path_default().relative(distDirectory, src);
323
420
  if (!relativePath) return true;
324
421
  const normalizedRelativePath = relativePath.replace(/\\/g, '/');
325
422
  const [topLevelDirectory] = normalizedRelativePath.split('/');
326
- return normalizedRelativePath !== ROUTE_SPEC_FILE && topLevelDirectory !== WORKER_BUNDLE_DIRECTORY && topLevelDirectory !== SERVER_BUNDLE_DIRECTORY;
423
+ return normalizedRelativePath !== ROUTE_SPEC_FILE && topLevelDirectory !== WORKER_BUNDLE_DIRECTORY && topLevelDirectory !== SERVER_BUNDLE_DIRECTORY && !publicAssetExcludes.some((exclude)=>normalizedRelativePath === exclude || normalizedRelativePath.startsWith(`${exclude}/`));
327
424
  };
328
425
  const shouldCopyToWorkerBundle = (src, workerBundleDirectory)=>{
329
426
  const relativePath = external_node_path_default().relative(workerBundleDirectory, src);
@@ -345,15 +442,16 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
345
442
  const workerEntryPath = external_node_path_default().join(outputDirectory, WORKER_ENTRY);
346
443
  const workerManifestPath = external_node_path_default().join(outputDirectory, WORKER_MANIFEST);
347
444
  const routeSpecOutputPath = external_node_path_default().join(outputDirectory, ROUTE_SPEC_OUTPUT);
348
- const wranglerConfigPath = external_node_path_default().join(outputDirectory, 'wrangler.json');
349
- const workerName = getConfiguredWorkerName(appDirectory, modernConfig);
445
+ const wranglerConfigPath = external_node_path_default().join(outputDirectory, WRANGLER_CONFIG_FILE);
446
+ const cloudflareArtifacts = getCloudflareArtifacts(modernConfig);
447
+ const publicAssetExcludes = getPublicAssetExcludes(appDirectory, modernConfig);
350
448
  return {
351
449
  async prepare () {
352
450
  await utils_namespaceObject.fs.remove(outputDirectory);
353
451
  },
354
452
  async writeOutput () {
355
453
  await utils_namespaceObject.fs.copy(distDirectory, publicDirectory, {
356
- filter: (src)=>shouldCopyToPublicAssets(src, distDirectory)
454
+ filter: (src)=>shouldCopyToPublicAssets(src, distDirectory, publicAssetExcludes)
357
455
  });
358
456
  await utils_namespaceObject.fs.ensureDir(external_node_path_default().dirname(workerEntryPath));
359
457
  await utils_namespaceObject.fs.ensureDir(external_node_path_default().dirname(workerManifestPath));
@@ -369,27 +467,14 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
369
467
  type: 'commonjs'
370
468
  });
371
469
  }
372
- await utils_namespaceObject.fs.writeJSON(wranglerConfigPath, {
373
- $schema: 'node_modules/wrangler/config-schema.json',
374
- name: workerName,
375
- main: WORKER_ENTRY,
376
- compatibility_date: getCompatibilityDate(modernConfig),
377
- compatibility_flags: [
378
- 'nodejs_compat',
379
- 'global_fetch_strictly_public'
380
- ],
381
- assets: {
382
- directory: `./${PUBLIC_ASSETS_DIRECTORY}`,
383
- binding: ASSETS_BINDING,
384
- run_worker_first: true
385
- }
386
- }, {
470
+ await copyCloudflareArtifacts(appDirectory, outputDirectory, cloudflareArtifacts);
471
+ await utils_namespaceObject.fs.writeJSON(wranglerConfigPath, createWranglerConfig(appDirectory, modernConfig), {
387
472
  spaces: 2
388
473
  });
389
474
  await utils_namespaceObject.fs.writeJSON(workerManifestPath, await createWorkerManifest(outputDirectory, modernConfig), {
390
475
  spaces: 2
391
476
  });
392
- await utils_namespaceObject.fs.writeJSON(external_node_path_default().join(outputDirectory, 'package.json'), {
477
+ await utils_namespaceObject.fs.writeJSON(external_node_path_default().join(outputDirectory, OUTPUT_PACKAGE_FILE), {
393
478
  type: 'module'
394
479
  });
395
480
  },
@@ -312,6 +312,19 @@ function getRemoteManifestUrl(remote, request) {
312
312
  if ('string' != typeof entry || 0 === entry.length) return;
313
313
  return new URL(entry, request.url).toString();
314
314
  }
315
+ function ensureTrailingSlash(value) {
316
+ return value.endsWith('/') ? value : `${value}/`;
317
+ }
318
+ function getRemoteAssetBase(remoteManifest, manifestUrl) {
319
+ const publicPath = 'string' == typeof remoteManifest?.metaData?.publicPath ? remoteManifest.metaData.publicPath : void 0;
320
+ const fallbackBase = new URL('.', manifestUrl).toString();
321
+ if (!publicPath || 'auto' === publicPath) return fallbackBase;
322
+ try {
323
+ return new URL(ensureTrailingSlash(publicPath), manifestUrl).toString();
324
+ } catch {
325
+ return fallbackBase;
326
+ }
327
+ }
315
328
  async function fetchRemoteJson(jsonUrl) {
316
329
  if (!remoteJsonPromises.has(jsonUrl)) remoteJsonPromises.set(jsonUrl, fetch(jsonUrl).then((response)=>{
317
330
  if (!response.ok) {
@@ -374,7 +387,7 @@ async function collectRenderedRemoteCssHrefs(html, request, env) {
374
387
  if (!manifestUrl) return;
375
388
  const remoteManifest = await fetchRemoteJson(manifestUrl);
376
389
  const remoteExpose = findRemoteExpose(remoteManifest, expose);
377
- const publicPath = 'string' == typeof remoteManifest?.metaData?.publicPath ? remoteManifest.metaData.publicPath : manifestUrl;
390
+ const publicPath = getRemoteAssetBase(remoteManifest, manifestUrl);
378
391
  const remoteRouteManifest = await fetchRemoteJson(new URL('routes-manifest.json', publicPath).toString());
379
392
  for (const asset of collectCssAssetEntries(remoteExpose?.assets))hrefs.add(new URL(asset, publicPath).toString());
380
393
  for (const asset of collectRouteManifestCssAssets(remoteRouteManifest))hrefs.add(new URL(asset, publicPath).toString());
@@ -94,8 +94,11 @@ function getCloudflareWorkerNodeExternals() {
94
94
  function getEffectBffEntry(normalizedConfig, appContext) {
95
95
  if (!normalizedConfig.bff || 'hono' === normalizedConfig.bff.runtimeFramework) return;
96
96
  const configuredEntry = normalizedConfig.bff.effect?.entry;
97
- const entryWithoutOrWithExtension = configuredEntry ? node_path.isAbsolute(configuredEntry) ? configuredEntry : node_path.resolve(appContext.appDirectory, configuredEntry) : node_path.resolve(appContext.apiDirectory, 'effect', 'index');
98
- return resolveJsOrTsEntry(entryWithoutOrWithExtension);
97
+ if (configuredEntry) {
98
+ const entryWithoutOrWithExtension = node_path.isAbsolute(configuredEntry) ? configuredEntry : node_path.resolve(appContext.appDirectory, configuredEntry);
99
+ return resolveJsOrTsEntry(entryWithoutOrWithExtension);
100
+ }
101
+ return resolveJsOrTsEntry(node_path.resolve(appContext.apiDirectory, 'index'));
99
102
  }
100
103
  function isCloudflareWorkerDeploy(normalizedConfig) {
101
104
  return normalizedConfig.deploy?.target === 'cloudflare' || 'cloudflare' === process.env.MODERNJS_DEPLOY;
@@ -184,7 +187,7 @@ function getBuilderEnvironments(normalizedConfig, appContext, tempBuilderConfig)
184
187
  const useWorkerTarget = isServiceWorker(normalizedConfig);
185
188
  if (useWorkerTarget) {
186
189
  const useCloudflareModuleWorker = isCloudflareWorkerDeploy(normalizedConfig);
187
- const effectBffEntry = useCloudflareModuleWorker ? getEffectBffEntry(normalizedConfig, appContext) : void 0;
190
+ const effectApiEntry = useCloudflareModuleWorker ? getEffectBffEntry(normalizedConfig, appContext) : void 0;
188
191
  const tanstackRouterSsrServerFile = useCloudflareModuleWorker ? resolvePackageFile('@tanstack/router-core', 'dist/esm/ssr/ssr-server.js', [
189
192
  appContext.appDirectory,
190
193
  process.cwd()
@@ -226,10 +229,10 @@ function getBuilderEnvironments(normalizedConfig, appContext, tempBuilderConfig)
226
229
  const fsPromisesWorkerFile = useCloudflareModuleWorker ? getCloudflareWorkerCompatFile('cloudflare-worker-fs-promises.mjs') : void 0;
227
230
  const pathWorkerFile = useCloudflareModuleWorker ? getCloudflareWorkerCompatFile('cloudflare-worker-path.mjs') : void 0;
228
231
  const baseWorkerEntries = useCloudflareModuleWorker ? cloudflareWorkerServerEntries : serverEntries;
229
- const workerEntries = effectBffEntry ? {
232
+ const workerEntries = effectApiEntry ? {
230
233
  ...baseWorkerEntries,
231
234
  [BFF_EFFECT_WORKER_ENTRY_NAME]: [
232
- `${effectBffEntry}?${BFF_EFFECT_WORKER_RUNTIME_QUERY}`
235
+ `${effectApiEntry}?${BFF_EFFECT_WORKER_RUNTIME_QUERY}`
233
236
  ]
234
237
  } : baseWorkerEntries;
235
238
  environments[SERVICE_WORKER_ENVIRONMENT_NAME] = {
@@ -3,6 +3,8 @@ import { fs } from "@modern-js/utils";
3
3
  import { readTemplate } from "../utils/index.mjs";
4
4
  const WORKER_ENTRY = 'server/index.mjs';
5
5
  const WORKER_MANIFEST = 'server/modern-worker-manifest.json';
6
+ const WRANGLER_CONFIG_FILE = 'wrangler.json';
7
+ const OUTPUT_PACKAGE_FILE = 'package.json';
6
8
  const ASSETS_BINDING = 'ASSETS';
7
9
  const ROUTE_SPEC_FILE = 'route.json';
8
10
  const ROUTE_SPEC_OUTPUT = `server/${ROUTE_SPEC_FILE}`;
@@ -11,10 +13,28 @@ const ROUTE_MANIFEST_FILE = 'routes-manifest.json';
11
13
  const PUBLIC_ASSETS_DIRECTORY = 'public';
12
14
  const WORKER_BUNDLE_DIRECTORY = 'worker';
13
15
  const SERVER_BUNDLE_DIRECTORY = 'bundles';
16
+ const SERVER_OUTPUT_DIRECTORY = 'server';
17
+ const DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES = [
18
+ 'api',
19
+ 'shared'
20
+ ];
14
21
  const BFF_EFFECT_WORKER_ENTRY = `${WORKER_BUNDLE_DIRECTORY}/__modern_bff_effect.js`;
15
- const EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE = 'Ensure the Effect BFF entry exists at api/effect/index.ts or bff.effect.entry, and import Cloudflare edge handlers from @modern-js/plugin-bff/effect-edge instead of lambda/Hono server helpers.';
22
+ const EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE = 'Ensure the Effect API entry exists at api/index.ts or bff.effect.entry, and import Cloudflare edge handlers from @modern-js/plugin-bff/effect-edge instead of lambda/Hono server helpers.';
16
23
  const DEFAULT_COMPATIBILITY_DATE = '2026-06-02';
17
24
  const COMPATIBILITY_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
25
+ const REQUIRED_COMPATIBILITY_FLAGS = [
26
+ 'nodejs_compat',
27
+ 'global_fetch_strictly_public'
28
+ ];
29
+ const RESERVED_ARTIFACT_DESTINATION_FILES = new Set([
30
+ WRANGLER_CONFIG_FILE,
31
+ OUTPUT_PACKAGE_FILE
32
+ ]);
33
+ const RESERVED_ARTIFACT_DESTINATION_DIRECTORIES = new Set([
34
+ PUBLIC_ASSETS_DIRECTORY,
35
+ SERVER_OUTPUT_DIRECTORY,
36
+ WORKER_BUNDLE_DIRECTORY
37
+ ]);
18
38
  const DEFAULT_SECURITY_HEADERS = {
19
39
  referrerPolicy: 'strict-origin-when-cross-origin',
20
40
  contentTypeOptions: 'nosniff',
@@ -103,6 +123,13 @@ const getConfiguredWorkerName = (appDirectory, modernConfig)=>{
103
123
  const configuredName = modernConfig.deploy?.worker?.name?.trim();
104
124
  return configuredName || getWorkerName(appDirectory);
105
125
  };
126
+ const normalizeRelativePath = (value, label, scope = 'app output')=>{
127
+ if ('string' != typeof value) throw new Error(`${label} must be a relative path inside the ${scope}.`);
128
+ const normalized = value.trim().replace(/\\/g, '/').replace(/^\.\/+/u, '').replace(/\/+$/u, '');
129
+ const segments = normalized.split('/');
130
+ if (!normalized || node_path.isAbsolute(normalized) || '.' === normalized || segments.includes('..')) throw new Error(`${label} must be a relative path inside the ${scope}.`);
131
+ return normalized;
132
+ };
106
133
  const normalizeDirectiveValues = (value)=>{
107
134
  const values = Array.isArray(value) ? value : [
108
135
  value
@@ -195,6 +222,66 @@ const createCloudflareWorkerSecurityPolicy = (modernConfig)=>{
195
222
  reason: security?.reason
196
223
  };
197
224
  };
225
+ const isJsonRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
226
+ const getConfiguredWrangler = (modernConfig)=>{
227
+ const wrangler = modernConfig.deploy?.worker?.wrangler;
228
+ if (void 0 === wrangler) return {};
229
+ if (!isJsonRecord(wrangler)) throw new Error('deploy.worker.wrangler must be a JSON object.');
230
+ return wrangler;
231
+ };
232
+ const createWranglerCompatibilityFlags = (configuredFlags)=>{
233
+ if (void 0 === configuredFlags) return [
234
+ ...REQUIRED_COMPATIBILITY_FLAGS
235
+ ];
236
+ if (!Array.isArray(configuredFlags) || configuredFlags.some((flag)=>'string' != typeof flag)) throw new Error('deploy.worker.wrangler.compatibility_flags must be an array of strings.');
237
+ return [
238
+ ...new Set([
239
+ ...configuredFlags,
240
+ ...REQUIRED_COMPATIBILITY_FLAGS
241
+ ])
242
+ ];
243
+ };
244
+ const createWranglerAssetsConfig = (configuredAssets)=>{
245
+ if (void 0 !== configuredAssets && !isJsonRecord(configuredAssets)) throw new Error('deploy.worker.wrangler.assets must be an object.');
246
+ return {
247
+ ...isJsonRecord(configuredAssets) ? configuredAssets : {},
248
+ directory: `./${PUBLIC_ASSETS_DIRECTORY}`,
249
+ binding: ASSETS_BINDING,
250
+ run_worker_first: true
251
+ };
252
+ };
253
+ const createWranglerConfig = (appDirectory, modernConfig)=>{
254
+ const wrangler = getConfiguredWrangler(modernConfig);
255
+ return {
256
+ $schema: 'node_modules/wrangler/config-schema.json',
257
+ name: getConfiguredWorkerName(appDirectory, modernConfig),
258
+ compatibility_date: getCompatibilityDate(modernConfig),
259
+ ...wrangler,
260
+ main: WORKER_ENTRY,
261
+ compatibility_flags: createWranglerCompatibilityFlags(wrangler.compatibility_flags),
262
+ assets: createWranglerAssetsConfig(wrangler.assets)
263
+ };
264
+ };
265
+ const normalizeCloudflareArtifact = (artifact, index)=>{
266
+ const from = normalizeRelativePath(artifact.from, `deploy.worker.artifacts[${index}].from`, 'app root');
267
+ const to = normalizeRelativePath(artifact.to, `deploy.worker.artifacts[${index}].to`, 'Cloudflare output');
268
+ const [topLevelDestination] = to.split('/');
269
+ const reservedDestination = RESERVED_ARTIFACT_DESTINATION_FILES.has(to) ? to : topLevelDestination;
270
+ if (RESERVED_ARTIFACT_DESTINATION_FILES.has(to) || RESERVED_ARTIFACT_DESTINATION_DIRECTORIES.has(topLevelDestination)) throw new Error(`deploy.worker.artifacts[${index}].to must not target generated Cloudflare output path ${JSON.stringify(reservedDestination)}.`);
271
+ return {
272
+ from,
273
+ to,
274
+ index
275
+ };
276
+ };
277
+ const getCloudflareArtifacts = (modernConfig)=>(modernConfig.deploy?.worker?.artifacts ?? []).map(normalizeCloudflareArtifact);
278
+ const copyCloudflareArtifacts = async (appDirectory, outputDirectory, artifacts)=>{
279
+ for (const artifact of artifacts){
280
+ const sourcePath = node_path.join(appDirectory, artifact.from);
281
+ if (!await fs.pathExists(sourcePath)) throw new Error(`deploy.worker.artifacts[${artifact.index}].from does not exist: ${artifact.from}`);
282
+ await fs.copy(sourcePath, node_path.join(outputDirectory, artifact.to));
283
+ }
284
+ };
198
285
  const readRouteSpec = async (outputDirectory)=>{
199
286
  const routeSpecPath = node_path.join(outputDirectory, ROUTE_SPEC_OUTPUT);
200
287
  if (!await fs.pathExists(routeSpecPath)) return {
@@ -206,7 +293,7 @@ const readRouteSpec = async (outputDirectory)=>{
206
293
  routes: Array.isArray(routeSpec.routes) ? routeSpec.routes : []
207
294
  };
208
295
  };
209
- const createMissingEffectBffWorkerError = (outputDirectory, worker)=>new Error(`Cloudflare Effect BFF is configured, but the BFF worker bundle is missing: ${node_path.join(outputDirectory, worker)}. ${EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE}`);
296
+ const createMissingEffectBffWorkerError = (outputDirectory, worker)=>new Error(`Cloudflare Effect API runtime is configured, but the BFF worker bundle is missing: ${node_path.join(outputDirectory, worker)}. ${EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE}`);
210
297
  const createWorkerManifest = async (outputDirectory, modernConfig)=>{
211
298
  const routeSpec = await readRouteSpec(outputDirectory);
212
299
  const routes = await Promise.all(routeSpec.routes.map(async (route)=>{
@@ -222,9 +309,9 @@ const createWorkerManifest = async (outputDirectory, modernConfig)=>{
222
309
  }));
223
310
  const bffPrefix = modernConfig.bff?.prefix;
224
311
  const primaryBffPrefix = Array.isArray(bffPrefix) ? bffPrefix[0] : bffPrefix;
225
- const isEffectBff = Boolean(modernConfig.bff) && modernConfig.bff?.runtimeFramework !== 'hono';
226
- const effectBffWorkerExists = await fs.pathExists(node_path.join(outputDirectory, BFF_EFFECT_WORKER_ENTRY));
227
- if (isEffectBff && primaryBffPrefix && !effectBffWorkerExists) throw createMissingEffectBffWorkerError(outputDirectory, BFF_EFFECT_WORKER_ENTRY);
312
+ const isEffectApi = Boolean(modernConfig.bff) && modernConfig.bff?.runtimeFramework !== 'hono';
313
+ const effectApiWorkerExists = await fs.pathExists(node_path.join(outputDirectory, BFF_EFFECT_WORKER_ENTRY));
314
+ if (isEffectApi && primaryBffPrefix && !effectApiWorkerExists) throw createMissingEffectBffWorkerError(outputDirectory, BFF_EFFECT_WORKER_ENTRY);
228
315
  return {
229
316
  version: 1,
230
317
  runtime: {
@@ -253,7 +340,7 @@ const createWorkerManifest = async (outputDirectory, modernConfig)=>{
253
340
  routeManifest: ROUTE_MANIFEST_FILE
254
341
  },
255
342
  security: createCloudflareWorkerSecurityPolicy(modernConfig),
256
- bff: isEffectBff && primaryBffPrefix && effectBffWorkerExists ? {
343
+ bff: isEffectApi && primaryBffPrefix && effectApiWorkerExists ? {
257
344
  runtimeFramework: 'effect',
258
345
  prefix: primaryBffPrefix,
259
346
  worker: BFF_EFFECT_WORKER_ENTRY
@@ -276,12 +363,22 @@ const createWorkerModuleLoaders = (manifest)=>{
276
363
  ].map(([worker, loader])=>`${JSON.stringify(worker)}: ${loader}`);
277
364
  return `{\n${loaders.join(',\n')}\n}`;
278
365
  };
279
- const shouldCopyToPublicAssets = (src, distDirectory)=>{
366
+ const getPublicAssetExcludes = (appDirectory, modernConfig)=>[
367
+ ...DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES.filter((directory)=>{
368
+ try {
369
+ return fs.statSync(node_path.join(appDirectory, directory)).isDirectory();
370
+ } catch {
371
+ return false;
372
+ }
373
+ }),
374
+ ...modernConfig.deploy?.worker?.publicAssetExcludes ?? []
375
+ ].map((entry)=>normalizeRelativePath(entry, 'deploy.worker.publicAssetExcludes'));
376
+ const shouldCopyToPublicAssets = (src, distDirectory, publicAssetExcludes)=>{
280
377
  const relativePath = node_path.relative(distDirectory, src);
281
378
  if (!relativePath) return true;
282
379
  const normalizedRelativePath = relativePath.replace(/\\/g, '/');
283
380
  const [topLevelDirectory] = normalizedRelativePath.split('/');
284
- return normalizedRelativePath !== ROUTE_SPEC_FILE && topLevelDirectory !== WORKER_BUNDLE_DIRECTORY && topLevelDirectory !== SERVER_BUNDLE_DIRECTORY;
381
+ return normalizedRelativePath !== ROUTE_SPEC_FILE && topLevelDirectory !== WORKER_BUNDLE_DIRECTORY && topLevelDirectory !== SERVER_BUNDLE_DIRECTORY && !publicAssetExcludes.some((exclude)=>normalizedRelativePath === exclude || normalizedRelativePath.startsWith(`${exclude}/`));
285
382
  };
286
383
  const shouldCopyToWorkerBundle = (src, workerBundleDirectory)=>{
287
384
  const relativePath = node_path.relative(workerBundleDirectory, src);
@@ -303,15 +400,16 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
303
400
  const workerEntryPath = node_path.join(outputDirectory, WORKER_ENTRY);
304
401
  const workerManifestPath = node_path.join(outputDirectory, WORKER_MANIFEST);
305
402
  const routeSpecOutputPath = node_path.join(outputDirectory, ROUTE_SPEC_OUTPUT);
306
- const wranglerConfigPath = node_path.join(outputDirectory, 'wrangler.json');
307
- const workerName = getConfiguredWorkerName(appDirectory, modernConfig);
403
+ const wranglerConfigPath = node_path.join(outputDirectory, WRANGLER_CONFIG_FILE);
404
+ const cloudflareArtifacts = getCloudflareArtifacts(modernConfig);
405
+ const publicAssetExcludes = getPublicAssetExcludes(appDirectory, modernConfig);
308
406
  return {
309
407
  async prepare () {
310
408
  await fs.remove(outputDirectory);
311
409
  },
312
410
  async writeOutput () {
313
411
  await fs.copy(distDirectory, publicDirectory, {
314
- filter: (src)=>shouldCopyToPublicAssets(src, distDirectory)
412
+ filter: (src)=>shouldCopyToPublicAssets(src, distDirectory, publicAssetExcludes)
315
413
  });
316
414
  await fs.ensureDir(node_path.dirname(workerEntryPath));
317
415
  await fs.ensureDir(node_path.dirname(workerManifestPath));
@@ -327,27 +425,14 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
327
425
  type: 'commonjs'
328
426
  });
329
427
  }
330
- await fs.writeJSON(wranglerConfigPath, {
331
- $schema: 'node_modules/wrangler/config-schema.json',
332
- name: workerName,
333
- main: WORKER_ENTRY,
334
- compatibility_date: getCompatibilityDate(modernConfig),
335
- compatibility_flags: [
336
- 'nodejs_compat',
337
- 'global_fetch_strictly_public'
338
- ],
339
- assets: {
340
- directory: `./${PUBLIC_ASSETS_DIRECTORY}`,
341
- binding: ASSETS_BINDING,
342
- run_worker_first: true
343
- }
344
- }, {
428
+ await copyCloudflareArtifacts(appDirectory, outputDirectory, cloudflareArtifacts);
429
+ await fs.writeJSON(wranglerConfigPath, createWranglerConfig(appDirectory, modernConfig), {
345
430
  spaces: 2
346
431
  });
347
432
  await fs.writeJSON(workerManifestPath, await createWorkerManifest(outputDirectory, modernConfig), {
348
433
  spaces: 2
349
434
  });
350
- await fs.writeJSON(node_path.join(outputDirectory, 'package.json'), {
435
+ await fs.writeJSON(node_path.join(outputDirectory, OUTPUT_PACKAGE_FILE), {
351
436
  type: 'module'
352
437
  });
353
438
  },
@@ -312,6 +312,19 @@ function getRemoteManifestUrl(remote, request) {
312
312
  if ('string' != typeof entry || 0 === entry.length) return;
313
313
  return new URL(entry, request.url).toString();
314
314
  }
315
+ function ensureTrailingSlash(value) {
316
+ return value.endsWith('/') ? value : `${value}/`;
317
+ }
318
+ function getRemoteAssetBase(remoteManifest, manifestUrl) {
319
+ const publicPath = 'string' == typeof remoteManifest?.metaData?.publicPath ? remoteManifest.metaData.publicPath : void 0;
320
+ const fallbackBase = new URL('.', manifestUrl).toString();
321
+ if (!publicPath || 'auto' === publicPath) return fallbackBase;
322
+ try {
323
+ return new URL(ensureTrailingSlash(publicPath), manifestUrl).toString();
324
+ } catch {
325
+ return fallbackBase;
326
+ }
327
+ }
315
328
  async function fetchRemoteJson(jsonUrl) {
316
329
  if (!remoteJsonPromises.has(jsonUrl)) remoteJsonPromises.set(jsonUrl, fetch(jsonUrl).then((response)=>{
317
330
  if (!response.ok) {
@@ -374,7 +387,7 @@ async function collectRenderedRemoteCssHrefs(html, request, env) {
374
387
  if (!manifestUrl) return;
375
388
  const remoteManifest = await fetchRemoteJson(manifestUrl);
376
389
  const remoteExpose = findRemoteExpose(remoteManifest, expose);
377
- const publicPath = 'string' == typeof remoteManifest?.metaData?.publicPath ? remoteManifest.metaData.publicPath : manifestUrl;
390
+ const publicPath = getRemoteAssetBase(remoteManifest, manifestUrl);
378
391
  const remoteRouteManifest = await fetchRemoteJson(new URL('routes-manifest.json', publicPath).toString());
379
392
  for (const asset of collectCssAssetEntries(remoteExpose?.assets))hrefs.add(new URL(asset, publicPath).toString());
380
393
  for (const asset of collectRouteManifestCssAssets(remoteRouteManifest))hrefs.add(new URL(asset, publicPath).toString());
@@ -99,8 +99,11 @@ function getCloudflareWorkerNodeExternals() {
99
99
  function getEffectBffEntry(normalizedConfig, appContext) {
100
100
  if (!normalizedConfig.bff || 'hono' === normalizedConfig.bff.runtimeFramework) return;
101
101
  const configuredEntry = normalizedConfig.bff.effect?.entry;
102
- const entryWithoutOrWithExtension = configuredEntry ? node_path.isAbsolute(configuredEntry) ? configuredEntry : node_path.resolve(appContext.appDirectory, configuredEntry) : node_path.resolve(appContext.apiDirectory, 'effect', 'index');
103
- return resolveJsOrTsEntry(entryWithoutOrWithExtension);
102
+ if (configuredEntry) {
103
+ const entryWithoutOrWithExtension = node_path.isAbsolute(configuredEntry) ? configuredEntry : node_path.resolve(appContext.appDirectory, configuredEntry);
104
+ return resolveJsOrTsEntry(entryWithoutOrWithExtension);
105
+ }
106
+ return resolveJsOrTsEntry(node_path.resolve(appContext.apiDirectory, 'index'));
104
107
  }
105
108
  function isCloudflareWorkerDeploy(normalizedConfig) {
106
109
  return normalizedConfig.deploy?.target === 'cloudflare' || 'cloudflare' === process.env.MODERNJS_DEPLOY;
@@ -189,7 +192,7 @@ function getBuilderEnvironments(normalizedConfig, appContext, tempBuilderConfig)
189
192
  const useWorkerTarget = isServiceWorker(normalizedConfig);
190
193
  if (useWorkerTarget) {
191
194
  const useCloudflareModuleWorker = isCloudflareWorkerDeploy(normalizedConfig);
192
- const effectBffEntry = useCloudflareModuleWorker ? getEffectBffEntry(normalizedConfig, appContext) : void 0;
195
+ const effectApiEntry = useCloudflareModuleWorker ? getEffectBffEntry(normalizedConfig, appContext) : void 0;
193
196
  const tanstackRouterSsrServerFile = useCloudflareModuleWorker ? resolvePackageFile('@tanstack/router-core', 'dist/esm/ssr/ssr-server.js', [
194
197
  appContext.appDirectory,
195
198
  process.cwd()
@@ -231,10 +234,10 @@ function getBuilderEnvironments(normalizedConfig, appContext, tempBuilderConfig)
231
234
  const fsPromisesWorkerFile = useCloudflareModuleWorker ? getCloudflareWorkerCompatFile('cloudflare-worker-fs-promises.mjs') : void 0;
232
235
  const pathWorkerFile = useCloudflareModuleWorker ? getCloudflareWorkerCompatFile('cloudflare-worker-path.mjs') : void 0;
233
236
  const baseWorkerEntries = useCloudflareModuleWorker ? cloudflareWorkerServerEntries : serverEntries;
234
- const workerEntries = effectBffEntry ? {
237
+ const workerEntries = effectApiEntry ? {
235
238
  ...baseWorkerEntries,
236
239
  [BFF_EFFECT_WORKER_ENTRY_NAME]: [
237
- `${effectBffEntry}?${BFF_EFFECT_WORKER_RUNTIME_QUERY}`
240
+ `${effectApiEntry}?${BFF_EFFECT_WORKER_RUNTIME_QUERY}`
238
241
  ]
239
242
  } : baseWorkerEntries;
240
243
  environments[SERVICE_WORKER_ENVIRONMENT_NAME] = {
@@ -4,6 +4,8 @@ import { fs } from "@modern-js/utils";
4
4
  import { readTemplate } from "../utils/index.mjs";
5
5
  const WORKER_ENTRY = 'server/index.mjs';
6
6
  const WORKER_MANIFEST = 'server/modern-worker-manifest.json';
7
+ const WRANGLER_CONFIG_FILE = 'wrangler.json';
8
+ const OUTPUT_PACKAGE_FILE = 'package.json';
7
9
  const ASSETS_BINDING = 'ASSETS';
8
10
  const ROUTE_SPEC_FILE = 'route.json';
9
11
  const ROUTE_SPEC_OUTPUT = `server/${ROUTE_SPEC_FILE}`;
@@ -12,10 +14,28 @@ const ROUTE_MANIFEST_FILE = 'routes-manifest.json';
12
14
  const PUBLIC_ASSETS_DIRECTORY = 'public';
13
15
  const WORKER_BUNDLE_DIRECTORY = 'worker';
14
16
  const SERVER_BUNDLE_DIRECTORY = 'bundles';
17
+ const SERVER_OUTPUT_DIRECTORY = 'server';
18
+ const DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES = [
19
+ 'api',
20
+ 'shared'
21
+ ];
15
22
  const BFF_EFFECT_WORKER_ENTRY = `${WORKER_BUNDLE_DIRECTORY}/__modern_bff_effect.js`;
16
- const EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE = 'Ensure the Effect BFF entry exists at api/effect/index.ts or bff.effect.entry, and import Cloudflare edge handlers from @modern-js/plugin-bff/effect-edge instead of lambda/Hono server helpers.';
23
+ const EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE = 'Ensure the Effect API entry exists at api/index.ts or bff.effect.entry, and import Cloudflare edge handlers from @modern-js/plugin-bff/effect-edge instead of lambda/Hono server helpers.';
17
24
  const DEFAULT_COMPATIBILITY_DATE = '2026-06-02';
18
25
  const COMPATIBILITY_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
26
+ const REQUIRED_COMPATIBILITY_FLAGS = [
27
+ 'nodejs_compat',
28
+ 'global_fetch_strictly_public'
29
+ ];
30
+ const RESERVED_ARTIFACT_DESTINATION_FILES = new Set([
31
+ WRANGLER_CONFIG_FILE,
32
+ OUTPUT_PACKAGE_FILE
33
+ ]);
34
+ const RESERVED_ARTIFACT_DESTINATION_DIRECTORIES = new Set([
35
+ PUBLIC_ASSETS_DIRECTORY,
36
+ SERVER_OUTPUT_DIRECTORY,
37
+ WORKER_BUNDLE_DIRECTORY
38
+ ]);
19
39
  const DEFAULT_SECURITY_HEADERS = {
20
40
  referrerPolicy: 'strict-origin-when-cross-origin',
21
41
  contentTypeOptions: 'nosniff',
@@ -104,6 +124,13 @@ const getConfiguredWorkerName = (appDirectory, modernConfig)=>{
104
124
  const configuredName = modernConfig.deploy?.worker?.name?.trim();
105
125
  return configuredName || getWorkerName(appDirectory);
106
126
  };
127
+ const normalizeRelativePath = (value, label, scope = 'app output')=>{
128
+ if ('string' != typeof value) throw new Error(`${label} must be a relative path inside the ${scope}.`);
129
+ const normalized = value.trim().replace(/\\/g, '/').replace(/^\.\/+/u, '').replace(/\/+$/u, '');
130
+ const segments = normalized.split('/');
131
+ if (!normalized || node_path.isAbsolute(normalized) || '.' === normalized || segments.includes('..')) throw new Error(`${label} must be a relative path inside the ${scope}.`);
132
+ return normalized;
133
+ };
107
134
  const normalizeDirectiveValues = (value)=>{
108
135
  const values = Array.isArray(value) ? value : [
109
136
  value
@@ -196,6 +223,66 @@ const createCloudflareWorkerSecurityPolicy = (modernConfig)=>{
196
223
  reason: security?.reason
197
224
  };
198
225
  };
226
+ const isJsonRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
227
+ const getConfiguredWrangler = (modernConfig)=>{
228
+ const wrangler = modernConfig.deploy?.worker?.wrangler;
229
+ if (void 0 === wrangler) return {};
230
+ if (!isJsonRecord(wrangler)) throw new Error('deploy.worker.wrangler must be a JSON object.');
231
+ return wrangler;
232
+ };
233
+ const createWranglerCompatibilityFlags = (configuredFlags)=>{
234
+ if (void 0 === configuredFlags) return [
235
+ ...REQUIRED_COMPATIBILITY_FLAGS
236
+ ];
237
+ if (!Array.isArray(configuredFlags) || configuredFlags.some((flag)=>'string' != typeof flag)) throw new Error('deploy.worker.wrangler.compatibility_flags must be an array of strings.');
238
+ return [
239
+ ...new Set([
240
+ ...configuredFlags,
241
+ ...REQUIRED_COMPATIBILITY_FLAGS
242
+ ])
243
+ ];
244
+ };
245
+ const createWranglerAssetsConfig = (configuredAssets)=>{
246
+ if (void 0 !== configuredAssets && !isJsonRecord(configuredAssets)) throw new Error('deploy.worker.wrangler.assets must be an object.');
247
+ return {
248
+ ...isJsonRecord(configuredAssets) ? configuredAssets : {},
249
+ directory: `./${PUBLIC_ASSETS_DIRECTORY}`,
250
+ binding: ASSETS_BINDING,
251
+ run_worker_first: true
252
+ };
253
+ };
254
+ const createWranglerConfig = (appDirectory, modernConfig)=>{
255
+ const wrangler = getConfiguredWrangler(modernConfig);
256
+ return {
257
+ $schema: 'node_modules/wrangler/config-schema.json',
258
+ name: getConfiguredWorkerName(appDirectory, modernConfig),
259
+ compatibility_date: getCompatibilityDate(modernConfig),
260
+ ...wrangler,
261
+ main: WORKER_ENTRY,
262
+ compatibility_flags: createWranglerCompatibilityFlags(wrangler.compatibility_flags),
263
+ assets: createWranglerAssetsConfig(wrangler.assets)
264
+ };
265
+ };
266
+ const normalizeCloudflareArtifact = (artifact, index)=>{
267
+ const from = normalizeRelativePath(artifact.from, `deploy.worker.artifacts[${index}].from`, 'app root');
268
+ const to = normalizeRelativePath(artifact.to, `deploy.worker.artifacts[${index}].to`, 'Cloudflare output');
269
+ const [topLevelDestination] = to.split('/');
270
+ const reservedDestination = RESERVED_ARTIFACT_DESTINATION_FILES.has(to) ? to : topLevelDestination;
271
+ if (RESERVED_ARTIFACT_DESTINATION_FILES.has(to) || RESERVED_ARTIFACT_DESTINATION_DIRECTORIES.has(topLevelDestination)) throw new Error(`deploy.worker.artifacts[${index}].to must not target generated Cloudflare output path ${JSON.stringify(reservedDestination)}.`);
272
+ return {
273
+ from,
274
+ to,
275
+ index
276
+ };
277
+ };
278
+ const getCloudflareArtifacts = (modernConfig)=>(modernConfig.deploy?.worker?.artifacts ?? []).map(normalizeCloudflareArtifact);
279
+ const copyCloudflareArtifacts = async (appDirectory, outputDirectory, artifacts)=>{
280
+ for (const artifact of artifacts){
281
+ const sourcePath = node_path.join(appDirectory, artifact.from);
282
+ if (!await fs.pathExists(sourcePath)) throw new Error(`deploy.worker.artifacts[${artifact.index}].from does not exist: ${artifact.from}`);
283
+ await fs.copy(sourcePath, node_path.join(outputDirectory, artifact.to));
284
+ }
285
+ };
199
286
  const readRouteSpec = async (outputDirectory)=>{
200
287
  const routeSpecPath = node_path.join(outputDirectory, ROUTE_SPEC_OUTPUT);
201
288
  if (!await fs.pathExists(routeSpecPath)) return {
@@ -207,7 +294,7 @@ const readRouteSpec = async (outputDirectory)=>{
207
294
  routes: Array.isArray(routeSpec.routes) ? routeSpec.routes : []
208
295
  };
209
296
  };
210
- const createMissingEffectBffWorkerError = (outputDirectory, worker)=>new Error(`Cloudflare Effect BFF is configured, but the BFF worker bundle is missing: ${node_path.join(outputDirectory, worker)}. ${EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE}`);
297
+ const createMissingEffectBffWorkerError = (outputDirectory, worker)=>new Error(`Cloudflare Effect API runtime is configured, but the BFF worker bundle is missing: ${node_path.join(outputDirectory, worker)}. ${EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE}`);
211
298
  const createWorkerManifest = async (outputDirectory, modernConfig)=>{
212
299
  const routeSpec = await readRouteSpec(outputDirectory);
213
300
  const routes = await Promise.all(routeSpec.routes.map(async (route)=>{
@@ -223,9 +310,9 @@ const createWorkerManifest = async (outputDirectory, modernConfig)=>{
223
310
  }));
224
311
  const bffPrefix = modernConfig.bff?.prefix;
225
312
  const primaryBffPrefix = Array.isArray(bffPrefix) ? bffPrefix[0] : bffPrefix;
226
- const isEffectBff = Boolean(modernConfig.bff) && modernConfig.bff?.runtimeFramework !== 'hono';
227
- const effectBffWorkerExists = await fs.pathExists(node_path.join(outputDirectory, BFF_EFFECT_WORKER_ENTRY));
228
- if (isEffectBff && primaryBffPrefix && !effectBffWorkerExists) throw createMissingEffectBffWorkerError(outputDirectory, BFF_EFFECT_WORKER_ENTRY);
313
+ const isEffectApi = Boolean(modernConfig.bff) && modernConfig.bff?.runtimeFramework !== 'hono';
314
+ const effectApiWorkerExists = await fs.pathExists(node_path.join(outputDirectory, BFF_EFFECT_WORKER_ENTRY));
315
+ if (isEffectApi && primaryBffPrefix && !effectApiWorkerExists) throw createMissingEffectBffWorkerError(outputDirectory, BFF_EFFECT_WORKER_ENTRY);
229
316
  return {
230
317
  version: 1,
231
318
  runtime: {
@@ -254,7 +341,7 @@ const createWorkerManifest = async (outputDirectory, modernConfig)=>{
254
341
  routeManifest: ROUTE_MANIFEST_FILE
255
342
  },
256
343
  security: createCloudflareWorkerSecurityPolicy(modernConfig),
257
- bff: isEffectBff && primaryBffPrefix && effectBffWorkerExists ? {
344
+ bff: isEffectApi && primaryBffPrefix && effectApiWorkerExists ? {
258
345
  runtimeFramework: 'effect',
259
346
  prefix: primaryBffPrefix,
260
347
  worker: BFF_EFFECT_WORKER_ENTRY
@@ -277,12 +364,22 @@ const createWorkerModuleLoaders = (manifest)=>{
277
364
  ].map(([worker, loader])=>`${JSON.stringify(worker)}: ${loader}`);
278
365
  return `{\n${loaders.join(',\n')}\n}`;
279
366
  };
280
- const shouldCopyToPublicAssets = (src, distDirectory)=>{
367
+ const getPublicAssetExcludes = (appDirectory, modernConfig)=>[
368
+ ...DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES.filter((directory)=>{
369
+ try {
370
+ return fs.statSync(node_path.join(appDirectory, directory)).isDirectory();
371
+ } catch {
372
+ return false;
373
+ }
374
+ }),
375
+ ...modernConfig.deploy?.worker?.publicAssetExcludes ?? []
376
+ ].map((entry)=>normalizeRelativePath(entry, 'deploy.worker.publicAssetExcludes'));
377
+ const shouldCopyToPublicAssets = (src, distDirectory, publicAssetExcludes)=>{
281
378
  const relativePath = node_path.relative(distDirectory, src);
282
379
  if (!relativePath) return true;
283
380
  const normalizedRelativePath = relativePath.replace(/\\/g, '/');
284
381
  const [topLevelDirectory] = normalizedRelativePath.split('/');
285
- return normalizedRelativePath !== ROUTE_SPEC_FILE && topLevelDirectory !== WORKER_BUNDLE_DIRECTORY && topLevelDirectory !== SERVER_BUNDLE_DIRECTORY;
382
+ return normalizedRelativePath !== ROUTE_SPEC_FILE && topLevelDirectory !== WORKER_BUNDLE_DIRECTORY && topLevelDirectory !== SERVER_BUNDLE_DIRECTORY && !publicAssetExcludes.some((exclude)=>normalizedRelativePath === exclude || normalizedRelativePath.startsWith(`${exclude}/`));
286
383
  };
287
384
  const shouldCopyToWorkerBundle = (src, workerBundleDirectory)=>{
288
385
  const relativePath = node_path.relative(workerBundleDirectory, src);
@@ -304,15 +401,16 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
304
401
  const workerEntryPath = node_path.join(outputDirectory, WORKER_ENTRY);
305
402
  const workerManifestPath = node_path.join(outputDirectory, WORKER_MANIFEST);
306
403
  const routeSpecOutputPath = node_path.join(outputDirectory, ROUTE_SPEC_OUTPUT);
307
- const wranglerConfigPath = node_path.join(outputDirectory, 'wrangler.json');
308
- const workerName = getConfiguredWorkerName(appDirectory, modernConfig);
404
+ const wranglerConfigPath = node_path.join(outputDirectory, WRANGLER_CONFIG_FILE);
405
+ const cloudflareArtifacts = getCloudflareArtifacts(modernConfig);
406
+ const publicAssetExcludes = getPublicAssetExcludes(appDirectory, modernConfig);
309
407
  return {
310
408
  async prepare () {
311
409
  await fs.remove(outputDirectory);
312
410
  },
313
411
  async writeOutput () {
314
412
  await fs.copy(distDirectory, publicDirectory, {
315
- filter: (src)=>shouldCopyToPublicAssets(src, distDirectory)
413
+ filter: (src)=>shouldCopyToPublicAssets(src, distDirectory, publicAssetExcludes)
316
414
  });
317
415
  await fs.ensureDir(node_path.dirname(workerEntryPath));
318
416
  await fs.ensureDir(node_path.dirname(workerManifestPath));
@@ -328,27 +426,14 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
328
426
  type: 'commonjs'
329
427
  });
330
428
  }
331
- await fs.writeJSON(wranglerConfigPath, {
332
- $schema: 'node_modules/wrangler/config-schema.json',
333
- name: workerName,
334
- main: WORKER_ENTRY,
335
- compatibility_date: getCompatibilityDate(modernConfig),
336
- compatibility_flags: [
337
- 'nodejs_compat',
338
- 'global_fetch_strictly_public'
339
- ],
340
- assets: {
341
- directory: `./${PUBLIC_ASSETS_DIRECTORY}`,
342
- binding: ASSETS_BINDING,
343
- run_worker_first: true
344
- }
345
- }, {
429
+ await copyCloudflareArtifacts(appDirectory, outputDirectory, cloudflareArtifacts);
430
+ await fs.writeJSON(wranglerConfigPath, createWranglerConfig(appDirectory, modernConfig), {
346
431
  spaces: 2
347
432
  });
348
433
  await fs.writeJSON(workerManifestPath, await createWorkerManifest(outputDirectory, modernConfig), {
349
434
  spaces: 2
350
435
  });
351
- await fs.writeJSON(node_path.join(outputDirectory, 'package.json'), {
436
+ await fs.writeJSON(node_path.join(outputDirectory, OUTPUT_PACKAGE_FILE), {
352
437
  type: 'module'
353
438
  });
354
439
  },
@@ -312,6 +312,19 @@ function getRemoteManifestUrl(remote, request) {
312
312
  if ('string' != typeof entry || 0 === entry.length) return;
313
313
  return new URL(entry, request.url).toString();
314
314
  }
315
+ function ensureTrailingSlash(value) {
316
+ return value.endsWith('/') ? value : `${value}/`;
317
+ }
318
+ function getRemoteAssetBase(remoteManifest, manifestUrl) {
319
+ const publicPath = 'string' == typeof remoteManifest?.metaData?.publicPath ? remoteManifest.metaData.publicPath : void 0;
320
+ const fallbackBase = new URL('.', manifestUrl).toString();
321
+ if (!publicPath || 'auto' === publicPath) return fallbackBase;
322
+ try {
323
+ return new URL(ensureTrailingSlash(publicPath), manifestUrl).toString();
324
+ } catch {
325
+ return fallbackBase;
326
+ }
327
+ }
315
328
  async function fetchRemoteJson(jsonUrl) {
316
329
  if (!remoteJsonPromises.has(jsonUrl)) remoteJsonPromises.set(jsonUrl, fetch(jsonUrl).then((response)=>{
317
330
  if (!response.ok) {
@@ -374,7 +387,7 @@ async function collectRenderedRemoteCssHrefs(html, request, env) {
374
387
  if (!manifestUrl) return;
375
388
  const remoteManifest = await fetchRemoteJson(manifestUrl);
376
389
  const remoteExpose = findRemoteExpose(remoteManifest, expose);
377
- const publicPath = 'string' == typeof remoteManifest?.metaData?.publicPath ? remoteManifest.metaData.publicPath : manifestUrl;
390
+ const publicPath = getRemoteAssetBase(remoteManifest, manifestUrl);
378
391
  const remoteRouteManifest = await fetchRemoteJson(new URL('routes-manifest.json', publicPath).toString());
379
392
  for (const asset of collectCssAssetEntries(remoteExpose?.assets))hrefs.add(new URL(asset, publicPath).toString());
380
393
  for (const asset of collectRouteManifestCssAssets(remoteRouteManifest))hrefs.add(new URL(asset, publicPath).toString());
@@ -1,5 +1,19 @@
1
1
  export type DeployTarget = 'node' | 'vercel' | 'netlify' | 'ghPages' | 'cloudflare';
2
2
  export type CloudflareWorkerSecurityCspMode = 'enforce' | 'report-only' | 'off';
3
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
4
+ [key: string]: JsonValue;
5
+ };
6
+ export interface CloudflareWorkerArtifactConfig {
7
+ /**
8
+ * Source file or directory, relative to the app root, to copy into
9
+ * `.output` after framework output has been staged.
10
+ */
11
+ from: string;
12
+ /**
13
+ * Destination path relative to `.output`.
14
+ */
15
+ to: string;
16
+ }
3
17
  export interface CloudflareWorkerSecurityCspConfig {
4
18
  mode?: CloudflareWorkerSecurityCspMode;
5
19
  directives?: Record<string, string[] | string | false>;
@@ -1,5 +1,5 @@
1
- import type { CloudflareWorkerSecurityConfig, DeployTarget } from './cloudflareDeploy';
2
- export type { CloudflareWorkerSecurityConfig, CloudflareWorkerSecurityCorsConfig, CloudflareWorkerSecurityCspConfig, CloudflareWorkerSecurityCspMode, CloudflareWorkerSecurityNoindexConfig, DeployTarget, } from './cloudflareDeploy';
1
+ import type { CloudflareWorkerArtifactConfig, CloudflareWorkerSecurityConfig, DeployTarget, JsonValue } from './cloudflareDeploy';
2
+ export type { CloudflareWorkerArtifactConfig, CloudflareWorkerSecurityConfig, CloudflareWorkerSecurityCorsConfig, CloudflareWorkerSecurityCspConfig, CloudflareWorkerSecurityCspMode, CloudflareWorkerSecurityNoindexConfig, DeployTarget, JsonValue, } from './cloudflareDeploy';
3
3
  export interface MicroFrontend {
4
4
  /**
5
5
  * Specifies whether to enable the HTML entry.
@@ -36,5 +36,24 @@ export interface DeployUserConfig {
36
36
  compatibilityDate?: string;
37
37
  ssr?: boolean;
38
38
  security?: CloudflareWorkerSecurityConfig;
39
+ /**
40
+ * Raw Wrangler-compatible config merged into `.output/wrangler.json`.
41
+ * Framework-owned worker invariants still win for `main`, the assets
42
+ * binding/directory/run mode, and required compatibility flags.
43
+ */
44
+ wrangler?: Record<string, JsonValue>;
45
+ /**
46
+ * Additional app-root files or directories to stage under `.output`.
47
+ * Use this for provider resources such as migrations or generated config.
48
+ */
49
+ artifacts?: CloudflareWorkerArtifactConfig[];
50
+ /**
51
+ * Dist output paths that must not be copied into Cloudflare public assets.
52
+ * Entries are slash-normalized path prefixes relative to the app dist root.
53
+ * Top-level `api` and `shared` dist directories are excluded when matching
54
+ * source directories exist in the app root because they conventionally
55
+ * contain server-only implementation code.
56
+ */
57
+ publicAssetExcludes?: string[];
39
58
  };
40
59
  }
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.4.0-ultramodern.9",
20
+ "version": "3.5.0-ultramodern.0",
21
21
  "types": "./dist/types/index.d.ts",
22
22
  "main": "./dist/cjs/index.js",
23
23
  "exports": {
@@ -90,16 +90,16 @@
90
90
  "ndepe": "^0.1.13",
91
91
  "pkg-types": "^2.3.1",
92
92
  "std-env": "4.1.0",
93
- "@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.4.0-ultramodern.9",
94
- "@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.4.0-ultramodern.9",
95
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.4.0-ultramodern.9",
96
- "@modern-js/plugin-data-loader": "npm:@bleedingdev/modern-js-plugin-data-loader@3.4.0-ultramodern.9",
97
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.4.0-ultramodern.9",
98
- "@modern-js/server": "npm:@bleedingdev/modern-js-server@3.4.0-ultramodern.9",
99
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.4.0-ultramodern.9",
100
- "@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.4.0-ultramodern.9",
101
- "@modern-js/prod-server": "npm:@bleedingdev/modern-js-prod-server@3.4.0-ultramodern.9",
102
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.4.0-ultramodern.9"
93
+ "@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.5.0-ultramodern.0",
94
+ "@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.5.0-ultramodern.0",
95
+ "@modern-js/plugin-data-loader": "npm:@bleedingdev/modern-js-plugin-data-loader@3.5.0-ultramodern.0",
96
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.0",
97
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.0",
98
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.0",
99
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.0",
100
+ "@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.5.0-ultramodern.0",
101
+ "@modern-js/prod-server": "npm:@bleedingdev/modern-js-prod-server@3.5.0-ultramodern.0",
102
+ "@modern-js/server": "npm:@bleedingdev/modern-js-server@3.5.0-ultramodern.0"
103
103
  },
104
104
  "devDependencies": {
105
105
  "@rslib/core": "0.23.0",