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 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`)**: Patches Vinext runtime route wiring, route matching, optimistic route template resolution, and prefetch learning so pending prefetches with already cached templates don't block navigation, and leading `:locale` segments route correctly.
271
- 10. **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.
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: true, // Enable vinext route wiring, matching, and prefetch fixes (default: true, or options object)
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.getPerformance(app);
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,
@@ -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";
@@ -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;
@@ -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
- plugins.push(autoDynamicPagesPlugin(typeof options.autoDynamicPages === "object"
24
- ? options.autoDynamicPages
25
- : undefined));
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,