cloudflare-next-intl 0.9.20 → 0.9.22

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.
@@ -1,3 +1,4 @@
1
1
  export declare function isRecentBuild(setAt: number | null, now: number, windowMs?: number): boolean;
2
- export declare function shouldRecoverFromStaleDeploy(error: unknown, buildId: string, marker: string | null, _recentBuild?: boolean): boolean;
2
+ export declare function shouldRecoverFromStaleDeploy(error: unknown, buildId: string, marker: string | null, recentBuild?: boolean, reloadTime?: number | null, now?: number, throttleMs?: number): boolean;
3
+ export declare function performCacheBustReload(): void;
3
4
  export default function useStaleDeployRecovery(error: unknown, onRecover?: () => Promise<unknown>, delayMs?: number): boolean;
@@ -3,9 +3,11 @@ import { useEffect, useState } from 'react';
3
3
  import isStaleDeployError from './is_stale_deploy_error.js';
4
4
  import clearClientCache from './clear_client_cache.js';
5
5
  const RECOVERY_RELOAD_KEY = 'stale-deploy-recovery-reloaded';
6
+ const RECOVERY_TIME_KEY = 'stale-deploy-recovery-time';
6
7
  const BUILD_ID_KEY = 'buildId';
7
8
  const BUILD_ID_SET_AT_KEY = 'buildIdSetAt';
8
9
  const RECENT_BUILD_WINDOW_MS = 60000;
