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

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,111 @@ 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 assertNonEmptyString = (value, label)=>{
296
+ if ('string' != typeof value || 0 === value.trim().length) throw new Error(`${label} must be a non-empty string.`);
297
+ return value.trim();
298
+ };
299
+ const normalizeD1Database = (database, index)=>{
300
+ const binding = assertNonEmptyString(database.binding, `deploy.worker.d1Databases[${index}].binding`);
301
+ const databaseName = assertNonEmptyString(database.databaseName, `deploy.worker.d1Databases[${index}].databaseName`);
302
+ const databaseId = assertNonEmptyString(database.databaseId, `deploy.worker.d1Databases[${index}].databaseId`);
303
+ const migrationsDir = void 0 === database.migrationsDir ? void 0 : normalizeRelativePath(database.migrationsDir, `deploy.worker.d1Databases[${index}].migrationsDir`, 'app root');
304
+ const previewDatabaseId = void 0 === database.previewDatabaseId ? void 0 : assertNonEmptyString(database.previewDatabaseId, `deploy.worker.d1Databases[${index}].previewDatabaseId`);
305
+ return {
306
+ binding,
307
+ database_name: databaseName,
308
+ database_id: databaseId,
309
+ ...void 0 === migrationsDir ? {} : {
310
+ migrations_dir: migrationsDir
311
+ },
312
+ ...void 0 === previewDatabaseId ? {} : {
313
+ preview_database_id: previewDatabaseId
314
+ },
315
+ ...void 0 === database.remote ? {} : {
316
+ remote: database.remote
317
+ }
318
+ };
319
+ };
320
+ const createWranglerD1Databases = (modernConfig, configuredWranglerD1)=>{
321
+ const d1Databases = modernConfig.deploy?.worker?.d1Databases;
322
+ if (void 0 === d1Databases) return configuredWranglerD1;
323
+ if (void 0 !== configuredWranglerD1) throw new Error('Use deploy.worker.d1Databases or deploy.worker.wrangler.d1_databases, not both.');
324
+ if (!Array.isArray(d1Databases)) throw new Error('deploy.worker.d1Databases must be an array.');
325
+ return d1Databases.map(normalizeD1Database);
326
+ };
327
+ const createWranglerConfig = (appDirectory, modernConfig)=>{
328
+ const wrangler = getConfiguredWrangler(modernConfig);
329
+ const d1Databases = createWranglerD1Databases(modernConfig, wrangler.d1_databases);
330
+ return {
331
+ $schema: 'node_modules/wrangler/config-schema.json',
332
+ name: getConfiguredWorkerName(appDirectory, modernConfig),
333
+ compatibility_date: getCompatibilityDate(modernConfig),
334
+ ...wrangler,
335
+ main: WORKER_ENTRY,
336
+ compatibility_flags: createWranglerCompatibilityFlags(wrangler.compatibility_flags),
337
+ assets: createWranglerAssetsConfig(wrangler.assets),
338
+ ...void 0 === d1Databases ? {} : {
339
+ d1_databases: d1Databases
340
+ }
341
+ };
342
+ };
343
+ const normalizeCloudflareArtifact = (artifact, index)=>{
344
+ const from = normalizeRelativePath(artifact.from, `deploy.worker.artifacts[${index}].from`, 'app root');
345
+ const to = normalizeRelativePath(artifact.to, `deploy.worker.artifacts[${index}].to`, 'Cloudflare output');
346
+ const [topLevelDestination] = to.split('/');
347
+ const reservedDestination = RESERVED_ARTIFACT_DESTINATION_FILES.has(to) ? to : topLevelDestination;
348
+ 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)}.`);
349
+ return {
350
+ from,
351
+ to,
352
+ index
353
+ };
354
+ };
355
+ const getCloudflareArtifacts = (modernConfig)=>(modernConfig.deploy?.worker?.artifacts ?? []).map(normalizeCloudflareArtifact);
356
+ const copyCloudflareArtifacts = async (appDirectory, outputDirectory, artifacts)=>{
357
+ for (const artifact of artifacts){
358
+ const sourcePath = external_node_path_default().join(appDirectory, artifact.from);
359
+ if (!await utils_namespaceObject.fs.pathExists(sourcePath)) throw new Error(`deploy.worker.artifacts[${artifact.index}].from does not exist: ${artifact.from}`);
360
+ await utils_namespaceObject.fs.copy(sourcePath, external_node_path_default().join(outputDirectory, artifact.to));
361
+ }
362
+ };
363
+ const copyCloudflareD1Migrations = async (appDirectory, outputDirectory, modernConfig)=>{
364
+ for (const [index, database] of (modernConfig.deploy?.worker?.d1Databases ?? []).entries()){
365
+ if (!database.migrationsDir) continue;
366
+ const migrationsDir = normalizeRelativePath(database.migrationsDir, `deploy.worker.d1Databases[${index}].migrationsDir`, 'app root');
367
+ const sourcePath = external_node_path_default().join(appDirectory, migrationsDir);
368
+ if (!await utils_namespaceObject.fs.pathExists(sourcePath)) throw new Error(`deploy.worker.d1Databases[${index}].migrationsDir does not exist: ${migrationsDir}`);
369
+ await utils_namespaceObject.fs.copy(sourcePath, external_node_path_default().join(outputDirectory, migrationsDir));
370
+ }
371
+ };
240
372
  const readRouteSpec = async (outputDirectory)=>{
241
373
  const routeSpecPath = external_node_path_default().join(outputDirectory, ROUTE_SPEC_OUTPUT);
242
374
  if (!await utils_namespaceObject.fs.pathExists(routeSpecPath)) return {
@@ -248,7 +380,7 @@ const readRouteSpec = async (outputDirectory)=>{
248
380
  routes: Array.isArray(routeSpec.routes) ? routeSpec.routes : []
249
381
  };
250
382
  };
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}`);
383
+ 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
384
  const createWorkerManifest = async (outputDirectory, modernConfig)=>{
253
385
  const routeSpec = await readRouteSpec(outputDirectory);
254
386
  const routes = await Promise.all(routeSpec.routes.map(async (route)=>{
@@ -264,9 +396,9 @@ const createWorkerManifest = async (outputDirectory, modernConfig)=>{
264
396
  }));
265
397
  const bffPrefix = modernConfig.bff?.prefix;
266
398
  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);
399
+ const isEffectApi = Boolean(modernConfig.bff) && modernConfig.bff?.runtimeFramework !== 'hono';
400
+ const effectApiWorkerExists = await utils_namespaceObject.fs.pathExists(external_node_path_default().join(outputDirectory, BFF_EFFECT_WORKER_ENTRY));
401
+ if (isEffectApi && primaryBffPrefix && !effectApiWorkerExists) throw createMissingEffectBffWorkerError(outputDirectory, BFF_EFFECT_WORKER_ENTRY);
270
402
  return {
271
403
  version: 1,
272
404
  runtime: {
@@ -295,7 +427,7 @@ const createWorkerManifest = async (outputDirectory, modernConfig)=>{
295
427
  routeManifest: ROUTE_MANIFEST_FILE
296
428
  },
297
429
  security: createCloudflareWorkerSecurityPolicy(modernConfig),
298
- bff: isEffectBff && primaryBffPrefix && effectBffWorkerExists ? {
430
+ bff: isEffectApi && primaryBffPrefix && effectApiWorkerExists ? {
299
431
  runtimeFramework: 'effect',
300
432
  prefix: primaryBffPrefix,
301
433
  worker: BFF_EFFECT_WORKER_ENTRY
@@ -318,12 +450,22 @@ const createWorkerModuleLoaders = (manifest)=>{
318
450
  ].map(([worker, loader])=>`${JSON.stringify(worker)}: ${loader}`);
319
451
  return `{\n${loaders.join(',\n')}\n}`;
320
452
  };
321
- const shouldCopyToPublicAssets = (src, distDirectory)=>{
453
+ const getPublicAssetExcludes = (appDirectory, modernConfig)=>[
454
+ ...DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES.filter((directory)=>{
455
+ try {
456
+ return utils_namespaceObject.fs.statSync(external_node_path_default().join(appDirectory, directory)).isDirectory();
457
+ } catch {
458
+ return false;
459
+ }
460
+ }),
461
+ ...modernConfig.deploy?.worker?.publicAssetExcludes ?? []
462
+ ].map((entry)=>normalizeRelativePath(entry, 'deploy.worker.publicAssetExcludes'));
463
+ const shouldCopyToPublicAssets = (src, distDirectory, publicAssetExcludes)=>{
322
464
  const relativePath = external_node_path_default().relative(distDirectory, src);
323
465
  if (!relativePath) return true;
324
466
  const normalizedRelativePath = relativePath.replace(/\\/g, '/');
325
467
  const [topLevelDirectory] = normalizedRelativePath.split('/');
326
- return normalizedRelativePath !== ROUTE_SPEC_FILE && topLevelDirectory !== WORKER_BUNDLE_DIRECTORY && topLevelDirectory !== SERVER_BUNDLE_DIRECTORY;
468
+ return normalizedRelativePath !== ROUTE_SPEC_FILE && topLevelDirectory !== WORKER_BUNDLE_DIRECTORY && topLevelDirectory !== SERVER_BUNDLE_DIRECTORY && !publicAssetExcludes.some((exclude)=>normalizedRelativePath === exclude || normalizedRelativePath.startsWith(`${exclude}/`));
327
469
  };
328
470
  const shouldCopyToWorkerBundle = (src, workerBundleDirectory)=>{
329
471
  const relativePath = external_node_path_default().relative(workerBundleDirectory, src);
@@ -345,15 +487,16 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
345
487
  const workerEntryPath = external_node_path_default().join(outputDirectory, WORKER_ENTRY);
346
488
  const workerManifestPath = external_node_path_default().join(outputDirectory, WORKER_MANIFEST);
347
489
  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);
490
+ const wranglerConfigPath = external_node_path_default().join(outputDirectory, WRANGLER_CONFIG_FILE);
491
+ const cloudflareArtifacts = getCloudflareArtifacts(modernConfig);
492
+ const publicAssetExcludes = getPublicAssetExcludes(appDirectory, modernConfig);
350
493
  return {
351
494
  async prepare () {
352
495
  await utils_namespaceObject.fs.remove(outputDirectory);
353
496
  },
354
497
  async writeOutput () {
355
498
  await utils_namespaceObject.fs.copy(distDirectory, publicDirectory, {
356
- filter: (src)=>shouldCopyToPublicAssets(src, distDirectory)
499
+ filter: (src)=>shouldCopyToPublicAssets(src, distDirectory, publicAssetExcludes)
357
500
  });
358
501
  await utils_namespaceObject.fs.ensureDir(external_node_path_default().dirname(workerEntryPath));
359
502
  await utils_namespaceObject.fs.ensureDir(external_node_path_default().dirname(workerManifestPath));
@@ -369,27 +512,15 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
369
512
  type: 'commonjs'
370
513
  });
371
514
  }
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
- }, {
515
+ await copyCloudflareArtifacts(appDirectory, outputDirectory, cloudflareArtifacts);
516
+ await copyCloudflareD1Migrations(appDirectory, outputDirectory, modernConfig);
517
+ await utils_namespaceObject.fs.writeJSON(wranglerConfigPath, createWranglerConfig(appDirectory, modernConfig), {
387
518
  spaces: 2
388
519
  });
389
520
  await utils_namespaceObject.fs.writeJSON(workerManifestPath, await createWorkerManifest(outputDirectory, modernConfig), {
390
521
  spaces: 2
391
522
  });
392
- await utils_namespaceObject.fs.writeJSON(external_node_path_default().join(outputDirectory, 'package.json'), {
523
+ await utils_namespaceObject.fs.writeJSON(external_node_path_default().join(outputDirectory, OUTPUT_PACKAGE_FILE), {
393
524
  type: 'module'
394
525
  });
395
526
  },
@@ -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,111 @@ 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 assertNonEmptyString = (value, label)=>{
254
+ if ('string' != typeof value || 0 === value.trim().length) throw new Error(`${label} must be a non-empty string.`);
255
+ return value.trim();
256
+ };
257
+ const normalizeD1Database = (database, index)=>{
258
+ const binding = assertNonEmptyString(database.binding, `deploy.worker.d1Databases[${index}].binding`);
259
+ const databaseName = assertNonEmptyString(database.databaseName, `deploy.worker.d1Databases[${index}].databaseName`);
260
+ const databaseId = assertNonEmptyString(database.databaseId, `deploy.worker.d1Databases[${index}].databaseId`);
261
+ const migrationsDir = void 0 === database.migrationsDir ? void 0 : normalizeRelativePath(database.migrationsDir, `deploy.worker.d1Databases[${index}].migrationsDir`, 'app root');
262
+ const previewDatabaseId = void 0 === database.previewDatabaseId ? void 0 : assertNonEmptyString(database.previewDatabaseId, `deploy.worker.d1Databases[${index}].previewDatabaseId`);
263
+ return {
264
+ binding,
265
+ database_name: databaseName,
266
+ database_id: databaseId,
267
+ ...void 0 === migrationsDir ? {} : {
268
+ migrations_dir: migrationsDir
269
+ },
270
+ ...void 0 === previewDatabaseId ? {} : {
271
+ preview_database_id: previewDatabaseId
272
+ },
273
+ ...void 0 === database.remote ? {} : {
274
+ remote: database.remote
275
+ }
276
+ };
277
+ };
278
+ const createWranglerD1Databases = (modernConfig, configuredWranglerD1)=>{
279
+ const d1Databases = modernConfig.deploy?.worker?.d1Databases;
280
+ if (void 0 === d1Databases) return configuredWranglerD1;
281
+ if (void 0 !== configuredWranglerD1) throw new Error('Use deploy.worker.d1Databases or deploy.worker.wrangler.d1_databases, not both.');
282
+ if (!Array.isArray(d1Databases)) throw new Error('deploy.worker.d1Databases must be an array.');
283
+ return d1Databases.map(normalizeD1Database);
284
+ };
285
+ const createWranglerConfig = (appDirectory, modernConfig)=>{
286
+ const wrangler = getConfiguredWrangler(modernConfig);
287
+ const d1Databases = createWranglerD1Databases(modernConfig, wrangler.d1_databases);
288
+ return {
289
+ $schema: 'node_modules/wrangler/config-schema.json',
290
+ name: getConfiguredWorkerName(appDirectory, modernConfig),
291
+ compatibility_date: getCompatibilityDate(modernConfig),
292
+ ...wrangler,
293
+ main: WORKER_ENTRY,
294
+ compatibility_flags: createWranglerCompatibilityFlags(wrangler.compatibility_flags),
295
+ assets: createWranglerAssetsConfig(wrangler.assets),
296
+ ...void 0 === d1Databases ? {} : {
297
+ d1_databases: d1Databases
298
+ }
299
+ };
300
+ };
301
+ const normalizeCloudflareArtifact = (artifact, index)=>{
302
+ const from = normalizeRelativePath(artifact.from, `deploy.worker.artifacts[${index}].from`, 'app root');
303
+ const to = normalizeRelativePath(artifact.to, `deploy.worker.artifacts[${index}].to`, 'Cloudflare output');
304
+ const [topLevelDestination] = to.split('/');
305
+ const reservedDestination = RESERVED_ARTIFACT_DESTINATION_FILES.has(to) ? to : topLevelDestination;
306
+ 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)}.`);
307
+ return {
308
+ from,
309
+ to,
310
+ index
311
+ };
312
+ };
313
+ const getCloudflareArtifacts = (modernConfig)=>(modernConfig.deploy?.worker?.artifacts ?? []).map(normalizeCloudflareArtifact);
314
+ const copyCloudflareArtifacts = async (appDirectory, outputDirectory, artifacts)=>{
315
+ for (const artifact of artifacts){
316
+ const sourcePath = node_path.join(appDirectory, artifact.from);
317
+ if (!await fs.pathExists(sourcePath)) throw new Error(`deploy.worker.artifacts[${artifact.index}].from does not exist: ${artifact.from}`);
318
+ await fs.copy(sourcePath, node_path.join(outputDirectory, artifact.to));
319
+ }
320
+ };
321
+ const copyCloudflareD1Migrations = async (appDirectory, outputDirectory, modernConfig)=>{
322
+ for (const [index, database] of (modernConfig.deploy?.worker?.d1Databases ?? []).entries()){
323
+ if (!database.migrationsDir) continue;
324
+ const migrationsDir = normalizeRelativePath(database.migrationsDir, `deploy.worker.d1Databases[${index}].migrationsDir`, 'app root');
325
+ const sourcePath = node_path.join(appDirectory, migrationsDir);
326
+ if (!await fs.pathExists(sourcePath)) throw new Error(`deploy.worker.d1Databases[${index}].migrationsDir does not exist: ${migrationsDir}`);
327
+ await fs.copy(sourcePath, node_path.join(outputDirectory, migrationsDir));
328
+ }
329
+ };
198
330
  const readRouteSpec = async (outputDirectory)=>{
199
331
  const routeSpecPath = node_path.join(outputDirectory, ROUTE_SPEC_OUTPUT);
200
332
  if (!await fs.pathExists(routeSpecPath)) return {
@@ -206,7 +338,7 @@ const readRouteSpec = async (outputDirectory)=>{
206
338
  routes: Array.isArray(routeSpec.routes) ? routeSpec.routes : []
207
339
  };
208
340
  };
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}`);
341
+ 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
342
  const createWorkerManifest = async (outputDirectory, modernConfig)=>{
211
343
  const routeSpec = await readRouteSpec(outputDirectory);
212
344
  const routes = await Promise.all(routeSpec.routes.map(async (route)=>{
@@ -222,9 +354,9 @@ const createWorkerManifest = async (outputDirectory, modernConfig)=>{
222
354
  }));
223
355
  const bffPrefix = modernConfig.bff?.prefix;
224
356
  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);
357
+ const isEffectApi = Boolean(modernConfig.bff) && modernConfig.bff?.runtimeFramework !== 'hono';
358
+ const effectApiWorkerExists = await fs.pathExists(node_path.join(outputDirectory, BFF_EFFECT_WORKER_ENTRY));
359
+ if (isEffectApi && primaryBffPrefix && !effectApiWorkerExists) throw createMissingEffectBffWorkerError(outputDirectory, BFF_EFFECT_WORKER_ENTRY);
228
360
  return {
229
361
  version: 1,
230
362
  runtime: {
@@ -253,7 +385,7 @@ const createWorkerManifest = async (outputDirectory, modernConfig)=>{
253
385
  routeManifest: ROUTE_MANIFEST_FILE
254
386
  },
255
387
  security: createCloudflareWorkerSecurityPolicy(modernConfig),
256
- bff: isEffectBff && primaryBffPrefix && effectBffWorkerExists ? {
388
+ bff: isEffectApi && primaryBffPrefix && effectApiWorkerExists ? {
257
389
  runtimeFramework: 'effect',
258
390
  prefix: primaryBffPrefix,
259
391
  worker: BFF_EFFECT_WORKER_ENTRY
@@ -276,12 +408,22 @@ const createWorkerModuleLoaders = (manifest)=>{
276
408
  ].map(([worker, loader])=>`${JSON.stringify(worker)}: ${loader}`);
277
409
  return `{\n${loaders.join(',\n')}\n}`;
278
410
  };
279
- const shouldCopyToPublicAssets = (src, distDirectory)=>{
411
+ const getPublicAssetExcludes = (appDirectory, modernConfig)=>[
412
+ ...DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES.filter((directory)=>{
413
+ try {
414
+ return fs.statSync(node_path.join(appDirectory, directory)).isDirectory();
415
+ } catch {
416
+ return false;
417
+ }
418
+ }),
419
+ ...modernConfig.deploy?.worker?.publicAssetExcludes ?? []
420
+ ].map((entry)=>normalizeRelativePath(entry, 'deploy.worker.publicAssetExcludes'));
421
+ const shouldCopyToPublicAssets = (src, distDirectory, publicAssetExcludes)=>{
280
422
  const relativePath = node_path.relative(distDirectory, src);
281
423
  if (!relativePath) return true;
282
424
  const normalizedRelativePath = relativePath.replace(/\\/g, '/');
283
425
  const [topLevelDirectory] = normalizedRelativePath.split('/');
284
- return normalizedRelativePath !== ROUTE_SPEC_FILE && topLevelDirectory !== WORKER_BUNDLE_DIRECTORY && topLevelDirectory !== SERVER_BUNDLE_DIRECTORY;
426
+ return normalizedRelativePath !== ROUTE_SPEC_FILE && topLevelDirectory !== WORKER_BUNDLE_DIRECTORY && topLevelDirectory !== SERVER_BUNDLE_DIRECTORY && !publicAssetExcludes.some((exclude)=>normalizedRelativePath === exclude || normalizedRelativePath.startsWith(`${exclude}/`));
285
427
  };
286
428
  const shouldCopyToWorkerBundle = (src, workerBundleDirectory)=>{
287
429
  const relativePath = node_path.relative(workerBundleDirectory, src);
@@ -303,15 +445,16 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
303
445
  const workerEntryPath = node_path.join(outputDirectory, WORKER_ENTRY);
304
446
  const workerManifestPath = node_path.join(outputDirectory, WORKER_MANIFEST);
305
447
  const routeSpecOutputPath = node_path.join(outputDirectory, ROUTE_SPEC_OUTPUT);
306
- const wranglerConfigPath = node_path.join(outputDirectory, 'wrangler.json');
307
- const workerName = getConfiguredWorkerName(appDirectory, modernConfig);
448
+ const wranglerConfigPath = node_path.join(outputDirectory, WRANGLER_CONFIG_FILE);
449
+ const cloudflareArtifacts = getCloudflareArtifacts(modernConfig);
450
+ const publicAssetExcludes = getPublicAssetExcludes(appDirectory, modernConfig);
308
451
  return {
309
452
  async prepare () {
310
453
  await fs.remove(outputDirectory);
311
454
  },
312
455
  async writeOutput () {
313
456
  await fs.copy(distDirectory, publicDirectory, {
314
- filter: (src)=>shouldCopyToPublicAssets(src, distDirectory)
457
+ filter: (src)=>shouldCopyToPublicAssets(src, distDirectory, publicAssetExcludes)
315
458
  });
316
459
  await fs.ensureDir(node_path.dirname(workerEntryPath));
317
460
  await fs.ensureDir(node_path.dirname(workerManifestPath));
@@ -327,27 +470,15 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
327
470
  type: 'commonjs'
328
471
  });
329
472
  }
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
- }, {
473
+ await copyCloudflareArtifacts(appDirectory, outputDirectory, cloudflareArtifacts);
474
+ await copyCloudflareD1Migrations(appDirectory, outputDirectory, modernConfig);
475
+ await fs.writeJSON(wranglerConfigPath, createWranglerConfig(appDirectory, modernConfig), {
345
476
  spaces: 2
346
477
  });
347
478
  await fs.writeJSON(workerManifestPath, await createWorkerManifest(outputDirectory, modernConfig), {
348
479
  spaces: 2
349
480
  });
350
- await fs.writeJSON(node_path.join(outputDirectory, 'package.json'), {
481
+ await fs.writeJSON(node_path.join(outputDirectory, OUTPUT_PACKAGE_FILE), {
351
482
  type: 'module'
352
483
  });
353
484
  },
@@ -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,111 @@ 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 assertNonEmptyString = (value, label)=>{
255
+ if ('string' != typeof value || 0 === value.trim().length) throw new Error(`${label} must be a non-empty string.`);
256
+ return value.trim();
257
+ };
258
+ const normalizeD1Database = (database, index)=>{
259
+ const binding = assertNonEmptyString(database.binding, `deploy.worker.d1Databases[${index}].binding`);
260
+ const databaseName = assertNonEmptyString(database.databaseName, `deploy.worker.d1Databases[${index}].databaseName`);
261
+ const databaseId = assertNonEmptyString(database.databaseId, `deploy.worker.d1Databases[${index}].databaseId`);
262
+ const migrationsDir = void 0 === database.migrationsDir ? void 0 : normalizeRelativePath(database.migrationsDir, `deploy.worker.d1Databases[${index}].migrationsDir`, 'app root');
263
+ const previewDatabaseId = void 0 === database.previewDatabaseId ? void 0 : assertNonEmptyString(database.previewDatabaseId, `deploy.worker.d1Databases[${index}].previewDatabaseId`);
264
+ return {
265
+ binding,
266
+ database_name: databaseName,
267
+ database_id: databaseId,
268
+ ...void 0 === migrationsDir ? {} : {
269
+ migrations_dir: migrationsDir
270
+ },
271
+ ...void 0 === previewDatabaseId ? {} : {
272
+ preview_database_id: previewDatabaseId
273
+ },
274
+ ...void 0 === database.remote ? {} : {
275
+ remote: database.remote
276
+ }
277
+ };
278
+ };
279
+ const createWranglerD1Databases = (modernConfig, configuredWranglerD1)=>{
280
+ const d1Databases = modernConfig.deploy?.worker?.d1Databases;
281
+ if (void 0 === d1Databases) return configuredWranglerD1;
282
+ if (void 0 !== configuredWranglerD1) throw new Error('Use deploy.worker.d1Databases or deploy.worker.wrangler.d1_databases, not both.');
283
+ if (!Array.isArray(d1Databases)) throw new Error('deploy.worker.d1Databases must be an array.');
284
+ return d1Databases.map(normalizeD1Database);
285
+ };
286
+ const createWranglerConfig = (appDirectory, modernConfig)=>{
287
+ const wrangler = getConfiguredWrangler(modernConfig);
288
+ const d1Databases = createWranglerD1Databases(modernConfig, wrangler.d1_databases);
289
+ return {
290
+ $schema: 'node_modules/wrangler/config-schema.json',
291
+ name: getConfiguredWorkerName(appDirectory, modernConfig),
292
+ compatibility_date: getCompatibilityDate(modernConfig),
293
+ ...wrangler,
294
+ main: WORKER_ENTRY,
295
+ compatibility_flags: createWranglerCompatibilityFlags(wrangler.compatibility_flags),
296
+ assets: createWranglerAssetsConfig(wrangler.assets),
297
+ ...void 0 === d1Databases ? {} : {
298
+ d1_databases: d1Databases
299
+ }
300
+ };
301
+ };
302
+ const normalizeCloudflareArtifact = (artifact, index)=>{
303
+ const from = normalizeRelativePath(artifact.from, `deploy.worker.artifacts[${index}].from`, 'app root');
304
+ const to = normalizeRelativePath(artifact.to, `deploy.worker.artifacts[${index}].to`, 'Cloudflare output');
305
+ const [topLevelDestination] = to.split('/');
306
+ const reservedDestination = RESERVED_ARTIFACT_DESTINATION_FILES.has(to) ? to : topLevelDestination;
307
+ 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)}.`);
308
+ return {
309
+ from,
310
+ to,
311
+ index
312
+ };
313
+ };
314
+ const getCloudflareArtifacts = (modernConfig)=>(modernConfig.deploy?.worker?.artifacts ?? []).map(normalizeCloudflareArtifact);
315
+ const copyCloudflareArtifacts = async (appDirectory, outputDirectory, artifacts)=>{
316
+ for (const artifact of artifacts){
317
+ const sourcePath = node_path.join(appDirectory, artifact.from);
318
+ if (!await fs.pathExists(sourcePath)) throw new Error(`deploy.worker.artifacts[${artifact.index}].from does not exist: ${artifact.from}`);
319
+ await fs.copy(sourcePath, node_path.join(outputDirectory, artifact.to));
320
+ }
321
+ };
322
+ const copyCloudflareD1Migrations = async (appDirectory, outputDirectory, modernConfig)=>{
323
+ for (const [index, database] of (modernConfig.deploy?.worker?.d1Databases ?? []).entries()){
324
+ if (!database.migrationsDir) continue;
325
+ const migrationsDir = normalizeRelativePath(database.migrationsDir, `deploy.worker.d1Databases[${index}].migrationsDir`, 'app root');
326
+ const sourcePath = node_path.join(appDirectory, migrationsDir);
327
+ if (!await fs.pathExists(sourcePath)) throw new Error(`deploy.worker.d1Databases[${index}].migrationsDir does not exist: ${migrationsDir}`);
328
+ await fs.copy(sourcePath, node_path.join(outputDirectory, migrationsDir));
329
+ }
330
+ };
199
331
  const readRouteSpec = async (outputDirectory)=>{
200
332
  const routeSpecPath = node_path.join(outputDirectory, ROUTE_SPEC_OUTPUT);
201
333
  if (!await fs.pathExists(routeSpecPath)) return {
@@ -207,7 +339,7 @@ const readRouteSpec = async (outputDirectory)=>{
207
339
  routes: Array.isArray(routeSpec.routes) ? routeSpec.routes : []
208
340
  };
209
341
  };
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}`);
342
+ 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
343
  const createWorkerManifest = async (outputDirectory, modernConfig)=>{
212
344
  const routeSpec = await readRouteSpec(outputDirectory);
213
345
  const routes = await Promise.all(routeSpec.routes.map(async (route)=>{
@@ -223,9 +355,9 @@ const createWorkerManifest = async (outputDirectory, modernConfig)=>{
223
355
  }));
224
356
  const bffPrefix = modernConfig.bff?.prefix;
225
357
  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);
358
+ const isEffectApi = Boolean(modernConfig.bff) && modernConfig.bff?.runtimeFramework !== 'hono';
359
+ const effectApiWorkerExists = await fs.pathExists(node_path.join(outputDirectory, BFF_EFFECT_WORKER_ENTRY));
360
+ if (isEffectApi && primaryBffPrefix && !effectApiWorkerExists) throw createMissingEffectBffWorkerError(outputDirectory, BFF_EFFECT_WORKER_ENTRY);
229
361
  return {
230
362
  version: 1,
231
363
  runtime: {
@@ -254,7 +386,7 @@ const createWorkerManifest = async (outputDirectory, modernConfig)=>{
254
386
  routeManifest: ROUTE_MANIFEST_FILE
255
387
  },
256
388
  security: createCloudflareWorkerSecurityPolicy(modernConfig),
257
- bff: isEffectBff && primaryBffPrefix && effectBffWorkerExists ? {
389
+ bff: isEffectApi && primaryBffPrefix && effectApiWorkerExists ? {
258
390
  runtimeFramework: 'effect',
259
391
  prefix: primaryBffPrefix,
260
392
  worker: BFF_EFFECT_WORKER_ENTRY
@@ -277,12 +409,22 @@ const createWorkerModuleLoaders = (manifest)=>{
277
409
  ].map(([worker, loader])=>`${JSON.stringify(worker)}: ${loader}`);
278
410
  return `{\n${loaders.join(',\n')}\n}`;
279
411
  };
280
- const shouldCopyToPublicAssets = (src, distDirectory)=>{
412
+ const getPublicAssetExcludes = (appDirectory, modernConfig)=>[
413
+ ...DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES.filter((directory)=>{
414
+ try {
415
+ return fs.statSync(node_path.join(appDirectory, directory)).isDirectory();
416
+ } catch {
417
+ return false;
418
+ }
419
+ }),
420
+ ...modernConfig.deploy?.worker?.publicAssetExcludes ?? []
421
+ ].map((entry)=>normalizeRelativePath(entry, 'deploy.worker.publicAssetExcludes'));
422
+ const shouldCopyToPublicAssets = (src, distDirectory, publicAssetExcludes)=>{
281
423
  const relativePath = node_path.relative(distDirectory, src);
282
424
  if (!relativePath) return true;
283
425
  const normalizedRelativePath = relativePath.replace(/\\/g, '/');
284
426
  const [topLevelDirectory] = normalizedRelativePath.split('/');
285
- return normalizedRelativePath !== ROUTE_SPEC_FILE && topLevelDirectory !== WORKER_BUNDLE_DIRECTORY && topLevelDirectory !== SERVER_BUNDLE_DIRECTORY;
427
+ return normalizedRelativePath !== ROUTE_SPEC_FILE && topLevelDirectory !== WORKER_BUNDLE_DIRECTORY && topLevelDirectory !== SERVER_BUNDLE_DIRECTORY && !publicAssetExcludes.some((exclude)=>normalizedRelativePath === exclude || normalizedRelativePath.startsWith(`${exclude}/`));
286
428
  };
287
429
  const shouldCopyToWorkerBundle = (src, workerBundleDirectory)=>{
288
430
  const relativePath = node_path.relative(workerBundleDirectory, src);
@@ -304,15 +446,16 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
304
446
  const workerEntryPath = node_path.join(outputDirectory, WORKER_ENTRY);
305
447
  const workerManifestPath = node_path.join(outputDirectory, WORKER_MANIFEST);
306
448
  const routeSpecOutputPath = node_path.join(outputDirectory, ROUTE_SPEC_OUTPUT);
307
- const wranglerConfigPath = node_path.join(outputDirectory, 'wrangler.json');
308
- const workerName = getConfiguredWorkerName(appDirectory, modernConfig);
449
+ const wranglerConfigPath = node_path.join(outputDirectory, WRANGLER_CONFIG_FILE);
450
+ const cloudflareArtifacts = getCloudflareArtifacts(modernConfig);
451
+ const publicAssetExcludes = getPublicAssetExcludes(appDirectory, modernConfig);
309
452
  return {
310
453
  async prepare () {
311
454
  await fs.remove(outputDirectory);
312
455
  },
313
456
  async writeOutput () {
314
457
  await fs.copy(distDirectory, publicDirectory, {
315
- filter: (src)=>shouldCopyToPublicAssets(src, distDirectory)
458
+ filter: (src)=>shouldCopyToPublicAssets(src, distDirectory, publicAssetExcludes)
316
459
  });
317
460
  await fs.ensureDir(node_path.dirname(workerEntryPath));
318
461
  await fs.ensureDir(node_path.dirname(workerManifestPath));
@@ -328,27 +471,15 @@ const createCloudflarePreset = ({ appContext, modernConfig })=>{
328
471
  type: 'commonjs'
329
472
  });
330
473
  }
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
- }, {
474
+ await copyCloudflareArtifacts(appDirectory, outputDirectory, cloudflareArtifacts);
475
+ await copyCloudflareD1Migrations(appDirectory, outputDirectory, modernConfig);
476
+ await fs.writeJSON(wranglerConfigPath, createWranglerConfig(appDirectory, modernConfig), {
346
477
  spaces: 2
347
478
  });
348
479
  await fs.writeJSON(workerManifestPath, await createWorkerManifest(outputDirectory, modernConfig), {
349
480
  spaces: 2
350
481
  });
351
- await fs.writeJSON(node_path.join(outputDirectory, 'package.json'), {
482
+ await fs.writeJSON(node_path.join(outputDirectory, OUTPUT_PACKAGE_FILE), {
352
483
  type: 'module'
353
484
  });
354
485
  },
@@ -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,46 @@
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
+ }
17
+ export interface CloudflareWorkerD1DatabaseConfig {
18
+ /**
19
+ * Worker binding name, for example `DB`.
20
+ */
21
+ binding: string;
22
+ /**
23
+ * Cloudflare D1 database name.
24
+ */
25
+ databaseName: string;
26
+ /**
27
+ * Cloudflare D1 database id.
28
+ */
29
+ databaseId: string;
30
+ /**
31
+ * Optional local migrations directory, relative to the app root. When set,
32
+ * Modern.js stages it into `.output` and points Wrangler at the staged copy.
33
+ */
34
+ migrationsDir?: string;
35
+ /**
36
+ * Optional preview database id used by Wrangler preview/local flows.
37
+ */
38
+ previewDatabaseId?: string;
39
+ /**
40
+ * Wrangler remote flag for D1 commands.
41
+ */
42
+ remote?: boolean;
43
+ }
3
44
  export interface CloudflareWorkerSecurityCspConfig {
4
45
  mode?: CloudflareWorkerSecurityCspMode;
5
46
  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, CloudflareWorkerD1DatabaseConfig, CloudflareWorkerSecurityConfig, DeployTarget, JsonValue } from './cloudflareDeploy';
2
+ export type { CloudflareWorkerArtifactConfig, CloudflareWorkerD1DatabaseConfig, 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,30 @@ 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
+ * First-class Cloudflare D1 bindings. Modern.js writes these to
52
+ * `wrangler.json` as `d1_databases` and stages configured migration
53
+ * directories into `.output`.
54
+ */
55
+ d1Databases?: CloudflareWorkerD1DatabaseConfig[];
56
+ /**
57
+ * Dist output paths that must not be copied into Cloudflare public assets.
58
+ * Entries are slash-normalized path prefixes relative to the app dist root.
59
+ * Top-level `api` and `shared` dist directories are excluded when matching
60
+ * source directories exist in the app root because they conventionally
61
+ * contain server-only implementation code.
62
+ */
63
+ publicAssetExcludes?: string[];
39
64
  };
40
65
  }
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.1",
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.1",
94
+ "@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.5.0-ultramodern.1",
95
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.1",
96
+ "@modern-js/plugin-data-loader": "npm:@bleedingdev/modern-js-plugin-data-loader@3.5.0-ultramodern.1",
97
+ "@modern-js/prod-server": "npm:@bleedingdev/modern-js-prod-server@3.5.0-ultramodern.1",
98
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.1",
99
+ "@modern-js/server": "npm:@bleedingdev/modern-js-server@3.5.0-ultramodern.1",
100
+ "@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.5.0-ultramodern.1",
101
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.1",
102
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.1"
103
103
  },
104
104
  "devDependencies": {
105
105
  "@rslib/core": "0.23.0",