cloudflare-next-intl 0.9.14 → 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 +19 -2
- package/dist/src/db/resolve_hyperdrive_connection_string.d.ts +1 -1
- package/dist/src/db/resolve_hyperdrive_connection_string.js +3 -2
- package/dist/src/db/resolve_mode.js +1 -1
- package/dist/src/dynamic_pages_check/check_dynamic_pages.d.ts +3 -1
- package/dist/src/dynamic_pages_check/check_dynamic_pages.js +5 -0
- package/dist/src/dynamic_pages_check/collect_reachable_files.d.ts +7 -0
- package/dist/src/dynamic_pages_check/collect_reachable_files.js +36 -0
- package/dist/src/dynamic_pages_check/detect_dynamic_usage.d.ts +1 -0
- package/dist/src/dynamic_pages_check/detect_dynamic_usage.js +17 -2
- package/dist/src/dynamic_pages_check/find_report_error_calls.d.ts +5 -0
- package/dist/src/dynamic_pages_check/find_report_error_calls.js +71 -0
- package/dist/src/dynamic_pages_check/sync_error_reporting_auth_user.d.ts +21 -0
- package/dist/src/dynamic_pages_check/sync_error_reporting_auth_user.js +73 -0
- package/dist/src/dynamic_pages_check/trace_dynamic_usage.d.ts +3 -5
- package/dist/src/dynamic_pages_check/trace_dynamic_usage.js +4 -26
- package/dist/src/firebase_auth/server/resolve_optional_auth_user.d.ts +3 -0
- package/dist/src/firebase_auth/server/resolve_optional_auth_user.js +5 -0
- package/dist/src/types/types.d.ts +2 -0
- package/dist/src/vite/auto_dynamic_pages_plugin.d.ts +1 -0
- package/dist/src/vite/auto_dynamic_pages_plugin.js +1 -0
- package/llms.txt +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -258,9 +258,9 @@ 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
|
-
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.
|
|
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.
|
|
265
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.
|
|
266
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).
|
|
@@ -480,6 +480,23 @@ Features include:
|
|
|
480
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`).
|
|
481
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
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
|
+
```
|
|
483
500
|
|
|
484
501
|
```tsx
|
|
485
502
|
import ThemeSwitcher from "cloudflare-next-intl/ThemeSwitcher";
|
|
@@ -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
|
-
|
|
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
|
|
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,3 +1,4 @@
|
|
|
1
|
+
import { type SyncErrorReportingAuthUserReport } from './sync_error_reporting_auth_user.js';
|
|
1
2
|
import type { AliasConfig } from './resolve_local_imports.js';
|
|
2
3
|
export type DynamicPagesCheckMode = 'off' | 'report' | 'fix';
|
|
3
4
|
export interface CheckDynamicPagesOptions {
|
|
@@ -7,6 +8,7 @@ export interface CheckDynamicPagesOptions {
|
|
|
7
8
|
skip?: readonly string[];
|
|
8
9
|
resolveImports?: boolean;
|
|
9
10
|
aliases?: readonly AliasConfig[];
|
|
11
|
+
syncErrorReportingAuthUser?: boolean;
|
|
10
12
|
}
|
|
11
13
|
export interface CheckDynamicPagesReport {
|
|
12
14
|
file: string;
|
|
@@ -18,4 +20,4 @@ export interface CheckDynamicPagesIo {
|
|
|
18
20
|
writeFile?: (file: string, contents: string) => void;
|
|
19
21
|
isFile?: (file: string) => boolean;
|
|
20
22
|
}
|
|
21
|
-
export declare function checkDynamicPages(options: CheckDynamicPagesOptions, io?: CheckDynamicPagesIo): Promise<CheckDynamicPagesReport[]>;
|
|
23
|
+
export declare function checkDynamicPages(options: CheckDynamicPagesOptions, io?: CheckDynamicPagesIo): Promise<(CheckDynamicPagesReport | SyncErrorReportingAuthUserReport)[]>;
|
|
@@ -4,6 +4,7 @@ import { findPageFiles as findPageFilesImpl } from './find_page_files.js';
|
|
|
4
4
|
import { detectDynamicUsage } from './detect_dynamic_usage.js';
|
|
5
5
|
import { traceDynamicUsage } from './trace_dynamic_usage.js';
|
|
6
6
|
import { insertDynamicExport } from './insert_dynamic_export.js';
|
|
7
|
+
import { syncErrorReportingAuthUser } from './sync_error_reporting_auth_user.js';
|
|
7
8
|
function defaultIsFile(path) {
|
|
8
9
|
try {
|
|
9
10
|
return statSync(path).isFile();
|
|
@@ -62,5 +63,9 @@ export async function checkDynamicPages(options, io = {}) {
|
|
|
62
63
|
reports.push({ file, action: 'would-add-force-dynamic' });
|
|
63
64
|
}
|
|
64
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
|
+
}
|
|
65
70
|
return reports;
|
|
66
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,36 @@
|
|
|
1
|
+
import { extractImportSpecifiers, resolveLocalImport } from './resolve_local_imports.js';
|
|
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
|
+
}
|
|
7
|
+
export function collectReachableFiles(entryFile, entrySource, aliases, io) {
|
|
8
|
+
const isFile = io.isFile ?? (() => false);
|
|
9
|
+
const files = new Map([[entryFile, entrySource]]);
|
|
10
|
+
const queue = [entryFile];
|
|
11
|
+
while (queue.length > 0) {
|
|
12
|
+
const current = queue.shift();
|
|
13
|
+
if (files.size >= MAX_FILES_VISITED)
|
|
14
|
+
continue;
|
|
15
|
+
const source = files.get(current);
|
|
16
|
+
for (const specifier of extractImportSpecifiers(source)) {
|
|
17
|
+
if (files.size >= MAX_FILES_VISITED)
|
|
18
|
+
break;
|
|
19
|
+
const resolved = resolveLocalImport(specifier, current, aliases, isFile);
|
|
20
|
+
if (resolved === null || files.has(resolved))
|
|
21
|
+
continue;
|
|
22
|
+
let importedSource;
|
|
23
|
+
try {
|
|
24
|
+
importedSource = io.readFile(resolved);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (hasLeadingUseServerDirective(importedSource))
|
|
30
|
+
continue;
|
|
31
|
+
files.set(resolved, importedSource);
|
|
32
|
+
queue.push(resolved);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return files;
|
|
36
|
+
}
|
|
@@ -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;
|
|
@@ -7,13 +7,28 @@ 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
|
|
22
|
+
detectedDynamicApis,
|
|
18
23
|
};
|
|
19
24
|
}
|
|
25
|
+
const EXPLICIT_DYNAMIC_EXPORT_VALUE = /export\s+const\s+dynamic\s*=\s*['"]([^'"]+)['"]/;
|
|
26
|
+
export function readExplicitDynamicValue(sourceText) {
|
|
27
|
+
const match = EXPLICIT_DYNAMIC_EXPORT_VALUE.exec(sourceText);
|
|
28
|
+
if (match === null)
|
|
29
|
+
return null;
|
|
30
|
+
const value = match[1];
|
|
31
|
+
if (value === 'force-static' || value === 'force-dynamic' || value === 'auto' || value === 'error')
|
|
32
|
+
return value;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
@@ -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,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
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { type DynamicDetectionResult } from './detect_dynamic_usage.js';
|
|
2
|
-
import { type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
isFile?: (file: string) => boolean;
|
|
6
|
-
}
|
|
2
|
+
import { type CollectReachableFilesIo } from './collect_reachable_files.js';
|
|
3
|
+
import type { AliasConfig } from './resolve_local_imports.js';
|
|
4
|
+
export type TraceDynamicUsageIo = CollectReachableFilesIo;
|
|
7
5
|
export declare function traceDynamicUsage(entryFile: string, entrySource: string, aliases: readonly AliasConfig[], io: TraceDynamicUsageIo): DynamicDetectionResult;
|
|
@@ -1,39 +1,17 @@
|
|
|
1
1
|
import { detectDynamicUsage } from './detect_dynamic_usage.js';
|
|
2
|
-
import {
|
|
3
|
-
const MAX_FILES_VISITED = 300;
|
|
2
|
+
import { collectReachableFiles } from './collect_reachable_files.js';
|
|
4
3
|
export function traceDynamicUsage(entryFile, entrySource, aliases, io) {
|
|
5
|
-
const
|
|
6
|
-
const visited = new Set([entryFile]);
|
|
7
|
-
const queue = [{ file: entryFile, source: entrySource }];
|
|
4
|
+
const files = collectReachableFiles(entryFile, entrySource, aliases, io);
|
|
8
5
|
let hasExplicitDynamicExport = false;
|
|
9
6
|
const detectedApis = new Set();
|
|
10
7
|
let first = true;
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const detection = detectDynamicUsage(current.source);
|
|
8
|
+
for (const source of files.values()) {
|
|
9
|
+
const detection = detectDynamicUsage(source);
|
|
14
10
|
if (first) {
|
|
15
11
|
hasExplicitDynamicExport = detection.hasExplicitDynamicExport;
|
|
16
12
|
first = false;
|
|
17
13
|
}
|
|
18
14
|
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
15
|
}
|
|
38
16
|
return { hasExplicitDynamicExport, detectedDynamicApis: [...detectedApis] };
|
|
39
17
|
}
|
|
@@ -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;
|
|
@@ -209,6 +210,7 @@ export interface SupabaseDbConfig {
|
|
|
209
210
|
export interface DbRoutingConfig {
|
|
210
211
|
connectionString?: FallibleConfigValue<string>;
|
|
211
212
|
autoHyperdrive?: boolean;
|
|
213
|
+
autoHyperdriveSkipUrls?: string[];
|
|
212
214
|
disconnectAfterRequest?: boolean;
|
|
213
215
|
authenticatedRole?: string | (() => string | Promise<string>);
|
|
214
216
|
authenticatedRoleClaim?: string | false;
|
|
@@ -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;
|
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).
|