cloudflare-next-intl 0.3.2 → 0.3.3

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.
@@ -5,10 +5,12 @@ interface LocaleContextType {
5
5
  messages: TranslationObject;
6
6
  }
7
7
  export declare const LocaleContext: import("react").Context<LocaleContextType | undefined>;
8
- export default function LocationzationClientProvider({ language, messages, initialAuthUser, children }: {
8
+ export default function LocationzationClientProvider({ language, messages, initialAuthUser, skipAuthProvider, children }: {
9
9
  language: string;
10
10
  messages: TranslationObject;
11
11
  initialAuthUser?: SerializedAuthUser | null;
12
+ /** Set when `firebaseAuth.autoWireClientProvider` is `false` — skips wrapping `children` in the client `AuthUserProvider` entirely. */
13
+ skipAuthProvider?: boolean;
12
14
  children: React.ReactNode;
13
15
  }): Component;
14
16
  export {};
@@ -5,7 +5,15 @@ import { createContext, useMemo } from "react";
5
5
  import dynamic from "next/dynamic";
6
6
  import config from "@intl-config";
7
7
  export const LocaleContext = createContext(undefined);
8
- export default function LocationzationClientProvider({ language, messages, initialAuthUser = null, children }) {
8
+ // Hoisted to module scope calling `dynamic()` inside the component body
9
+ // creates a brand-new component identity every render, forcing React to
10
+ // unmount/remount `AuthUserProvider` on every render instead of reusing the
11
+ // existing instance. That remount re-subscribes `onIdTokenChanged`, which
12
+ // Firebase immediately replays with the current user, triggering a state
13
+ // update (and a `getIdToken(true)` refresh) that causes another render —
14
+ // an infinite loop of session-cookie writes, one per render.
15
+ const AuthUserProvider = dynamic(() => import("../../firebase_auth/client/auth_user_provider"));
16
+ export default function LocationzationClientProvider({ language, messages, initialAuthUser = null, skipAuthProvider = false, children }) {
9
17
  setLocaleCache(language);
10
18
  setMessageForLocaleCache(language, messages);
11
19
  // `LocaleContext.Provider` stays the outermost element here — the
@@ -14,8 +22,7 @@ export default function LocationzationClientProvider({ language, messages, initi
14
22
  // sibling wrapping it, or those hooks would throw for running outside
15
23
  // the provider.
16
24
  let providedChildren = children;
17
- if (config.firebaseAuth) {
18
- const AuthUserProvider = dynamic(() => import("../../firebase_auth/client/auth_user_provider"));
25
+ if (config.firebaseAuth && !skipAuthProvider) {
19
26
  providedChildren = _jsx(AuthUserProvider, { initialUser: initialAuthUser, children: children });
20
27
  }
21
28
  const contextValue = useMemo(() => ({ language, messages }), [language, messages]);
@@ -10,8 +10,22 @@
10
10
  */
11
11
  export default function getCookie(name) {
12
12
  try {
13
- const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
14
- return match ? decodeURIComponent(match[1]) : null;
13
+ const cookie = document.cookie;
14
+ const prefix = `${name}=`;
15
+ let start = -1;
16
+ if (cookie.startsWith(prefix)) {
17
+ start = prefix.length;
18
+ }
19
+ else {
20
+ const idx = cookie.indexOf(`; ${prefix}`);
21
+ if (idx !== -1)
22
+ start = idx + 2 + prefix.length;
23
+ }
24
+ if (start === -1)
25
+ return null;
26
+ const end = cookie.indexOf(';', start);
27
+ const value = end === -1 ? cookie.slice(start) : cookie.slice(start, end);
28
+ return decodeURIComponent(value);
15
29
  }
16
30
  catch (e) {
17
31
  console.error(`Get cookie on client side error: ${e}`);
@@ -11,13 +11,16 @@ const defaultCookieOption = {
11
11
  secure: false, // Send cookie only over HTTPS in production
12
12
  sameSite: sameSite, // Protection against CSRF attacks. 'strict' or 'lax' are good choices.
13
13
  };
14
+ let userAgentModule;
14
15
  async function getIsBotValue(userAgent) {
15
16
  if (userAgent === null)
16
17
  return false;
17
- const { isBot } = await import('next/dist/server/web/spec-extension/user-agent');
18
+ if (!userAgentModule) {
19
+ userAgentModule = await import('next/dist/server/web/spec-extension/user-agent');
20
+ }
18
21
  // Unreachable: userAgent is already narrowed to non-null string above,
19
22
  // so the ?? '' fallback never triggers.
20
- return isBot(userAgent ?? '');
23
+ return userAgentModule.isBot(userAgent ?? '');
21
24
  }
22
25
  const getIsBotValueCache = cache(getIsBotValue);
23
26
  export const localesSet = new Set(config.locales);
@@ -11,6 +11,7 @@ const fa = {
11
11
  vi.mock('@intl-config', () => ({ default: { firebaseAuth: fa } }));
12
12
  vi.mock('./firebase_client', () => ({
13
13
  getFirebaseAuthClient: vi.fn(async () => ({ auth: {} })),
14
+ getFirebaseAuthModule: () => import('firebase/auth'),
14
15
  }));
15
16
  vi.mock('../error_messages/firebase_auth_error_helper', () => ({
16
17
  default: vi.fn(() => 'translated error'),
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import config from '@intl-config';
3
3
  import requireFirebaseAuthConfig from '../require_config';
4
- import { getFirebaseAuthClient } from './firebase_client';
4
+ import { getFirebaseAuthClient, getFirebaseAuthModule } from './firebase_client';
5
5
  import firebaseAuthErrorMessage from '../error_messages/firebase_auth_error_helper';
6
6
  function readCredentials(formData) {
7
7
  return {
@@ -27,7 +27,7 @@ export function createLoginAction(locale, messages) {
27
27
  return async function loginAction(_prevState, formData) {
28
28
  requireFirebaseAuthConfig(config.firebaseAuth);
29
29
  const { auth } = await getFirebaseAuthClient();
30
- const { signInWithEmailAndPassword } = await import('firebase/auth');
30
+ const { signInWithEmailAndPassword } = await getFirebaseAuthModule();
31
31
  const { email, password } = readCredentials(formData);
32
32
  try {
33
33
  await signInWithEmailAndPassword(auth, email, password);
@@ -57,7 +57,7 @@ export function createSignUpAction(locale, messages) {
57
57
  return async function signUpAction(_prevState, formData) {
58
58
  requireFirebaseAuthConfig(config.firebaseAuth);
59
59
  const { auth } = await getFirebaseAuthClient();
60
- const { createUserWithEmailAndPassword } = await import('firebase/auth');
60
+ const { createUserWithEmailAndPassword } = await getFirebaseAuthModule();
61
61
  const { email, password } = readCredentials(formData);
62
62
  const confirmPassword = (formData.get('confirmPassword')?.toString() ?? '').trim();
63
63
  if (messages.mismatch && password !== confirmPassword) {
@@ -89,7 +89,7 @@ export function createForgotPasswordAction(locale, messages) {
89
89
  return async function forgotPasswordAction(_prevState, formData) {
90
90
  requireFirebaseAuthConfig(config.firebaseAuth);
91
91
  const { auth } = await getFirebaseAuthClient();
92
- const { sendPasswordResetEmail } = await import('firebase/auth');
92
+ const { sendPasswordResetEmail } = await getFirebaseAuthModule();
93
93
  const email = (formData.get('email')?.toString() ?? '').trim();
94
94
  try {
95
95
  await sendPasswordResetEmail(auth, email);
@@ -5,18 +5,19 @@ import { useRouter } from 'next/navigation';
5
5
  import usePathname from '../../client/hooks/use_path_name';
6
6
  import config from '@intl-config';
7
7
  import requireFirebaseAuthConfig from '../require_config';
8
- import { getFirebaseAuthClient } from './firebase_client';
8
+ import { getFirebaseAuthClient, getFirebaseAuthModule } from './firebase_client';
9
9
  import { setAuthUserCache } from './auth_user_cache';
10
10
  import { defaultSessionCookieName } from '../middleware/update_session';
11
+ import setCookie from '../../client/functions/set_cookie';
11
12
  // `null` default (instead of a `{ loading: true, ... }` stand-in) lets
12
13
  // `useAuthUser` distinguish "not wrapped in AuthUserProvider" (throw) from
13
14
  // "wrapped, still loading" (`loading: true`).
14
15
  export const AuthUserContext = createContext(null);
15
16
  function writeSessionCookie(sessionCookieName, idToken, maxAge) {
16
- document.cookie = `${sessionCookieName}=${idToken}; path=/; max-age=${maxAge}`;
17
+ setCookie({ name: sessionCookieName, value: idToken, maxAge });
17
18
  }
18
19
  function clearSessionCookie(sessionCookieName) {
19
- document.cookie = `${sessionCookieName}=; path=/; max-age=0`;
20
+ setCookie({ name: sessionCookieName, value: '', maxAge: 0 });
20
21
  }
21
22
  /**
22
23
  * Client-side auth-state provider for `firebase_auth`. Wrap your root layout
@@ -49,7 +50,15 @@ export default function AuthUserProvider({ initialUser = null, children }) {
49
50
  user: initialUser,
50
51
  loading: initialUser === null,
51
52
  });
53
+ // The signed-in state the last successful cookie write left behind, so a
54
+ // plain token refresh (same state) does not trigger a needless re-render.
52
55
  const syncedSignedIn = useRef(undefined);
56
+ // Consecutive `onIdTokenChanged(null)` callbacks since the last confirmed
57
+ // user. A single null here can be a transient client-SDK hiccup (e.g. its
58
+ // token-refresh scheduling misbehaving under local clock skew) rather
59
+ // than a real sign-out — the server already proved the session valid via
60
+ // `initialUser`, so redirecting on the very first null caused a
61
+ // login-then-bounce-home flash whenever the two disagreed.
53
62
  const consecutiveNulls = useRef(0);
54
63
  const [confirmedSignedOut, setConfirmedSignedOut] = useState(initialUser === null);
55
64
  useEffect(() => {
@@ -71,7 +80,7 @@ export default function AuthUserProvider({ initialUser = null, children }) {
71
80
  getFirebaseAuthClient().then(async ({ auth }) => {
72
81
  if (cancelled)
73
82
  return;
74
- const { onIdTokenChanged } = await import('firebase/auth');
83
+ const { onIdTokenChanged } = await getFirebaseAuthModule();
75
84
  unsubscribe = onIdTokenChanged(auth, async (user) => {
76
85
  const isSignedIn = !!user;
77
86
  const previous = syncedSignedIn.current;
@@ -119,24 +128,29 @@ export default function AuthUserProvider({ initialUser = null, children }) {
119
128
  const user = auth.currentUser;
120
129
  if (!user)
121
130
  return;
122
- const { reload } = await import('firebase/auth');
123
- await reload(user);
124
- writeSessionCookie(sessionCookieName, await user.getIdToken(true), maxAge);
125
- setAuthUserCache(user);
126
- setState({ user, loading: false });
127
- }, [maxAge, sessionCookieName]);
131
+ try {
132
+ const { reload } = await getFirebaseAuthModule();
133
+ await reload(user);
134
+ writeSessionCookie(sessionCookieName, await user.getIdToken(true), maxAge);
135
+ setAuthUserCache(user);
136
+ setState({ user, loading: false });
137
+ }
138
+ catch (e) {
139
+ console.error('AuthUserProvider: reloadUser failed', e);
140
+ }
141
+ }, []);
128
142
  const sendVerificationEmail = useCallback(async () => {
129
143
  const { auth } = await getFirebaseAuthClient();
130
144
  const user = auth.currentUser;
131
145
  if (!user)
132
146
  return;
133
- const { sendEmailVerification } = await import('firebase/auth');
147
+ const { sendEmailVerification } = await getFirebaseAuthModule();
134
148
  await sendEmailVerification(user);
135
149
  }, []);
136
150
  const logout = useCallback(async () => {
137
151
  try {
138
152
  const { auth } = await getFirebaseAuthClient();
139
- const { signOut } = await import('firebase/auth');
153
+ const { signOut } = await getFirebaseAuthModule();
140
154
  await signOut(auth);
141
155
  }
142
156
  finally {
@@ -16,3 +16,5 @@ export declare function getFirebaseAuthClientSync(): {
16
16
  app: FirebaseApp;
17
17
  auth: Auth;
18
18
  } | undefined;
19
+ /** Memoized `import('firebase/auth')` — see {@link getFirebaseAuthClient} for why this is worth caching. */
20
+ export declare function getFirebaseAuthModule(): Promise<typeof import('firebase/auth')>;
@@ -38,3 +38,11 @@ export async function getFirebaseAuthClient() {
38
38
  export function getFirebaseAuthClientSync() {
39
39
  return cached;
40
40
  }
41
+ let cachedAuthModule;
42
+ /** Memoized `import('firebase/auth')` — see {@link getFirebaseAuthClient} for why this is worth caching. */
43
+ export function getFirebaseAuthModule() {
44
+ if (!cachedAuthModule) {
45
+ cachedAuthModule = import('firebase/auth');
46
+ }
47
+ return cachedAuthModule;
48
+ }
@@ -1,5 +1,5 @@
1
1
  import { getTranslationsImpl } from '../../general/general_functions';
2
- import { getMessageCache } from '../../general/cache_variables';
2
+ import { getMessageCache, getTranslationCache } from '../../general/cache_variables';
3
3
  import { DEFAULT_MESSAGES_EN } from './default_messages.en';
4
4
  const ERROR_CODE_TO_KEY = {
5
5
  'auth/invalid-email': 'invalidEmail',
@@ -29,7 +29,8 @@ export default function firebaseAuthErrorMessage(locale, error) {
29
29
  const messages = getMessageCache(locale);
30
30
  if (messages) {
31
31
  try {
32
- const t = getTranslationsImpl(locale, messages, 'firebaseAuth');
32
+ const cacheKey = `${locale}-firebaseAuth`;
33
+ const t = getTranslationCache(cacheKey) ?? getTranslationsImpl(locale, messages, 'firebaseAuth', cacheKey);
33
34
  const translated = t(key);
34
35
  if (typeof translated === 'string' && translated !== key)
35
36
  return translated;
@@ -232,11 +232,7 @@ export default async function updateSession(request, baseResponse, locale) {
232
232
  }
233
233
  if (refreshedToken) {
234
234
  response.cookies.set(sessionCookieName, refreshedToken.idToken, {
235
- // Not httpOnly: the client provider also writes this cookie
236
- // directly (via document.cookie) after `getIdToken(true)`, so it
237
- // must stay client-writable — a JS cookie write can never carry
238
- // httpOnly anyway, and two same-name cookies with conflicting
239
- // flags is what actually caused ambiguity here.
235
+ httpOnly: true,
240
236
  secure: request.nextUrl.protocol === 'https',
241
237
  sameSite: 'lax',
242
238
  path: '/',
@@ -3,11 +3,16 @@ import type { SerializedAuthUser } from '../types';
3
3
  * Resolves the signed-in user from the session cookie and performs the
4
4
  * authoritative pre-render redirect (guest→`redirectAuthPath`, signed-in→
5
5
  * `homePath` on auth pages) — middleware only checks cookie *presence*, not
6
- * validity. Plain async function, not a component: callers decide
7
- * where/how to use the resolved user relative to their own component tree
8
- * (see `AuthUserServerProvider` below for the simple case, and
9
- * `IntlProvider`'s auto-wiring for the case where ordering against
10
- * `LocaleContext` matters).
6
+ * validity; a forged, expired, or otherwise invalid-but-present cookie
7
+ * sails through it. Only this function's token validation
8
+ * (`getAuthenticatedAppForUser`) catches that, so this redirect must happen
9
+ * here, before any HTML is sent relying solely on the client
10
+ * `AuthUserProvider` effect to redirect afterwards produces a visible
11
+ * flash (page renders signed-in, then bounces). Plain async function, not
12
+ * a component: callers decide where/how to use the resolved user relative
13
+ * to their own component tree (see `AuthUserServerProvider` below for the
14
+ * simple case, and `IntlProvider`'s auto-wiring for the case where ordering
15
+ * against `LocaleContext` matters).
11
16
  */
12
17
  export declare function resolveAuthUserAndRedirect(): Promise<SerializedAuthUser | null>;
13
18
  /**
@@ -10,11 +10,16 @@ const AuthUserProvider = dynamic(() => import('../client/auth_user_provider'));
10
10
  * Resolves the signed-in user from the session cookie and performs the
11
11
  * authoritative pre-render redirect (guest→`redirectAuthPath`, signed-in→
12
12
  * `homePath` on auth pages) — middleware only checks cookie *presence*, not
13
- * validity. Plain async function, not a component: callers decide
14
- * where/how to use the resolved user relative to their own component tree
15
- * (see `AuthUserServerProvider` below for the simple case, and
16
- * `IntlProvider`'s auto-wiring for the case where ordering against
17
- * `LocaleContext` matters).
13
+ * validity; a forged, expired, or otherwise invalid-but-present cookie
14
+ * sails through it. Only this function's token validation
15
+ * (`getAuthenticatedAppForUser`) catches that, so this redirect must happen
16
+ * here, before any HTML is sent relying solely on the client
17
+ * `AuthUserProvider` effect to redirect afterwards produces a visible
18
+ * flash (page renders signed-in, then bounces). Plain async function, not
19
+ * a component: callers decide where/how to use the resolved user relative
20
+ * to their own component tree (see `AuthUserServerProvider` below for the
21
+ * simple case, and `IntlProvider`'s auto-wiring for the case where ordering
22
+ * against `LocaleContext` matters).
18
23
  */
19
24
  export async function resolveAuthUserAndRedirect() {
20
25
  const fa = config.firebaseAuth;
@@ -4,6 +4,8 @@ import config from '@intl-config';
4
4
  import requireFirebaseAuthConfig from '../require_config';
5
5
  import { defaultSessionCookieName } from '../middleware/update_session';
6
6
  let baseApp;
7
+ let firebaseAppModule;
8
+ let firebaseAuthModule;
7
9
  /**
8
10
  * Resolves the signed-in user on the server from the session cookie.
9
11
  * `initializeServerApp` validates the token with the Auth service, so a
@@ -22,8 +24,12 @@ export const getAuthenticatedAppForUser = cache(async function getAuthenticatedA
22
24
  return { firebaseServerApp: null, currentUser: null };
23
25
  }
24
26
  try {
25
- const { initializeApp, initializeServerApp } = await import('firebase/app');
26
- const { getAuth } = await import('firebase/auth');
27
+ if (!firebaseAppModule)
28
+ firebaseAppModule = await import('firebase/app');
29
+ if (!firebaseAuthModule)
30
+ firebaseAuthModule = await import('firebase/auth');
31
+ const { initializeApp, initializeServerApp } = firebaseAppModule;
32
+ const { getAuth } = firebaseAuthModule;
27
33
  const firebaseConfig = {
28
34
  apiKey: fa.apiKey,
29
35
  authDomain: fa.authDomain,
@@ -5,6 +5,7 @@ import dynamic from "next/dynamic";
5
5
  import { localesSet } from "../../config/middleware";
6
6
  import config from "../../config/intl_config";
7
7
  const LocationzationClientProvider = dynamic(() => import("../../client/components/client_provider"));
8
+ let authUserServerProviderModule;
8
9
  /**
9
10
  * Server component that provides locale/messages context to the rest of the
10
11
  * tree. Exported publicly as `IntlProvider` from `cloudflare-next-intl/serverProvider`.
@@ -47,9 +48,12 @@ export default async function LocationzationProvider({ language, messages, child
47
48
  }
48
49
  const messagesValue = messages ?? await getMessage(language);
49
50
  let initialAuthUser = null;
50
- if (config.firebaseAuth) {
51
- const { resolveAuthUserAndRedirect } = await import("../../firebase_auth/server/auth_user_server_provider");
52
- initialAuthUser = await resolveAuthUserAndRedirect();
51
+ const autoWireClientProvider = config.firebaseAuth?.autoWireClientProvider !== false;
52
+ if (config.firebaseAuth && autoWireClientProvider) {
53
+ if (!authUserServerProviderModule) {
54
+ authUserServerProviderModule = await import("../../firebase_auth/server/auth_user_server_provider");
55
+ }
56
+ initialAuthUser = await authUserServerProviderModule.resolveAuthUserAndRedirect();
53
57
  }
54
- return _jsx(LocationzationClientProvider, { language: language, messages: messagesValue, initialAuthUser: initialAuthUser, children: children });
58
+ return _jsx(LocationzationClientProvider, { language: language, messages: messagesValue, initialAuthUser: initialAuthUser, skipAuthProvider: !autoWireClientProvider, children: children });
55
59
  }
@@ -5,6 +5,7 @@ import { getLocaleCache, getMessageCache, getTranslationCache, setLocaleCache, s
5
5
  import { cache } from "react";
6
6
  import { localesSet } from "../../config/middleware";
7
7
  const isDev = process.env.NODE_ENV === 'development';
8
+ let nextHeadersModule;
8
9
  /**
9
10
  * Loads and caches messages for a specific locale using dynamic import.
10
11
  * Prevents redundant file loads and handles import errors gracefully.
@@ -104,8 +105,10 @@ async function iGetLocale() {
104
105
  // Dynamically import "next/headers" only when needed.
105
106
  // This ensures it's loaded only on the server where cookies are accessible,
106
107
  // preventing client-side import errors and reducing bundle size.
107
- const { cookies } = await import("next/headers");
108
- const cookieStore = await cookies();
108
+ if (!nextHeadersModule) {
109
+ nextHeadersModule = await import("next/headers");
110
+ }
111
+ const cookieStore = await nextHeadersModule.cookies();
109
112
  const localeCookie = cookieStore.get(localeCookieName);
110
113
  // Use the cookie value or fall back to the default locale.
111
114
  const localeValue = localeCookie?.value ?? config.defaultLocale;
@@ -92,6 +92,17 @@ export interface FirebaseAuthRoutingConfig {
92
92
  * middleware redirect logic yourself instead.
93
93
  */
94
94
  middlewareEnabled?: boolean;
95
+ /**
96
+ * Whether `IntlProvider` should automatically wrap your app in the
97
+ * client `AuthUserProvider` and call `resolveAuthUser` server-side.
98
+ * Defaults to `true`. Set `false` if you drive auth entirely from your
99
+ * own middleware (like `middlewareEnabled: false`'s manual-override
100
+ * case, but for the client/RSC layer) and don't want this package
101
+ * rendering any auth-related React tree on top of it — e.g. if you
102
+ * only use `intlMiddleware`'s built-in session-refresh/redirect logic
103
+ * and have no use for `useAuthUser()`/`AuthUserProvider` at all.
104
+ */
105
+ autoWireClientProvider?: boolean;
95
106
  /** Firebase project's Web API key (`NEXT_PUBLIC_FIREBASE_API_KEY` equivalent). */
96
107
  apiKey: string;
97
108
  /** Firebase project's auth domain, e.g. "my-app.firebaseapp.com". */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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",