cloudflare-next-intl 0.9.16 → 0.9.17

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
@@ -258,7 +258,7 @@ export default defineConfig({
258
258
  ##### What `cloudflareNextIntl()` Does
259
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
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.
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. A locally-imported file that opens with a `"use server"` directive is never opened by the scan (its exports are Server Actions, invoked only on explicit call — never merely by being imported), the same treatment a bare npm-package import already gets. 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) — except a `useAuthUser()` call in a file that opens with `"use client"`, which is this package's client-side hook (a different export under the same name) and contributes no signal. Set `resolveImports: false` on `checkDynamicPages` to restore the original single-file-only scan, or pass `aliases` to override the default `@/` → `<appDir>/..` mapping.
262
262
 
263
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.
264
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.
@@ -2,4 +2,4 @@ import type { GenerateRoutingConfig } from '../types/types.js';
2
2
  export interface HyperdriveBindingLike {
3
3
  connectionString: string;
4
4
  }
5
- export declare function resolveHyperdriveConnectionString(generate?: GenerateRoutingConfig): Promise<string | undefined>;
5
+ export declare function resolveHyperdriveConnectionString(generate?: GenerateRoutingConfig, skipUrls?: readonly string[]): Promise<string | undefined>;
@@ -1,10 +1,11 @@
1
1
  import { resolveEnv } from '../server/functions/geo.js';
2
2
  const WRANGLER_DEV_PLACEHOLDER = 'postgresql://user:pass@localhost:5432/db';
3
- export async function resolveHyperdriveConnectionString(generate) {
3
+ const DEFAULT_SKIP_URLS = [WRANGLER_DEV_PLACEHOLDER];
4
+ export async function resolveHyperdriveConnectionString(generate, skipUrls = DEFAULT_SKIP_URLS) {
4
5
  const env = await resolveEnv(generate);
5
6
  const binding = env?.HYPERDRIVE;
6
7
  const connectionString = binding?.connectionString;
7
- if (!connectionString || connectionString === WRANGLER_DEV_PLACEHOLDER)
8
+ if (!connectionString || skipUrls.includes(connectionString))
8
9
  return undefined;
9
10
  return connectionString;
10
11
  }
@@ -5,7 +5,7 @@ export default async function resolveDbMode(db, generate) {
5
5
  if (connectionString)
6
6
  return { mode: 'postgres', connectionString };
7
7
  if (db.autoHyperdrive !== false) {
8
- const hyperdriveConnectionString = await resolveHyperdriveConnectionString(generate);
8
+ const hyperdriveConnectionString = await resolveHyperdriveConnectionString(generate, db.autoHyperdriveSkipUrls);
9
9
  if (hyperdriveConnectionString)
10
10
  return { mode: 'postgres', connectionString: hyperdriveConnectionString };
11
11
  }
@@ -1,5 +1,9 @@
1
1
  import { extractImportSpecifiers, resolveLocalImport } from './resolve_local_imports.js';
2
2
  export const MAX_FILES_VISITED = 300;
3
+ const USE_SERVER_DIRECTIVE = /^(?:\s*['"]use \w[\w-]*['"]\s*;?\s*)*['"]use server['"]\s*;?/;
4
+ function hasLeadingUseServerDirective(source) {
5
+ return USE_SERVER_DIRECTIVE.test(source);
6
+ }
3
7
  export function collectReachableFiles(entryFile, entrySource, aliases, io) {
4
8
  const isFile = io.isFile ?? (() => false);
5
9
  const files = new Map([[entryFile, entrySource]]);
@@ -22,6 +26,8 @@ export function collectReachableFiles(entryFile, entrySource, aliases, io) {
22
26
  catch {
23
27
  continue;
24
28
  }
29
+ if (hasLeadingUseServerDirective(importedSource))
30
+ continue;
25
31
  files.set(resolved, importedSource);
26
32
  queue.push(resolved);
27
33
  }
@@ -7,14 +7,19 @@ const DYNAMIC_API_CHECKS = [
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
9
  { name: 'getAuthUser()', pattern: /\bgetAuthUser\s*\(/ },
10
- { name: 'useAuthUser()', pattern: /\buseAuthUser\s*\(/ },
11
10
  { name: 'withUserDb()', pattern: /\bwithUserDb\s*\(/ },
12
11
  ];
12
+ const USE_AUTH_USER_CALL = /\buseAuthUser\s*\(/;
13
+ const USE_CLIENT_DIRECTIVE = /^(?:\s*['"]use \w[\w-]*['"]\s*;?\s*)*['"]use client['"]\s*;?/;
13
14
  const EXPLICIT_DYNAMIC_EXPORT = /export\s+const\s+dynamic\s*=/;
14
15
  export function detectDynamicUsage(sourceText) {
16
+ const detectedDynamicApis = DYNAMIC_API_CHECKS.filter(({ pattern }) => pattern.test(sourceText)).map(({ name }) => name);
17
+ if (USE_AUTH_USER_CALL.test(sourceText) && !USE_CLIENT_DIRECTIVE.test(sourceText)) {
18
+ detectedDynamicApis.push('useAuthUser()');
19
+ }
15
20
  return {
16
21
  hasExplicitDynamicExport: EXPLICIT_DYNAMIC_EXPORT.test(sourceText),
17
- detectedDynamicApis: DYNAMIC_API_CHECKS.filter(({ pattern }) => pattern.test(sourceText)).map(({ name }) => name),
22
+ detectedDynamicApis,
18
23
  };
19
24
  }
20
25
  const EXPLICIT_DYNAMIC_EXPORT_VALUE = /export\s+const\s+dynamic\s*=\s*['"]([^'"]+)['"]/;
@@ -210,6 +210,7 @@ export interface SupabaseDbConfig {
210
210
  export interface DbRoutingConfig {
211
211
  connectionString?: FallibleConfigValue<string>;
212
212
  autoHyperdrive?: boolean;
213
+ autoHyperdriveSkipUrls?: string[];
213
214
  disconnectAfterRequest?: boolean;
214
215
  authenticatedRole?: string | (() => string | Promise<string>);
215
216
  authenticatedRoleClaim?: string | false;
package/llms.txt CHANGED
@@ -68,6 +68,8 @@ state then isn't shared. Peer resolution guarantees one shared copy instead.
68
68
  Two transports, picked by which `db` fields are set — `pg`/`drizzle-orm`/`@supabase/supabase-js` ship as dependencies and load via dynamic `import()`, so nothing bundles unless a `db` export is called.
69
69
 
70
70
  - Direct Postgres (wins if configured): `db.connectionString` — Postgres connection string, or a sync/async function returning one (resolved on each connect). The function form is the way to read a value unavailable at module scope, e.g. a Cloudflare Hyperdrive binding: `connectionString: async () => (await getCloudflareContext({ async: true })).env.HYPERDRIVE.connectionString`. There is no separate `hyperdriveBinding` option.
71
+ - `db.autoHyperdrive` — when `true` (default) and `db.connectionString` is unset, `env.HYPERDRIVE.connectionString` is read automatically before falling through to `db.supabase`. Set `false` to skip this and go straight to `supabase` (or the "no connection string" error).
72
+ - `db.autoHyperdriveSkipUrls` — connection strings from `env.HYPERDRIVE.connectionString` treated as "no connection" by auto-Hyperdrive (e.g. `wrangler dev`'s unconfigured placeholder). Defaults to `['postgresql://user:pass@localhost:5432/db']`.
71
73
  - Supabase Data API (used only when neither of the above is set): `db.supabase` — `{ url?, anonKey?, execFunction?, rawSql? }` where `url`/`anonKey` each accept a string or a sync/async function returning one, defaulting `url`/`anonKey` to `NEXT_PUBLIC_SUPABASE_URL`/`NEXT_PUBLIC_SUPABASE_ANON_KEY`. Statements are translated to PostgREST REST calls first; unsupported statements fall back to `supabase/cfni_exec.sql` (a `security invoker` SQL-exec function) in your database — `cfni-db-codegen`/`cfni-db-install-exec` can install it for you (see below). No multi-statement transactions — each statement in a `withUserDb` callback is its own round-trip; `.transaction()` throws instead of running non-atomically. `rawSql: false` disables `cfni_exec` fallback, throwing an informative error when a query cannot be served over REST.
72
74
  - `db.disconnectAfterRequest` — deprecated, ignored since 0.8.23. Each `withPublicDb`/`withUserDb` call opens and closes its own client; Hyperdrive pools the server-side connection.
73
75
  - `db.authenticatedRole` — direct-Postgres mode only: Postgres role assumed inside `withUserDb`'s transaction. Accepts a string or a sync/async function returning one. Defaults to `'authenticated'` (Supabase RLS convention).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.9.16",
3
+ "version": "0.9.17",
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",