cloudflare-next-intl 0.2.2 → 0.3.0

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.
Files changed (97) hide show
  1. package/README.md +2 -3
  2. package/dist/src/client/components/client_provider.d.ts +3 -1
  3. package/dist/src/client/components/client_provider.js +16 -3
  4. package/dist/src/client/functions/get_cookie.bench.d.ts +1 -0
  5. package/dist/src/client/functions/get_cookie.bench.js +11 -0
  6. package/dist/src/client/functions/set_cookie.bench.d.ts +1 -0
  7. package/dist/src/client/functions/set_cookie.bench.js +10 -0
  8. package/dist/src/client/functions/set_cookie.d.ts +4 -2
  9. package/dist/src/client/functions/set_cookie.js +3 -1
  10. package/dist/src/client/hooks/client_hooks.d.ts +5 -0
  11. package/dist/src/client/hooks/client_hooks.js +7 -2
  12. package/dist/src/client/index.js +3 -0
  13. package/dist/src/config/cookie_key.bench.d.ts +1 -0
  14. package/dist/src/config/cookie_key.bench.js +9 -0
  15. package/dist/src/config/init_config.bench.d.ts +1 -0
  16. package/dist/src/config/init_config.bench.js +7 -0
  17. package/dist/src/config/intl_config.js +4 -1
  18. package/dist/src/config/intl_sitemap.bench.d.ts +1 -0
  19. package/dist/src/config/intl_sitemap.bench.js +18 -0
  20. package/dist/src/config/intl_sitemap.js +1 -1
  21. package/dist/src/config/middleware.bench.d.ts +1 -0
  22. package/dist/src/config/middleware.bench.js +34 -0
  23. package/dist/src/config/middleware.js +39 -8
  24. package/dist/src/firebase_auth/client/auth_actions.bench.d.ts +1 -0
  25. package/dist/src/firebase_auth/client/auth_actions.bench.js +36 -0
  26. package/dist/src/firebase_auth/client/auth_actions.d.ts +46 -0
  27. package/dist/src/firebase_auth/client/auth_actions.js +102 -0
  28. package/dist/src/firebase_auth/client/auth_user_cache.bench.d.ts +1 -0
  29. package/dist/src/firebase_auth/client/auth_user_cache.bench.js +11 -0
  30. package/dist/src/firebase_auth/client/auth_user_cache.d.ts +4 -0
  31. package/dist/src/firebase_auth/client/auth_user_cache.js +14 -0
  32. package/dist/src/firebase_auth/client/auth_user_provider.d.ts +36 -0
  33. package/dist/src/firebase_auth/client/auth_user_provider.js +148 -0
  34. package/dist/src/firebase_auth/client/firebase_client.bench.d.ts +1 -0
  35. package/dist/src/firebase_auth/client/firebase_client.bench.js +28 -0
  36. package/dist/src/firebase_auth/client/firebase_client.d.ts +18 -0
  37. package/dist/src/firebase_auth/client/firebase_client.js +40 -0
  38. package/dist/src/firebase_auth/client/use_auth_user.d.ts +12 -0
  39. package/dist/src/firebase_auth/client/use_auth_user.js +20 -0
  40. package/dist/src/firebase_auth/error_messages/default_messages.en.bench.d.ts +1 -0
  41. package/dist/src/firebase_auth/error_messages/default_messages.en.bench.js +10 -0
  42. package/dist/src/firebase_auth/error_messages/default_messages.en.d.ts +1 -0
  43. package/dist/src/firebase_auth/error_messages/default_messages.en.js +14 -0
  44. package/dist/src/firebase_auth/error_messages/firebase_auth_error_helper.bench.d.ts +1 -0
  45. package/dist/src/firebase_auth/error_messages/firebase_auth_error_helper.bench.js +15 -0
  46. package/dist/src/firebase_auth/error_messages/firebase_auth_error_helper.d.ts +6 -0
  47. package/dist/src/firebase_auth/error_messages/firebase_auth_error_helper.js +42 -0
  48. package/dist/src/firebase_auth/index.d.ts +9 -0
  49. package/dist/src/firebase_auth/index.js +13 -0
  50. package/dist/src/firebase_auth/middleware/update_session.bench.d.ts +1 -0
  51. package/dist/src/firebase_auth/middleware/update_session.bench.js +37 -0
  52. package/dist/src/firebase_auth/middleware/update_session.d.ts +22 -0
  53. package/dist/src/firebase_auth/middleware/update_session.js +213 -0
  54. package/dist/src/firebase_auth/require_config.bench.d.ts +1 -0
  55. package/dist/src/firebase_auth/require_config.bench.js +24 -0
  56. package/dist/src/firebase_auth/require_config.d.ts +8 -0
  57. package/dist/src/firebase_auth/require_config.js +13 -0
  58. package/dist/src/firebase_auth/server/auth_user_server_provider.d.ts +24 -0
  59. package/dist/src/firebase_auth/server/auth_user_server_provider.js +51 -0
  60. package/dist/src/firebase_auth/server/firebase_server.bench.d.ts +1 -0
  61. package/dist/src/firebase_auth/server/firebase_server.bench.js +27 -0
  62. package/dist/src/firebase_auth/server/firebase_server.d.ts +15 -0
  63. package/dist/src/firebase_auth/server/firebase_server.js +50 -0
  64. package/dist/src/firebase_auth/server/use_auth_user_server.bench.d.ts +1 -0
  65. package/dist/src/firebase_auth/server/use_auth_user_server.bench.js +14 -0
  66. package/dist/src/firebase_auth/server/use_auth_user_server.d.ts +19 -0
  67. package/dist/src/firebase_auth/server/use_auth_user_server.js +19 -0
  68. package/dist/src/firebase_auth/types.d.ts +22 -0
  69. package/dist/src/firebase_auth/types.js +1 -0
  70. package/dist/src/general/cache_variables.bench.d.ts +1 -0
  71. package/dist/src/general/cache_variables.bench.js +19 -0
  72. package/dist/src/general/cache_variables.d.ts +1 -0
  73. package/dist/src/general/cache_variables.js +4 -6
  74. package/dist/src/general/general_functions.bench.d.ts +1 -0
  75. package/dist/src/general/general_functions.bench.js +18 -0
  76. package/dist/src/general/general_functions.js +4 -0
  77. package/dist/src/general/index.js +0 -1
  78. package/dist/src/general/metadata.bench.d.ts +1 -0
  79. package/dist/src/general/metadata.bench.js +15 -0
  80. package/dist/src/general/metadata.js +1 -1
  81. package/dist/src/server/components/server_provider.js +7 -1
  82. package/dist/src/server/functions/get_user_locale.bench.d.ts +1 -0
  83. package/dist/src/server/functions/get_user_locale.bench.js +13 -0
  84. package/dist/src/server/functions/get_user_locale.js +7 -4
  85. package/dist/src/server/functions/server.bench.d.ts +1 -0
  86. package/dist/src/server/functions/server.bench.js +39 -0
  87. package/dist/src/server/functions/server.js +9 -5
  88. package/dist/src/server/functions/use_functions.bench.d.ts +1 -0
  89. package/dist/src/server/functions/use_functions.bench.js +23 -0
  90. package/dist/src/server/functions/use_functions.d.ts +5 -0
  91. package/dist/src/server/functions/use_functions.js +19 -4
  92. package/dist/src/server/index.js +4 -1
  93. package/dist/src/test_utils/mock_next_server.d.ts +5 -0
  94. package/dist/src/test_utils/mock_next_server.js +10 -0
  95. package/dist/src/types/types.d.ts +48 -1
  96. package/llms.txt +47 -0
  97. package/package.json +46 -8
