@bleedingdev/modern-js-app-tools 3.5.0-ultramodern.2 → 3.5.0-ultramodern.20

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.
@@ -223,6 +223,255 @@ function routeMatches(route, pathname) {
223
223
  const requestPath = normalizeRoutePath(pathname);
224
224
  return routePath === requestPath || '/' === routePath && route.isSSR || requestPath.startsWith(`${routePath}/`);
225
225
  }
226
+ function getRouteI18nEntry(route) {
227
+ if (!route || 'string' != typeof route.entryName) return;
228
+ return MODERN_WORKER_MANIFEST.i18n?.entries?.[route.entryName];
229
+ }
230
+ function getRouteBasePath(route) {
231
+ const routePath = 'string' == typeof route?.urlPath ? normalizeRoutePath(route.urlPath) : '/';
232
+ return routePath || '/';
233
+ }
234
+ function getRouteRemainingPathname(route, pathname) {
235
+ const basePath = getRouteBasePath(route);
236
+ if ('/' === basePath) return pathname || '/';
237
+ if (!matchesPrefix(pathname, basePath)) return pathname || '/';
238
+ const remaining = pathname.slice(basePath.length);
239
+ return remaining ? remaining.startsWith('/') ? remaining : `/${remaining}` : '/';
240
+ }
241
+ function normalizeLanguage(value, languages) {
242
+ if ('string' != typeof value || '' === value.trim()) return null;
243
+ const normalized = value.trim().toLowerCase();
244
+ const exact = languages.find((language)=>language.toLowerCase() === normalized);
245
+ if (exact) return exact;
246
+ const baseLanguage = normalized.split(/[-_]/u)[0];
247
+ return languages.find((language)=>language.toLowerCase() === baseLanguage) || null;
248
+ }
249
+ function getLanguageFromPath(route, pathname, languages) {
250
+ const remainingPathname = getRouteRemainingPathname(route, pathname);
251
+ const firstSegment = remainingPathname.split('/').filter(Boolean)[0];
252
+ return normalizeLanguage(firstSegment, languages);
253
+ }
254
+ function safeDecodeURIComponent(value) {
255
+ try {
256
+ return decodeURIComponent(value);
257
+ } catch {
258
+ return value;
259
+ }
260
+ }
261
+ function detectLanguageFromCookie(cookieHeader, lookupCookie, languages) {
262
+ if (!cookieHeader) return null;
263
+ for (const cookiePart of cookieHeader.split(';')){
264
+ const [rawName, ...rawValue] = cookiePart.trim().split('=');
265
+ if (rawName === lookupCookie && 0 !== rawValue.length) return normalizeLanguage(safeDecodeURIComponent(rawValue.join('=')), languages);
266
+ }
267
+ return null;
268
+ }
269
+ function detectLanguageFromAcceptLanguage(acceptLanguage, languages) {
270
+ if (!acceptLanguage) return null;
271
+ return acceptLanguage.split(',').map((entry, index)=>{
272
+ const [rawRange, ...rawParameters] = entry.trim().split(';');
273
+ const qualityParameter = rawParameters.find((parameter)=>parameter.trim().startsWith('q='));
274
+ const quality = qualityParameter ? Number.parseFloat(qualityParameter.split('=')[1] || '') : 1;
275
+ return {
276
+ index,
277
+ language: normalizeLanguage(rawRange, languages),
278
+ quality: Number.isFinite(quality) ? quality : 1
279
+ };
280
+ }).filter((candidate)=>candidate.language && candidate.quality > 0).sort((left, right)=>right.quality - left.quality || left.index - right.index)[0]?.language || null;
281
+ }
282
+ function detectRedirectLanguage(request, entry) {
283
+ if (!entry.i18nextDetector) return null;
284
+ const url = new URL(request.url);
285
+ const detection = entry.detection || {};
286
+ const order = Array.isArray(detection.order) ? detection.order : [
287
+ 'querystring',
288
+ 'cookie',
289
+ 'header'
290
+ ];
291
+ for (const method of order){
292
+ 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;
293
+ if (language) return language;
294
+ }
295
+ return null;
296
+ }
297
+ function normaliseSlashes(pathname) {
298
+ const withoutDuplicateSlashes = pathname.replace(/\/+/g, '/');
299
+ const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
300
+ return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/u, '') : withLeadingSlash;
301
+ }
302
+ function normalisePathPattern(pathname) {
303
+ return normaliseSlashes(pathname).replace(/\[(.+?)\]/g, ':$1');
304
+ }
305
+ function stripLanguagePrefix(pathname, languages) {
306
+ const segments = pathname.split('/').filter(Boolean);
307
+ const firstSegment = segments[0];
308
+ if (firstSegment && normalizeLanguage(firstSegment, languages)) return `/${segments.slice(1).join('/')}` || '/';
309
+ return pathname || '/';
310
+ }
311
+ function escapeRegExp(value) {
312
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
313
+ }
314
+ function getPatternParamName(segment) {
315
+ return segment.slice(1).replace(/\?$/, '');
316
+ }
317
+ function compilePathPattern(pattern) {
318
+ const names = [];
319
+ const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
320
+ const source = segments.map((segment)=>{
321
+ if (segment.startsWith(':')) {
322
+ names.push(getPatternParamName(segment));
323
+ return segment.endsWith('?') ? '(?:/([^/]+))?' : '/([^/]+)';
324
+ }
325
+ if ('*' === segment) {
326
+ names.push('*');
327
+ return '/(.*)';
328
+ }
329
+ return `/${escapeRegExp(segment)}`;
330
+ }).join('');
331
+ return {
332
+ names,
333
+ regexp: new RegExp(`^${source || '/'}$`)
334
+ };
335
+ }
336
+ function matchPathPattern(pathname, pattern) {
337
+ const { names, regexp } = compilePathPattern(pattern);
338
+ const match = regexp.exec(normaliseSlashes(pathname));
339
+ if (!match) return null;
340
+ const params = {};
341
+ for(let index = 0; index < names.length; index++)try {
342
+ params[names[index]] = decodeURIComponent(match[index + 1] || '');
343
+ } catch {
344
+ return null;
345
+ }
346
+ return params;
347
+ }
348
+ function buildPathFromPattern(pattern, params) {
349
+ const path = normalisePathPattern(pattern).split('/').filter(Boolean).map((segment)=>{
350
+ if (segment.startsWith(':')) {
351
+ const param = params[getPatternParamName(segment)];
352
+ return param ? encodeURIComponent(param) : '';
353
+ }
354
+ if ('*' === segment) return params['*'] || '';
355
+ return segment;
356
+ }).filter(Boolean).join('/');
357
+ return `/${path}`;
358
+ }
359
+ function patternSpecificity(pattern) {
360
+ const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
361
+ let staticSegments = 0;
362
+ let dynamicSegments = 0;
363
+ let splatSegments = 0;
364
+ for (const segment of segments)if ('*' === segment) splatSegments++;
365
+ else if (segment.startsWith(':')) dynamicSegments++;
366
+ else staticSegments++;
367
+ return {
368
+ dynamicSegments,
369
+ splatSegments,
370
+ staticSegments,
371
+ totalSegments: segments.length
372
+ };
373
+ }
374
+ function comparePatternSpecificity(left, right) {
375
+ const leftSpecificity = patternSpecificity(left);
376
+ const rightSpecificity = patternSpecificity(right);
377
+ return rightSpecificity.staticSegments - leftSpecificity.staticSegments || rightSpecificity.totalSegments - leftSpecificity.totalSegments || leftSpecificity.splatSegments - rightSpecificity.splatSegments || leftSpecificity.dynamicSegments - rightSpecificity.dynamicSegments;
378
+ }
379
+ function sortLocalisedPatterns(patterns) {
380
+ return patterns.map((pattern, index)=>({
381
+ pattern,
382
+ index
383
+ })).sort((left, right)=>comparePatternSpecificity(left.pattern.pattern, right.pattern.pattern) || left.index - right.index).map(({ pattern })=>pattern);
384
+ }
385
+ function resolveLocalisedPath(pathname, targetLanguage, languages, localisedUrls) {
386
+ const normalizedPathname = normaliseSlashes(pathname);
387
+ const canonicalCandidates = sortLocalisedPatterns(Object.entries(localisedUrls || {}).map(([canonicalPattern, localisedUrlEntry])=>({
388
+ canonicalPattern,
389
+ localisedUrlEntry,
390
+ pattern: canonicalPattern
391
+ })));
392
+ for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
393
+ const targetPattern = localisedUrlEntry[targetLanguage];
394
+ const params = targetPattern ? matchPathPattern(normalizedPathname, canonicalPattern) : null;
395
+ if (params) return buildPathFromPattern(targetPattern, params);
396
+ }
397
+ const localisedCandidates = sortLocalisedPatterns(Object.values(localisedUrls || {}).flatMap((localisedUrlEntry)=>{
398
+ const targetPattern = localisedUrlEntry[targetLanguage];
399
+ if (!targetPattern) return [];
400
+ return languages.map((language)=>localisedUrlEntry[language]).filter(Boolean).map((sourcePattern)=>({
401
+ pattern: sourcePattern,
402
+ sourcePattern,
403
+ targetPattern
404
+ }));
405
+ }));
406
+ for (const { sourcePattern, targetPattern } of localisedCandidates){
407
+ const params = matchPathPattern(normalizedPathname, sourcePattern);
408
+ if (params) return buildPathFromPattern(targetPattern, params);
409
+ }
410
+ return normalizedPathname;
411
+ }
412
+ function localiseTargetPathname(pathname, language, languages, localisedUrls) {
413
+ const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
414
+ const resolvedPath = localisedUrls ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrls) : pathWithoutLanguage;
415
+ const resolvedSegments = resolvedPath.split('/').filter(Boolean);
416
+ return `/${[
417
+ language,
418
+ ...resolvedSegments
419
+ ].join('/')}`;
420
+ }
421
+ function shouldIgnoreLocaleRedirect(pathname, route, entry) {
422
+ const remainingPathname = getRouteRemainingPathname(route, pathname);
423
+ const normalizedPathname = remainingPathname.startsWith('/') ? remainingPathname : `/${remainingPathname}`;
424
+ const ignoredPrefixes = [
425
+ ...entry.ignoreRedirectRoutes || [],
426
+ ...entry.staticRoutePrefixes || [],
427
+ '/static',
428
+ '/upload'
429
+ ];
430
+ return ignoredPrefixes.some((prefix)=>{
431
+ const normalizedPrefix = normaliseSlashes(prefix);
432
+ return normalizedPathname === normalizedPrefix || normalizedPathname.startsWith(`${normalizedPrefix}/`);
433
+ });
434
+ }
435
+ function buildLocalizedUrl(route, request, language, entry) {
436
+ const url = new URL(request.url);
437
+ const basePath = getRouteBasePath(route);
438
+ const remainingPathname = getRouteRemainingPathname(route, url.pathname);
439
+ const localizedPathname = localiseTargetPathname(remainingPathname, language, entry.languages, entry.localisedUrls);
440
+ const nextPathname = '/' === basePath ? localizedPathname : normaliseSlashes(`${basePath}/${localizedPathname}`);
441
+ return `${nextPathname}${url.search}`;
442
+ }
443
+ function createLocaleRedirectResponse(location, entry) {
444
+ const headers = new Headers({
445
+ 'cache-control': 'private, no-store',
446
+ location
447
+ });
448
+ const vary = [];
449
+ const detectionOrder = entry.i18nextDetector ? entry.detection?.order || [] : [];
450
+ if (detectionOrder.includes('header')) vary.push('Accept-Language');
451
+ if (detectionOrder.includes('cookie')) vary.push('Cookie');
452
+ if (vary.length > 0) headers.set('vary', vary.join(', '));
453
+ return new Response(null, {
454
+ headers,
455
+ status: 302
456
+ });
457
+ }
458
+ function createLocaleRedirectResponseForRequest(route, request) {
459
+ const entry = getRouteI18nEntry(route);
460
+ if (!entry || !Array.isArray(entry.languages) || 0 === entry.languages.length) return null;
461
+ const { pathname, search } = new URL(request.url);
462
+ if (isAssetLikePathname(pathname)) return null;
463
+ if (shouldIgnoreLocaleRedirect(pathname, route, entry)) return null;
464
+ const language = getLanguageFromPath(route, pathname, entry.languages);
465
+ if (!language) {
466
+ const targetLanguage = detectRedirectLanguage(request, entry) || entry.fallbackLanguage;
467
+ return createLocaleRedirectResponse(buildLocalizedUrl(route, request, targetLanguage, entry), entry);
468
+ }
469
+ if (entry.localisedUrls) {
470
+ const expectedUrl = buildLocalizedUrl(route, request, language, entry);
471
+ if (expectedUrl !== `${pathname}${search}`) return createLocaleRedirectResponse(expectedUrl, entry);
472
+ }
473
+ return null;
474
+ }
226
475
  function findRoute(request) {
227
476
  const { pathname } = new URL(request.url);
228
477
  const routes = MODERN_WORKER_MANIFEST.routeSpec.routes;
@@ -399,21 +648,40 @@ async function collectRenderedRemoteCssHrefs(html, request, env) {
399
648
  function escapeAttribute(value) {
400
649
  return String(value).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
401
650
  }
651
+ function isAbsoluteUrl(value) {
652
+ return /^[a-z][a-z\d+.-]*:/iu.test(value);
653
+ }
654
+ function toRouteCssHtmlHref(asset) {
655
+ if (isAbsoluteUrl(asset) || asset.startsWith('/')) return asset;
656
+ return `/${asset.replace(/^\/+/u, '')}`;
657
+ }
402
658
  async function withRouteCssLinks(response, route, routeManifest, request, env) {
403
659
  const contentType = response.headers.get('content-type') || '';
404
660
  if (!contentType.includes('text/html')) return response;
405
661
  const html = await response.text();
406
- const cssHrefs = [
407
- ...collectRouteCssAssets(route, routeManifest).map((asset)=>new URL(asset, request.url).toString()),
408
- ...await collectRenderedRemoteCssHrefs(html, request, env)
409
- ];
410
- if (0 === cssHrefs.length) return response;
411
- const uniqueCssHrefs = [
412
- ...new Set(cssHrefs)
662
+ const cssEntries = [
663
+ ...collectRouteCssAssets(route, routeManifest).map((asset)=>{
664
+ const href = toRouteCssHtmlHref(asset);
665
+ return {
666
+ href,
667
+ preloadHref: new URL(href, request.url).toString()
668
+ };
669
+ }),
670
+ ...(await collectRenderedRemoteCssHrefs(html, request, env)).map((href)=>({
671
+ href,
672
+ preloadHref: new URL(href, request.url).toString()
673
+ }))
413
674
  ];
675
+ if (0 === cssEntries.length) return response;
676
+ const seenCssHrefs = new Set();
677
+ const uniqueCssEntries = cssEntries.filter((entry)=>{
678
+ if (seenCssHrefs.has(entry.preloadHref)) return false;
679
+ seenCssHrefs.add(entry.preloadHref);
680
+ return true;
681
+ });
414
682
  const headers = new Headers(response.headers);
415
- for (const href of uniqueCssHrefs)headers.append('link', `<${href}>; rel=preload; as=style`);
416
- const links = uniqueCssHrefs.filter((href)=>!html.includes(href)).map((href)=>`<link rel="stylesheet" href="${escapeAttribute(href)}">`);
683
+ for (const { preloadHref } of uniqueCssEntries)headers.append('link', `<${preloadHref}>; rel=preload; as=style`);
684
+ const links = uniqueCssEntries.filter(({ href, preloadHref })=>!html.includes(href) && !html.includes(preloadHref)).map(({ href })=>`<link rel="stylesheet" href="${escapeAttribute(href)}">`);
417
685
  if (0 === links.length || !html.includes('</head>')) return new Response(html, {
418
686
  headers,
419
687
  status: response.status,
@@ -554,21 +822,27 @@ export default {
554
822
  async fetch (request, env, ctx) {
555
823
  const corsPreflightResponse = await createCorsPreflightResponse(request, env);
556
824
  if (corsPreflightResponse) return finalizeResponseForRequest(corsPreflightResponse, request);
557
- const assetResponse = await fetchAsset(request, env);
558
- if (assetResponse) return finalizeResponseForRequest(assetResponse, request);
559
825
  const bffResponse = await dispatchBffRequest(request, env);
560
826
  if (bffResponse) return finalizeResponseForRequest(withAppCorsHeaders(bffResponse, request), request);
561
827
  const route = findRoute(request);
562
828
  const { pathname } = new URL(request.url);
563
- if (isAssetLikePathname(pathname) && !routeMatchesExactly(route, pathname)) return finalizeResponseForRequest(withAppCorsHeaders(new Response('Not found', {
564
- status: 404
565
- }), request), request);
829
+ if (isAssetLikePathname(pathname) && !routeMatchesExactly(route, pathname)) {
830
+ const assetResponse = await fetchAsset(request, env);
831
+ if (assetResponse) return finalizeResponseForRequest(assetResponse, request);
832
+ return finalizeResponseForRequest(withAppCorsHeaders(new Response('Not found', {
833
+ status: 404
834
+ }), request), request);
835
+ }
836
+ const localeRedirectResponse = createLocaleRedirectResponseForRequest(route, request);
837
+ if (localeRedirectResponse) return finalizeResponseForRequest(withAppCorsHeaders(localeRedirectResponse, request), request);
566
838
  if (route?.worker) {
567
839
  const renderableRequest = createRenderableRequest(request);
568
840
  return finalizeResponseForRequest(withAppCorsHeaders(await dispatchRouteWorker(route, renderableRequest, env, ctx), request), request);
569
841
  }
570
842
  const htmlResponse = await fetchRouteHtml(route, request, env);
571
843
  if (htmlResponse) return finalizeResponseForRequest(htmlResponse, request);
844
+ const assetResponse = await fetchAsset(request, env);
845
+ if (assetResponse) return finalizeResponseForRequest(assetResponse, request);
572
846
  return finalizeResponseForRequest(withAppCorsHeaders(new Response('Not found', {
573
847
  status: 404
574
848
  }), request), request);
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
- // reference @rsbuild/core/types, but there are some differences, such as svg
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
  */
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.5.0-ultramodern.2",
20
+ "version": "3.5.0-ultramodern.20",
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.5.0-ultramodern.2",
94
- "@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.5.0-ultramodern.2",
95
- "@modern-js/plugin-data-loader": "npm:@bleedingdev/modern-js-plugin-data-loader@3.5.0-ultramodern.2",
96
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.2",
97
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.2",
98
- "@modern-js/prod-server": "npm:@bleedingdev/modern-js-prod-server@3.5.0-ultramodern.2",
99
- "@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.5.0-ultramodern.2",
100
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.2",
101
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.2",
102
- "@modern-js/server": "npm:@bleedingdev/modern-js-server@3.5.0-ultramodern.2"
93
+ "@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.5.0-ultramodern.20",
94
+ "@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.5.0-ultramodern.20",
95
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.20",
96
+ "@modern-js/plugin-data-loader": "npm:@bleedingdev/modern-js-plugin-data-loader@3.5.0-ultramodern.20",
97
+ "@modern-js/prod-server": "npm:@bleedingdev/modern-js-prod-server@3.5.0-ultramodern.20",
98
+ "@modern-js/server": "npm:@bleedingdev/modern-js-server@3.5.0-ultramodern.20",
99
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.20",
100
+ "@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.5.0-ultramodern.20",
101
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.20",
102
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.20"
103
103
  },
104
104
  "devDependencies": {
105
105
  "@rslib/core": "0.23.1",