@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.
- package/dist/cjs/builder/generator/getBuilderEnvironments.js +8 -5
- package/dist/cjs/plugins/deploy/platforms/cloudflare.js +158 -27
- package/dist/cjs/plugins/deploy/platforms/templates/cloudflare-entry.mjs +14 -1
- package/dist/esm/builder/generator/getBuilderEnvironments.mjs +8 -5
- package/dist/esm/plugins/deploy/platforms/cloudflare.mjs +158 -27
- package/dist/esm/plugins/deploy/platforms/templates/cloudflare-entry.mjs +14 -1
- package/dist/esm-node/builder/generator/getBuilderEnvironments.mjs +8 -5
- package/dist/esm-node/plugins/deploy/platforms/cloudflare.mjs +158 -27
- package/dist/esm-node/plugins/deploy/platforms/templates/cloudflare-entry.mjs +14 -1
- package/dist/types/types/config/cloudflareDeploy.d.ts +41 -0
- package/dist/types/types/config/deploy.d.ts +27 -2
- package/package.json +11 -11
|
@@ -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
|
-
|
|
141
|
-
|
|
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
|
|
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 =
|
|
275
|
+
const workerEntries = effectApiEntry ? {
|
|
273
276
|
...baseWorkerEntries,
|
|
274
277
|
[BFF_EFFECT_WORKER_ENTRY_NAME]: [
|
|
275
|
-
`${
|
|
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
|
|
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
|
|
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
|
|
268
|
-
const
|
|
269
|
-
if (
|
|
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:
|
|
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
|
|
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,
|
|
349
|
-
const
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
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 =
|
|
232
|
+
const workerEntries = effectApiEntry ? {
|
|
230
233
|
...baseWorkerEntries,
|
|
231
234
|
[BFF_EFFECT_WORKER_ENTRY_NAME]: [
|
|
232
|
-
`${
|
|
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
|
|
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
|
|
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
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
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:
|
|
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
|
|
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,
|
|
307
|
-
const
|
|
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
|
|
331
|
-
|
|
332
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
103
|
-
|
|
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
|
|
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 =
|
|
237
|
+
const workerEntries = effectApiEntry ? {
|
|
235
238
|
...baseWorkerEntries,
|
|
236
239
|
[BFF_EFFECT_WORKER_ENTRY_NAME]: [
|
|
237
|
-
`${
|
|
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
|
|
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
|
|
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
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
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:
|
|
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
|
|
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,
|
|
308
|
-
const
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
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,
|
|
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 =
|
|
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.
|
|
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.
|
|
94
|
-
"@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.
|
|
95
|
-
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.
|
|
96
|
-
"@modern-js/plugin-data-loader": "npm:@bleedingdev/modern-js-plugin-data-loader@3.
|
|
97
|
-
"@modern-js/server
|
|
98
|
-
"@modern-js/server": "npm:@bleedingdev/modern-js-server@3.
|
|
99
|
-
"@modern-js/
|
|
100
|
-
"@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.
|
|
101
|
-
"@modern-js/
|
|
102
|
-
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.
|
|
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",
|