package/README.md CHANGED
@@ -65,9 +65,8 @@ const nextConfig: NextConfig = {
65
65
  export default nextConfig;
66
66
  ```
67
67
 
68
- If this alias is missing, `cloudflare-next-intl/middleware` throws
69
- `Please set config file and set path to it in next.config as in the example`
70
- at startup.
68
+ If this alias is missing, any import from `cloudflare-next-intl` throws an
69
+ error naming the missing `@intl-config` alias at startup.
71
70
 
72
71
  ### 3. Wire up the middleware
73
72
 
@@ -1,12 +1,14 @@
1
1
  import type { TranslationObject } from "../../types/types";
2
+ import type { SerializedAuthUser } from "../../firebase_auth/types";
2
3
  interface LocaleContextType {
3
4
  language: string;
4
5
  messages: TranslationObject;
5
6
  }
6
7
  export declare const LocaleContext: import("react").Context<LocaleContextType | undefined>;
7
- export default function LocationzationClientProvider({ language, messages, children }: {
8
+ export default function LocationzationClientProvider({ language, messages, initialAuthUser, children }: {
8
9
  language: string;
9
10
  messages: TranslationObject;
11
+ initialAuthUser?: SerializedAuthUser | null;
10
12
  children: React.ReactNode;
11
13
  }): Component;
12
14
  export {};
@@ -1,10 +1,23 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { setLocaleCache, setMessageForLocaleCache } from "../../general/cache_variables";
4
- import { createContext } from "react";
4
+ import { createContext, useMemo } from "react";
5
+ import dynamic from "next/dynamic";
6
+ import config from "@intl-config";
5
7
  export const LocaleContext = createContext(undefined);
6
- export default function LocationzationClientProvider({ language, messages, children }) {
8
+ export default function LocationzationClientProvider({ language, messages, initialAuthUser = null, children }) {
7
9
  setLocaleCache(language);
8
10
  setMessageForLocaleCache(language, messages);
9
- return _jsx(LocaleContext.Provider, { value: { language, messages }, children: children });
11
+ // `LocaleContext.Provider` stays the outermost element here the
12
+ // client `AuthUserProvider` (and its descendants calling
13
+ // usePathname()/useLocale()) must render as a CHILD of it, not a
14
+ // sibling wrapping it, or those hooks would throw for running outside
15
+ // the provider.
16
+ let providedChildren = children;
17
+ if (config.firebaseAuth) {
18
+ const AuthUserProvider = dynamic(() => import("../../firebase_auth/client/auth_user_provider"));
19
+ providedChildren = _jsx(AuthUserProvider, { initialUser: initialAuthUser, children: children });
20
+ }
21
+ const contextValue = useMemo(() => ({ language, messages }), [language, messages]);
22
+ return _jsx(LocaleContext.Provider, { value: contextValue, children: providedChildren });
10
23
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { bench, describe } from 'vitest';
2
+ import getCookie from './get_cookie';
3
+ describe('getCookie', () => {
4
+ document.cookie = 'bench-cookie=bar%20baz';
5
+ bench('reads and decodes an existing cookie', () => {
6
+ getCookie('bench-cookie');
7
+ });
8
+ bench('returns null for a missing cookie', () => {
9
+ getCookie('missing-cookie');
10
+ });
11
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { bench, describe } from 'vitest';
2
+ import setCookie from './set_cookie';
3
+ describe('setCookie', () => {
4
+ bench('writes a cookie with the default maxAge', () => {
5
+ setCookie({ name: 'theme', value: 'dark' });
6
+ });
7
+ bench('writes a cookie with a custom maxAge', () => {
8
+ setCookie({ name: 'theme', value: 'light', maxAge: 60 });
9
+ });
10
+ });
@@ -6,7 +6,9 @@
6
6
  *
7
7
  * @param name Cookie name.
8
8
  * @param value Cookie value; stringified via template literal (no encoding
9
- * applied — avoid values containing `;`).
9
+ * applied — avoid values containing `;`). Must be a primitive
10
+ * (`string`/`number`/`boolean`) — objects would silently serialize to
11
+ * `"[object Object]"`.
10
12
  * @param maxAge Seconds until expiry. Defaults to 1 year.
11
13
  *
12
14
  * Always `path=/; SameSite=Lax`. Swallows errors (e.g. in restrictive
@@ -14,6 +16,6 @@
14
16
  */
15
17
  export default function setCookie({ name, value, maxAge }: {
16
18
  name: string;
17
- value: unknown;
19
+ value: string | number | boolean;
18
20
  maxAge?: number;
19
21
  }): void;
@@ -7,7 +7,9 @@
7
7
  *
8
8
  * @param name Cookie name.
9
9
  * @param value Cookie value; stringified via template literal (no encoding
10
- * applied — avoid values containing `;`).
10
+ * applied — avoid values containing `;`). Must be a primitive
11
+ * (`string`/`number`/`boolean`) — objects would silently serialize to
12
+ * `"[object Object]"`.
11
13
  * @param maxAge Seconds until expiry. Defaults to 1 year.
12
14
  *
13
15
  * Always `path=/; SameSite=Lax`. Swallows errors (e.g. in restrictive
@@ -9,6 +9,8 @@ import type { TranslatorReturnType } from "../../types/types";
9
9
  *
10
10
  * @returns The current locale (e.g. `"en"`).
11
11
  * @throws If rendered outside `IntlProvider`.
12
+ * @example
13
+ * const locale = useLocale(); // "en"
12
14
  */
13
15
  export declare function useLocale(): string;
14
16
  /**
@@ -19,5 +21,8 @@ export declare function useLocale(): string;
19
21
  * @returns A `(key: string) => string` translation function, memoized on
20
22
  * `[language, messages, namespace]`.
21
23
  * @throws If rendered outside `IntlProvider`.
24
+ * @example
25
+ * const t = useTranslations("Index");
26
+ * return <h1>{t("title")}</h1>;
22
27
  */
23
28
  export declare function useTranslations(namespace: string): TranslatorReturnType;
@@ -12,11 +12,13 @@ import { getTranslationsImpl } from "../../general/general_functions";
12
12
  *
13
13
  * @returns The current locale (e.g. `"en"`).
14
14
  * @throws If rendered outside `IntlProvider`.
15
+ * @example
16
+ * const locale = useLocale(); // "en"
15
17
  */
16
18
  export function useLocale() {
17
19
  const context = useContext(LocaleContext);
18
20
  if (context === undefined) {
19
- throw new Error('useLocale must be used within a LocaleContext');
21
+ throw new Error('useLocale must be used within an IntlProvider');
20
22
  }
21
23
  return context.language;
22
24
  }
@@ -28,11 +30,14 @@ export function useLocale() {
28
30
  * @returns A `(key: string) => string` translation function, memoized on
29
31
  * `[language, messages, namespace]`.
30
32
  * @throws If rendered outside `IntlProvider`.
33
+ * @example
34
+ * const t = useTranslations("Index");
35
+ * return <h1>{t("title")}</h1>;
31
36
  */
32
37
  export function useTranslations(namespace) {
33
38
  const context = useContext(LocaleContext);
34
39
  if (context === undefined) {
35
- throw new Error('useTranslations must be used within a LocaleContext');
40
+ throw new Error('useTranslations must be used within an IntlProvider');
36
41
  }
37
42
  const { language, messages } = context;
38
43
  return useMemo(() => getTranslationsImpl(language, messages, namespace), [language, messages, namespace]);
@@ -2,4 +2,7 @@ export { default as LocaleLink } from './components/locale_link';
2
2
  export { default as usePathname } from './hooks/use_path_name';
3
3
  export { default as setCookieClient } from './functions/set_cookie';
4
4
  export { default as getCookieClient } from './functions/get_cookie';
5
+ // Intentionally not re-exported here: useLocale/useTranslations are public
6
+ // via the "cloudflare-next-intl/use" subpath instead, which resolves to the
7
+ // react-server or client implementation automatically via export conditions.
5
8
  // export { useLocale, useTranslations } from './hooks/client_hooks';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { bench, describe } from 'vitest';
2
+ import { localeCookieName, isBotCookieKey, isDarkCookieKey } from './cookie_key';
3
+ describe('cookie_key constants', () => {
4
+ bench('reads the exported constant references', () => {
5
+ void localeCookieName;
6
+ void isBotCookieKey;
7
+ void isDarkCookieKey;
8
+ });
9
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import { bench, describe } from 'vitest';
2
+ import { setIntlConfig } from './init_config';
3
+ describe('setIntlConfig', () => {
4
+ bench('identity pass-through of a routing config object', () => {
5
+ setIntlConfig({ locales: ['en', 'fr'], defaultLocale: 'en' });
6
+ });
7
+ });
@@ -5,7 +5,10 @@ function getConfig() {
5
5
  return intlConfig;
6
6
  }
7
7
  else {
8
- throw Error('Please set config file and set path to it in next.config as in the example');
8
+ throw Error('cloudflare-next-intl: the `@intl-config` alias is not set. ' +
9
+ 'Create a config file (e.g. `src/i18n/intl_config.ts`) that default-exports ' +
10
+ 'your `RoutingConfig` (see `setIntlConfig`), then point `@intl-config` at it via ' +
11
+ 'your bundler/tsconfig path alias — see the README "Setup" section, step 2.');
9
12
  }
10
13
  }
11
14
  const config = getConfig();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { bench, describe } from 'vitest';
2
+ import generateIntlSitemap from './intl_sitemap';
3
+ const smallRoutes = [
4
+ { link: '/about', lastModified: '2024-01-01' },
5
+ { link: '/contact', lastModified: '2024-01-01' },
6
+ ];
7
+ const largeRoutes = Array.from({ length: 100 }, (_, i) => ({
8
+ link: `/route-${i}`,
9
+ lastModified: '2024-01-01',
10
+ }));
11
+ describe('generateIntlSitemap', () => {
12
+ bench('small site: 2 routes x 2 locales', () => {
13
+ generateIntlSitemap({ intlSitemap: smallRoutes, url: `https://example.com/${Math.random()}` });
14
+ });
15
+ bench('large site: 100 routes x 2 locales', () => {
16
+ generateIntlSitemap({ intlSitemap: largeRoutes, url: `https://example.com/${Math.random()}` });
17
+ });
18
+ });
@@ -21,7 +21,7 @@ function generateIntlSitemapIml({ intlSitemap, url }) {
21
21
  });