10
+ const RELOAD_THROTTLE_MS = 15000;
9
11
  function currentBuildId() {
10
12
  try {
11
13
  return localStorage.getItem(BUILD_ID_KEY) ?? 'unknown';
@@ -26,21 +28,56 @@ function buildIdSetAt() {
26
28
  export function isRecentBuild(setAt, now, windowMs = RECENT_BUILD_WINDOW_MS) {
27
29
  return setAt !== null && now - setAt < windowMs;
28
30
  }
29
- export function shouldRecoverFromStaleDeploy(error, buildId, marker, _recentBuild = false) {
30
- const isMarkerActive = marker !== null && marker !== '' && (buildId === 'unknown' || marker === buildId);
31
- return isStaleDeployError(error) && !isMarkerActive;
31
+ export function shouldRecoverFromStaleDeploy(error, buildId, marker, recentBuild = false, reloadTime = null, now = Date.now(), throttleMs = RELOAD_THROTTLE_MS) {
32
+ if (!isStaleDeployError(error))
33
+ return false;
34
+ const isRecentlyReloaded = reloadTime !== null && now - reloadTime < throttleMs;
35
+ const isSameBuildMarker = marker !== null && marker !== '' && (buildId === 'unknown' || marker === buildId);
36
+ if (isSameBuildMarker && isRecentlyReloaded && !recentBuild) {
37
+ return false;
38
+ }
39
+ return true;
32
40
  }
33
41
  function canRecover(error) {
34
42
  if (typeof window === 'undefined')
35
43
  return false;
36
44
  try {
37
- return shouldRecoverFromStaleDeploy(error, currentBuildId(), sessionStorage.getItem(RECOVERY_RELOAD_KEY), isRecentBuild(buildIdSetAt(), Date.now()));
45
+ const reloadTimeRaw = sessionStorage.getItem(RECOVERY_TIME_KEY);
46
+ const reloadTime = reloadTimeRaw ? Number(reloadTimeRaw) : null;
47
+ const bId = currentBuildId();
48
+ const marker = sessionStorage.getItem(RECOVERY_RELOAD_KEY);
49
+ const isRecent = isRecentBuild(buildIdSetAt(), Date.now());
50
+ const isStale = isStaleDeployError(error);
51
+ const result = shouldRecoverFromStaleDeploy(error, bId, marker, isRecent, reloadTime, Date.now());
52
+ console.warn('[useStaleDeployRecovery]', {
53
+ error,
54
+ isStale,
55
+ bId,
56
+ marker,
57
+ isRecent,
58
+ reloadTime,
59
+ result,
60
+ });
61
+ return result;
38
62
  }
39
- catch {
63
+ catch (e) {
64
+ console.error('[useStaleDeployRecovery] Error in canRecover:', e);
40
65
  return false;
41
66
  }
42
67
  }
43
- export default function useStaleDeployRecovery(error, onRecover, delayMs = 5000) {
68
+ export function performCacheBustReload() {
69
+ if (typeof window === 'undefined')
70
+ return;
71
+ try {
72
+ const url = new URL(window.location.href);
73
+ url.searchParams.set('_stale_reload', String(Date.now()));
74
+ window.location.replace(url.toString());
75
+ }
76
+ catch {
77
+ window.location.reload();
78
+ }
79
+ }
80
+ export default function useStaleDeployRecovery(error, onRecover, delayMs = 1000) {
44
81
  const [recovering] = useState(() => canRecover(error));
45
82
  const [initialOnRecover] = useState(() => onRecover);
46
83
  const [initialDelayMs] = useState(() => delayMs);
@@ -53,9 +90,10 @@ export default function useStaleDeployRecovery(error, onRecover, delayMs = 5000)
53
90
  .finally(() => {
54
91
  try {
55
92
  sessionStorage.setItem(RECOVERY_RELOAD_KEY, buildId);
93
+ sessionStorage.setItem(RECOVERY_TIME_KEY, String(Date.now()));
56
94
  }
57
95
  catch { }
58
- window.location.reload();
96
+ performCacheBustReload();
59
97
  });
60
98
  }, initialDelayMs);
61
99
  return () => clearTimeout(timeout);
@@ -23,31 +23,35 @@ export default function HelperScript() {
23
23
  }
24
24
  return false;
25
25
  }
26
- function recover(msg) {
27
- // One reload attempt per page load, no matter how many matching
28
- // errors fire in a row (a single stale deploy commonly throws
29
- // several near-simultaneous chunk failures) — without this guard
30
- // each one would race to read-then-write the same sessionStorage
31
- // key and could re-trigger reload() multiple times before the
32
- // first navigation lands.
26
+ function recover(msg, source) {
33
27
  if (attemptedThisLoad) return;
34
28
  try {
35
- if (!isStale(msg)) return;
29
+ var stale = isStale(msg);
30
+ console.warn('[StaleDeploy early-catch] Intercepted:', { source: source, msg: msg, isStale: stale });
31
+ if (!stale) return;
36
32
  var buildId = localStorage.getItem('buildId') || 'unknown';
37
- // Same marker/key as useStaleDeployRecovery: one reload per
38
- // build id, so a repeat failure on a build that already spent
39
- // its reload falls through instead of reloading forever.
40
- if (sessionStorage.getItem(key) === buildId) return;
33
+ var marker = sessionStorage.getItem(key);
34
+ if (marker === buildId) {
35
+ console.warn('[StaleDeploy early-catch] Skipping reload, already attempted for buildId:', buildId);
36
+ return;
37
+ }
41
38
  attemptedThisLoad = true;
42
39
  sessionStorage.setItem(key, buildId);
43
- window.location.reload();
40
+ console.warn('[StaleDeploy early-catch] Reloading for buildId:', buildId);
41
+ try {
42
+ var u = new URL(window.location.href);
43
+ u.searchParams.set('_stale_reload', String(Date.now()));
44
+ window.location.replace(u.toString());
45
+ } catch (e) {
46
+ window.location.reload();
47
+ }
44
48
  } catch (e) {
45
49
  console.error('Stale Deploy Early Catch Script Error:', e);
46
50
  }
47
51
  }
48
- window.addEventListener('error', function(e) { recover(e.message); });
52
+ window.addEventListener('error', function(e) { recover(e.message, 'error-event'); });
49
53
  window.addEventListener('unhandledrejection', function(e) {
50
- recover(e.reason && e.reason.message);
54
+ recover(e.reason && (e.reason.message || e.reason), 'unhandledrejection');
51
55
  });
52
56
  })();`
53
57
  } }), !isDev &&
@@ -107,7 +111,16 @@ export default function HelperScript() {
107
111
  // 3. Handle Locale Redirect.
108
112
  // The logic is clearer: redirect only if a non-default locale is set
109
113
  // and the URL isn't already localized.
114
+ // Clean up stale reload query parameter if present
110
115
  const { pathname, search, hash } = window.location;
116
+ if (search && search.indexOf('_stale_reload=') > -1) {
117
+ try {
118
+ const cleanUrl = new URL(window.location.href);
119
+ cleanUrl.searchParams.delete('_stale_reload');
120
+ window.history.replaceState(history.state, '', cleanUrl.pathname + cleanUrl.search + cleanUrl.hash);
121
+ } catch (e) {}
122
+ }
123
+
111
124
  if (locale && locale !== '${config.defaultLocale}' && !pathname.startsWith(\`/\${locale}\`)) {
112
125
  const newPath = \`/\${locale}\${pathname === '/' ? '' : pathname}\${search}\${hash}\`;
113
126
  // Redirecting will stop further script execution on this page.
@@ -1,5 +1,8 @@
1
1
  import { type LinkProps } from 'next/link.js';
2
2
  import { type ComponentProps } from 'react';
3
- type NextLinkProps = Omit<ComponentProps<'a'>, keyof LinkProps> & Omit<LinkProps, 'locale'>;
3
+ export type PrefetchType = 'custom' | 'default';
4
+ type NextLinkProps = Omit<ComponentProps<'a'>, keyof LinkProps> & Omit<LinkProps, 'locale'> & {
5
+ prefetchType?: PrefetchType;
6
+ };
4
7
  declare const Link: import("react").ForwardRefExoticComponent<Omit<NextLinkProps, "ref"> & import("react").RefAttributes<HTMLAnchorElement>>;
5
8
  export default Link;
@@ -1,26 +1,79 @@
1
+ "use client";
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  import LinkComponent from 'next/link.js';
3
- import { forwardRef, } from 'react';
4
+ import { forwardRef, useEffect, useState, useTransition, } from 'react';
4
5
  import config from '../../config/intl_config.js';
5
6
  import { getLocaleCache } from '../../general/cache_variables.js';
6
- function CustomLinkFunction({ href, prefetch, ...rest }, ref) {
7
+ import { usePathname, useRouter } from 'next/navigation.js';
8
+ const prefetchedRoutes = new Set();
9
+ function CustomLinkFunction({ href, prefetch, prefetchType = 'custom', onClick, onMouseEnter, onPointerDown, ...rest }, ref) {
7
10
  const localeValue = getLocaleCache();
11
+ const router = useRouter();
12
+ const pathname = usePathname();
13
+ const [isPending, startTransition] = useTransition();
14
+ const [isNavigating, setIsNavigating] = useState(false);
8
15
  const needsLangPath = localeValue !== config.defaultLocale || !localeValue;
9
16
  let pathnames;
17
+ let urlString;
10
18
  if (needsLangPath) {
11
- let pathname;
12
- if (typeof href === 'object') {
13
- pathname = href.pathname || '';
14
- }
15
- else {
16
- pathname = href;
17
- }
18
- pathnames = `/${localeValue}${pathname}`;
19
+ const pathPart = typeof href === 'object' ? (href.pathname || '') : (href || '');
20
+ pathnames = `/${localeValue}${pathPart}`;
21
+ urlString = pathnames;
19
22
  }
20
23
  else {
21
24
  pathnames = href;
25
+ urlString = typeof href === 'object' ? (href.pathname || '') : (href || '');
22
26
  }
23
- return _jsx(LinkComponent, { ref: ref, href: pathnames, prefetch: prefetch, ...rest });
27
+ const isCustom = prefetchType === 'custom';
28
+ useEffect(() => {
29
+ setIsNavigating(false);
30
+ }, [pathname]);
31
+ const triggerPrefetch = () => {
32
+ if (!isCustom || !urlString || urlString.startsWith('#') || prefetchedRoutes.has(urlString)) {
33
+ return;
34
+ }
35
+ prefetchedRoutes.add(urlString);
36
+ try {
37
+ router.prefetch(typeof pathnames === 'string' ? pathnames : urlString);
38
+ }
39
+ catch {
40
+ }
41
+ };
42
+ useEffect(() => {
43
+ if (!isCustom)
44
+ return;
45
+ const timer = setTimeout(triggerPrefetch, 600);
46
+ return () => clearTimeout(timer);
47
+ }, [urlString, isCustom]);
48
+ const handleHoverPrefetch = () => {
49
+ if (isCustom) {
50
+ triggerPrefetch();
51
+ }
52
+ };
53
+ const handleClick = (e) => {
54
+ onClick?.(e);
55
+ if (e.defaultPrevented)
56
+ return;
57
+ if (isCustom && (isNavigating || isPending)) {
58
+ e.preventDefault();
59
+ return;
60
+ }
61
+ if (isCustom) {
62
+ setIsNavigating(true);
63
+ startTransition(() => {
64
+ router.push(typeof pathnames === 'string' ? pathnames : urlString);
65
+ });
66
+ e.preventDefault();
67
+ }
68
+ };
69
+ const effectivePrefetch = isCustom ? (prefetch ?? false) : prefetch;
70
+ return _jsx(LinkComponent, { ref: ref, href: pathnames, prefetch: effectivePrefetch, onClick: handleClick, onMouseEnter: (e) => {
71
+ handleHoverPrefetch();
72
+ onMouseEnter?.(e);
73
+ }, onPointerDown: (e) => {
74
+ handleHoverPrefetch();
75
+ onPointerDown?.(e);
76
+ }, ...rest });
24
77
  }
25
78
  const Link = forwardRef(CustomLinkFunction);
26
79
  export default Link;
@@ -54,12 +54,13 @@ export async function getCountry(input, generate, headerNames) {
54
54
  const names = headerNames
55
55
  ?? gen?.countryHeaderNames
56
56
  ?? defaultCountryHeaderNames;
57
+ const cookieName = gen?.countryCookieName ?? countryCookieKey;
57
58
  if (input) {
58
59
  if ('headers' in input && input.headers) {
59
60
  const country = extractFromHeaderNames(input.headers, names);
60
61
  if (country)
61
62
  return country;
62
- const cookieCountry = extractCookieHeader(input.headers, countryCookieKey);
63
+ const cookieCountry = extractCookieHeader(input.headers, cookieName);
63
64
  if (cookieCountry)
64
65
  return cookieCountry;
65
66
  }
@@ -67,11 +68,11 @@ export async function getCountry(input, generate, headerNames) {
67
68
  const country = extractFromHeaderNames(input, names);
68
69
  if (country)
69
70
  return country;
70
- const cookieCountry = extractCookieHeader(input, countryCookieKey);
71
+ const cookieCountry = extractCookieHeader(input, cookieName);
71
72
  if (cookieCountry)
72
73
  return cookieCountry;
73
74
  }
74
- const cookieCountry = extractCookie(input.cookies, countryCookieKey);
75
+ const cookieCountry = extractCookie(input.cookies, cookieName);
75
76
  if (cookieCountry) {
76
77
  return cookieCountry;
77
78
  }
@@ -86,7 +87,7 @@ export async function getCountry(input, generate, headerNames) {
86
87
  const country = extractFromHeaderNames(h, names);
87
88
  if (country)
88
89
  return country;
89
- const cookieCountry = extractCookieHeader(h, countryCookieKey);
90
+ const cookieCountry = extractCookieHeader(h, cookieName);
90
91
  if (cookieCountry)
91
92
  return cookieCountry;
92
93
  }
@@ -95,7 +96,7 @@ export async function getCountry(input, generate, headerNames) {
95
96
  try {
96
97
  const { cookies } = await import('next/headers.js');
97
98
  const c = await cookies();
98
- const country = extractCookie(c, countryCookieKey);
99
+ const country = extractCookie(c, cookieName);
99
100
  if (country)
100
101
  return country;
101
102
  }
@@ -129,12 +130,13 @@ export async function getTimezone(input, fallback, generate, headerNames) {
129
130
  const names = headerNames
130
131
  ?? gen?.timezoneHeaderNames
131
132
  ?? defaultTimezoneHeaderNames;
133
+ const cookieName = gen?.timezoneCookieName ?? timezoneCookieKey;
132
134
  if (input) {
133
135
  if ('headers' in input && input.headers) {
134
136
  const tz = extractFromHeaderNames(input.headers, names);
135
137
  if (tz)
136
138
  return tz;
137
- const cookieTimezone = extractCookieHeader(input.headers, timezoneCookieKey);
139
+ const cookieTimezone = extractCookieHeader(input.headers, cookieName);
138
140
  if (cookieTimezone)
139
141
  return cookieTimezone;
140
142
  }
@@ -142,11 +144,11 @@ export async function getTimezone(input, fallback, generate, headerNames) {
142
144
  const tz = extractFromHeaderNames(input, names);
143
145
  if (tz)
144
146
  return tz;
145
- const cookieTimezone = extractCookieHeader(input, timezoneCookieKey);
147
+ const cookieTimezone = extractCookieHeader(input, cookieName);
146
148
  if (cookieTimezone)
147
149
  return cookieTimezone;
148
150
  }
149
- const cookieTimezone = extractCookie(input.cookies, timezoneCookieKey);
151
+ const cookieTimezone = extractCookie(input.cookies, cookieName);
150
152
  if (cookieTimezone) {
151
153
  return cookieTimezone;
152
154
  }
@@ -161,7 +163,7 @@ export async function getTimezone(input, fallback, generate, headerNames) {
161
163
  const tz = extractFromHeaderNames(h, names);
162
164
  if (tz)
163
165
  return tz;
164
- const cookieTimezone = extractCookieHeader(h, timezoneCookieKey);
166
+ const cookieTimezone = extractCookieHeader(h, cookieName);
165
167
  if (cookieTimezone)
166
168
  return cookieTimezone;
167
169
  }
@@ -170,7 +172,7 @@ export async function getTimezone(input, fallback, generate, headerNames) {
170
172
  try {
171
173
  const { cookies } = await import('next/headers.js');
172
174
  const c = await cookies();
173
- const tz = extractCookie(c, timezoneCookieKey);
175
+ const tz = extractCookie(c, cookieName);
174
176
  if (tz)
175
177
  return tz;
176
178
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.9.20",
3
+ "version": "0.9.22",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",