@bleedingdev/modern-js-app-tools 3.5.0-ultramodern.3 → 3.5.0-ultramodern.30
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 +1 -0
- package/dist/cjs/index.js +50 -44
- package/dist/cjs/plugins/backendFederationBuild.js +347 -0
- package/dist/cjs/plugins/deploy/platforms/cloudflare-output-contract.js +91 -0
- package/dist/cjs/plugins/deploy/platforms/cloudflare-output-plan.js +93 -0
- package/dist/cjs/plugins/deploy/platforms/cloudflare-output-verifier.js +402 -0
- package/dist/cjs/plugins/deploy/platforms/cloudflare.js +272 -29
- package/dist/cjs/plugins/deploy/platforms/templates/cloudflare-entry.mjs +346 -43
- package/dist/esm/builder/generator/getBuilderEnvironments.mjs +1 -0
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/plugins/backendFederationBuild.mjs +296 -0
- package/dist/esm/plugins/deploy/platforms/cloudflare-output-contract.mjs +17 -0
- package/dist/esm/plugins/deploy/platforms/cloudflare-output-plan.mjs +45 -0
- package/dist/esm/plugins/deploy/platforms/cloudflare-output-verifier.mjs +347 -0
- package/dist/esm/plugins/deploy/platforms/cloudflare.mjs +272 -29
- package/dist/esm/plugins/deploy/platforms/templates/cloudflare-entry.mjs +346 -43
- package/dist/esm-node/builder/generator/getBuilderEnvironments.mjs +1 -0
- package/dist/esm-node/index.mjs +2 -0
- package/dist/esm-node/plugins/backendFederationBuild.mjs +297 -0
- package/dist/esm-node/plugins/deploy/platforms/cloudflare-output-contract.mjs +18 -0
- package/dist/esm-node/plugins/deploy/platforms/cloudflare-output-plan.mjs +46 -0
- package/dist/esm-node/plugins/deploy/platforms/cloudflare-output-verifier.mjs +348 -0
- package/dist/esm-node/plugins/deploy/platforms/cloudflare.mjs +272 -29
- package/dist/esm-node/plugins/deploy/platforms/templates/cloudflare-entry.mjs +346 -43
- package/dist/types/plugins/backendFederationBuild.d.ts +11 -0
- package/dist/types/plugins/deploy/platforms/cloudflare-output-contract.d.ts +13 -0
- package/dist/types/plugins/deploy/platforms/cloudflare-output-plan.d.ts +33 -0
- package/dist/types/plugins/deploy/platforms/cloudflare-output-verifier.d.ts +32 -0
- package/dist/types/types/config/cloudflareDeploy.d.ts +22 -0
- package/dist/types/types/config/deploy.d.ts +15 -2
- package/lib/types.d.ts +2 -3
- package/package.json +17 -11
|
@@ -2,6 +2,8 @@ const ASSETS_BINDING = 'ASSETS';
|
|
|
2
2
|
const MODERN_WORKER_MANIFEST = p_workerManifest;
|
|
3
3
|
const WORKER_MODULE_LOADERS = p_workerModuleLoaders;
|
|
4
4
|
const workerModulePromises = new Map();
|
|
5
|
+
const effectBffDispatcherPromises = new Map();
|
|
6
|
+
let effectEdgeRuntimePromise;
|
|
5
7
|
const remoteJsonPromises = new Map();
|
|
6
8
|
const CORS_POLICY = MODERN_WORKER_MANIFEST.security?.cors || {};
|
|
7
9
|
const ASSET_CORS_ENABLED = false !== CORS_POLICY.assets;
|
|
@@ -150,19 +152,10 @@ async function createCorsPreflightResponse(request, env) {
|
|
|
150
152
|
status: 204
|
|
151
153
|
});
|
|
152
154
|
}
|
|
153
|
-
|
|
154
|
-
const assets = env?.[ASSETS_BINDING];
|
|
155
|
-
if (!assets || 'function' != typeof assets.fetch) return null;
|
|
156
|
-
const assetResponse = await assets.fetch(new Request(request.url, {
|
|
157
|
-
method: 'HEAD'
|
|
158
|
-
}));
|
|
159
|
-
if (!assetResponse || 404 === assetResponse.status) return null;
|
|
160
|
-
return new Response(null, {
|
|
161
|
-
headers: ASSET_CORS_HEADERS,
|
|
162
|
-
status: 204
|
|
163
|
-
});
|
|
155
|
+
return null;
|
|
164
156
|
}
|
|
165
157
|
async function fetchAsset(request, env) {
|
|
158
|
+
if ('GET' !== request.method && 'HEAD' !== request.method) return null;
|
|
166
159
|
const assets = env?.[ASSETS_BINDING];
|
|
167
160
|
if (!assets || 'function' != typeof assets.fetch) return null;
|
|
168
161
|
const response = await assets.fetch(request);
|
|
@@ -211,7 +204,7 @@ function getPathExtension(pathname) {
|
|
|
211
204
|
}
|
|
212
205
|
function isAssetLikePathname(pathname) {
|
|
213
206
|
const extension = getPathExtension(pathname);
|
|
214
|
-
return '' !== extension
|
|
207
|
+
return '' !== extension;
|
|
215
208
|
}
|
|
216
209
|
function routeMatchesExactly(route, pathname) {
|
|
217
210
|
if ('string' != typeof route?.urlPath) return false;
|
|
@@ -223,6 +216,255 @@ function routeMatches(route, pathname) {
|
|
|
223
216
|
const requestPath = normalizeRoutePath(pathname);
|
|
224
217
|
return routePath === requestPath || '/' === routePath && route.isSSR || requestPath.startsWith(`${routePath}/`);
|
|
225
218
|
}
|
|
219
|
+
function getRouteI18nEntry(route) {
|
|
220
|
+
if (!route || 'string' != typeof route.entryName) return;
|
|
221
|
+
return MODERN_WORKER_MANIFEST.i18n?.entries?.[route.entryName];
|
|
222
|
+
}
|
|
223
|
+
function getRouteBasePath(route) {
|
|
224
|
+
const routePath = 'string' == typeof route?.urlPath ? normalizeRoutePath(route.urlPath) : '/';
|
|
225
|
+
return routePath || '/';
|
|
226
|
+
}
|
|
227
|
+
function getRouteRemainingPathname(route, pathname) {
|
|
228
|
+
const basePath = getRouteBasePath(route);
|
|
229
|
+
if ('/' === basePath) return pathname || '/';
|
|
230
|
+
if (!matchesPrefix(pathname, basePath)) return pathname || '/';
|
|
231
|
+
const remaining = pathname.slice(basePath.length);
|
|
232
|
+
return remaining ? remaining.startsWith('/') ? remaining : `/${remaining}` : '/';
|
|
233
|
+
}
|
|
234
|
+
function normalizeLanguage(value, languages) {
|
|
235
|
+
if ('string' != typeof value || '' === value.trim()) return null;
|
|
236
|
+
const normalized = value.trim().toLowerCase();
|
|
237
|
+
const exact = languages.find((language)=>language.toLowerCase() === normalized);
|
|
238
|
+
if (exact) return exact;
|
|
239
|
+
const baseLanguage = normalized.split(/[-_]/u)[0];
|
|
240
|
+
return languages.find((language)=>language.toLowerCase() === baseLanguage) || null;
|
|
241
|
+
}
|
|
242
|
+
function getLanguageFromPath(route, pathname, languages) {
|
|
243
|
+
const remainingPathname = getRouteRemainingPathname(route, pathname);
|
|
244
|
+
const firstSegment = remainingPathname.split('/').filter(Boolean)[0];
|
|
245
|
+
return normalizeLanguage(firstSegment, languages);
|
|
246
|
+
}
|
|
247
|
+
function safeDecodeURIComponent(value) {
|
|
248
|
+
try {
|
|
249
|
+
return decodeURIComponent(value);
|
|
250
|
+
} catch {
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function detectLanguageFromCookie(cookieHeader, lookupCookie, languages) {
|
|
255
|
+
if (!cookieHeader) return null;
|
|
256
|
+
for (const cookiePart of cookieHeader.split(';')){
|
|
257
|
+
const [rawName, ...rawValue] = cookiePart.trim().split('=');
|
|
258
|
+
if (rawName === lookupCookie && 0 !== rawValue.length) return normalizeLanguage(safeDecodeURIComponent(rawValue.join('=')), languages);
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
function detectLanguageFromAcceptLanguage(acceptLanguage, languages) {
|
|
263
|
+
if (!acceptLanguage) return null;
|
|
264
|
+
return acceptLanguage.split(',').map((entry, index)=>{
|
|
265
|
+
const [rawRange, ...rawParameters] = entry.trim().split(';');
|
|
266
|
+
const qualityParameter = rawParameters.find((parameter)=>parameter.trim().startsWith('q='));
|
|
267
|
+
const quality = qualityParameter ? Number.parseFloat(qualityParameter.split('=')[1] || '') : 1;
|
|
268
|
+
return {
|
|
269
|
+
index,
|
|
270
|
+
language: normalizeLanguage(rawRange, languages),
|
|
271
|
+
quality: Number.isFinite(quality) ? quality : 1
|
|
272
|
+
};
|
|
273
|
+
}).filter((candidate)=>candidate.language && candidate.quality > 0).sort((left, right)=>right.quality - left.quality || left.index - right.index)[0]?.language || null;
|
|
274
|
+
}
|
|
275
|
+
function detectRedirectLanguage(request, entry) {
|
|
276
|
+
if (!entry.i18nextDetector) return null;
|
|
277
|
+
const url = new URL(request.url);
|
|
278
|
+
const detection = entry.detection || {};
|
|
279
|
+
const order = Array.isArray(detection.order) ? detection.order : [
|
|
280
|
+
'querystring',
|
|
281
|
+
'cookie',
|
|
282
|
+
'header'
|
|
283
|
+
];
|
|
284
|
+
for (const method of order){
|
|
285
|
+
const language = 'querystring' === method ? normalizeLanguage(url.searchParams.get(detection.lookupQuerystring || 'lng'), entry.languages) : 'cookie' === method ? detectLanguageFromCookie(request.headers.get('cookie'), detection.lookupCookie || 'i18next', entry.languages) : 'header' === method ? detectLanguageFromAcceptLanguage(request.headers.get(detection.lookupHeader || 'accept-language'), entry.languages) : null;
|
|
286
|
+
if (language) return language;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
function normaliseSlashes(pathname) {
|
|
291
|
+
const withoutDuplicateSlashes = pathname.replace(/\/+/g, '/');
|
|
292
|
+
const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
|
|
293
|
+
return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/u, '') : withLeadingSlash;
|
|
294
|
+
}
|
|
295
|
+
function normalisePathPattern(pathname) {
|
|
296
|
+
return normaliseSlashes(pathname).replace(/\[(.+?)\]/g, ':$1');
|
|
297
|
+
}
|
|
298
|
+
function stripLanguagePrefix(pathname, languages) {
|
|
299
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
300
|
+
const firstSegment = segments[0];
|
|
301
|
+
if (firstSegment && normalizeLanguage(firstSegment, languages)) return `/${segments.slice(1).join('/')}` || '/';
|
|
302
|
+
return pathname || '/';
|
|
303
|
+
}
|
|
304
|
+
function escapeRegExp(value) {
|
|
305
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
306
|
+
}
|
|
307
|
+
function getPatternParamName(segment) {
|
|
308
|
+
return segment.slice(1).replace(/\?$/, '');
|
|
309
|
+
}
|
|
310
|
+
function compilePathPattern(pattern) {
|
|
311
|
+
const names = [];
|
|
312
|
+
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
313
|
+
const source = segments.map((segment)=>{
|
|
314
|
+
if (segment.startsWith(':')) {
|
|
315
|
+
names.push(getPatternParamName(segment));
|
|
316
|
+
return segment.endsWith('?') ? '(?:/([^/]+))?' : '/([^/]+)';
|
|
317
|
+
}
|
|
318
|
+
if ('*' === segment) {
|
|
319
|
+
names.push('*');
|
|
320
|
+
return '/(.*)';
|
|
321
|
+
}
|
|
322
|
+
return `/${escapeRegExp(segment)}`;
|
|
323
|
+
}).join('');
|
|
324
|
+
return {
|
|
325
|
+
names,
|
|
326
|
+
regexp: new RegExp(`^${source || '/'}$`)
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function matchPathPattern(pathname, pattern) {
|
|
330
|
+
const { names, regexp } = compilePathPattern(pattern);
|
|
331
|
+
const match = regexp.exec(normaliseSlashes(pathname));
|
|
332
|
+
if (!match) return null;
|
|
333
|
+
const params = {};
|
|
334
|
+
for(let index = 0; index < names.length; index++)try {
|
|
335
|
+
params[names[index]] = decodeURIComponent(match[index + 1] || '');
|
|
336
|
+
} catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
return params;
|
|
340
|
+
}
|
|
341
|
+
function buildPathFromPattern(pattern, params) {
|
|
342
|
+
const path = normalisePathPattern(pattern).split('/').filter(Boolean).map((segment)=>{
|
|
343
|
+
if (segment.startsWith(':')) {
|
|
344
|
+
const param = params[getPatternParamName(segment)];
|
|
345
|
+
return param ? encodeURIComponent(param) : '';
|
|
346
|
+
}
|
|
347
|
+
if ('*' === segment) return params['*'] || '';
|
|
348
|
+
return segment;
|
|
349
|
+
}).filter(Boolean).join('/');
|
|
350
|
+
return `/${path}`;
|
|
351
|
+
}
|
|
352
|
+
function patternSpecificity(pattern) {
|
|
353
|
+
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
354
|
+
let staticSegments = 0;
|
|
355
|
+
let dynamicSegments = 0;
|
|
356
|
+
let splatSegments = 0;
|
|
357
|
+
for (const segment of segments)if ('*' === segment) splatSegments++;
|
|
358
|
+
else if (segment.startsWith(':')) dynamicSegments++;
|
|
359
|
+
else staticSegments++;
|
|
360
|
+
return {
|
|
361
|
+
dynamicSegments,
|
|
362
|
+
splatSegments,
|
|
363
|
+
staticSegments,
|
|
364
|
+
totalSegments: segments.length
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function comparePatternSpecificity(left, right) {
|
|
368
|
+
const leftSpecificity = patternSpecificity(left);
|
|
369
|
+
const rightSpecificity = patternSpecificity(right);
|
|
370
|
+
return rightSpecificity.staticSegments - leftSpecificity.staticSegments || rightSpecificity.totalSegments - leftSpecificity.totalSegments || leftSpecificity.splatSegments - rightSpecificity.splatSegments || leftSpecificity.dynamicSegments - rightSpecificity.dynamicSegments;
|
|
371
|
+
}
|
|
372
|
+
function sortLocalisedPatterns(patterns) {
|
|
373
|
+
return patterns.map((pattern, index)=>({
|
|
374
|
+
pattern,
|
|
375
|
+
index
|
|
376
|
+
})).sort((left, right)=>comparePatternSpecificity(left.pattern.pattern, right.pattern.pattern) || left.index - right.index).map(({ pattern })=>pattern);
|
|
377
|
+
}
|
|
378
|
+
function resolveLocalisedPath(pathname, targetLanguage, languages, localisedUrls) {
|
|
379
|
+
const normalizedPathname = normaliseSlashes(pathname);
|
|
380
|
+
const canonicalCandidates = sortLocalisedPatterns(Object.entries(localisedUrls || {}).map(([canonicalPattern, localisedUrlEntry])=>({
|
|
381
|
+
canonicalPattern,
|
|
382
|
+
localisedUrlEntry,
|
|
383
|
+
pattern: canonicalPattern
|
|
384
|
+
})));
|
|
385
|
+
for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
|
|
386
|
+
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
387
|
+
const params = targetPattern ? matchPathPattern(normalizedPathname, canonicalPattern) : null;
|
|
388
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
389
|
+
}
|
|
390
|
+
const localisedCandidates = sortLocalisedPatterns(Object.values(localisedUrls || {}).flatMap((localisedUrlEntry)=>{
|
|
391
|
+
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
392
|
+
if (!targetPattern) return [];
|
|
393
|
+
return languages.map((language)=>localisedUrlEntry[language]).filter(Boolean).map((sourcePattern)=>({
|
|
394
|
+
pattern: sourcePattern,
|
|
395
|
+
sourcePattern,
|
|
396
|
+
targetPattern
|
|
397
|
+
}));
|
|
398
|
+
}));
|
|
399
|
+
for (const { sourcePattern, targetPattern } of localisedCandidates){
|
|
400
|
+
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
401
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
402
|
+
}
|
|
403
|
+
return normalizedPathname;
|
|
404
|
+
}
|
|
405
|
+
function localiseTargetPathname(pathname, language, languages, localisedUrls) {
|
|
406
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
|
|
407
|
+
const resolvedPath = localisedUrls ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrls) : pathWithoutLanguage;
|
|
408
|
+
const resolvedSegments = resolvedPath.split('/').filter(Boolean);
|
|
409
|
+
return `/${[
|
|
410
|
+
language,
|
|
411
|
+
...resolvedSegments
|
|
412
|
+
].join('/')}`;
|
|
413
|
+
}
|
|
414
|
+
function shouldIgnoreLocaleRedirect(pathname, route, entry) {
|
|
415
|
+
const remainingPathname = getRouteRemainingPathname(route, pathname);
|
|
416
|
+
const normalizedPathname = remainingPathname.startsWith('/') ? remainingPathname : `/${remainingPathname}`;
|
|
417
|
+
const ignoredPrefixes = [
|
|
418
|
+
...entry.ignoreRedirectRoutes || [],
|
|
419
|
+
...entry.staticRoutePrefixes || [],
|
|
420
|
+
'/static',
|
|
421
|
+
'/upload'
|
|
422
|
+
];
|
|
423
|
+
return ignoredPrefixes.some((prefix)=>{
|
|
424
|
+
const normalizedPrefix = normaliseSlashes(prefix);
|
|
425
|
+
return normalizedPathname === normalizedPrefix || normalizedPathname.startsWith(`${normalizedPrefix}/`);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
function buildLocalizedUrl(route, request, language, entry) {
|
|
429
|
+
const url = new URL(request.url);
|
|
430
|
+
const basePath = getRouteBasePath(route);
|
|
431
|
+
const remainingPathname = getRouteRemainingPathname(route, url.pathname);
|
|
432
|
+
const localizedPathname = localiseTargetPathname(remainingPathname, language, entry.languages, entry.localisedUrls);
|
|
433
|
+
const nextPathname = '/' === basePath ? localizedPathname : normaliseSlashes(`${basePath}/${localizedPathname}`);
|
|
434
|
+
return `${nextPathname}${url.search}`;
|
|
435
|
+
}
|
|
436
|
+
function createLocaleRedirectResponse(location, entry) {
|
|
437
|
+
const headers = new Headers({
|
|
438
|
+
'cache-control': 'private, no-store',
|
|
439
|
+
location
|
|
440
|
+
});
|
|
441
|
+
const vary = [];
|
|
442
|
+
const detectionOrder = entry.i18nextDetector ? entry.detection?.order || [] : [];
|
|
443
|
+
if (detectionOrder.includes('header')) vary.push('Accept-Language');
|
|
444
|
+
if (detectionOrder.includes('cookie')) vary.push('Cookie');
|
|
445
|
+
if (vary.length > 0) headers.set('vary', vary.join(', '));
|
|
446
|
+
return new Response(null, {
|
|
447
|
+
headers,
|
|
448
|
+
status: 302
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function createLocaleRedirectResponseForRequest(route, request) {
|
|
452
|
+
const entry = getRouteI18nEntry(route);
|
|
453
|
+
if (!entry || !Array.isArray(entry.languages) || 0 === entry.languages.length) return null;
|
|
454
|
+
const { pathname, search } = new URL(request.url);
|
|
455
|
+
if (isAssetLikePathname(pathname)) return null;
|
|
456
|
+
if (shouldIgnoreLocaleRedirect(pathname, route, entry)) return null;
|
|
457
|
+
const language = getLanguageFromPath(route, pathname, entry.languages);
|
|
458
|
+
if (!language) {
|
|
459
|
+
const targetLanguage = detectRedirectLanguage(request, entry) || entry.fallbackLanguage;
|
|
460
|
+
return createLocaleRedirectResponse(buildLocalizedUrl(route, request, targetLanguage, entry), entry);
|
|
461
|
+
}
|
|
462
|
+
if (entry.localisedUrls) {
|
|
463
|
+
const expectedUrl = buildLocalizedUrl(route, request, language, entry);
|
|
464
|
+
if (expectedUrl !== `${pathname}${search}`) return createLocaleRedirectResponse(expectedUrl, entry);
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
226
468
|
function findRoute(request) {
|
|
227
469
|
const { pathname } = new URL(request.url);
|
|
228
470
|
const routes = MODERN_WORKER_MANIFEST.routeSpec.routes;
|
|
@@ -399,21 +641,40 @@ async function collectRenderedRemoteCssHrefs(html, request, env) {
|
|
|
399
641
|
function escapeAttribute(value) {
|
|
400
642
|
return String(value).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
401
643
|
}
|
|
644
|
+
function isAbsoluteUrl(value) {
|
|
645
|
+
return /^[a-z][a-z\d+.-]*:/iu.test(value);
|
|
646
|
+
}
|
|
647
|
+
function toRouteCssHtmlHref(asset) {
|
|
648
|
+
if (isAbsoluteUrl(asset) || asset.startsWith('/')) return asset;
|
|
649
|
+
return `/${asset.replace(/^\/+/u, '')}`;
|
|
650
|
+
}
|
|
402
651
|
async function withRouteCssLinks(response, route, routeManifest, request, env) {
|
|
403
652
|
const contentType = response.headers.get('content-type') || '';
|
|
404
653
|
if (!contentType.includes('text/html')) return response;
|
|
405
654
|
const html = await response.text();
|
|
406
|
-
const
|
|
407
|
-
...collectRouteCssAssets(route, routeManifest).map((asset)=>
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
655
|
+
const cssEntries = [
|
|
656
|
+
...collectRouteCssAssets(route, routeManifest).map((asset)=>{
|
|
657
|
+
const href = toRouteCssHtmlHref(asset);
|
|
658
|
+
return {
|
|
659
|
+
href,
|
|
660
|
+
preloadHref: new URL(href, request.url).toString()
|
|
661
|
+
};
|
|
662
|
+
}),
|
|
663
|
+
...(await collectRenderedRemoteCssHrefs(html, request, env)).map((href)=>({
|
|
664
|
+
href,
|
|
665
|
+
preloadHref: new URL(href, request.url).toString()
|
|
666
|
+
}))
|
|
413
667
|
];
|
|
668
|
+
if (0 === cssEntries.length) return response;
|
|
669
|
+
const seenCssHrefs = new Set();
|
|
670
|
+
const uniqueCssEntries = cssEntries.filter((entry)=>{
|
|
671
|
+
if (seenCssHrefs.has(entry.preloadHref)) return false;
|
|
672
|
+
seenCssHrefs.add(entry.preloadHref);
|
|
673
|
+
return true;
|
|
674
|
+
});
|
|
414
675
|
const headers = new Headers(response.headers);
|
|
415
|
-
for (const
|
|
416
|
-
const links =
|
|
676
|
+
for (const { preloadHref } of uniqueCssEntries)headers.append('link', `<${preloadHref}>; rel=preload; as=style`);
|
|
677
|
+
const links = uniqueCssEntries.filter(({ href, preloadHref })=>!html.includes(href) && !html.includes(preloadHref)).map(({ href })=>`<link rel="stylesheet" href="${escapeAttribute(href)}">`);
|
|
417
678
|
if (0 === links.length || !html.includes('</head>')) return new Response(html, {
|
|
418
679
|
headers,
|
|
419
680
|
status: response.status,
|
|
@@ -444,6 +705,10 @@ async function loadWorkerModule(workerPath) {
|
|
|
444
705
|
if (!workerModulePromises.has(workerPath)) workerModulePromises.set(workerPath, loader());
|
|
445
706
|
return workerModulePromises.get(workerPath);
|
|
446
707
|
}
|
|
708
|
+
async function loadEffectEdgeRuntime() {
|
|
709
|
+
effectEdgeRuntimePromise ??= import('@modern-js/plugin-bff/effect-edge');
|
|
710
|
+
return effectEdgeRuntimePromise;
|
|
711
|
+
}
|
|
447
712
|
function getRuntimeModule(workerModule) {
|
|
448
713
|
const defaultExport = workerModule.default;
|
|
449
714
|
const nestedDefaultExport = defaultExport && 'object' == typeof defaultExport ? defaultExport.default : void 0;
|
|
@@ -508,21 +773,6 @@ function createRequestForMountedPrefix(request, prefix) {
|
|
|
508
773
|
url.pathname = nextPath.startsWith('/') ? nextPath : `/${nextPath}`;
|
|
509
774
|
return new Request(url, request);
|
|
510
775
|
}
|
|
511
|
-
function createEffectContext(originalRequest, mountedRequest, env) {
|
|
512
|
-
const url = new URL(originalRequest.url);
|
|
513
|
-
return {
|
|
514
|
-
request: mountedRequest,
|
|
515
|
-
env: env || {},
|
|
516
|
-
path: url.pathname,
|
|
517
|
-
method: originalRequest.method,
|
|
518
|
-
operationContext: {
|
|
519
|
-
request: mountedRequest,
|
|
520
|
-
env: env || {},
|
|
521
|
-
path: url.pathname,
|
|
522
|
-
method: originalRequest.method
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
776
|
async function dispatchBffRequest(request, env) {
|
|
527
777
|
const bff = MODERN_WORKER_MANIFEST.bff;
|
|
528
778
|
if (!bff?.worker || !matchesPrefix(new URL(request.url).pathname, bff.prefix)) return null;
|
|
@@ -535,9 +785,23 @@ async function dispatchBffRequest(request, env) {
|
|
|
535
785
|
}
|
|
536
786
|
});
|
|
537
787
|
const mountedRequest = createRequestForMountedPrefix(request, bff.prefix);
|
|
538
|
-
const effectContext = createEffectContext(request, mountedRequest, env);
|
|
539
788
|
const defaultExport = workerModule.default;
|
|
540
789
|
const runtime = getRuntimeModule(workerModule);
|
|
790
|
+
if ('effect' === bff.runtimeFramework) {
|
|
791
|
+
let effectDispatcherPromise = effectBffDispatcherPromises.get(bff.worker);
|
|
792
|
+
if (!effectDispatcherPromise) {
|
|
793
|
+
const { createEffectBffEdgeDispatcher } = await loadEffectEdgeRuntime();
|
|
794
|
+
effectDispatcherPromise = createEffectBffEdgeDispatcher({
|
|
795
|
+
module: runtime,
|
|
796
|
+
prefix: bff.prefix
|
|
797
|
+
});
|
|
798
|
+
effectBffDispatcherPromises.set(bff.worker, effectDispatcherPromise);
|
|
799
|
+
}
|
|
800
|
+
const effectDispatcher = await effectDispatcherPromise;
|
|
801
|
+
return effectDispatcher.dispatch(request, {
|
|
802
|
+
env
|
|
803
|
+
});
|
|
804
|
+
}
|
|
541
805
|
const directHandler = 'function' == typeof runtime.handler && runtime.handler || 'function' == typeof defaultExport && defaultExport;
|
|
542
806
|
const createdHandler = 'function' == typeof runtime.createHandler ? runtime.createHandler().handler : void 0;
|
|
543
807
|
const handler = directHandler || createdHandler;
|
|
@@ -548,27 +812,66 @@ async function dispatchBffRequest(request, env) {
|
|
|
548
812
|
'x-modern-js-bff-worker': bff.worker
|
|
549
813
|
}
|
|
550
814
|
});
|
|
551
|
-
|
|
815
|
+
const effectContext = {
|
|
816
|
+
request: mountedRequest,
|
|
817
|
+
env: env || {},
|
|
818
|
+
path: new URL(request.url).pathname,
|
|
819
|
+
method: request.method,
|
|
820
|
+
operationContext: {
|
|
821
|
+
request: mountedRequest,
|
|
822
|
+
env: env || {},
|
|
823
|
+
path: new URL(request.url).pathname,
|
|
824
|
+
method: request.method
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
return handler(mountedRequest, effectContext);
|
|
828
|
+
}
|
|
829
|
+
async function dispatchServiceBindingRequest(request, env) {
|
|
830
|
+
const serviceBindings = MODERN_WORKER_MANIFEST.serviceBindings;
|
|
831
|
+
if (!Array.isArray(serviceBindings) || 0 === serviceBindings.length) return null;
|
|
832
|
+
const pathname = new URL(request.url).pathname;
|
|
833
|
+
for (const binding of serviceBindings){
|
|
834
|
+
if (!binding?.binding || !binding?.prefix) continue;
|
|
835
|
+
if (!matchesPrefix(pathname, binding.prefix)) continue;
|
|
836
|
+
const service = env?.[binding.binding];
|
|
837
|
+
if (!service || 'function' != typeof service.fetch) return new Response(`Cloudflare service binding not available: ${binding.binding}`, {
|
|
838
|
+
status: 502,
|
|
839
|
+
headers: {
|
|
840
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
841
|
+
'x-modern-js-service-binding': binding.binding
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
return service.fetch(request);
|
|
845
|
+
}
|
|
846
|
+
return null;
|
|
552
847
|
}
|
|
553
848
|
export default {
|
|
554
849
|
async fetch (request, env, ctx) {
|
|
555
850
|
const corsPreflightResponse = await createCorsPreflightResponse(request, env);
|
|
556
851
|
if (corsPreflightResponse) return finalizeResponseForRequest(corsPreflightResponse, request);
|
|
557
|
-
const assetResponse = await fetchAsset(request, env);
|
|
558
|
-
if (assetResponse) return finalizeResponseForRequest(assetResponse, request);
|
|
559
852
|
const bffResponse = await dispatchBffRequest(request, env);
|
|
560
853
|
if (bffResponse) return finalizeResponseForRequest(withAppCorsHeaders(bffResponse, request), request);
|
|
854
|
+
const serviceBindingResponse = await dispatchServiceBindingRequest(request, env);
|
|
855
|
+
if (serviceBindingResponse) return finalizeResponseForRequest(withAppCorsHeaders(serviceBindingResponse, request), request);
|
|
561
856
|
const route = findRoute(request);
|
|
562
857
|
const { pathname } = new URL(request.url);
|
|
563
|
-
if (isAssetLikePathname(pathname) && !routeMatchesExactly(route, pathname))
|
|
564
|
-
|
|
565
|
-
|
|
858
|
+
if (isAssetLikePathname(pathname) && !routeMatchesExactly(route, pathname)) {
|
|
859
|
+
const assetResponse = await fetchAsset(request, env);
|
|
860
|
+
if (assetResponse) return finalizeResponseForRequest(assetResponse, request);
|
|
861
|
+
return finalizeResponseForRequest(withAppCorsHeaders(new Response('Not found', {
|
|
862
|
+
status: 404
|
|
863
|
+
}), request), request);
|
|
864
|
+
}
|
|
865
|
+
const localeRedirectResponse = createLocaleRedirectResponseForRequest(route, request);
|
|
866
|
+
if (localeRedirectResponse) return finalizeResponseForRequest(withAppCorsHeaders(localeRedirectResponse, request), request);
|
|
566
867
|
if (route?.worker) {
|
|
567
868
|
const renderableRequest = createRenderableRequest(request);
|
|
568
869
|
return finalizeResponseForRequest(withAppCorsHeaders(await dispatchRouteWorker(route, renderableRequest, env, ctx), request), request);
|
|
569
870
|
}
|
|
570
871
|
const htmlResponse = await fetchRouteHtml(route, request, env);
|
|
571
872
|
if (htmlResponse) return finalizeResponseForRequest(htmlResponse, request);
|
|
873
|
+
const assetResponse = await fetchAsset(request, env);
|
|
874
|
+
if (assetResponse) return finalizeResponseForRequest(assetResponse, request);
|
|
572
875
|
return finalizeResponseForRequest(withAppCorsHeaders(new Response('Not found', {
|
|
573
876
|
status: 404
|
|
574
877
|
}), request), request);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AppTools, CliPlugin } from '../types';
|
|
2
|
+
export type BackendFederationArtifactResult = {
|
|
3
|
+
appId: string;
|
|
4
|
+
manifestPath: string;
|
|
5
|
+
containerPath: string;
|
|
6
|
+
remoteName: string;
|
|
7
|
+
remoteType: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const emitBackendFederationArtifacts: (appDirectory: string, distDirectory: string) => Promise<BackendFederationArtifactResult | undefined>;
|
|
10
|
+
export default _default;
|
|
11
|
+
declare function _default(): CliPlugin<AppTools>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const CLOUDFLARE_WORKER_ENTRY = "server/index.mjs";
|
|
2
|
+
export declare const CLOUDFLARE_WORKER_MANIFEST = "server/modern-worker-manifest.json";
|
|
3
|
+
export declare const CLOUDFLARE_WRANGLER_CONFIG_FILE = "wrangler.json";
|
|
4
|
+
export declare const CLOUDFLARE_OUTPUT_PACKAGE_FILE = "package.json";
|
|
5
|
+
export declare const CLOUDFLARE_WORKER_PACKAGE_FILE = "worker/package.json";
|
|
6
|
+
export declare const CLOUDFLARE_ASSETS_BINDING = "ASSETS";
|
|
7
|
+
export declare const CLOUDFLARE_PUBLIC_ASSETS_DIRECTORY = "public";
|
|
8
|
+
export declare const CLOUDFLARE_WORKER_BUNDLE_DIRECTORY = "worker";
|
|
9
|
+
export declare const CLOUDFLARE_RUNTIME_TYPE = "cloudflare-module-worker";
|
|
10
|
+
export declare const CLOUDFLARE_OUTPUT_PACKAGE_TYPE = "module";
|
|
11
|
+
export declare const CLOUDFLARE_WORKER_PACKAGE_TYPE = "commonjs";
|
|
12
|
+
export declare const CLOUDFLARE_WORKER_BUNDLE_FORMAT = "commonjs";
|
|
13
|
+
export declare const CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS: readonly ['nodejs_compat', 'global_fetch_strictly_public'];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CLOUDFLARE_ASSETS_BINDING, CLOUDFLARE_OUTPUT_PACKAGE_TYPE, CLOUDFLARE_PUBLIC_ASSETS_DIRECTORY, CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS, CLOUDFLARE_WORKER_ENTRY, CLOUDFLARE_WORKER_PACKAGE_TYPE } from './cloudflare-output-contract';
|
|
2
|
+
export type CloudflareOutputPlan = {
|
|
3
|
+
outputDirectory: string;
|
|
4
|
+
requiredFiles: string[];
|
|
5
|
+
publicLeakDirectories: string[];
|
|
6
|
+
paths: {
|
|
7
|
+
workerEntry: string;
|
|
8
|
+
workerManifest: string;
|
|
9
|
+
wranglerConfig: string;
|
|
10
|
+
outputPackage: string;
|
|
11
|
+
workerPackage: string;
|
|
12
|
+
publicAssets: string;
|
|
13
|
+
workerBundle: string;
|
|
14
|
+
};
|
|
15
|
+
packages: {
|
|
16
|
+
output: {
|
|
17
|
+
type: typeof CLOUDFLARE_OUTPUT_PACKAGE_TYPE;
|
|
18
|
+
};
|
|
19
|
+
worker: {
|
|
20
|
+
type: typeof CLOUDFLARE_WORKER_PACKAGE_TYPE;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
wrangler: {
|
|
24
|
+
main: typeof CLOUDFLARE_WORKER_ENTRY;
|
|
25
|
+
assets: {
|
|
26
|
+
binding: typeof CLOUDFLARE_ASSETS_BINDING;
|
|
27
|
+
directory: `./${typeof CLOUDFLARE_PUBLIC_ASSETS_DIRECTORY}`;
|
|
28
|
+
run_worker_first: true;
|
|
29
|
+
};
|
|
30
|
+
requiredCompatibilityFlags: typeof CLOUDFLARE_REQUIRED_COMPATIBILITY_FLAGS;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export declare function createCloudflareOutputPlan(outputDirectory: string): CloudflareOutputPlan;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type CloudflareOutputVerifierIssueCode = 'missing-file' | 'invalid-manifest' | 'invalid-wrangler' | 'invalid-package' | 'public-output-leak' | 'missing-worker-bundle' | 'invalid-worker-bundle' | 'worker-import-failed' | 'delivery-unit-drift' | 'missing-delivery-unit' | 'forbidden-mutation-pattern';
|
|
2
|
+
export interface CloudflareOutputVerifierIssue {
|
|
3
|
+
code: CloudflareOutputVerifierIssueCode;
|
|
4
|
+
message: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CloudflareDeliveryUnitIdentity {
|
|
8
|
+
unitId: string;
|
|
9
|
+
buildMarker: string;
|
|
10
|
+
sourceRevision: string;
|
|
11
|
+
}
|
|
12
|
+
export interface VerifyCloudflareOutputOptions {
|
|
13
|
+
outputDirectory: string;
|
|
14
|
+
importWorker?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Topology-declared delivery-unit record (from the workspace compact config).
|
|
17
|
+
* When provided, the Cloudflare worker manifest must carry a matching
|
|
18
|
+
* `deliveryUnit` stamp so the deployed worker snapshot is proven to derive
|
|
19
|
+
* from the same delivery unit as the Node/API surfaces (ADR-0019 lane D).
|
|
20
|
+
*/
|
|
21
|
+
deliveryUnit?: CloudflareDeliveryUnitIdentity;
|
|
22
|
+
}
|
|
23
|
+
export interface VerifyCloudflareOutputMutationPolicyOptions {
|
|
24
|
+
scanRoots: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface CloudflareOutputVerifierResult {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
issues: CloudflareOutputVerifierIssue[];
|
|
29
|
+
}
|
|
30
|
+
export declare const verifyCloudflareOutputMutationPolicy: (options: VerifyCloudflareOutputMutationPolicyOptions) => Promise<CloudflareOutputVerifierResult>;
|
|
31
|
+
export declare const verifyCloudflareOutput: (options: VerifyCloudflareOutputOptions) => Promise<CloudflareOutputVerifierResult>;
|
|
32
|
+
export declare const assertCloudflareOutput: (options: VerifyCloudflareOutputOptions) => Promise<void>;
|
|
@@ -14,6 +14,17 @@ export interface CloudflareWorkerArtifactConfig {
|
|
|
14
14
|
*/
|
|
15
15
|
to: string;
|
|
16
16
|
}
|
|
17
|
+
export interface CloudflareWorkerPublicAssetConfig {
|
|
18
|
+
/**
|
|
19
|
+
* Source file or directory, relative to the app root, to copy into
|
|
20
|
+
* Cloudflare Worker Static Assets.
|
|
21
|
+
*/
|
|
22
|
+
from: string;
|
|
23
|
+
/**
|
|
24
|
+
* Destination path relative to `.output/public`.
|
|
25
|
+
*/
|
|
26
|
+
to: string;
|
|
27
|
+
}
|
|
17
28
|
export interface CloudflareWorkerD1DatabaseConfig {
|
|
18
29
|
/**
|
|
19
30
|
* Worker binding name, for example `DB`.
|
|
@@ -41,6 +52,17 @@ export interface CloudflareWorkerD1DatabaseConfig {
|
|
|
41
52
|
*/
|
|
42
53
|
remote?: boolean;
|
|
43
54
|
}
|
|
55
|
+
export interface CloudflareWorkerServiceBindingConfig {
|
|
56
|
+
/** Worker binding name exposed on the module worker `env` object. */
|
|
57
|
+
binding: string;
|
|
58
|
+
/** Target Cloudflare Worker service name. */
|
|
59
|
+
service: string;
|
|
60
|
+
/**
|
|
61
|
+
* Optional application path prefix that Modern.js should dispatch to this
|
|
62
|
+
* service binding with `env[binding].fetch(request)`.
|
|
63
|
+
*/
|
|
64
|
+
prefix?: string;
|
|
65
|
+
}
|
|
44
66
|
export interface CloudflareWorkerSecurityCspConfig {
|
|
45
67
|
mode?: CloudflareWorkerSecurityCspMode;
|
|
46
68
|
directives?: Record<string, string[] | string | false>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CloudflareWorkerArtifactConfig, CloudflareWorkerD1DatabaseConfig, CloudflareWorkerSecurityConfig, DeployTarget, JsonValue } from './cloudflareDeploy';
|
|
2
|
-
export type { CloudflareWorkerArtifactConfig, CloudflareWorkerD1DatabaseConfig, CloudflareWorkerSecurityConfig, CloudflareWorkerSecurityCorsConfig, CloudflareWorkerSecurityCspConfig, CloudflareWorkerSecurityCspMode, CloudflareWorkerSecurityNoindexConfig, DeployTarget, JsonValue, } from './cloudflareDeploy';
|
|
1
|
+
import type { CloudflareWorkerArtifactConfig, CloudflareWorkerD1DatabaseConfig, CloudflareWorkerPublicAssetConfig, CloudflareWorkerSecurityConfig, CloudflareWorkerServiceBindingConfig, DeployTarget, JsonValue } from './cloudflareDeploy';
|
|
2
|
+
export type { CloudflareWorkerArtifactConfig, CloudflareWorkerD1DatabaseConfig, CloudflareWorkerPublicAssetConfig, CloudflareWorkerSecurityConfig, CloudflareWorkerSecurityCorsConfig, CloudflareWorkerSecurityCspConfig, CloudflareWorkerSecurityCspMode, CloudflareWorkerSecurityNoindexConfig, CloudflareWorkerServiceBindingConfig, DeployTarget, JsonValue, } from './cloudflareDeploy';
|
|
3
3
|
export interface MicroFrontend {
|
|
4
4
|
/**
|
|
5
5
|
* Specifies whether to enable the HTML entry.
|
|
@@ -47,12 +47,25 @@ export interface DeployUserConfig {
|
|
|
47
47
|
* Use this for provider resources such as migrations or generated config.
|
|
48
48
|
*/
|
|
49
49
|
artifacts?: CloudflareWorkerArtifactConfig[];
|
|
50
|
+
/**
|
|
51
|
+
* Additional app-root files or directories to serve as Cloudflare Worker
|
|
52
|
+
* Static Assets under `.output/public`. Use `to: '.'` to copy a
|
|
53
|
+
* source directory's contents into the public asset root.
|
|
54
|
+
*/
|
|
55
|
+
publicAssets?: CloudflareWorkerPublicAssetConfig[];
|
|
50
56
|
/**
|
|
51
57
|
* First-class Cloudflare D1 bindings. Modern.js writes these to
|
|
52
58
|
* `wrangler.json` as `d1_databases` and stages configured migration
|
|
53
59
|
* directories into `.output`.
|
|
54
60
|
*/
|
|
55
61
|
d1Databases?: CloudflareWorkerD1DatabaseConfig[];
|
|
62
|
+
/**
|
|
63
|
+
* First-class Cloudflare service bindings. Modern.js writes these to
|
|
64
|
+
* `wrangler.json` as `services`; when a binding also has `prefix`,
|
|
65
|
+
* the generated Worker dispatches matching requests through
|
|
66
|
+
* `env[binding].fetch(request)`.
|
|
67
|
+
*/
|
|
68
|
+
services?: CloudflareWorkerServiceBindingConfig[];
|
|
56
69
|
/**
|
|
57
70
|
* Dist output paths that must not be copied into Cloudflare public assets.
|
|
58
71
|
* Entries are slash-normalized path prefixes relative to the app dist root.
|
package/lib/types.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="react" />
|
|
3
3
|
/// <reference types="react-dom" />
|
|
4
|
-
/// <reference types="@rsbuild/core/types" />
|
|
5
|
-
/// <reference path="../dist/types/index.d.ts" />
|
|
6
4
|
|
|
7
5
|
declare namespace NodeJS {
|
|
8
6
|
interface ProcessEnv {
|
|
@@ -12,7 +10,8 @@ declare namespace NodeJS {
|
|
|
12
10
|
}
|
|
13
11
|
}
|
|
14
12
|
|
|
15
|
-
//
|
|
13
|
+
// Keep this app-environment surface independent from framework configuration
|
|
14
|
+
// declarations so strict app typecheck does not enter toolchain internals.
|
|
16
15
|
/**
|
|
17
16
|
* Image assets
|
|
18
17
|
*/
|