22
22
  }
23
23
  }
24
- sitemap.sort((a, b) => a.url.localeCompare(b.url));
24
+ sitemap.sort((a, b) => (a.url < b.url ? -1 : a.url > b.url ? 1 : 0));
25
25
  return sitemap;
26
26
  }
27
27
  const generateIntlSitemap = cache(generateIntlSitemapIml);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ // @vitest-environment node
2
+ import { bench, describe, vi } from 'vitest';
3
+ import intlMiddleware from './middleware';
4
+ import { makeTestRequest } from '../test_utils/mock_next_server';
5
+ describe('intlMiddleware', () => {
6
+ bench('warm path: valid locale cookie present', async () => {
7
+ const req = makeTestRequest('https://example.com/en/page', { cookies: { __user_locale_key__: 'en' } });
8
+ await intlMiddleware(req);
9
+ });
10
+ bench('cold path: no locale cookie, accept-language parsing', async () => {
11
+ const req = makeTestRequest('https://example.com/page', { headers: { 'accept-language': 'de-DE,de;q=0.9' } });
12
+ await intlMiddleware(req);
13
+ });
14
+ });
15
+ vi.doMock('./intl_config', () => ({
16
+ default: {
17
+ locales: ['en'],
18
+ defaultLocale: 'en',
19
+ firebaseAuth: { middlewareEnabled: true, redirectAuthPath: '/login', homePath: '/', isAuthPath: () => true },
20
+ },
21
+ }));
22
+ describe('intlMiddleware with firebaseAuth configured', () => {
23
+ bench('first call: pays the dynamic import of update_session.ts', async () => {
24
+ vi.resetModules();
25
+ const { default: freshIntlMiddleware } = await import('./middleware');
26
+ const req = makeTestRequest('https://example.com/en/page', { cookies: { __user_locale_key__: 'en' } });
27
+ await freshIntlMiddleware(req);
28
+ });
29
+ bench('repeat call: memoized dynamic import (no re-import cost)', async () => {
30
+ const { default: intlMiddlewareWithAuth } = await import('./middleware');
31
+ const req = makeTestRequest('https://example.com/en/page', { cookies: { __user_locale_key__: 'en' } });
32
+ await intlMiddlewareWithAuth(req);
33
+ });
34
+ });
@@ -19,6 +19,12 @@ async function getIsBotValue(userAgent) {
19
19
  }
