cloudflare-next-intl 0.9.13 → 0.9.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -256,7 +256,11 @@ export default defineConfig({
256
256
  ```
257
257
 
258
258
  ##### What `cloudflareNextIntl()` Does
259
- 1. **Auto Dynamic Pages for SSG (`autoDynamicPages`)**: Automatically scans your Next.js/Vinext App Router pages during Vite configuration (`configResolved`) and inserts `export const dynamic = "force-static"` for all static pages that do not access dynamic APIs. This ensures Vinext builds all public marketing and static pages into SSG HTML automatically without extra build scripts or manually writing `export const dynamic = "force-static"`.
259
+ 1. **Auto Dynamic Pages for SSG (`autoDynamicPages`)**: Automatically scans your Next.js/Vinext App Router pages during Vite configuration (`configResolved`) and inserts `export const dynamic = "force-static"` for all static pages that do not access dynamic APIs, or `export const dynamic = "force-dynamic"` for pages that do. This ensures Vinext builds all public marketing and static pages into SSG HTML automatically without extra build scripts or manually writing `export const dynamic`.
260
+
261
+ The scan (`checkDynamicPages`, also usable standalone from `cloudflare-next-intl/checkDynamicPages`) is a text heuristic, not a real parser, so it's deliberately conservative and follows a page's own local (relative/`@/`-alias) imports transitively — cycle-safe, capped at 300 files — so a signal several files away (a component's repository calling `cookies()`) still marks the page dynamic, not just literal text in the page file itself. Recognized signals: `cookies()`, `headers()`, `searchParams`, `unstable_noStore()`, `connection()`, `cache: "no-store"`, `next: { revalidate: 0 }`, and this package's own `getAuthUser()`/`useAuthUser()`/`withUserDb()` (each of which reads `cookies()` internally). Set `resolveImports: false` on `checkDynamicPages` to restore the original single-file-only scan, or pass `aliases` to override the default `@/` → `<appDir>/..` mapping.
262
+
263
+ Being text-only and transitive, it can over-flag: any local file the page reaches — however many imports away — that merely *calls* a recognized signal counts, even along a branch that never runs in production (a `Config.isDev`-gated `fetch(..., { cache: "no-store" })`) or one that's optional/best-effort (a `try`/`catch`-wrapped `getAuthUser()` used only to tag a log line). It cannot see that a call is conditional or swallowed. For a page whose only reason for being flagged is that kind of optional read — attaching "whoever's signed in, if anyone" to an error report, analytics event, or log line — switch that read to `resolveOptionalAuthUser()` (`cloudflare-next-intl/resolveOptionalAuthUser`, see Firebase Auth below): it wraps `getAuthUser()` the same way but, being an npm-package import, is a boundary the scan doesn't open, so it contributes no signal. If that read lives in a shared `onError` sink used by many pages at once, `resolveErrorReportingUser(useAuthUser?)` (same subpath) is the more precise fix: it's off (`{ user: null }`, no `getAuthUser()` call) by default, and only a `reportError({ ..., useAuthUser: true })` call site that actually wants the user on that report opts in per-call — instead of every page reaching that sink getting flagged.
260
264
  2. **Build-Time & Dev Image Optimizer (`imageOptimizer`)**: Automatically scans your image directories (`public/images`, `public/icons`), downscales oversized assets, produces sibling formats (`webp` by default; also supports `avif`, `png`, `jpeg`, `gif`, `tiff`, `heif`, `jp2`, `jxl`), generates 8px `.blur.webp` thumbnails with Next.js-matching SVG Gaussian blur placeholders, and provides transparent `<Image placeholder="blur" />` shimming via virtual modules. When more than one format is generated for an image, the shim renders a `<picture>` with one `<source>` per format — ordered exactly as configured — so the browser picks the best format it supports, with the original untouched file as an `onError` fallback if a generated asset fails to load. When the same image is used at different widths across the codebase, each size gets its own generated variant, and each `<Image>` usage automatically resolves to the closest matching size.
261
265
  3. **Locale File Bundling & Resolution (`localeFiles`)**: Resolves `@locale-file/*` to your `./messages` directory and transforms dynamic imports into `import.meta.glob('/messages/*.json', { eager: true })` for lightning-fast locale loading on Cloudflare Workers.
262
266
  4. **User-Agent Stub (`userAgentStub`)**: Prevents Next.js `user-agent` from importing `node:fs` during workerd runtime execution (which otherwise causes runtime 404 / 500 crashes in Workers proxy/middleware).
@@ -475,6 +479,24 @@ Features include:
475
479
  - `sendVerificationEmail(actionCodeSettings?)` on `useAuthUser()`: Custom action email settings when resending email verification.
476
480
  - `followSameOriginContinueUrl`: Automatically forwards emailed action links with `continueUrl` to the specified path (or external URL) directly from `intlMiddleware` (default `true`; set `false` on `firebaseAuth` config to disable). If `continueUrl` points to home root (`/`), it resolves to `actionLinkPath` (if set) or the mode target path (e.g. `/reset-password`).
477
481
  - `appCheck`: Firebase App Check integration supporting reCAPTCHA Enterprise (`recaptchaEnterpriseSiteKey`) and reCAPTCHA v3 (`recaptchaV3SiteKey`). For reCAPTCHA v3, defaults to a `CustomProvider` using `IntlHelperScript`'s explicit script tag to avoid iframe/webworker CDN integrity issues in private windows (`useExplicitRecaptchaScript: false` to opt out). Supports server-side token minting via service accounts.
482
+ - `resolveOptionalAuthUser()` (`cloudflare-next-intl/resolveOptionalAuthUser`): best-effort variant of `getAuthUser()` for callers that want to *attach* the current user when one happens to be known — error/telemetry reporting, analytics, logging — without failing or changing behavior when no request/session context is available. Swallows every failure and resolves `{ user: null }` instead of throwing. Use `getAuthUser()` (not this) whenever your own output actually depends on who's signed in — `checkDynamicPages` (below) treats `getAuthUser()`/`useAuthUser()`/`withUserDb()` as page-blocking dynamic-API signals but does not follow into this helper, so it's also the way to read "whoever's signed in, if anyone" from a page or a module it imports without forcing that page dynamic.
483
+ - `resolveErrorReportingUser(useAuthUser?)` (same subpath): gated form of `resolveOptionalAuthUser()` for an `onError` sink — pass `ErrorHandlingParams.useAuthUser` straight through. Defaults to `false`: when not `true`, resolves `{ user: null }` immediately without calling `getAuthUser()` at all, so a page reached only through your `onError` sink's default path stays static-eligible. A specific `reportError({ ..., useAuthUser: true })` call site opts in per-call when it actually wants the user on that report.
484
+
485
+ ```ts
486
+ // your onError sink
487
+ import { resolveErrorReportingUser } from "cloudflare-next-intl/resolveOptionalAuthUser";
488
+
489
+ export default async function onError(params: ErrorHandlingParams) {
490
+ const { user } = await resolveErrorReportingUser(params.useAuthUser);
491
+ // ...write params + user?.email to your error store
492
+ }
493
+ ```
494
+
495
+ A specific call site rarely needs to set `useAuthUser: true` by hand: `syncErrorReportingAuthUser` (`cloudflare-next-intl/checkDynamicPages`, also `checkDynamicPages`'s and the Vite plugin's `syncErrorReportingAuthUser` option, default `false`) does it for you — it finds every `reportError()` call reached *only* from pages already confirmed `force-dynamic` and inserts `useAuthUser: true` there automatically, leaving alone any call reachable from even one static/unknown-status page. Opt in explicitly (it mutates call-site arguments across your app, a bigger change than the top-of-file `export const dynamic` insertion `checkDynamicPages` does by default):
496
+
497
+ ```ts
498
+ await checkDynamicPages({ appDir, mode: "fix", syncErrorReportingAuthUser: true });
499
+ ```
478
500
 
479
501
  ```tsx
480
502
  import ThemeSwitcher from "cloudflare-next-intl/ThemeSwitcher";
@@ -1,9 +1,14 @@
1
+ import { type SyncErrorReportingAuthUserReport } from './sync_error_reporting_auth_user.js';
2
+ import type { AliasConfig } from './resolve_local_imports.js';
1
3
  export type DynamicPagesCheckMode = 'off' | 'report' | 'fix';
2
4
  export interface CheckDynamicPagesOptions {
3
5
  appDir: string;
4
6
  mode?: DynamicPagesCheckMode;
5
7
  target?: 'next' | 'vinext';
6
8
  skip?: readonly string[];
9
+ resolveImports?: boolean;
10
+ aliases?: readonly AliasConfig[];
11
+ syncErrorReportingAuthUser?: boolean;
7
12
  }
8
13
  export interface CheckDynamicPagesReport {
9
14
  file: string;
@@ -13,5 +18,6 @@ export interface CheckDynamicPagesIo {
13
18
  findPageFiles?: (appDir: string) => string[];
14
19
  readFile?: (file: string) => string;
15
20
  writeFile?: (file: string, contents: string) => void;
21
+ isFile?: (file: string) => boolean;
16
22
  }
17
- export declare function checkDynamicPages(options: CheckDynamicPagesOptions, io?: CheckDynamicPagesIo): Promise<CheckDynamicPagesReport[]>;
23
+ export declare function checkDynamicPages(options: CheckDynamicPagesOptions, io?: CheckDynamicPagesIo): Promise<(CheckDynamicPagesReport | SyncErrorReportingAuthUserReport)[]>;
@@ -1,16 +1,32 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
1
+ import { readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
2
3
  import { findPageFiles as findPageFilesImpl } from './find_page_files.js';
3
4
  import { detectDynamicUsage } from './detect_dynamic_usage.js';
5
+ import { traceDynamicUsage } from './trace_dynamic_usage.js';
4
6
  import { insertDynamicExport } from './insert_dynamic_export.js';
7
+ import { syncErrorReportingAuthUser } from './sync_error_reporting_auth_user.js';
8
+ function defaultIsFile(path) {
9
+ try {
10
+ return statSync(path).isFile();
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
5
16
  export async function checkDynamicPages(options, io = {}) {
6
17
  const mode = options.mode ?? 'report';
7
18
  if (mode === 'off')
8
19
  return [];
9
20
  const target = options.target ?? 'next';
21
+ const resolveImports = options.resolveImports ?? true;
10
22
  const findPageFiles = io.findPageFiles ?? findPageFilesImpl;
11
23
  const readFile = io.readFile ?? ((file) => readFileSync(file, 'utf8'));
12
24
  const writeFile = io.writeFile ?? ((file, contents) => writeFileSync(file, contents, 'utf8'));
25
+ const isFile = io.isFile ?? defaultIsFile;
13
26
  const skipSet = new Set(options.skip ?? []);
27
+ const aliases = options.aliases ?? [
28
+ { prefix: '@/', replacement: resolve(options.appDir, '..') },
29
+ ];
14
30
  const reports = [];
15
31
  for (const file of findPageFiles(options.appDir)) {
16
32
  if (skipSet.has(file)) {
@@ -18,7 +34,9 @@ export async function checkDynamicPages(options, io = {}) {
18
34
  continue;
19
35
  }
20
36
  const source = readFile(file);
21
- const detection = detectDynamicUsage(source);
37
+ const detection = resolveImports
38
+ ? traceDynamicUsage(file, source, aliases, { readFile, isFile })
39
+ : detectDynamicUsage(source);
22
40
  if (detection.hasExplicitDynamicExport) {
23
41
  reports.push({ file, action: 'already-declared' });
24
42
  continue;
@@ -45,5 +63,9 @@ export async function checkDynamicPages(options, io = {}) {
45
63
  reports.push({ file, action: 'would-add-force-dynamic' });
46
64
  }
47
65
  }
66
+ if (options.syncErrorReportingAuthUser === true) {
67
+ const syncReports = await syncErrorReportingAuthUser({ appDir: options.appDir, mode: options.mode, target: options.target, skip: options.skip, aliases: options.aliases }, { findPageFiles, readFile, writeFile, isFile });
68
+ reports.push(...syncReports);
69
+ }
48
70
  return reports;
49
71
  }
@@ -0,0 +1,7 @@
1
+ import { type AliasConfig } from './resolve_local_imports.js';
2
+ export interface CollectReachableFilesIo {
3
+ readFile: (file: string) => string;
4
+ isFile?: (file: string) => boolean;
5
+ }
6
+ export declare const MAX_FILES_VISITED = 300;
7
+ export declare function collectReachableFiles(entryFile: string, entrySource: string, aliases: readonly AliasConfig[], io: CollectReachableFilesIo): Map<string, string>;
@@ -0,0 +1,30 @@
1
+ import { extractImportSpecifiers, resolveLocalImport } from './resolve_local_imports.js';
2
+ export const MAX_FILES_VISITED = 300;
3
+ export function collectReachableFiles(entryFile, entrySource, aliases, io) {
4
+ const isFile = io.isFile ?? (() => false);
5
+ const files = new Map([[entryFile, entrySource]]);
6
+ const queue = [entryFile];
7
+ while (queue.length > 0) {
8
+ const current = queue.shift();
9
+ if (files.size >= MAX_FILES_VISITED)
10
+ continue;
11
+ const source = files.get(current);
12
+ for (const specifier of extractImportSpecifiers(source)) {
13
+ if (files.size >= MAX_FILES_VISITED)
14
+ break;
15
+ const resolved = resolveLocalImport(specifier, current, aliases, isFile);
16
+ if (resolved === null || files.has(resolved))
17
+ continue;
18
+ let importedSource;
19
+ try {
20
+ importedSource = io.readFile(resolved);
21
+ }
22
+ catch {
23
+ continue;
24
+ }
25
+ files.set(resolved, importedSource);
26
+ queue.push(resolved);
27
+ }
28
+ }
29
+ return files;
30
+ }
@@ -3,3 +3,4 @@ export interface DynamicDetectionResult {
3
3
  detectedDynamicApis: string[];
4
4
  }
5
5
  export declare function detectDynamicUsage(sourceText: string): DynamicDetectionResult;
6
+ export declare function readExplicitDynamicValue(sourceText: string): 'force-static' | 'force-dynamic' | 'auto' | 'error' | null;
@@ -6,6 +6,9 @@ const DYNAMIC_API_CHECKS = [
6
6
  { name: 'connection()', pattern: /\bconnection\s*\(\s*\)/ },
7
7
  { name: 'cache: "no-store"', pattern: /cache:\s*['"]no-store['"]/ },
8
8
  { name: 'next: { revalidate: 0 }', pattern: /next:\s*\{\s*revalidate:\s*0\s*[,}]/ },
9
+ { name: 'getAuthUser()', pattern: /\bgetAuthUser\s*\(/ },
10
+ { name: 'useAuthUser()', pattern: /\buseAuthUser\s*\(/ },
11
+ { name: 'withUserDb()', pattern: /\bwithUserDb\s*\(/ },
9
12
  ];
10
13
  const EXPLICIT_DYNAMIC_EXPORT = /export\s+const\s+dynamic\s*=/;
11
14
  export function detectDynamicUsage(sourceText) {
@@ -14,3 +17,13 @@ export function detectDynamicUsage(sourceText) {
14
17
  detectedDynamicApis: DYNAMIC_API_CHECKS.filter(({ pattern }) => pattern.test(sourceText)).map(({ name }) => name),
15
18
  };
16
19
  }
20
+ const EXPLICIT_DYNAMIC_EXPORT_VALUE = /export\s+const\s+dynamic\s*=\s*['"]([^'"]+)['"]/;
21
+ export function readExplicitDynamicValue(sourceText) {
22
+ const match = EXPLICIT_DYNAMIC_EXPORT_VALUE.exec(sourceText);
23
+ if (match === null)
24
+ return null;
25
+ const value = match[1];
26
+ if (value === 'force-static' || value === 'force-dynamic' || value === 'auto' || value === 'error')
27
+ return value;
28
+ return null;
29
+ }
@@ -0,0 +1,5 @@
1
+ export interface ReportErrorCall {
2
+ insertPos: number | null;
3
+ hasExplicitUseAuthUser: boolean;
4
+ }
5
+ export declare function findReportErrorCalls(sourceText: string): ReportErrorCall[];
@@ -0,0 +1,71 @@
1
+ const REPORT_ERROR_CALL = /\breportError\s*\(/g;
2
+ export function findReportErrorCalls(sourceText) {
3
+ const calls = [];
4
+ REPORT_ERROR_CALL.lastIndex = 0;
5
+ let match;
6
+ while ((match = REPORT_ERROR_CALL.exec(sourceText)) !== null) {
7
+ calls.push(parseCallArgs(sourceText, match.index + match[0].length));
8
+ }
9
+ return calls;
10
+ }
11
+ function parseCallArgs(sourceText, start) {
12
+ let depth = 1;
13
+ let i = start;
14
+ let firstArgEnd = -1;
15
+ let callEnd = -1;
16
+ while (i < sourceText.length && callEnd === -1) {
17
+ const ch = sourceText[i];
18
+ if (ch === '"' || ch === "'" || ch === '`') {
19
+ i = skipStringLiteral(sourceText, i, ch);
20
+ continue;
21
+ }
22
+ if (ch === '/' && sourceText[i + 1] === '/') {
23
+ const nextNewline = sourceText.indexOf('\n', i);
24
+ i = nextNewline === -1 ? sourceText.length : nextNewline;
25
+ continue;
26
+ }
27
+ if (ch === '/' && sourceText[i + 1] === '*') {
28
+ const end = sourceText.indexOf('*/', i + 2);
29
+ i = end === -1 ? sourceText.length : end + 2;
30
+ continue;
31
+ }
32
+ if (ch === '(' || ch === '{' || ch === '[') {
33
+ depth += 1;
34
+ }
35
+ else if (ch === ')' || ch === '}' || ch === ']') {
36
+ depth -= 1;
37
+ if (depth === 0) {
38
+ callEnd = i;
39
+ break;
40
+ }
41
+ }
42
+ else if (ch === ',' && depth === 1 && firstArgEnd === -1) {
43
+ firstArgEnd = i;
44
+ }
45
+ i += 1;
46
+ }
47
+ if (firstArgEnd === -1 || callEnd === -1) {
48
+ return { insertPos: null, hasExplicitUseAuthUser: false };
49
+ }
50
+ const paramsText = sourceText.slice(firstArgEnd + 1, callEnd);
51
+ const hasExplicitUseAuthUser = /\buseAuthUser\b/.test(paramsText);
52
+ const leadingWhitespace = paramsText.length - paramsText.trimStart().length;
53
+ const paramsStart = firstArgEnd + 1 + leadingWhitespace;
54
+ if (sourceText[paramsStart] !== '{') {
55
+ return { insertPos: null, hasExplicitUseAuthUser };
56
+ }
57
+ return { insertPos: paramsStart + 1, hasExplicitUseAuthUser };
58
+ }
59
+ function skipStringLiteral(sourceText, start, quote) {
60
+ let i = start + 1;
61
+ while (i < sourceText.length) {
62
+ if (sourceText[i] === '\\') {
63
+ i += 2;
64
+ continue;
65
+ }
66
+ if (sourceText[i] === quote)
67
+ return i + 1;
68
+ i += 1;
69
+ }
70
+ return sourceText.length;
71
+ }
@@ -0,0 +1,6 @@
1
+ export interface AliasConfig {
2
+ prefix: string;
3
+ replacement: string;
4
+ }
5
+ export declare function extractImportSpecifiers(sourceText: string): string[];
6
+ export declare function resolveLocalImport(specifier: string, fromFile: string, aliases: readonly AliasConfig[], isFile?: (file: string) => boolean): string | null;
@@ -0,0 +1,55 @@
1
+ import { statSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ const FROM_SPECIFIER = /\bfrom\s*['"]([^'"]+)['"]/g;
4
+ const BARE_IMPORT_SPECIFIER = /(?:^|\n|;)\s*import\s*['"]([^'"]+)['"]/g;
5
+ export function extractImportSpecifiers(sourceText) {
6
+ const specifiers = [];
7
+ FROM_SPECIFIER.lastIndex = 0;
8
+ let match;
9
+ while ((match = FROM_SPECIFIER.exec(sourceText)) !== null) {
10
+ specifiers.push(match[1]);
11
+ }
12
+ BARE_IMPORT_SPECIFIER.lastIndex = 0;
13
+ while ((match = BARE_IMPORT_SPECIFIER.exec(sourceText)) !== null) {
14
+ specifiers.push(match[1]);
15
+ }
16
+ return specifiers;
17
+ }
18
+ const FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
19
+ function defaultIsFile(path) {
20
+ try {
21
+ return statSync(path).isFile();
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ export function resolveLocalImport(specifier, fromFile, aliases, isFile = defaultIsFile) {
28
+ let base = null;
29
+ if (specifier.startsWith('./') || specifier.startsWith('../')) {
30
+ base = resolve(dirname(fromFile), specifier);
31
+ }
32
+ else {
33
+ for (const alias of aliases) {
34
+ if (specifier.startsWith(alias.prefix)) {
35
+ base = join(alias.replacement, specifier.slice(alias.prefix.length));
36
+ break;
37
+ }
38
+ }
39
+ }
40
+ if (base === null)
41
+ return null;
42
+ if (isFile(base))
43
+ return base;
44
+ for (const ext of FILE_EXTENSIONS) {
45
+ const candidate = `${base}${ext}`;
46
+ if (isFile(candidate))
47
+ return candidate;
48
+ }
49
+ for (const ext of FILE_EXTENSIONS) {
50
+ const candidate = join(base, `index${ext}`);
51
+ if (isFile(candidate))
52
+ return candidate;
53
+ }
54
+ return null;
55
+ }
@@ -0,0 +1,21 @@
1
+ import type { AliasConfig } from './resolve_local_imports.js';
2
+ import type { DynamicPagesCheckMode } from './check_dynamic_pages.js';
3
+ export interface SyncErrorReportingAuthUserOptions {
4
+ appDir: string;
5
+ mode?: DynamicPagesCheckMode;
6
+ target?: 'next' | 'vinext';
7
+ skip?: readonly string[];
8
+ aliases?: readonly AliasConfig[];
9
+ }
10
+ export interface SyncErrorReportingAuthUserReport {
11
+ file: string;
12
+ action: 'added-use-auth-user' | 'would-add-use-auth-user';
13
+ callCount: number;
14
+ }
15
+ export interface SyncErrorReportingAuthUserIo {
16
+ findPageFiles?: (appDir: string) => string[];
17
+ readFile?: (file: string) => string;
18
+ writeFile?: (file: string, contents: string) => void;
19
+ isFile?: (file: string) => boolean;
20
+ }
21
+ export declare function syncErrorReportingAuthUser(options: SyncErrorReportingAuthUserOptions, io?: SyncErrorReportingAuthUserIo): Promise<SyncErrorReportingAuthUserReport[]>;
@@ -0,0 +1,73 @@
1
+ import { readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { findPageFiles as findPageFilesImpl } from './find_page_files.js';
4
+ import { detectDynamicUsage, readExplicitDynamicValue } from './detect_dynamic_usage.js';
5
+ import { collectReachableFiles } from './collect_reachable_files.js';
6
+ import { findReportErrorCalls } from './find_report_error_calls.js';
7
+ function defaultIsFile(path) {
8
+ try {
9
+ return statSync(path).isFile();
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ function isConfirmedDynamic(source, reachableApis, target) {
16
+ const explicit = readExplicitDynamicValue(source);
17
+ if (explicit !== null)
18
+ return explicit === 'force-dynamic';
19
+ if (target === 'vinext')
20
+ return reachableApis.length > 0;
21
+ return false;
22
+ }
23
+ export async function syncErrorReportingAuthUser(options, io = {}) {
24
+ const mode = options.mode ?? 'report';
25
+ if (mode === 'off')
26
+ return [];
27
+ const target = options.target ?? 'next';
28
+ const findPageFiles = io.findPageFiles ?? findPageFilesImpl;
29
+ const readFile = io.readFile ?? ((file) => readFileSync(file, 'utf8'));
30
+ const writeFile = io.writeFile ?? ((file, contents) => writeFileSync(file, contents, 'utf8'));
31
+ const isFile = io.isFile ?? defaultIsFile;
32
+ const skipSet = new Set(options.skip ?? []);
33
+ const aliases = options.aliases ?? [
34
+ { prefix: '@/', replacement: resolve(options.appDir, '..') },
35
+ ];
36
+ const dynamicReachable = new Set();
37
+ const notConfirmedReachable = new Set();
38
+ const fileSources = new Map();
39
+ for (const page of findPageFiles(options.appDir)) {
40
+ if (skipSet.has(page))
41
+ continue;
42
+ const source = readFile(page);
43
+ const files = collectReachableFiles(page, source, aliases, { readFile, isFile });
44
+ const apis = new Set();
45
+ for (const [file, fileSource] of files) {
46
+ fileSources.set(file, fileSource);
47
+ detectDynamicUsage(fileSource).detectedDynamicApis.forEach((api) => apis.add(api));
48
+ }
49
+ const bucket = isConfirmedDynamic(source, [...apis], target) ? dynamicReachable : notConfirmedReachable;
50
+ for (const file of files.keys())
51
+ bucket.add(file);
52
+ }
53
+ const safeFiles = [...dynamicReachable].filter((file) => !notConfirmedReachable.has(file));
54
+ const reports = [];
55
+ for (const file of safeFiles) {
56
+ const source = fileSources.get(file);
57
+ const calls = findReportErrorCalls(source).filter((call) => call.insertPos !== null && !call.hasExplicitUseAuthUser);
58
+ if (calls.length === 0)
59
+ continue;
60
+ if (mode === 'fix') {
61
+ let rewritten = source;
62
+ for (const call of [...calls].sort((a, b) => b.insertPos - a.insertPos)) {
63
+ rewritten = `${rewritten.slice(0, call.insertPos)}useAuthUser: true, ${rewritten.slice(call.insertPos)}`;
64
+ }
65
+ writeFile(file, rewritten);
66
+ reports.push({ file, action: 'added-use-auth-user', callCount: calls.length });
67
+ }
68
+ else {
69
+ reports.push({ file, action: 'would-add-use-auth-user', callCount: calls.length });
70
+ }
71
+ }
72
+ return reports;
73
+ }
@@ -0,0 +1,5 @@
1
+ import { type DynamicDetectionResult } from './detect_dynamic_usage.js';
2
+ import { type CollectReachableFilesIo } from './collect_reachable_files.js';
3
+ import type { AliasConfig } from './resolve_local_imports.js';
4
+ export type TraceDynamicUsageIo = CollectReachableFilesIo;
5
+ export declare function traceDynamicUsage(entryFile: string, entrySource: string, aliases: readonly AliasConfig[], io: TraceDynamicUsageIo): DynamicDetectionResult;
@@ -0,0 +1,17 @@
1
+ import { detectDynamicUsage } from './detect_dynamic_usage.js';
2
+ import { collectReachableFiles } from './collect_reachable_files.js';
3
+ export function traceDynamicUsage(entryFile, entrySource, aliases, io) {
4
+ const files = collectReachableFiles(entryFile, entrySource, aliases, io);
5
+ let hasExplicitDynamicExport = false;
6
+ const detectedApis = new Set();
7
+ let first = true;
8
+ for (const source of files.values()) {
9
+ const detection = detectDynamicUsage(source);
10
+ if (first) {
11
+ hasExplicitDynamicExport = detection.hasExplicitDynamicExport;
12
+ first = false;
13
+ }
14
+ detection.detectedDynamicApis.forEach((api) => detectedApis.add(api));
15
+ }
16
+ return { hasExplicitDynamicExport, detectedDynamicApis: [...detectedApis] };
17
+ }
@@ -0,0 +1,7 @@
1
+ import type { User } from '@firebase/auth';
2
+ export default function resolveOptionalAuthUser(): Promise<{
3
+ user: User | null;
4
+ }>;
5
+ export declare function resolveErrorReportingUser(useAuthUser?: boolean): Promise<{
6
+ user: User | null;
7
+ }>;
@@ -0,0 +1,15 @@
1
+ import { getAuthUser } from './use_auth_user_server.js';
2
+ export default async function resolveOptionalAuthUser() {
3
+ try {
4
+ const { user } = await getAuthUser();
5
+ return { user };
6
+ }
7
+ catch {
8
+ return { user: null };
9
+ }
10
+ }
11
+ export async function resolveErrorReportingUser(useAuthUser) {
12
+ if (useAuthUser !== true)
13
+ return { user: null };
14
+ return resolveOptionalAuthUser();
15
+ }
@@ -54,6 +54,7 @@ export interface ErrorHandlingParams {
54
54
  consent?: ConsentValue;
55
55
  formattedMessage?: string;
56
56
  dedupKey?: string;
57
+ useAuthUser?: boolean;
57
58
  }
58
59
  export interface ErrorHandlingRoutingConfig {
59
60
  enable?: boolean;
@@ -4,5 +4,6 @@ export interface AutoDynamicPagesPluginOptions {
4
4
  appDir?: string;
5
5
  mode?: DynamicPagesCheckMode;
6
6
  target?: 'next' | 'vinext';
7
+ syncErrorReportingAuthUser?: boolean;
7
8
  }
8
9
  export declare function autoDynamicPagesPlugin(options?: AutoDynamicPagesPluginOptions): Plugin;
@@ -28,6 +28,7 @@ export function autoDynamicPagesPlugin(options = {}) {
28
28
  appDir,
29
29
  mode: options.mode ?? "fix",
30
30
  target: options.target ?? "vinext",
31
+ syncErrorReportingAuthUser: options.syncErrorReportingAuthUser ?? false,
31
32
  });
32
33
  }
33
34
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.9.13",
3
+ "version": "0.9.16",
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",
@@ -142,6 +142,10 @@
142
142
  "types": "./dist/src/firebase_auth/server/use_auth_user_server.d.ts",
143
143
  "import": "./dist/src/firebase_auth/server/use_auth_user_server.js"
144
144
  },
145
+ "./resolveOptionalAuthUser": {
146
+ "types": "./dist/src/firebase_auth/server/resolve_optional_auth_user.d.ts",
147
+ "import": "./dist/src/firebase_auth/server/resolve_optional_auth_user.js"
148
+ },
145
149
  "./firebaseAuthActions": {
146
150
  "types": "./dist/src/firebase_auth/client/auth_actions.d.ts",
147
151
  "import": "./dist/src/firebase_auth/client/auth_actions.js"