cloudflare-next-intl 0.9.49 → 0.9.51
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 +24 -4
- package/bin/check_layout_queries.mjs +16 -0
- package/dist/src/dynamic_pages_check/check_dynamic_pages.d.ts +4 -0
- package/dist/src/dynamic_pages_check/check_dynamic_pages.js +33 -0
- package/dist/src/error_handling/is_stale_deploy_error.js +10 -0
- package/dist/src/firebase_auth/client/firebase_client.js +1 -1
- package/dist/src/layout_queries_check/check_layout_queries.d.ts +23 -0
- package/dist/src/layout_queries_check/check_layout_queries.js +172 -0
- package/dist/src/layout_queries_check/index.d.ts +1 -0
- package/dist/src/layout_queries_check/index.js +1 -0
- package/dist/src/vite/auto_dynamic_pages_plugin.d.ts +2 -0
- package/dist/src/vite/auto_dynamic_pages_plugin.js +3 -0
- package/dist/src/vite/index.d.ts +2 -1
- package/dist/src/vite/index.js +2 -1
- package/dist/src/vite/layout_queries_plugin.d.ts +7 -0
- package/dist/src/vite/layout_queries_plugin.js +42 -0
- package/dist/src/vite/plugin.d.ts +3 -0
- package/dist/src/vite/plugin.js +18 -6
- package/dist/src/vite/vinext_route_wiring_fix.d.ts +59 -2
- package/dist/src/vite/vinext_route_wiring_fix.js +532 -25
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -267,8 +267,9 @@ export default defineConfig({
|
|
|
267
267
|
6. **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).
|
|
268
268
|
7. **Cloudflare Workers Client Stub (`cfWorkersClientStub`)**: Stubs `cloudflare:workers` in client builds so shared modules can be referenced without client bundling errors.
|
|
269
269
|
8. **Build ID Asset Emission (`buildIdAsset`)**: Emits `BUILD_ID` static asset in the client build directory from `process.env.__VINEXT_SHARED_BUILD_ID` or `process.env.__VINEXT_BUILD_ID`.
|
|
270
|
-
9. **Vinext Route Wiring & Optimistic Prefetch Fix (`vinextRouteWiringFix`)**:
|
|
271
|
-
10. **
|
|
270
|
+
9. **Vinext Route Wiring & Optimistic Prefetch Fix (`vinextRouteWiringFix`)**: ⚠️ Monkey-patches Vinext's compiled runtime on disk (`node_modules/vinext/dist/**`) — **on by default** since 0.9.51 via `experimentalRouteLoadingFixes` (also defaulting to on). Fixes route wiring (a route-specific `loading.tsx` losing to an ancestor/root skeleton), route matching and optimistic (client-side) routing for leading `:locale` segments, prefetch learning (an in-flight prefetch losing the race so the previous page stays on screen), and a `refresh()` navigation cancelling an in-flight page-to-page navigation because both share the same counter. Every sub-patch is regex-based against vinext's exact compiled shape and checks that the vinext-internal identifiers it relies on (`makeThenableParams`, `trieMatch`, `getPrefetchCache`, …) are still present before touching a file — a future vinext release that renames or removes one is left unpatched (with a console warning) rather than being patched into a broken half-state. Set `experimentalRouteLoadingFixes: false` (or `vinextRouteWiringFix: false`) to opt out entirely; see **Plugin Options** below for the individual sub-patch flags (`routeWiring`, `routeMatching`, `optimisticRouting`, `prefetchLearning`, `suspenseProbe`, `renderDependency`, `optimisticLearningTimeout`, `pageInvokerSuspensionRelease`, `refreshDeferral`, `unblockRenderDependencies`, `unblockPageElementDependencies`).
|
|
271
|
+
10. **Layout DB Query Check (`layoutQueriesCheck`)**: Scans each `layout.tsx`/`layout.ts` and its reachable server-component import tree (also usable standalone from `cloudflare-next-intl/checkLayoutQueries`, or via the `cfni-check-layout-queries` CLI bin) for blocking `withUserDb()` / `withPublicDb()` calls. A layout re-renders on every route transition within its group, so a DB query anywhere in that tree blocks every page switch under it — the scan stops at Client Components (`'use client'`), which don't block server layout streaming. Prints a visible terminal warning with concrete fixes (move to a Client Component, wrap in `unstable_cache`, or move the query out of the shared layout into the page) by default; pass `{ strict: true }` to fail the build instead, or `false` to disable.
|
|
272
|
+
11. **Lucide & Next.js Specifier Optimizer (`lucideOptimizer`)**: Auto-detects `lucide-react` in project dependencies, rewrites named imports to direct deep icon paths (`lucide-react/dist/esm/icons/<icon>.mjs`) to avoid browser socket exhaustion (`ERR_INSUFFICIENT_RESOURCES`), and normalizes Next.js `.js` specifiers (`next/dynamic.js` -> `next/dynamic`) to prevent mid-session Vite re-optimization and React dispatcher splitting.
|
|
272
273
|
|
|
273
274
|
##### Plugin Options
|
|
274
275
|
All features are enabled by default, and can be individually configured or toggled off:
|
|
@@ -299,21 +300,40 @@ export default defineConfig({
|
|
|
299
300
|
localeParam: "locale", // Route param name to read (default: "locale")
|
|
300
301
|
skip: ["src/app/[locale]/(marketing)/**"], // Glob(s) to exclude from the scan
|
|
301
302
|
},
|
|
303
|
+
layoutQueriesCheck: { // Flag blocking DB queries in the layout tree (or `false` to disable)
|
|
304
|
+
strict: false, // Fail the build on violations instead of just warning (default: false)
|
|
305
|
+
runOnDev: true, // Also run on `vite dev`, not just build (default: true)
|
|
306
|
+
},
|
|
302
307
|
messagesDir: "./messages", // Path to locale JSON files (default: './messages')
|
|
303
308
|
intlConfigPath: "./src/l18n/intl_config.ts", // Path to intl config (auto-detected if omitted)
|
|
304
309
|
buildIdAsset: true, // Emit BUILD_ID asset (or custom string filename, default: true)
|
|
305
310
|
localeFiles: true, // Enable @locale-file & glob bundling (default: true)
|
|
306
311
|
userAgentStub: true, // Enable regex-based user-agent stub (default: true)
|
|
307
312
|
cfWorkersClientStub: true, // Enable client cloudflare:workers stub (default: true)
|
|
308
|
-
vinextRouteWiringFix:
|
|
313
|
+
vinextRouteWiringFix: { // ⚠️ DANGER: Monkey-patches vinext on disk (default: follows experimentalRouteLoadingFixes, i.e. true; or options object)
|
|
314
|
+
routeWiring: true, // Route-specific loading.tsx wins over ancestor/root skeletons (default: true)
|
|
315
|
+
routeMatching: true, // Leading `:locale` segment tried against active locale first (default: true)
|
|
316
|
+
optimisticRouting: true, // Same fix client-side for optimistic (instant) navigation (default: true)
|
|
317
|
+
prefetchLearning: true, // Wait for an in-flight prefetch of the nav target instead of giving up (default: true)
|
|
318
|
+
optimisticLearningTimeout: true, // Lower the fixed safety-cap on that wait; pass a number for a custom ms cap (default: true → 200ms)
|
|
319
|
+
suspenseProbe: true, // Respect <Suspense> boundaries when probing for async page dependencies (default: true)
|
|
320
|
+
renderDependency: true, // Release a render dependency when its component suspends (default: true)
|
|
321
|
+
pageInvokerSuspensionRelease: true, // Same release, unconditionally, for the page component's own barrier (default: true)
|
|
322
|
+
refreshDeferral: true, // Defer a refresh() navigation while a normal navigation is in flight (default: true)
|
|
323
|
+
unblockRenderDependencies: true, // Strip layout/template/slot/route dependency ordering (default: true)
|
|
324
|
+
unblockPageElementDependencies: false, // Also strip it from the PAGE element itself (default: false — see warning below)
|
|
325
|
+
},
|
|
326
|
+
experimentalRouteLoadingFixes: true, // ⚠️ DANGER: Unified switch enabling both vinextRouteWiringFix and SSG on loading.* (default: true)
|
|
309
327
|
lucideOptimizer: true, // Auto-optimize lucide-react deep imports and normalize next/*.js (default: true, or options object)
|
|
310
328
|
}),
|
|
311
329
|
],
|
|
312
330
|
});
|
|
313
331
|
```
|
|
314
332
|
|
|
333
|
+
> **Note:** `unblockPageElementDependencies: true` was measured, in a real app, to make page-to-page navigation noticeably slower rather than faster — it strips ordering vinext's own page-element wiring relies on. Leave it off unless you've verified otherwise for your app.
|
|
334
|
+
|
|
315
335
|
Individual standalone plugins are also exported if you only need a specific feature:
|
|
316
|
-
`imageOptimizerPlugin` (or `imageOptimizer`), `autoLocaleParamsPlugin`, `buildIdAsset`, `localeFilePlugin`, `userAgentStubPlugin`, `cfWorkersClientStubPlugin`, `vinextRouteWiringFixPlugin`, `lucideOptimizerPlugin`.
|
|
336
|
+
`imageOptimizerPlugin` (or `imageOptimizer`), `autoLocaleParamsPlugin`, `layoutQueriesPlugin` (or `layoutQueriesCheck`), `buildIdAsset`, `localeFilePlugin`, `userAgentStubPlugin`, `cfWorkersClientStubPlugin`, `vinextRouteWiringFixPlugin`, `lucideOptimizerPlugin`.
|
|
317
337
|
|
|
318
338
|
##### Per-Image Optimizer Settings
|
|
319
339
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { checkLayoutQueries } from "../dist/src/layout_queries_check/index.js";
|
|
3
|
+
|
|
4
|
+
const isStrict = process.argv.includes("--strict");
|
|
5
|
+
const report = checkLayoutQueries({
|
|
6
|
+
rootDir: process.cwd(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
if (!report.valid) {
|
|
10
|
+
console.error(report.formattedMessage);
|
|
11
|
+
if (isStrict) {
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
} else {
|
|
15
|
+
console.log("✅ [cloudflare-next-intl] No blocking database queries found in layout tree.");
|
|
16
|
+
}
|
|
@@ -9,6 +9,9 @@ export interface CheckDynamicPagesOptions {
|
|
|
9
9
|
mode?: DynamicPagesCheckMode;
|
|
10
10
|
target?: 'next' | 'vinext';
|
|
11
11
|
skip?: readonly string[];
|
|
12
|
+
includeLoading?: boolean;
|
|
13
|
+
verifyVinextRouteWiring?: boolean;
|
|
14
|
+
projectRoot?: string;
|
|
12
15
|
resolveImports?: boolean;
|
|
13
16
|
aliases?: readonly AliasConfig[];
|
|
14
17
|
extraChecks?: readonly DynamicApiCheck[];
|
|
@@ -28,5 +31,6 @@ export interface CheckDynamicPagesIo {
|
|
|
28
31
|
readFile?: (file: string) => string;
|
|
29
32
|
writeFile?: (file: string, contents: string) => void;
|
|
30
33
|
isFile?: (file: string) => boolean;
|
|
34
|
+
isVinextRouteWiringSafe?: (root: string) => boolean;
|
|
31
35
|
}
|
|
32
36
|
export declare function checkDynamicPages(options: CheckDynamicPagesOptions, io?: CheckDynamicPagesIo): Promise<(CheckDynamicPagesReport | SyncErrorReportingAuthUserReport)[]>;
|
|
@@ -6,6 +6,7 @@ import { traceDynamicUsage } from './trace_dynamic_usage.js';
|
|
|
6
6
|
import { insertDynamicExport } from './insert_dynamic_export.js';
|
|
7
7
|
import { syncErrorReportingAuthUser } from './sync_error_reporting_auth_user.js';
|
|
8
8
|
import { deriveRoute, isApiRoute, makePageLabeler } from './derive_page_label.js';
|
|
9
|
+
import { isVinextAppPageRouteWiringSafeOnDisk } from '../vite/vinext_route_wiring_fix.js';
|
|
9
10
|
const LEGEND = 'λ API ƒ Dynamic (SSR) ○ Static (SSG) = Already declared - Unclear (framework decides) · Skipped';
|
|
10
11
|
function actionGlyph(report, isApi) {
|
|
11
12
|
if (isApi && report.action !== 'skipped')
|
|
@@ -81,12 +82,33 @@ function defaultIsFile(path) {
|
|
|
81
82
|
return false;
|
|
82
83
|
}
|
|
83
84
|
}
|
|
85
|
+
function isSsgAction(report) {
|
|
86
|
+
if (report.action === 'added-force-static' || report.action === 'would-add-force-static') {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (report.action === 'already-declared') {
|
|
90
|
+
return report.explicitValue === 'force-static';
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
84
94
|
export async function checkDynamicPages(options, io = {}) {
|
|
85
95
|
const mode = options.mode ?? 'report';
|
|
86
96
|
if (mode === 'off')
|
|
87
97
|
return [];
|
|
88
98
|
const target = options.target ?? 'next';
|
|
89
99
|
const resolveImports = options.resolveImports ?? true;
|
|
100
|
+
let includeLoading = options.includeLoading ?? false;
|
|
101
|
+
if (includeLoading && target === 'vinext' && options.verifyVinextRouteWiring !== false) {
|
|
102
|
+
const projectRoot = options.projectRoot ?? resolve(options.appDir, '..');
|
|
103
|
+
const checkSafe = io.isVinextRouteWiringSafe ?? isVinextAppPageRouteWiringSafeOnDisk;
|
|
104
|
+
if (!checkSafe(projectRoot)) {
|
|
105
|
+
console.warn('[cloudflare-next-intl] WARNING: Vinext route wiring fix is not verified on disk (vinext files may have changed, failed to patch, or patch is disabled). SSG was NOT added to loading.* files.');
|
|
106
|
+
includeLoading = false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (includeLoading) {
|
|
110
|
+
console.warn('[cloudflare-next-intl] WARNING: includeLoading is enabled. Forcing SSG on loading.* files is dangerous and can break route rendering, streaming, or hydration.');
|
|
111
|
+
}
|
|
90
112
|
const findPageFiles = io.findPageFiles ?? findPageFilesImpl;
|
|
91
113
|
const readFile = io.readFile ?? ((file) => readFileSync(file, 'utf8'));
|
|
92
114
|
const writeFile = io.writeFile ?? ((file, contents) => writeFileSync(file, contents, 'utf8'));
|
|
@@ -98,6 +120,9 @@ export async function checkDynamicPages(options, io = {}) {
|
|
|
98
120
|
const extraChecks = options.extraChecks ?? [];
|
|
99
121
|
const reports = [];
|
|
100
122
|
for (const file of findPageFiles(options.appDir)) {
|
|
123
|
+
if (!includeLoading && fileKind(file) === 'loading') {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
101
126
|
if (skipSet.has(file)) {
|
|
102
127
|
reports.push({ file, action: 'skipped' });
|
|
103
128
|
continue;
|
|
@@ -135,6 +160,14 @@ export async function checkDynamicPages(options, io = {}) {
|
|
|
135
160
|
reports.push({ file, action: 'would-add-force-dynamic', signals });
|
|
136
161
|
}
|
|
137
162
|
}
|
|
163
|
+
if (includeLoading) {
|
|
164
|
+
for (const report of reports) {
|
|
165
|
+
const r = report;
|
|
166
|
+
if (fileKind(r.file) === 'loading' && !isSsgAction(r)) {
|
|
167
|
+
console.warn(`[cloudflare-next-intl] WARNING: Loading file is not static (SSG): ${displayPath(r.file)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
138
171
|
if (options.verbose) {
|
|
139
172
|
const pageLabelStyle = typeof options.verbose === 'object' ? options.verbose.pageLabel : undefined;
|
|
140
173
|
const pageLabel = makePageLabeler(options.appDir, pageLabelStyle, displayPath);
|
|
@@ -6,6 +6,16 @@ export const defaultStaleDeployPatterns = [
|
|
|
6
6
|
'connection closed',
|
|
7
7
|
'rsc payload',
|
|
8
8
|
'minified react error #412',
|
|
9
|
+
'minified react error #418',
|
|
10
|
+
'minified react error #419',
|
|
11
|
+
'minified react error #421',
|
|
12
|
+
'minified react error #422',
|
|
13
|
+
'minified react error #423',
|
|
14
|
+
'minified react error #425',
|
|
15
|
+
'minified react error #426',
|
|
16
|
+
'an error occurred in the server components render',
|
|
17
|
+
'server components render',
|
|
18
|
+
'digest property is included on this error instance',
|
|
9
19
|
'the above error occurred in a react component',
|
|
10
20
|
'the connection to the page was unexpectedly closed',
|
|
11
21
|
'readablestream',
|
|
@@ -157,7 +157,7 @@ export async function getFirebaseAuthClient() {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
if (perfModule) {
|
|
160
|
-
cachedPerformance = perfModule.
|
|
160
|
+
cachedPerformance = perfModule.initializePerformance(app, { instrumentationEnabled: false });
|
|
161
161
|
}
|
|
162
162
|
const auth = getAuth(app);
|
|
163
163
|
cached = { app, auth };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface LayoutDbViolation {
|
|
2
|
+
layoutFile: string;
|
|
3
|
+
sourceFile: string;
|
|
4
|
+
lineNumber: number;
|
|
5
|
+
lineContent: string;
|
|
6
|
+
signal: string;
|
|
7
|
+
importTrace: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface CheckLayoutQueriesOptions {
|
|
10
|
+
appDir?: string;
|
|
11
|
+
rootDir?: string;
|
|
12
|
+
aliases?: Record<string, string>;
|
|
13
|
+
maxDepth?: number;
|
|
14
|
+
throwOnError?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface CheckLayoutQueriesReport {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
violations: LayoutDbViolation[];
|
|
19
|
+
formattedMessage: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function findLayoutFiles(dir: string): string[];
|
|
22
|
+
export declare function formatLayoutDbViolationMessage(violations: LayoutDbViolation[]): string;
|
|
23
|
+
export declare function checkLayoutQueries(options?: CheckLayoutQueriesOptions): CheckLayoutQueriesReport;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
const DB_SIGNALS = [
|
|
4
|
+
{ name: "withUserDb()", pattern: /\bwithUserDb\s*\(/ },
|
|
5
|
+
{ name: "withPublicDb()", pattern: /\bwithPublicDb\s*\(/ },
|
|
6
|
+
];
|
|
7
|
+
const SUPPORTED_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs"];
|
|
8
|
+
function isClientComponent(content) {
|
|
9
|
+
return /^\s*["']use client["']/m.test(content);
|
|
10
|
+
}
|
|
11
|
+
export function findLayoutFiles(dir) {
|
|
12
|
+
const results = [];
|
|
13
|
+
if (!existsSync(dir))
|
|
14
|
+
return results;
|
|
15
|
+
function walk(currentDir) {
|
|
16
|
+
const entries = readdirSync(currentDir);
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (entry.startsWith(".") || entry === "node_modules")
|
|
19
|
+
continue;
|
|
20
|
+
const fullPath = join(currentDir, entry);
|
|
21
|
+
const stat = statSync(fullPath);
|
|
22
|
+
if (stat.isDirectory()) {
|
|
23
|
+
walk(fullPath);
|
|
24
|
+
}
|
|
25
|
+
else if (/^layout\.(tsx|ts|jsx|js)$/.test(entry)) {
|
|
26
|
+
results.push(fullPath);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
walk(dir);
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
function resolveImportPath(specifier, importerFile, rootDir, aliases) {
|
|
34
|
+
for (const [alias, target] of Object.entries(aliases)) {
|
|
35
|
+
if (specifier === alias || specifier.startsWith(alias + "/")) {
|
|
36
|
+
const remainder = specifier.slice(alias.length).replace(/^\//, "");
|
|
37
|
+
const basePath = resolve(rootDir, target, remainder);
|
|
38
|
+
return probeExtensions(basePath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
|
42
|
+
const basePath = resolve(dirname(importerFile), specifier);
|
|
43
|
+
return probeExtensions(basePath);
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function probeExtensions(basePath) {
|
|
48
|
+
if (existsSync(basePath) && statSync(basePath).isFile()) {
|
|
49
|
+
return basePath;
|
|
50
|
+
}
|
|
51
|
+
for (const ext of SUPPORTED_EXTENSIONS) {
|
|
52
|
+
const withExt = basePath + ext;
|
|
53
|
+
if (existsSync(withExt) && statSync(withExt).isFile()) {
|
|
54
|
+
return withExt;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const ext of SUPPORTED_EXTENSIONS) {
|
|
58
|
+
const indexFile = join(basePath, "index" + ext);
|
|
59
|
+
if (existsSync(indexFile) && statSync(indexFile).isFile()) {
|
|
60
|
+
return indexFile;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function extractImports(content) {
|
|
66
|
+
const imports = [];
|
|
67
|
+
const importRegex = /(?:import\s+(?:[\w*\s{},]*\s+from\s+)?|import\s*\(\s*)["']([^"']+)["']/g;
|
|
68
|
+
let match;
|
|
69
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
70
|
+
if (match[1]) {
|
|
71
|
+
imports.push(match[1]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return imports;
|
|
75
|
+
}
|
|
76
|
+
export function formatLayoutDbViolationMessage(violations) {
|
|
77
|
+
if (violations.length === 0)
|
|
78
|
+
return "";
|
|
79
|
+
const separator = "=".repeat(84);
|
|
80
|
+
const thinSeparator = "-".repeat(84);
|
|
81
|
+
const lines = [
|
|
82
|
+
"",
|
|
83
|
+
separator,
|
|
84
|
+
"🚨 [cloudflare-next-intl] BLOCKING DATABASE QUERY DETECTED IN LAYOUT",
|
|
85
|
+
separator,
|
|
86
|
+
"",
|
|
87
|
+
"⚠️ WHY THIS IS DANGEROUS:",
|
|
88
|
+
" Layouts are re-evaluated by the server on every route transition within their group.",
|
|
89
|
+
" Executing database queries (`withUserDb` / `withPublicDb`) inside the layout tree",
|
|
90
|
+
" blocks the entire RSC response on every page switch. This causes page navigation",
|
|
91
|
+
" to freeze/hang and completely breaks instant client transitions.",
|
|
92
|
+
"",
|
|
93
|
+
"📋 VIOLATIONS FOUND (" + violations.length + "):",
|
|
94
|
+
thinSeparator,
|
|
95
|
+
];
|
|
96
|
+
violations.forEach((v, i) => {
|
|
97
|
+
lines.push(` [${i + 1}] Signal: ${v.signal}`);
|
|
98
|
+
lines.push(` Layout: ${v.layoutFile}`);
|
|
99
|
+
lines.push(` File: ${v.sourceFile}:${v.lineNumber}`);
|
|
100
|
+
lines.push(` Line: ${v.lineContent.trim()}`);
|
|
101
|
+
if (v.importTrace.length > 1) {
|
|
102
|
+
lines.push(` Trace: ${v.importTrace.join(" -> ")}`);
|
|
103
|
+
}
|
|
104
|
+
lines.push(thinSeparator);
|
|
105
|
+
});
|
|
106
|
+
lines.push("", "💡 HOW TO FIX:", " 1. Move to a Client Component ('use client'):", " Fetch the data in useEffect / SWR / React Query or a Supabase Realtime subscription.", " Client components do not block server layout streaming during navigation.", "", " 2. Use Cross-Request Caching (`unstable_cache`):", " Wrap the database query with `unstable_cache` (from 'next/cache') and a cache tag.", " This caches results in Cloudflare KV so layout renders in 0ms without hitting Postgres.", "", " 3. Move out of Shared Layout:", " If only a specific route needs this data, move the component from layout.tsx into that", " page's page.tsx.", separator, "");
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
export function checkLayoutQueries(options = {}) {
|
|
110
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
111
|
+
const appDir = options.appDir ?? resolve(rootDir, "src/app");
|
|
112
|
+
const aliases = options.aliases ?? {
|
|
113
|
+
"@": resolve(rootDir, "src"),
|
|
114
|
+
};
|
|
115
|
+
const maxDepth = options.maxDepth ?? 20;
|
|
116
|
+
const layoutFiles = findLayoutFiles(appDir);
|
|
117
|
+
const violations = [];
|
|
118
|
+
for (const layoutFile of layoutFiles) {
|
|
119
|
+
const visited = new Set();
|
|
120
|
+
function traverse(file, trace, depth) {
|
|
121
|
+
if (depth > maxDepth || visited.has(file))
|
|
122
|
+
return;
|
|
123
|
+
visited.add(file);
|
|
124
|
+
let content;
|
|
125
|
+
try {
|
|
126
|
+
content = readFileSync(file, "utf-8");
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (isClientComponent(content)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const lines = content.split("\n");
|
|
135
|
+
lines.forEach((line, index) => {
|
|
136
|
+
const trimmed = line.trim();
|
|
137
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*"))
|
|
138
|
+
return;
|
|
139
|
+
for (const signal of DB_SIGNALS) {
|
|
140
|
+
if (signal.pattern.test(line)) {
|
|
141
|
+
violations.push({
|
|
142
|
+
layoutFile,
|
|
143
|
+
sourceFile: file,
|
|
144
|
+
lineNumber: index + 1,
|
|
145
|
+
lineContent: line,
|
|
146
|
+
signal: signal.name,
|
|
147
|
+
importTrace: [...trace, file],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
const imports = extractImports(content);
|
|
153
|
+
for (const imp of imports) {
|
|
154
|
+
const resolved = resolveImportPath(imp, file, rootDir, aliases);
|
|
155
|
+
if (resolved) {
|
|
156
|
+
traverse(resolved, [...trace, file], depth + 1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
traverse(layoutFile, [], 0);
|
|
161
|
+
}
|
|
162
|
+
const valid = violations.length === 0;
|
|
163
|
+
const formattedMessage = formatLayoutDbViolationMessage(violations);
|
|
164
|
+
if (!valid && options.throwOnError) {
|
|
165
|
+
throw new Error(formattedMessage);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
valid,
|
|
169
|
+
violations,
|
|
170
|
+
formattedMessage,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { checkLayoutQueries, formatLayoutDbViolationMessage, findLayoutFiles, type LayoutDbViolation, type CheckLayoutQueriesOptions, type CheckLayoutQueriesReport, } from "./check_layout_queries.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { checkLayoutQueries, formatLayoutDbViolationMessage, findLayoutFiles, } from "./check_layout_queries.js";
|
|
@@ -4,6 +4,8 @@ export interface AutoDynamicPagesPluginOptions {
|
|
|
4
4
|
appDir?: string;
|
|
5
5
|
mode?: DynamicPagesCheckMode;
|
|
6
6
|
target?: 'next' | 'vinext';
|
|
7
|
+
includeLoading?: boolean;
|
|
8
|
+
verifyVinextRouteWiring?: boolean;
|
|
7
9
|
syncErrorReportingAuthUser?: boolean;
|
|
8
10
|
extraChecks?: readonly DynamicApiCheck[];
|
|
9
11
|
verbose?: boolean | {
|
|
@@ -32,8 +32,11 @@ export function autoDynamicPagesPlugin(options = {}) {
|
|
|
32
32
|
try {
|
|
33
33
|
const reports = await checkDynamicPages({
|
|
34
34
|
appDir,
|
|
35
|
+
projectRoot: root,
|
|
35
36
|
mode: options.mode ?? "fix",
|
|
36
37
|
target: options.target ?? "vinext",
|
|
38
|
+
includeLoading: options.includeLoading ?? false,
|
|
39
|
+
verifyVinextRouteWiring: options.verifyVinextRouteWiring ?? true,
|
|
37
40
|
syncErrorReportingAuthUser: options.syncErrorReportingAuthUser ?? false,
|
|
38
41
|
extraChecks: options.extraChecks ?? [],
|
|
39
42
|
verbose: options.verbose ?? false,
|
package/dist/src/vite/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { autoDynamicPagesPlugin, type AutoDynamicPagesPluginOptions } from "./auto_dynamic_pages_plugin.js";
|
|
2
2
|
export { autoLocaleParamsPlugin, type AutoLocaleParamsPluginOptions } from "./auto_locale_params_plugin.js";
|
|
3
|
+
export { layoutQueriesPlugin, type LayoutQueriesPluginOptions } from "./layout_queries_plugin.js";
|
|
3
4
|
export { buildIdAsset } from "./build_id_asset.js";
|
|
4
5
|
export { userAgentStubPlugin, USER_AGENT_STUB_ID, USER_AGENT_STUB_CODE } from "./user_agent_stub.js";
|
|
5
6
|
export { cfWorkersClientStubPlugin, CF_WORKERS_CLIENT_STUB_ID, CF_WORKERS_CLIENT_STUB_CODE } from "./cf_workers_client_stub.js";
|
|
6
|
-
export { vinextRouteWiringFixPlugin, patchAppPageRouteWiring, isAppPageRouteWiringFile, isAppPageRouteWiringAlreadyFixed, type VinextRouteWiringFixPluginOptions } from "./vinext_route_wiring_fix.js";
|
|
7
|
+
export { vinextRouteWiringFixPlugin, patchAppPageRouteWiring, isAppPageRouteWiringFile, isAppPageRouteWiringAlreadyFixed, isVinextAppPageRouteWiringSafeOnDisk, patchAppPageProbe, isAppPageProbeFile, isAppPageProbeAlreadyFixed, isVinextAppPageProbeSafeOnDisk, type VinextRouteWiringFixPluginOptions, } from "./vinext_route_wiring_fix.js";
|
|
7
8
|
export { localeFilePlugin, resolveDefaultIntlConfigPath, type LocaleFilePluginOptions } from "./locale_file_plugin.js";
|
|
8
9
|
export { lucideOptimizerPlugin, detectLucideReact, resolveLucideEsmEntry, parseLucideIconMap, transformLucideImports, transformNextJsImports, type LucideOptimizerPluginOptions, } from "./lucide_optimizer_plugin.js";
|
|
9
10
|
export { cloudflareNextIntl, cloudflareNextIntlPlugin, type CloudflareNextIntlOptions, default } from "./plugin.js";
|
package/dist/src/vite/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { autoDynamicPagesPlugin } from "./auto_dynamic_pages_plugin.js";
|
|
2
2
|
export { autoLocaleParamsPlugin } from "./auto_locale_params_plugin.js";
|
|
3
|
+
export { layoutQueriesPlugin } from "./layout_queries_plugin.js";
|
|
3
4
|
export { buildIdAsset } from "./build_id_asset.js";
|
|
4
5
|
export { userAgentStubPlugin, USER_AGENT_STUB_ID, USER_AGENT_STUB_CODE } from "./user_agent_stub.js";
|
|
5
6
|
export { cfWorkersClientStubPlugin, CF_WORKERS_CLIENT_STUB_ID, CF_WORKERS_CLIENT_STUB_CODE } from "./cf_workers_client_stub.js";
|
|
6
|
-
export { vinextRouteWiringFixPlugin, patchAppPageRouteWiring, isAppPageRouteWiringFile, isAppPageRouteWiringAlreadyFixed } from "./vinext_route_wiring_fix.js";
|
|
7
|
+
export { vinextRouteWiringFixPlugin, patchAppPageRouteWiring, isAppPageRouteWiringFile, isAppPageRouteWiringAlreadyFixed, isVinextAppPageRouteWiringSafeOnDisk, patchAppPageProbe, isAppPageProbeFile, isAppPageProbeAlreadyFixed, isVinextAppPageProbeSafeOnDisk, } from "./vinext_route_wiring_fix.js";
|
|
7
8
|
export { localeFilePlugin, resolveDefaultIntlConfigPath } from "./locale_file_plugin.js";
|
|
8
9
|
export { lucideOptimizerPlugin, detectLucideReact, resolveLucideEsmEntry, parseLucideIconMap, transformLucideImports, transformNextJsImports, } from "./lucide_optimizer_plugin.js";
|
|
9
10
|
export { cloudflareNextIntl, cloudflareNextIntlPlugin, default } from "./plugin.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { type CheckLayoutQueriesOptions } from "../layout_queries_check/index.js";
|
|
3
|
+
export interface LayoutQueriesPluginOptions extends CheckLayoutQueriesOptions {
|
|
4
|
+
strict?: boolean;
|
|
5
|
+
runOnDev?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function layoutQueriesPlugin(options?: LayoutQueriesPluginOptions): Plugin;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { checkLayoutQueries } from "../layout_queries_check/index.js";
|
|
4
|
+
export function layoutQueriesPlugin(options = {}) {
|
|
5
|
+
let ran = false;
|
|
6
|
+
return {
|
|
7
|
+
name: "cloudflare-next-intl-layout-queries-check",
|
|
8
|
+
enforce: "pre",
|
|
9
|
+
configResolved(config) {
|
|
10
|
+
if (ran)
|
|
11
|
+
return;
|
|
12
|
+
const isBuild = config.command === "build";
|
|
13
|
+
const isDev = config.command === "serve";
|
|
14
|
+
const runOnDev = options.runOnDev ?? true;
|
|
15
|
+
if (!isBuild && !(isDev && runOnDev))
|
|
16
|
+
return;
|
|
17
|
+
const root = config.root || process.cwd();
|
|
18
|
+
const candidateAppDirs = [
|
|
19
|
+
options.appDir,
|
|
20
|
+
resolve(root, "src/app"),
|
|
21
|
+
resolve(root, "app"),
|
|
22
|
+
].filter((dir) => !!dir && existsSync(dir));
|
|
23
|
+
const appDir = candidateAppDirs[0];
|
|
24
|
+
if (!appDir)
|
|
25
|
+
return;
|
|
26
|
+
ran = true;
|
|
27
|
+
const report = checkLayoutQueries({
|
|
28
|
+
appDir,
|
|
29
|
+
rootDir: root,
|
|
30
|
+
aliases: options.aliases,
|
|
31
|
+
maxDepth: options.maxDepth,
|
|
32
|
+
throwOnError: false,
|
|
33
|
+
});
|
|
34
|
+
if (!report.valid) {
|
|
35
|
+
console.warn(report.formattedMessage);
|
|
36
|
+
if (options.strict) {
|
|
37
|
+
throw new Error("[cloudflare-next-intl] Build failed: Blocking database queries detected in layout tree. See details above.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -3,6 +3,7 @@ import { type LocaleFilePluginOptions } from "./locale_file_plugin.js";
|
|
|
3
3
|
import { type ImageOptimizerPluginOptions } from "../image_optimizer/index.js";
|
|
4
4
|
import { type AutoDynamicPagesPluginOptions } from "./auto_dynamic_pages_plugin.js";
|
|
5
5
|
import { type AutoLocaleParamsPluginOptions } from "./auto_locale_params_plugin.js";
|
|
6
|
+
import { type LayoutQueriesPluginOptions } from "./layout_queries_plugin.js";
|
|
6
7
|
import { type VinextRouteWiringFixPluginOptions } from "./vinext_route_wiring_fix.js";
|
|
7
8
|
import { type LucideOptimizerPluginOptions } from "./lucide_optimizer_plugin.js";
|
|
8
9
|
export interface CloudflareNextIntlOptions extends LocaleFilePluginOptions {
|
|
@@ -15,6 +16,8 @@ export interface CloudflareNextIntlOptions extends LocaleFilePluginOptions {
|
|
|
15
16
|
imageOptimizer?: boolean | ImageOptimizerPluginOptions;
|
|
16
17
|
vinextRouteWiringFix?: boolean | VinextRouteWiringFixPluginOptions;
|
|
17
18
|
autoLocaleParams?: boolean | AutoLocaleParamsPluginOptions;
|
|
19
|
+
layoutQueriesCheck?: boolean | LayoutQueriesPluginOptions;
|
|
20
|
+
experimentalRouteLoadingFixes?: boolean;
|
|
18
21
|
}
|
|
19
22
|
export declare function cloudflareNextIntl(options?: CloudflareNextIntlOptions): Plugin[];
|
|
20
23
|
export declare const cloudflareNextIntlPlugin: typeof cloudflareNextIntl;
|
package/dist/src/vite/plugin.js
CHANGED
|
@@ -5,24 +5,39 @@ import { localeFilePlugin } from "./locale_file_plugin.js";
|
|
|
5
5
|
import { imageOptimizerPlugin } from "../image_optimizer/index.js";
|
|
6
6
|
import { autoDynamicPagesPlugin } from "./auto_dynamic_pages_plugin.js";
|
|
7
7
|
import { autoLocaleParamsPlugin } from "./auto_locale_params_plugin.js";
|
|
8
|
+
import { layoutQueriesPlugin } from "./layout_queries_plugin.js";
|
|
8
9
|
import { vinextRouteWiringFixPlugin } from "./vinext_route_wiring_fix.js";
|
|
9
10
|
import { lucideOptimizerPlugin } from "./lucide_optimizer_plugin.js";
|
|
10
11
|
export function cloudflareNextIntl(options = {}) {
|
|
11
12
|
const plugins = [];
|
|
13
|
+
if (options.layoutQueriesCheck !== false) {
|
|
14
|
+
plugins.push(layoutQueriesPlugin(typeof options.layoutQueriesCheck === "object"
|
|
15
|
+
? options.layoutQueriesCheck
|
|
16
|
+
: undefined));
|
|
17
|
+
}
|
|
12
18
|
if (options.lucideOptimizer !== false) {
|
|
13
19
|
plugins.push(lucideOptimizerPlugin(typeof options.lucideOptimizer === "object"
|
|
14
20
|
? options.lucideOptimizer
|
|
15
21
|
: { root: options.root }));
|
|
16
22
|
}
|
|
23
|
+
const enableRouteLoadingFixes = options.experimentalRouteLoadingFixes !== false;
|
|
24
|
+
const shouldEnableVinextFix = options.vinextRouteWiringFix !== undefined
|
|
25
|
+
? Boolean(options.vinextRouteWiringFix)
|
|
26
|
+
: enableRouteLoadingFixes;
|
|
27
|
+
if (shouldEnableVinextFix) {
|
|
28
|
+
plugins.push(vinextRouteWiringFixPlugin(typeof options.vinextRouteWiringFix === "object" ? options.vinextRouteWiringFix : {}));
|
|
29
|
+
}
|
|
17
30
|
if (options.autoLocaleParams !== false) {
|
|
18
31
|
plugins.push(autoLocaleParamsPlugin(typeof options.autoLocaleParams === "object"
|
|
19
32
|
? options.autoLocaleParams
|
|
20
33
|
: undefined));
|
|
21
34
|
}
|
|
22
35
|
if (options.autoDynamicPages !== false) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
const autoDynamicPagesOptions = typeof options.autoDynamicPages === "object" ? { ...options.autoDynamicPages } : {};
|
|
37
|
+
if (enableRouteLoadingFixes && autoDynamicPagesOptions.includeLoading === undefined) {
|
|
38
|
+
autoDynamicPagesOptions.includeLoading = true;
|
|
39
|
+
}
|
|
40
|
+
plugins.push(autoDynamicPagesPlugin(autoDynamicPagesOptions));
|
|
26
41
|
}
|
|
27
42
|
if (options.imageOptimizer !== false) {
|
|
28
43
|
plugins.push(imageOptimizerPlugin(typeof options.imageOptimizer === "object"
|
|
@@ -39,9 +54,6 @@ export function cloudflareNextIntl(options = {}) {
|
|
|
39
54
|
if (options.userAgentStub !== false) {
|
|
40
55
|
plugins.push(userAgentStubPlugin());
|
|
41
56
|
}
|
|
42
|
-
if (options.vinextRouteWiringFix !== false) {
|
|
43
|
-
plugins.push(vinextRouteWiringFixPlugin(typeof options.vinextRouteWiringFix === "object" ? options.vinextRouteWiringFix : {}));
|
|
44
|
-
}
|
|
45
57
|
if (options.localeFiles !== false) {
|
|
46
58
|
plugins.push(localeFilePlugin({
|
|
47
59
|
messagesDir: options.messagesDir,
|