20
20
  const getIsBotValueCache = cache(getIsBotValue);
21
21
  export const localesSet = new Set(config.locales);
22
+ // Memoized after the first call — `import()` is not free even when the
23
+ // module is already in the runtime's module cache (a microtask round-trip
24
+ // plus a Promise allocation), and this middleware runs on every request.
25
+ // Still fully lazy: consumers who never set `config.firebaseAuth` never
26
+ // reach the branch that assigns this, so they never pay the import at all.
27
+ let updateSessionModule;
22
28
  /**
23
29
  * This middleware function runs for every incoming request. Handles locale
24
30
  * detection/routing, then optionally defers to your own custom logic.
@@ -50,15 +56,23 @@ export default async function intlMiddleware(request, options) {
50
56
  const { pathname, search, hash } = request.nextUrl;
51
57
  let urlLocale;
52
58
  let pathWithoutLocale;
53
- const pathSegments = pathname.split('/').filter(Boolean); // e.g., ['', 'en', 'about'] -> ['en', 'about']
54
- const firstSegment = pathSegments[0];
55
- const languageValue = firstSegment;
59
+ // Avoids split('/').filter(Boolean) array allocation on every request:
60
+ // scan for the first segment's bounds directly.
61
+ const segmentStart = pathname.charCodeAt(0) === 47 /* '/' */ ? 1 : 0;
62
+ let segmentEnd = pathname.indexOf('/', segmentStart);
63
+ if (segmentEnd === -1)
64
+ segmentEnd = pathname.length;
65
+ const languageValue = pathname.slice(segmentStart, segmentEnd);
56
66
  // Check if the first segment of the path is one of the supported locales
