cloudflare-next-intl 0.9.13 → 0.9.14
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 +6 -1
- package/dist/src/dynamic_pages_check/check_dynamic_pages.d.ts +4 -0
- package/dist/src/dynamic_pages_check/check_dynamic_pages.js +19 -2
- package/dist/src/dynamic_pages_check/detect_dynamic_usage.js +3 -0
- package/dist/src/dynamic_pages_check/resolve_local_imports.d.ts +6 -0
- package/dist/src/dynamic_pages_check/resolve_local_imports.js +55 -0
- package/dist/src/dynamic_pages_check/trace_dynamic_usage.d.ts +7 -0
- package/dist/src/dynamic_pages_check/trace_dynamic_usage.js +39 -0
- package/dist/src/firebase_auth/server/resolve_optional_auth_user.d.ts +4 -0
- package/dist/src/firebase_auth/server/resolve_optional_auth_user.js +10 -0
- package/package.json +5 -1
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
|
|
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.
|
|
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,7 @@ 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.
|
|
478
483
|
|
|
479
484
|
```tsx
|
|
480
485
|
import ThemeSwitcher from "cloudflare-next-intl/ThemeSwitcher";
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { AliasConfig } from './resolve_local_imports.js';
|
|
1
2
|
export type DynamicPagesCheckMode = 'off' | 'report' | 'fix';
|
|
2
3
|
export interface CheckDynamicPagesOptions {
|
|
3
4
|
appDir: string;
|
|
4
5
|
mode?: DynamicPagesCheckMode;
|
|
5
6
|
target?: 'next' | 'vinext';
|
|
6
7
|
skip?: readonly string[];
|
|
8
|
+
resolveImports?: boolean;
|
|
9
|
+
aliases?: readonly AliasConfig[];
|
|
7
10
|
}
|
|
8
11
|
export interface CheckDynamicPagesReport {
|
|
9
12
|
file: string;
|
|
@@ -13,5 +16,6 @@ export interface CheckDynamicPagesIo {
|
|
|
13
16
|
findPageFiles?: (appDir: string) => string[];
|
|
14
17
|
readFile?: (file: string) => string;
|
|
15
18
|
writeFile?: (file: string, contents: string) => void;
|
|
19
|
+
isFile?: (file: string) => boolean;
|
|
16
20
|
}
|
|
17
21
|
export declare function checkDynamicPages(options: CheckDynamicPagesOptions, io?: CheckDynamicPagesIo): Promise<CheckDynamicPagesReport[]>;
|
|
@@ -1,16 +1,31 @@
|
|
|
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
|
+
function defaultIsFile(path) {
|
|
8
|
+
try {
|
|
9
|
+
return statSync(path).isFile();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
5
15
|
export async function checkDynamicPages(options, io = {}) {
|
|
6
16
|
const mode = options.mode ?? 'report';
|
|
7
17
|
if (mode === 'off')
|
|
8
18
|
return [];
|
|
9
19
|
const target = options.target ?? 'next';
|
|
20
|
+
const resolveImports = options.resolveImports ?? true;
|
|
10
21
|
const findPageFiles = io.findPageFiles ?? findPageFilesImpl;
|
|
11
22
|
const readFile = io.readFile ?? ((file) => readFileSync(file, 'utf8'));
|
|
12
23
|
const writeFile = io.writeFile ?? ((file, contents) => writeFileSync(file, contents, 'utf8'));
|
|
24
|
+
const isFile = io.isFile ?? defaultIsFile;
|
|
13
25
|
const skipSet = new Set(options.skip ?? []);
|
|
26
|
+
const aliases = options.aliases ?? [
|
|
27
|
+
{ prefix: '@/', replacement: resolve(options.appDir, '..') },
|
|
28
|
+
];
|
|
14
29
|
const reports = [];
|
|
15
30
|
for (const file of findPageFiles(options.appDir)) {
|
|
16
31
|
if (skipSet.has(file)) {
|
|
@@ -18,7 +33,9 @@ export async function checkDynamicPages(options, io = {}) {
|
|
|
18
33
|
continue;
|
|
19
34
|
}
|
|
20
35
|
const source = readFile(file);
|
|
21
|
-
const detection =
|
|
36
|
+
const detection = resolveImports
|
|
37
|
+
? traceDynamicUsage(file, source, aliases, { readFile, isFile })
|
|
38
|
+
: detectDynamicUsage(source);
|
|
22
39
|
if (detection.hasExplicitDynamicExport) {
|
|
23
40
|
reports.push({ file, action: 'already-declared' });
|
|
24
41
|
continue;
|
|
@@ -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) {
|
|
@@ -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,7 @@
|
|
|
1
|
+
import { type DynamicDetectionResult } from './detect_dynamic_usage.js';
|
|
2
|
+
import { type AliasConfig } from './resolve_local_imports.js';
|
|
3
|
+
export interface TraceDynamicUsageIo {
|
|
4
|
+
readFile: (file: string) => string;
|
|
5
|
+
isFile?: (file: string) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function traceDynamicUsage(entryFile: string, entrySource: string, aliases: readonly AliasConfig[], io: TraceDynamicUsageIo): DynamicDetectionResult;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { detectDynamicUsage } from './detect_dynamic_usage.js';
|
|
2
|
+
import { extractImportSpecifiers, resolveLocalImport } from './resolve_local_imports.js';
|
|
3
|
+
const MAX_FILES_VISITED = 300;
|
|
4
|
+
export function traceDynamicUsage(entryFile, entrySource, aliases, io) {
|
|
5
|
+
const isFile = io.isFile ?? (() => false);
|
|
6
|
+
const visited = new Set([entryFile]);
|
|
7
|
+
const queue = [{ file: entryFile, source: entrySource }];
|
|
8
|
+
let hasExplicitDynamicExport = false;
|
|
9
|
+
const detectedApis = new Set();
|
|
10
|
+
let first = true;
|
|
11
|
+
while (queue.length > 0) {
|
|
12
|
+
const current = queue.shift();
|
|
13
|
+
const detection = detectDynamicUsage(current.source);
|
|
14
|
+
if (first) {
|
|
15
|
+
hasExplicitDynamicExport = detection.hasExplicitDynamicExport;
|
|
16
|
+
first = false;
|
|
17
|
+
}
|
|
18
|
+
detection.detectedDynamicApis.forEach((api) => detectedApis.add(api));
|
|
19
|
+
if (visited.size >= MAX_FILES_VISITED)
|
|
20
|
+
continue;
|
|
21
|
+
for (const specifier of extractImportSpecifiers(current.source)) {
|
|
22
|
+
if (visited.size >= MAX_FILES_VISITED)
|
|
23
|
+
break;
|
|
24
|
+
const resolved = resolveLocalImport(specifier, current.file, aliases, isFile);
|
|
25
|
+
if (resolved === null || visited.has(resolved))
|
|
26
|
+
continue;
|
|
27
|
+
visited.add(resolved);
|
|
28
|
+
let importedSource;
|
|
29
|
+
try {
|
|
30
|
+
importedSource = io.readFile(resolved);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
queue.push({ file: resolved, source: importedSource });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { hasExplicitDynamicExport, detectedDynamicApis: [...detectedApis] };
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudflare-next-intl",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
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"
|