57
- if (pathSegments.length > 0 && localesSet.has(languageValue)) {
67
+ if (languageValue && localesSet.has(languageValue)) {
58
68
  urlLocale = languageValue;
59
- pathWithoutLocale = '/' + pathSegments.slice(1).join('/'); // Remove the locale segment
60
- if (pathWithoutLocale === '')
61
- pathWithoutLocale = '/'; // Ensure it's '/' for root after removing locale
69
+ let rest = pathname.slice(segmentEnd);
70
+ if (rest.endsWith('/'))
71
+ rest = rest.slice(0, -1);
72
+ if (rest.includes('//')) {
73
+ rest = '/' + rest.split('/').filter(Boolean).join('/');
74
+ }
75
+ pathWithoutLocale = rest || '/';
62
76
  }
63
77
  else {
64
78
  // No locale prefix in the URL. The actual pathname is the full original pathname.
@@ -106,10 +120,27 @@ export default async function intlMiddleware(request, options) {
106
120
  }
107
121
  }
108
122
  response.headers.set('Content-Language', effectiveLocaleForRequest);
123
+ response.headers.set('x-pathname', pathWithoutLocale);
124
+ // Auto-wires the firebase_auth submodule's redirect/session-refresh
125
+ // logic when `firebaseAuth` is configured — dynamic import so this
126
+ // file never pulls in firebase_auth/** (and transitively firebase/*)
127
+ // for consumers who never set `firebaseAuth` at all. Runs last, so
128
+ // it composes onto (rather than discards) the locale cookie, bot
129
+ // cookie, and Content-Language header already finalized above.
130
+ // Skipped entirely when a locale redirect is already happening
131
+ // (`isRedirect`) — the locale redirect itself is the response, same
132
+ // as `middlewareHandler` is also skipped on this path by default.
133
+ if (!isRedirect && config.firebaseAuth && config.firebaseAuth.middlewareEnabled !== false) {
134
+ if (!updateSessionModule) {
135
+ updateSessionModule = await import('../firebase_auth/middleware/update_session');
136
+ }
137
+ response = await updateSessionModule.default(request, response, effectiveLocaleForRequest);
138
+ }
109
139
  return response;
110
140
  }
111
141
  catch (e) {
112
- console.error(`Middleware Error ${e}`);
142
+ console.error('[cloudflare-next-intl] intlMiddleware failed and fell back to passing the request through unmodified ' +
143
+ '(no locale rewrite/redirect, no firebase_auth session refresh). Underlying error:', e);
113
144
  return NextResponse.next({
114
145
  request,
115
146
  });
@@ -0,0 +1,36 @@
1
+ import { bench, describe, vi } from 'vitest';
2
+ const fa = {
3
+ apiKey: 'key',
4
+ authDomain: 'domain',
5
+ projectId: 'proj',
6
+ appId: 'app',
7
+ redirectAuthPath: '/login',
8
+ homePath: '/',
9
+ isAuthPath: () => false,
10
+ };
11
+ vi.mock('@intl-config', () => ({ default: { firebaseAuth: fa } }));
12
+ vi.mock('./firebase_client', () => ({
13
+ getFirebaseAuthClient: vi.fn(async () => ({ auth: {} })),
14
+ }));
15
+ vi.mock('../error_messages/firebase_auth_error_helper', () => ({
16
+ default: vi.fn(() => 'translated error'),
17
+ }));
18
+ const signInWithEmailAndPassword = vi.fn(async (_auth, _email, _password) => undefined);
19
+ vi.mock('firebase/auth', () => ({
20
+ signInWithEmailAndPassword: (auth, email, password) => signInWithEmailAndPassword(auth, email, password),
21
+ createUserWithEmailAndPassword: vi.fn(async () => undefined),
22
+ sendPasswordResetEmail: vi.fn(async () => undefined),
23
+ }));
24
+ function makeFormData(fields) {
25
+ const fd = new FormData();
26
+ for (const [key, value] of Object.entries(fields))
27
+ fd.set(key, value);
28
+ return fd;
29
+ }
30
+ describe('createLoginAction', () => {
31
+ bench('creates and invokes a login action (successful sign-in)', async () => {
32
+ const { createLoginAction } = await import('./auth_actions');
33
+ const action = createLoginAction('en', {});
34
+ await action({}, makeFormData({ email: 'a@b.com', password: 'pw' }));
35
+ });
36
+ });
@@ -0,0 +1,46 @@
1
+ import type { AuthActionMessages, AuthFormState } from '../types';
2
+ /**
3
+ * Builds a login server action for React's `useActionState` form hook.
4
+ * The returned function has the `(prevState, formData) => Promise<AuthFormState>`
5
+ * shape `useActionState` expects — pass it directly as the action.
6
+ *
7
+ * `formData` must contain `email` and `password` fields.
8
+ *
9
+ * @param locale Used to localize the returned error message.
10
+ * @param messages Localized action messages (currently unused by this action).
11
+ * @returns A form action: `{ success: true }` on success, `{ error }` on failure.
12
+ * @example
13
+ * const [state, action] = useActionState(createLoginAction(locale, messages), {});
14
+ * <form action={action}>...</form>
15
+ */
16
+ export declare function createLoginAction(locale: string, messages: AuthActionMessages): (_prevState: AuthFormState, formData: FormData) => Promise<AuthFormState>;
17
+ /**
18
+ * Builds a sign-up server action for React's `useActionState` form hook.
19
+ * Same shape as {@link createLoginAction}.
20
+ *
21
+ * `formData` must contain `email`, `password`, and `confirmPassword` fields.
22
+ *
23
+ * @param locale Used to localize the returned error message.
24
+ * @param messages `messages.mismatch` is returned as the error if
25
+ * `password` !== `confirmPassword`; other fields on `AuthActionMessages`
26
+ * are not read by this action.
27
+ * @returns A form action: `{ success: true }` on success, `{ error }` on failure.
28
+ * @example
29
+ * const [state, action] = useActionState(createSignUpAction(locale, messages), {});
30
+ * <form action={action}>...</form>
31
+ */
32
+ export declare function createSignUpAction(locale: string, messages: AuthActionMessages): (_prevState: AuthFormState, formData: FormData) => Promise<AuthFormState>;
33
+ /**
34
+ * Builds a forgot-password server action for React's `useActionState` form
35
+ * hook. Same shape as {@link createLoginAction}.
36
+ *
37
+ * `formData` must contain an `email` field.
38
+ *
39
+ * @param locale Used to localize the returned error message.
40
+ * @param messages Localized action messages (currently unused by this action).
41
+ * @returns A form action: `{ success: true }` on success, `{ error }` on failure.
42
+ * @example
43
+ * const [state, action] = useActionState(createForgotPasswordAction(locale, messages), {});
44
+ * <form action={action}>...</form>
45
+ */
46
+ export declare function createForgotPasswordAction(locale: string, messages: AuthActionMessages): (_prevState: AuthFormState, formData: FormData) => Promise<AuthFormState>;
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+ import config from '@intl-config';
3
+ import requireFirebaseAuthConfig from '../require_config';
4
+ import { getFirebaseAuthClient } from './firebase_client';
5
+ import firebaseAuthErrorMessage from '../error_messages/firebase_auth_error_helper';
6
+ function readCredentials(formData) {
7
+ return {
8
+ email: (formData.get('email')?.toString() ?? '').trim(),
9
+ password: (formData.get('password')?.toString() ?? '').trim(),
10
+ };
11
+ }
12
+ /**
13
+ * Builds a login server action for React's `useActionState` form hook.
14
+ * The returned function has the `(prevState, formData) => Promise<AuthFormState>`
15
+ * shape `useActionState` expects — pass it directly as the action.
16
+ *
17
+ * `formData` must contain `email` and `password` fields.
18
+ *
19
+ * @param locale Used to localize the returned error message.
20
+ * @param messages Localized action messages (currently unused by this action).
21
+ * @returns A form action: `{ success: true }` on success, `{ error }` on failure.
22
+ * @example
23
+ * const [state, action] = useActionState(createLoginAction(locale, messages), {});
24
+ * <form action={action}>...</form>
25
+ */
26
+ export function createLoginAction(locale, messages) {
27
+ return async function loginAction(_prevState, formData) {
28
+ requireFirebaseAuthConfig(config.firebaseAuth);
29
+ const { auth } = await getFirebaseAuthClient();
30
+ const { signInWithEmailAndPassword } = await import('firebase/auth');
31
+ const { email, password } = readCredentials(formData);
32
+ try {
33
+ await signInWithEmailAndPassword(auth, email, password);
34
+ return { success: true };
35
+ }
36
+ catch (e) {
37
+ return { error: firebaseAuthErrorMessage(locale, e) };
38
+ }
39
+ };
40
+ }
41
+ /**
42
+ * Builds a sign-up server action for React's `useActionState` form hook.
43
+ * Same shape as {@link createLoginAction}.
44
+ *
45
+ * `formData` must contain `email`, `password`, and `confirmPassword` fields.
46
+ *
47
+ * @param locale Used to localize the returned error message.
48
+ * @param messages `messages.mismatch` is returned as the error if
49
+ * `password` !== `confirmPassword`; other fields on `AuthActionMessages`
50
+ * are not read by this action.
51
+ * @returns A form action: `{ success: true }` on success, `{ error }` on failure.
52
+ * @example
53
+ * const [state, action] = useActionState(createSignUpAction(locale, messages), {});
54
+ * <form action={action}>...</form>
55
+ */
56
+ export function createSignUpAction(locale, messages) {
57
+ return async function signUpAction(_prevState, formData) {
58
+ requireFirebaseAuthConfig(config.firebaseAuth);
59
+ const { auth } = await getFirebaseAuthClient();
60
+ const { createUserWithEmailAndPassword } = await import('firebase/auth');
61
+ const { email, password } = readCredentials(formData);
62
+ const confirmPassword = (formData.get('confirmPassword')?.toString() ?? '').trim();
63
+ if (messages.mismatch && password !== confirmPassword) {
64
+ return { error: messages.mismatch };
65
+ }
66
+ try {
67
+ await createUserWithEmailAndPassword(auth, email, password);
68
+ return { success: true };
69
+ }
70
+ catch (e) {
71
+ return { error: firebaseAuthErrorMessage(locale, e) };
72
+ }
73
+ };
74
+ }
75
+ /**
76
+ * Builds a forgot-password server action for React's `useActionState` form
77
+ * hook. Same shape as {@link createLoginAction}.
78
+ *
79
+ * `formData` must contain an `email` field.
80
+ *
81
+ * @param locale Used to localize the returned error message.
82
+ * @param messages Localized action messages (currently unused by this action).
83
+ * @returns A form action: `{ success: true }` on success, `{ error }` on failure.
84
+ * @example
85
+ * const [state, action] = useActionState(createForgotPasswordAction(locale, messages), {});
86
+ * <form action={action}>...</form>
87
+ */
88
+ export function createForgotPasswordAction(locale, messages) {
89
+ return async function forgotPasswordAction(_prevState, formData) {
90
+ requireFirebaseAuthConfig(config.firebaseAuth);
91
+ const { auth } = await getFirebaseAuthClient();
92
+ const { sendPasswordResetEmail } = await import('firebase/auth');
93
+ const email = (formData.get('email')?.toString() ?? '').trim();
94
+ try {
95
+ await sendPasswordResetEmail(auth, email);
96
+ return { success: true };
97
+ }
98
+ catch (e) {
99
+ return { error: firebaseAuthErrorMessage(locale, e) };
100
+ }
101
+ };
102
+ }
@@ -0,0 +1,11 @@
1
+ import { bench, describe } from 'vitest';
2
+ import { setAuthUserCache, getAuthUserCache, isAuthUserLoadingCache } from './auth_user_cache';
3
+ describe('auth_user_cache', () => {
4
+ bench('setAuthUserCache + getAuthUserCache round-trip', () => {
5
+ setAuthUserCache({ uid: 'bench-user' });
6
+ getAuthUserCache();
7
+ });
8
+ bench('isAuthUserLoadingCache read', () => {
9
+ isAuthUserLoadingCache();
10
+ });
11
+ });
@@ -0,0 +1,4 @@
1
+ import type { AuthUser } from '../types';
2
+ export declare function setAuthUserCache(user: AuthUser | null): void;
3
+ export declare function getAuthUserCache(): AuthUser | null;
4
+ export declare function isAuthUserLoadingCache(): boolean;
@@ -0,0 +1,14 @@
1
+ // Module-scope cache so non-React code can read the current client auth
2
+ // user synchronously, without needing to be inside AuthUserProvider's tree.
3
+ let cachedUser = null;
4
+ let cachedLoading = true;
5
+ export function setAuthUserCache(user) {
6
+ cachedUser = user;
7
+ cachedLoading = false;
8
+ }
9
+ export function getAuthUserCache() {
10
+ return cachedUser;
11
+ }
12
+ export function isAuthUserLoadingCache() {
13
+ return cachedLoading;
14
+ }
@@ -0,0 +1,36 @@
1
+ import type { AuthUser, SerializedAuthUser } from '../types';
2
+ export interface AuthUserContextType {
3
+ /** Current Firebase user, or `null` if signed out (or not yet resolved while `loading`). */
4
+ user: AuthUser | null;
5
+ /** `true` until the initial auth state has resolved on the client. */
6
+ loading: boolean;
7
+ /** Force-refreshes the current user's ID token/claims and re-syncs the session cookie. */
8
+ reloadUser: () => Promise<void>;
9
+ /** Sends a verification email to the currently signed-in user. */
10
+ sendVerificationEmail: () => Promise<void>;
11
+ /** Signs out, clears the session cookie, and redirects to `firebaseAuth.redirectAuthPath`. */
12
+ logout: () => Promise<void>;
13
+ }
14
+ export declare const AuthUserContext: import("react").Context<AuthUserContextType | null>;
15
+ /**
16
+ * Client-side auth-state provider for `firebase_auth`. Wrap your root layout
17
+ * (or a client boundary below it) with this to make `useAuthUser()`
18
+ * (`cloudflare-next-intl/useFirebaseAuthUser`, client variant) resolve
19
+ * `{ user, loading }` from the live Firebase `onIdTokenChanged` listener,
20
+ * and to get automatic session-cookie sync + redirect-on-sign-out/verify
21
+ * behavior driven by `firebaseAuth.isAuthPath` / `whiteListPaths` /
22
+ * `redirectAuthPath` / `verifyEmailPath` on your `RoutingConfig`.
23
+ *
24
+ * Requires `firebaseAuth` to be set on the config passed to `setIntlConfig`
25
+ * — throws via {@link requireFirebaseAuthConfig} otherwise.
26
+ *
27
+ * @param initialUser Server-resolved user (e.g. from
28
+ * `useFirebaseAuthUser`'s `react-server` variant) to avoid a
29
+ * loading flash on first paint; pass `null`/omit if unavailable.
30
+ * @example
31
+ * <AuthUserProvider initialUser={initialUser}>{children}</AuthUserProvider>
32
+ */
33
+ export default function AuthUserProvider({ initialUser, children }: {
34
+ initialUser?: SerializedAuthUser | null;
35
+ children: React.ReactNode;
36
+ }): import("react").JSX.Element;