cloudflare-next-intl 0.9.50 → 0.9.52

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,22 +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: false, // ⚠️ DANGER: Monkey-patches vinext on disk (default: false, or options object)
309
- experimentalRouteLoadingFixes: false, // ⚠️ DANGER: Unified switch enabling both vinextRouteWiringFix and SSG on loading.* (default: false)
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)
310
327
  lucideOptimizer: true, // Auto-optimize lucide-react deep imports and normalize next/*.js (default: true, or options object)
311
328
  }),
312
329
  ],
313
330
  });
314
331
  ```
315
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
+
316
335
  Individual standalone plugins are also exported if you only need a specific feature:
317
- `imageOptimizerPlugin` (or `imageOptimizer`), `autoLocaleParamsPlugin`, `buildIdAsset`, `localeFilePlugin`, `userAgentStubPlugin`, `cfWorkersClientStubPlugin`, `vinextRouteWiringFixPlugin`, `lucideOptimizerPlugin`.
336
+ `imageOptimizerPlugin` (or `imageOptimizer`), `autoLocaleParamsPlugin`, `layoutQueriesPlugin` (or `layoutQueriesCheck`), `buildIdAsset`, `localeFilePlugin`, `userAgentStubPlugin`, `cfWorkersClientStubPlugin`, `vinextRouteWiringFixPlugin`, `lucideOptimizerPlugin`.
318
337
 
319
338
  ##### Per-Image Optimizer Settings
320
339
 
@@ -671,7 +690,7 @@ through `@intl-config`.
671
690
 
672
691
  When a new version of your application is deployed to Cloudflare Workers, users on older client sessions may encounter `ChunkLoadError` or failed dynamic imports when requesting outdated chunks.
673
692
 
674
- `IntlHelperScript` renders an early-catch `<script>` (production only, id `stale-deploy-early-catch`) that runs before hydration and listens for `window.error`/`unhandledrejection` events matching the same patterns as `isStaleDeployError` (inlined as JSON, so it stays in sync with `staleDeployPatterns` config), then force-reloads once per build id. This covers the case a React-level recovery (`useStaleDeployRecovery` below) cannot: when the chunk that failed to load is part of your own error boundary/global-error bundle, React never gets a chance to render the recovery UI. Both layers share the same `sessionStorage['stale-deploy-recovery-reloaded']` marker keyed by build id, so they can't double-reload each other. No setup beyond rendering `<IntlHelperScript />` is required.
693
+ `IntlHelperScript` renders an early-catch `<script>` (production only, id `stale-deploy-early-catch`) that runs before hydration and listens for `window.error`/`unhandledrejection` events matching the same patterns as `isStaleDeployError` (inlined as JSON, so it stays in sync with `staleDeployPatterns` config), then force-reloads once per build id (throttled to once per 15s so a later build-id marker can re-arm recovery instead of being blocked indefinitely). It also listens for `error` events during the capture phase to catch resource-load failures (a chunk `<script>`/`<link>` 404ing or served with a disallowed MIME type), which fire a non-bubbling, message-less `error` event on the element itself rather than surfacing as a catchable message. This covers the case a React-level recovery (`useStaleDeployRecovery` below) cannot: when the chunk that failed to load is part of your own error boundary/global-error bundle, React never gets a chance to render the recovery UI. Both layers share the same `sessionStorage['stale-deploy-recovery-reloaded']` marker keyed by build id, so they can't double-reload each other. No setup beyond rendering `<IntlHelperScript />` is required.
675
694
 
676
695
  For errors that don't crash the module graph itself (a normal thrown error reaching an error boundary), use `isStaleDeployError` and `clearClientCache` in error boundaries or global error handlers to automatically recover:
677
696
 
@@ -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
+ }
@@ -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";
@@ -17,6 +17,8 @@ export default function HelperScript() {
17
17
  try {
18
18
  var patterns = ${JSON.stringify(defaultStaleDeployPatterns)};
19
19
  var key = 'stale-deploy-recovery-reloaded';
20
+ var timeKey = 'stale-deploy-recovery-time';
21
+ var throttleMs = 15000;
20
22
  var attemptedThisLoad = false;
21
23
  function isStale(msg) {
22
24
  if (msg === undefined || msg === null) return true;
@@ -34,12 +36,16 @@ export default function HelperScript() {
34
36
  if (!stale) return;
35
37
  var buildId = localStorage.getItem('buildId') || 'unknown';
36
38
  var marker = sessionStorage.getItem(key);
37
- if (marker === buildId) {
39
+ var lastRaw = sessionStorage.getItem(timeKey);
40
+ var last = lastRaw ? Number(lastRaw) : null;
41
+ var throttled = last !== null && (Date.now() - last) < throttleMs;
42
+ if (marker === buildId && throttled) {
38
43
  console.warn('[StaleDeploy early-catch] Skipping reload, already attempted for buildId:', buildId);
39
44
  return;
40
45
  }
41
46
  attemptedThisLoad = true;
42
47
  sessionStorage.setItem(key, buildId);
48
+ sessionStorage.setItem(timeKey, String(Date.now()));
43
49
  try {
44
50
  if (document.documentElement) {
45
51
  document.documentElement.style.backgroundColor = '#ffffff';
@@ -61,6 +67,21 @@ export default function HelperScript() {
61
67
  }
62
68
  }
63
69
  window.addEventListener('error', function(e) { recover(e.message, 'error-event'); });
70
+ // Resource-load failures (a chunk 404ing or served with a
71
+ // disallowed MIME type) fire a non-bubbling 'error' event on the
72
+ // element itself, so they only reach window during capture, and
73
+ // they carry no message. Treat a failed script/link as stale.
74
+ window.addEventListener('error', function(e) {
75
+ try {
76
+ var el = e.target;
77
+ if (!el || el === window) return;
78
+ var tag = (el.tagName || '').toLowerCase();
79
+ if (tag !== 'script' && tag !== 'link') return;
80
+ var src = el.src || el.href || '';
81
+ if (!src) return;
82
+ recover('chunk resource failed to load: ' + src, 'resource-error');
83
+ } catch (err) {}
84
+ }, true);
64
85
  window.addEventListener('unhandledrejection', function(e) {
65
86
  recover(e.reason && (e.reason.message || e.reason), 'unhandledrejection');
66
87
  });
@@ -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, isVinextAppPageRouteWiringSafeOnDisk, 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, isVinextAppPageRouteWiringSafeOnDisk } 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,7 @@ export interface CloudflareNextIntlOptions extends LocaleFilePluginOptions {
15
16
  imageOptimizer?: boolean | ImageOptimizerPluginOptions;
16
17
  vinextRouteWiringFix?: boolean | VinextRouteWiringFixPluginOptions;
17
18
  autoLocaleParams?: boolean | AutoLocaleParamsPluginOptions;
19
+ layoutQueriesCheck?: boolean | LayoutQueriesPluginOptions;
18
20
  experimentalRouteLoadingFixes?: boolean;
19
21
  }
20
22
  export declare function cloudflareNextIntl(options?: CloudflareNextIntlOptions): Plugin[];
@@ -5,16 +5,22 @@ 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
  }
17
- const enableRouteLoadingFixes = options.experimentalRouteLoadingFixes === true;
23
+ const enableRouteLoadingFixes = options.experimentalRouteLoadingFixes !== false;
18
24
  const shouldEnableVinextFix = options.vinextRouteWiringFix !== undefined
19
25
  ? Boolean(options.vinextRouteWiringFix)
20
26
  : enableRouteLoadingFixes;
@@ -1,6 +1,20 @@
1
1
  import type { Plugin } from "vite";
2
- export declare function isAppPageRouteWiringAlreadyFixed(code: string): boolean;
3
- export declare function patchAppPageRouteWiring(code: string): string;
2
+ declare const REQUIRED_SYMBOLS: {
3
+ readonly routeWiring: readonly ["makeThenableParams", "resolveAppPageSegmentParams", "routeLoadingComponent", "ancestorLoadingEntry", "slotParams", "ownerLoadingEntry"];
4
+ readonly routeMatching: readonly ["trieMatch", "getOrBuildTrie", "normalizePathnameForRouteMatch"];
5
+ readonly optimisticRouting: readonly ["getRouteTrie", "matchNode", "decodeMatchedParams", "hrefToRouteParts"];
6
+ readonly prefetchLearning: readonly ["resolveOptimisticNavigationPayload", "__basePath", "optimisticRouteTemplates", "optimisticRouteTemplateSources", "optimisticRouteTemplateLearning", "getOptimisticPrefetchSourceKey", "parsePrefetchCacheKey", "getPrefetchCache", "isSettledPrefetchCacheEntry", "learnOptimisticRouteTemplateFromPrefetch", "currentHref", "rscUrl"];
7
+ readonly suspenseProbe: readonly ["REACT_CLIENT_REFERENCE_TYPE"];
8
+ };
9
+ export type VinextPatchName = keyof typeof REQUIRED_SYMBOLS;
10
+ export declare function hasRequiredSymbols(code: string, patchName: VinextPatchName): boolean;
11
+ export declare function missingRequiredSymbols(code: string, patchName: VinextPatchName): string[];
12
+ export interface AppPageRouteWiringPatchOptions {
13
+ unblockRenderDependencies?: boolean;
14
+ unblockPageElementDependencies?: boolean;
15
+ }
16
+ export declare function isAppPageRouteWiringAlreadyFixed(code: string, options?: AppPageRouteWiringPatchOptions): boolean;
17
+ export declare function patchAppPageRouteWiring(code: string, options?: AppPageRouteWiringPatchOptions): string;
4
18
  export declare function isRouteMatchingFile(id: string): boolean;
5
19
  export declare function isRouteMatchingAlreadyFixed(code: string): boolean;
6
20
  export declare function patchRouteMatching(code: string): string;
@@ -11,6 +25,32 @@ export declare function isPrefetchLearningFile(id: string): boolean;
11
25
  export declare function isPrefetchLearningAlreadyFixed(code: string): boolean;
12
26
  export declare function patchPrefetchLearning(code: string): string;
13
27
  export declare function resolveVinextBrowserEntryPath(root?: string): string | null;
28
+ export declare function isAppPageProbeFile(id: string): boolean;
29
+ export declare function isAppPageProbeAlreadyFixed(code: string): boolean;
30
+ export declare function patchAppPageProbe(code: string): string;
31
+ export declare function resolveVinextAppPageProbePath(root?: string): string | null;
32
+ export declare function isVinextAppPageProbeSafeOnDisk(root?: string): boolean;
33
+ export declare function isRenderDependencyFile(id: string): boolean;
34
+ export declare function isRenderDependencyAlreadyFixed(code: string): boolean;
35
+ export declare function patchRenderDependency(code: string): string;
36
+ export declare function resolveVinextRenderDependencyPath(root?: string): string | null;
37
+ export declare function isVinextRenderDependencySafeOnDisk(root?: string): boolean;
38
+ export declare function isOptimisticLearningTimeoutFile(id: string): boolean;
39
+ export declare function isOptimisticLearningTimeoutAlreadyFixed(code: string): boolean;
40
+ export declare function patchOptimisticLearningTimeout(code: string, timeoutMs?: number): string;
41
+ export declare function resolveVinextOptimisticLearningTimeoutPath(root?: string): string | null;
42
+ export declare function isVinextOptimisticLearningTimeoutSafeOnDisk(root?: string): boolean;
43
+ export declare function isPageInvokerSuspensionReleaseFile(id: string): boolean;
44
+ export declare function isPageInvokerSuspensionReleaseAlreadyFixed(code: string): boolean;
45
+ export declare function patchPageInvokerSuspensionRelease(code: string): string;
46
+ export declare function resolveVinextPageInvokerSuspensionReleasePath(root?: string): string | null;
47
+ export declare function isVinextPageInvokerSuspensionReleaseSafeOnDisk(root?: string): boolean;
48
+ export declare function isRefreshDeferralNavControllerFile(id: string): boolean;
49
+ export declare function isRefreshDeferralNavControllerAlreadyFixed(code: string): boolean;
50
+ export declare function patchRefreshDeferralNavController(code: string): string;
51
+ export declare function isRefreshDeferralEntryAlreadyFixed(code: string): boolean;
52
+ export declare function patchRefreshDeferralEntry(code: string): string;
53
+ export declare function resolveVinextNavControllerPath(root?: string): string | null;
14
54
  export declare function isAppPageRouteWiringFile(id: string): boolean;
15
55
  export declare function resolveVinextAppPageRouteWiringPath(root?: string): string | null;
16
56
  export declare function isVinextAppPageRouteWiringSafeOnDisk(root?: string): boolean;
@@ -18,16 +58,32 @@ export declare function resolveVinextRouteMatchingPath(root?: string): string |
18
58
  export declare function resolveVinextOptimisticRoutingPath(root?: string): string | null;
19
59
  export interface SyncPatchVinextOnDiskOptions {
20
60
  routeWiring?: boolean;
61
+ unblockRenderDependencies?: boolean;
62
+ unblockPageElementDependencies?: boolean;
63
+ refreshDeferral?: boolean;
21
64
  routeMatching?: boolean;
22
65
  optimisticRouting?: boolean;
23
66
  prefetchLearning?: boolean;
67
+ suspenseProbe?: boolean;
68
+ renderDependency?: boolean;
69
+ optimisticLearningTimeout?: boolean | number;
70
+ pageInvokerSuspensionRelease?: boolean;
24
71
  }
25
72
  export declare function syncPatchVinextOnDisk(root?: string, options?: SyncPatchVinextOnDiskOptions): boolean;
26
73
  export declare function bustVinextOptimizeDepsCache(cacheDir: string): boolean;
74
+ export declare function isVinextOptimizeDepsCacheStale(root: string, cacheDir: string): boolean;
27
75
  export interface VinextRouteWiringFixPluginOptions {
28
76
  routeWiring?: boolean;
29
77
  routeMatching?: boolean;
30
78
  optimisticRouting?: boolean;
31
79
  prefetchLearning?: boolean;
80
+ suspenseProbe?: boolean;
81
+ renderDependency?: boolean;
82
+ optimisticLearningTimeout?: boolean | number;
83
+ pageInvokerSuspensionRelease?: boolean;
84
+ unblockRenderDependencies?: boolean;
85
+ unblockPageElementDependencies?: boolean;
86
+ refreshDeferral?: boolean;
32
87
  }
33
88
  export declare function vinextRouteWiringFixPlugin(options?: VinextRouteWiringFixPluginOptions): Plugin;
89
+ export {};
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
1
+ import { existsSync, readFileSync, writeFileSync, rmSync, statSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  const PREFETCH_LOADING_FN_RE = /function\s+getPrefetchLoadingEntry\s*\(\s*route\s*\)\s*\{[\s\S]*?firstNestedEntry[\s\S]*?route\.loadings[\s\S]*?return\s+getDefaultExport\s*\(\s*route\.loading\s*\)\s*\?[\s\S]*?:\s*null\s*;\s*\}/;
4
4
  const FIXED_PREFETCH_LOADING_FN = `function getPrefetchLoadingEntry(route) {
@@ -26,7 +26,7 @@ const FIXED_PREFETCH_LOADING_FN = `function getPrefetchLoadingEntry(route) {
26
26
  if (rootEntry) return rootEntry;
27
27
  return null;
28
28
  }`;
29
- const ROUTE_LOADING_GUARD_RE = /if\s*\(\s*!isPrefetchLoadingShell\s*&&\s*treePosition\s*<\s*routeSegments\.length\s*\)\s*\{/g;
29
+ const ROUTE_LOADING_GUARD_RE = /if\s*\(\s*!isPrefetchLoadingShell\s*&&\s*treePosition\s*<\s*routeSegments\.length\s*\)\s*\{/;
30
30
  const FIXED_ROUTE_LOADING_GUARD = "if (!isPrefetchLoadingShell && treePosition < routeSegments.length && !routeLoadingComponent) {";
31
31
  const PAGE_LOADING_FALLBACK_RE = /fallback:\s*\/\*\s*@__PURE__\s*\*\/\s*jsx\s*\(\s*PageLoadingComponent\s*,\s*\{\s*\}\s*\)/;
32
32
  const FIXED_PAGE_LOADING_FALLBACK = "fallback: /* @__PURE__ */ jsx(PageLoadingComponent, { params: options.makeThenableParams(options.matchedParams) })";
@@ -44,7 +44,45 @@ const SEGMENT_LOADING_FALLBACK_RE = /fallback:\s*\/\*\s*@__PURE__\s*\*\/\s*jsx\s
44
44
  const FIXED_SEGMENT_LOADING_FALLBACK = "fallback: /* @__PURE__ */ jsx(segmentLoadingComponent, { params: options.makeThenableParams(resolveAppPageSegmentParams(options.route.routeSegments, treePosition, options.matchedParams)) })";
45
45
  const PREFETCH_SLOT_LOADING_CALL_RE = /slotElement\s*=\s*\/\*\s*@__PURE__\s*\*\/\s*jsx\s*\(\s*getDefaultExport\s*\(\s*prefetchSlotLoadingEntry\.loadingModule\s*\)\s*,\s*\{\s*\}\s*\)/;
46
46
  const FIXED_PREFETCH_SLOT_LOADING_CALL = "slotElement = /* @__PURE__ */ jsx(getDefaultExport(prefetchSlotLoadingEntry.loadingModule), { params: options.makeThenableParams(slotParams) })";
47
- export function isAppPageRouteWiringAlreadyFixed(code) {
47
+ const PAGE_RESULT_DEPS_RE = /pageRenderDependency\?\.setResultDependencies\(\s*pageDependencies\s*\);/;
48
+ const PAGE_ELEMENT_BLOCKING_DEP_RE = /elements\[pageElementId\]\s*=\s*isPrefetchLoadingShell\s*\?\s*null\s*:\s*pageRenderDependency\s*\?\s*pageElement\s*:\s*renderAfterAppDependencies\s*\(\s*pageElement\s*,\s*pageDependencies\s*\);/;
49
+ const REQUIRED_SYMBOLS = {
50
+ routeWiring: [
51
+ "makeThenableParams",
52
+ "resolveAppPageSegmentParams",
53
+ "routeLoadingComponent",
54
+ "ancestorLoadingEntry",
55
+ "slotParams",
56
+ "ownerLoadingEntry",
57
+ ],
58
+ routeMatching: ["trieMatch", "getOrBuildTrie", "normalizePathnameForRouteMatch"],
59
+ optimisticRouting: ["getRouteTrie", "matchNode", "decodeMatchedParams", "hrefToRouteParts"],
60
+ prefetchLearning: [
61
+ "resolveOptimisticNavigationPayload",
62
+ "__basePath",
63
+ "optimisticRouteTemplates",
64
+ "optimisticRouteTemplateSources",
65
+ "optimisticRouteTemplateLearning",
66
+ "getOptimisticPrefetchSourceKey",
67
+ "parsePrefetchCacheKey",
68
+ "getPrefetchCache",
69
+ "isSettledPrefetchCacheEntry",
70
+ "learnOptimisticRouteTemplateFromPrefetch",
71
+ "currentHref",
72
+ "rscUrl",
73
+ ],
74
+ suspenseProbe: ["REACT_CLIENT_REFERENCE_TYPE"],
75
+ };
76
+ export function hasRequiredSymbols(code, patchName) {
77
+ return REQUIRED_SYMBOLS[patchName].every((symbol) => code.includes(symbol));
78
+ }
79
+ export function missingRequiredSymbols(code, patchName) {
80
+ return REQUIRED_SYMBOLS[patchName].filter((symbol) => !code.includes(symbol));
81
+ }
82
+ function warnIncompatible(filePath, patchName, code) {
83
+ console.warn(`[cfni:vinext-route-wiring-fix] ${filePath} no longer exposes ${missingRequiredSymbols(code, patchName).join(", ")} — this vinext version may have changed; the ${patchName} fix was NOT applied.`);
84
+ }
85
+ export function isAppPageRouteWiringAlreadyFixed(code, options = {}) {
48
86
  const hasBuggyPrefetch = code.includes("firstNestedEntry") &&
49
87
  PREFETCH_LOADING_FN_RE.test(code);
50
88
  const hasBuggySuspense = !code.includes("!routeLoadingComponent") && ROUTE_LOADING_GUARD_RE.test(code);
@@ -56,10 +94,14 @@ export function isAppPageRouteWiringAlreadyFixed(code) {
56
94
  ROUTE_LOADING_FALLBACK_RE.test(code) ||
57
95
  SEGMENT_LOADING_FALLBACK_RE.test(code) ||
58
96
  PREFETCH_SLOT_LOADING_CALL_RE.test(code);
59
- return !hasBuggyPrefetch && !hasBuggySuspense && !hasEmptyLoadingProps;
97
+ const hasBlockingLayoutDependencies = options.unblockRenderDependencies === true &&
98
+ code.includes("elements[layoutEntry.id] = renderAfterAppDependencies(layoutElement, [...pageRenderDependency");
99
+ const hasBlockingPageDeps = options.unblockPageElementDependencies === true &&
100
+ (PAGE_RESULT_DEPS_RE.test(code) || PAGE_ELEMENT_BLOCKING_DEP_RE.test(code));
101
+ return !hasBuggyPrefetch && !hasBuggySuspense && !hasEmptyLoadingProps && !hasBlockingLayoutDependencies && !hasBlockingPageDeps;
60
102
  }
61
- export function patchAppPageRouteWiring(code) {
62
- if (isAppPageRouteWiringAlreadyFixed(code)) {
103
+ export function patchAppPageRouteWiring(code, options = {}) {
104
+ if (isAppPageRouteWiringAlreadyFixed(code, options)) {
63
105
  return code;
64
106
  }
65
107
  let result = code;
@@ -97,6 +139,30 @@ export function patchAppPageRouteWiring(code) {
97
139
  if (PREFETCH_SLOT_LOADING_CALL_RE.test(result)) {
98
140
  result = result.replace(PREFETCH_SLOT_LOADING_CALL_RE, FIXED_PREFETCH_SLOT_LOADING_CALL);
99
141
  }
142
+ if (options.unblockRenderDependencies === true) {
143
+ const LAYOUT_DEP_RE = /elements\[layoutEntry\.id\]\s*=\s*renderAfterAppDependencies\s*\(\s*layoutElement\s*,\s*\[\s*\.\.\.pageRenderDependency\s*\?\s*\[pageRenderDependency\]\s*:\s*\[\]\s*,\s*\.\.\.layoutDependenciesBefore\[index\]\s*\?\?\s*\[\]\s*\]\s*\);/;
144
+ if (LAYOUT_DEP_RE.test(result)) {
145
+ result = result.replace(LAYOUT_DEP_RE, "elements[layoutEntry.id] = renderAfterAppDependencies(layoutElement, layoutDependenciesBefore[index] ?? []);");
146
+ }
147
+ const TEMPLATE_DEP_RE = /elements\[templateEntry\.id\]\s*=\s*renderAfterAppDependencies\s*\(\s*templateElement\s*,\s*\[\s*\.\.\.pageRenderDependency\s*\?\s*\[pageRenderDependency\]\s*:\s*\[\]\s*,\s*\.\.\.templateDependenciesBeforeById\.get\(templateEntry\.id\)\s*\?\?\s*\[\]\s*\]\s*\);/;
148
+ if (TEMPLATE_DEP_RE.test(result)) {
149
+ result = result.replace(TEMPLATE_DEP_RE, "elements[templateEntry.id] = renderAfterAppDependencies(templateElement, templateDependenciesBeforeById.get(templateEntry.id) ?? []);");
150
+ }
151
+ const SLOT_DEP_RE = /elements\[slotId\]\s*=\s*renderAfterAppDependencies\s*\(\s*slotElement\s*,\s*\[\s*\.\.\.pageRenderDependency\s*\?\s*\[pageRenderDependency\]\s*:\s*\[\]\s*,\s*\.\.\.targetIndex\s*>=\s*0\s*\?\s*slotDependenciesByLayoutIndex\[targetIndex\]\s*\?\?\s*\[\]\s*:\s*\[\]\s*\]\s*\);/;
152
+ if (SLOT_DEP_RE.test(result)) {
153
+ result = result.replace(SLOT_DEP_RE, "elements[slotId] = renderAfterAppDependencies(slotElement, targetIndex >= 0 ? slotDependenciesByLayoutIndex[targetIndex] ?? [] : []);");
154
+ }
155
+ const ROUTE_DEP_RE = /elements\[routeId\]\s*=\s*pageRenderDependency\s*\?\s*renderAfterAppDependencies\s*\(\s*routeElement\s*,\s*\[pageRenderDependency\]\s*\)\s*:\s*routeElement;/;
156
+ if (ROUTE_DEP_RE.test(result)) {
157
+ result = result.replace(ROUTE_DEP_RE, "elements[routeId] = routeElement;");
158
+ }
159
+ if (options.unblockPageElementDependencies === true && PAGE_RESULT_DEPS_RE.test(result)) {
160
+ result = result.replace(PAGE_RESULT_DEPS_RE, "pageRenderDependency?.setResultDependencies([]);");
161
+ }
162
+ if (options.unblockPageElementDependencies === true && PAGE_ELEMENT_BLOCKING_DEP_RE.test(result)) {
163
+ result = result.replace(PAGE_ELEMENT_BLOCKING_DEP_RE, "elements[pageElementId] = isPrefetchLoadingShell ? null : pageElement;");
164
+ }
165
+ }
100
166
  return result;
101
167
  }
102
168
  export function isRouteMatchingFile(id) {
@@ -153,13 +219,13 @@ export function patchRouteMatching(code) {
153
219
  return code;
154
220
  }
155
221
  let result = code;
156
- if (!result.includes("hasLeadingLocaleParam") && MATCH_ROUTE_WITH_TRIE_RE.test(result)) {
222
+ if (MATCH_ROUTE_WITH_TRIE_RE.test(result)) {
157
223
  const replacement = result.includes("function getActiveRouteLocale")
158
224
  ? FIXED_MATCH_ROUTE_WITH_TRIE_BODY
159
225
  : `${GET_ACTIVE_ROUTE_LOCALE_FN}${FIXED_MATCH_ROUTE_WITH_TRIE_BODY}`;
160
226
  result = result.replace(MATCH_ROUTE_WITH_TRIE_RE, replacement);
161
227
  }
162
- if (!result.includes("hasLeadingLocaleParam") && MATCH_ROUTE_WITH_TRIE_RAW_RE.test(result)) {
228
+ if (MATCH_ROUTE_WITH_TRIE_RAW_RE.test(result)) {
163
229
  const replacement = result.includes("function getActiveRouteLocale")
164
230
  ? FIXED_MATCH_ROUTE_WITH_TRIE_RAW
165
231
  : `${GET_ACTIVE_ROUTE_LOCALE_FN}${FIXED_MATCH_ROUTE_WITH_TRIE_RAW}`;
@@ -336,6 +402,169 @@ export function resolveVinextBrowserEntryPath(root = process.cwd()) {
336
402
  const directPath = resolve(root, "node_modules/vinext/dist/server/app-browser-entry.js");
337
403
  return existsSync(directPath) ? directPath : null;
338
404
  }
405
+ export function isAppPageProbeFile(id) {
406
+ const cleanId = id.split("?")[0].replace(/\\/g, "/");
407
+ return cleanId.endsWith("/app-page-probe.js") || cleanId.endsWith("/app-page-probe.ts");
408
+ }
409
+ export function isAppPageProbeAlreadyFixed(code) {
410
+ return code.includes("react.suspense") && code.includes("REACT_SUSPENSE_TYPE");
411
+ }
412
+ const REACT_SUSPENSE_DECL_RE = /const\s+REACT_CLIENT_REFERENCE_TYPE\s*=\s*Symbol\.for\(["']react\.client\.reference["']\);/;
413
+ const PROBE_VISIT_FRAGMENT_RE = /if\s*\(\s*value\.type\s*===\s*Fragment\s*\|\|\s*typeof\s+value\.type\s*===\s*["']string["']\s*\)\s*\{/;
414
+ export function patchAppPageProbe(code) {
415
+ if (isAppPageProbeAlreadyFixed(code)) {
416
+ return code;
417
+ }
418
+ let result = code;
419
+ if (!result.includes("REACT_SUSPENSE_TYPE") && REACT_SUSPENSE_DECL_RE.test(result)) {
420
+ result = result.replace(REACT_SUSPENSE_DECL_RE, 'const REACT_CLIENT_REFERENCE_TYPE = Symbol.for("react.client.reference");\nconst REACT_SUSPENSE_TYPE = Symbol.for("react.suspense");');
421
+ }
422
+ if (result.includes("REACT_SUSPENSE_TYPE") && PROBE_VISIT_FRAGMENT_RE.test(result)) {
423
+ result = result.replace(PROBE_VISIT_FRAGMENT_RE, `if (value.type === Symbol.for("react.suspense") || value.type === REACT_SUSPENSE_TYPE) {\n\t\t\tif (value.props && "fallback" in value.props) await visit(value.props.fallback, depth + 1);\n\t\t\treturn;\n\t\t}\n\t\tif (value.type === Fragment || typeof value.type === "string") {`);
424
+ }
425
+ return result;
426
+ }
427
+ export function resolveVinextAppPageProbePath(root = process.cwd()) {
428
+ const directPath = resolve(root, "node_modules/vinext/dist/server/app-page-probe.js");
429
+ return existsSync(directPath) ? directPath : null;
430
+ }
431
+ export function isVinextAppPageProbeSafeOnDisk(root = process.cwd()) {
432
+ const filePath = resolveVinextAppPageProbePath(root);
433
+ if (!filePath)
434
+ return false;
435
+ try {
436
+ const content = readFileSync(filePath, "utf8");
437
+ return isAppPageProbeAlreadyFixed(content);
438
+ }
439
+ catch {
440
+ return false;
441
+ }
442
+ }
443
+ const SUSPENSION_GUARDED_RELEASE_RE = /if\s*\(\s*!isAppRenderSuspension\(\s*error\s*\)\s*\)\s*dependency\.release\(\);/;
444
+ export function isRenderDependencyFile(id) {
445
+ const cleanId = id.split("?")[0].replace(/\\/g, "/");
446
+ return cleanId.endsWith("/app-render-dependency.js") || cleanId.endsWith("/app-render-dependency.tsx") || cleanId.endsWith("/app-render-dependency.ts");
447
+ }
448
+ export function isRenderDependencyAlreadyFixed(code) {
449
+ if (!code.includes("renderAppComponentWithDependencyBarrier"))
450
+ return true;
451
+ return !SUSPENSION_GUARDED_RELEASE_RE.test(code);
452
+ }
453
+ export function patchRenderDependency(code) {
454
+ if (!SUSPENSION_GUARDED_RELEASE_RE.test(code))
455
+ return code;
456
+ return code.replace(SUSPENSION_GUARDED_RELEASE_RE, "dependency.release();");
457
+ }
458
+ export function resolveVinextRenderDependencyPath(root = process.cwd()) {
459
+ const directPath = resolve(root, "node_modules/vinext/dist/server/app-render-dependency.js");
460
+ return existsSync(directPath) ? directPath : null;
461
+ }
462
+ export function isVinextRenderDependencySafeOnDisk(root = process.cwd()) {
463
+ const filePath = resolveVinextRenderDependencyPath(root);
464
+ if (!filePath)
465
+ return false;
466
+ try {
467
+ return isRenderDependencyAlreadyFixed(readFileSync(filePath, "utf8"));
468
+ }
469
+ catch {
470
+ return false;
471
+ }
472
+ }
473
+ const OPTIMISTIC_LEARNING_TIMEOUT_MS = 200;
474
+ const OPTIMISTIC_LEARNING_BLOCKING_TIMEOUT_RE = /(new Promise\(\(resolve\)\s*=>\s*setTimeout\(resolve,\s*)3000(\)\))/;
475
+ export function isOptimisticLearningTimeoutFile(id) {
476
+ const cleanId = id.split("?")[0].replace(/\\/g, "/");
477
+ return cleanId.endsWith("/app-browser-entry.js") || cleanId.endsWith("/app-browser-entry.ts") || cleanId.endsWith("/app-browser-entry.tsx");
478
+ }
479
+ export function isOptimisticLearningTimeoutAlreadyFixed(code) {
480
+ return !OPTIMISTIC_LEARNING_BLOCKING_TIMEOUT_RE.test(code);
481
+ }
482
+ export function patchOptimisticLearningTimeout(code, timeoutMs = OPTIMISTIC_LEARNING_TIMEOUT_MS) {
483
+ if (!OPTIMISTIC_LEARNING_BLOCKING_TIMEOUT_RE.test(code))
484
+ return code;
485
+ return code.replace(OPTIMISTIC_LEARNING_BLOCKING_TIMEOUT_RE, `$1${timeoutMs}$2`);
486
+ }
487
+ export function resolveVinextOptimisticLearningTimeoutPath(root = process.cwd()) {
488
+ const directPath = resolve(root, "node_modules/vinext/dist/server/app-browser-entry.js");
489
+ return existsSync(directPath) ? directPath : null;
490
+ }
491
+ export function isVinextOptimisticLearningTimeoutSafeOnDisk(root = process.cwd()) {
492
+ const filePath = resolveVinextOptimisticLearningTimeoutPath(root);
493
+ if (!filePath)
494
+ return false;
495
+ try {
496
+ return isOptimisticLearningTimeoutAlreadyFixed(readFileSync(filePath, "utf8"));
497
+ }
498
+ catch {
499
+ return false;
500
+ }
501
+ }
502
+ const PAGE_INVOKER_GUARDED_RELEASE_RE = /if\s*\(renderDependency\s*&&\s*hasPageLoadingBoundary\)\s*Promise\.resolve\(\)\.then\(\(\)\s*=>\s*renderDependency\.release\(\)\);/;
503
+ export function isPageInvokerSuspensionReleaseFile(id) {
504
+ const cleanId = id.split("?")[0].replace(/\\/g, "/");
505
+ return cleanId.endsWith("/app-page-element-builder.js") || cleanId.endsWith("/app-page-element-builder.ts") || cleanId.endsWith("/app-page-element-builder.tsx");
506
+ }
507
+ export function isPageInvokerSuspensionReleaseAlreadyFixed(code) {
508
+ return !PAGE_INVOKER_GUARDED_RELEASE_RE.test(code);
509
+ }
510
+ export function patchPageInvokerSuspensionRelease(code) {
511
+ if (!PAGE_INVOKER_GUARDED_RELEASE_RE.test(code))
512
+ return code;
513
+ return code.replace(PAGE_INVOKER_GUARDED_RELEASE_RE, "if (renderDependency) Promise.resolve().then(() => renderDependency.release());");
514
+ }
515
+ export function resolveVinextPageInvokerSuspensionReleasePath(root = process.cwd()) {
516
+ const directPath = resolve(root, "node_modules/vinext/dist/server/app-page-element-builder.js");
517
+ return existsSync(directPath) ? directPath : null;
518
+ }
519
+ export function isVinextPageInvokerSuspensionReleaseSafeOnDisk(root = process.cwd()) {
520
+ const filePath = resolveVinextPageInvokerSuspensionReleasePath(root);
521
+ if (!filePath)
522
+ return false;
523
+ try {
524
+ return isPageInvokerSuspensionReleaseAlreadyFixed(readFileSync(filePath, "utf8"));
525
+ }
526
+ catch {
527
+ return false;
528
+ }
529
+ }
530
+ const NAV_CONTROLLER_BEGIN_NAVIGATION_RE = /function\s+beginNavigation\(\s*\)\s*\{(\n([\t ]*))latestHmrUpdateId\s*\+=\s*1;/;
531
+ const NAV_CONTROLLER_STATE_DECL_RE = /(\n\s*let\s+latestHmrUpdateId\s*=\s*0;)/;
532
+ const NAV_CONTROLLER_RETURN_RE = /(\n\s*return\s*\{\s*\n\s*beginNavigation,)/;
533
+ const NAV_ENTRY_NAVIGATE_FN_RE = /(navigate:\s*async function navigateRsc\([^)]*\)\s*\{)(\n([\t ]*))/;
534
+ const NAV_ENTRY_BEGIN_NAVIGATION_RE = /(\n[\t ]*const navId = browserNavigationController\.)beginNavigation\(\);/;
535
+ const REFRESH_DEFERRAL_WINDOW_MS = 600;
536
+ export function isRefreshDeferralNavControllerFile(id) {
537
+ const cleanId = id.split("?")[0].replace(/\\/g, "/");
538
+ return cleanId.endsWith("/app-browser-navigation-controller.js") || cleanId.endsWith("/app-browser-navigation-controller.ts");
539
+ }
540
+ export function isRefreshDeferralNavControllerAlreadyFixed(code) {
541
+ return code.includes("isRecentNonRefreshNavigationInFlight") || !NAV_CONTROLLER_BEGIN_NAVIGATION_RE.test(code);
542
+ }
543
+ export function patchRefreshDeferralNavController(code) {
544
+ if (isRefreshDeferralNavControllerAlreadyFixed(code))
545
+ return code;
546
+ if (!NAV_CONTROLLER_STATE_DECL_RE.test(code) || !NAV_CONTROLLER_RETURN_RE.test(code))
547
+ return code;
548
+ let result = code.replace(NAV_CONTROLLER_STATE_DECL_RE, "\n\tlet lastNonRefreshNavigationStartedAt = 0;$1");
549
+ result = result.replace(NAV_CONTROLLER_BEGIN_NAVIGATION_RE, (_m, _nl, indent) => `function beginNavigation(kind) {\n${indent}latestHmrUpdateId += 1;\n${indent}if (kind !== "refresh") lastNonRefreshNavigationStartedAt = Date.now();`);
550
+ result = result.replace(/(\n\s*function getActiveNavigationId\(\) \{)/, `\n\tconst REFRESH_DEFERRAL_WINDOW_MS = ${REFRESH_DEFERRAL_WINDOW_MS};\n\tfunction isRecentNonRefreshNavigationInFlight() {\n\t\treturn lastNonRefreshNavigationStartedAt !== 0 && Date.now() - lastNonRefreshNavigationStartedAt < REFRESH_DEFERRAL_WINDOW_MS;\n\t}$1`);
551
+ result = result.replace(NAV_CONTROLLER_RETURN_RE, "$1\n\t\tisRecentNonRefreshNavigationInFlight,");
552
+ return result;
553
+ }
554
+ export function isRefreshDeferralEntryAlreadyFixed(code) {
555
+ return code.includes("isRecentNonRefreshNavigationInFlight()") || !NAV_ENTRY_NAVIGATE_FN_RE.test(code);
556
+ }
557
+ export function patchRefreshDeferralEntry(code) {
558
+ if (isRefreshDeferralEntryAlreadyFixed(code))
559
+ return code;
560
+ let result = code.replace(NAV_ENTRY_NAVIGATE_FN_RE, (_m, signature, _nl, indent) => `${signature}\n${indent}if (navigationKind === "refresh") {\n${indent}\twhile (browserNavigationController.isRecentNonRefreshNavigationInFlight()) await new Promise((resolve) => setTimeout(resolve, 50));\n${indent}}\n${indent}`);
561
+ result = result.replace(NAV_ENTRY_BEGIN_NAVIGATION_RE, "$1beginNavigation(navigationKind);");
562
+ return result;
563
+ }
564
+ export function resolveVinextNavControllerPath(root = process.cwd()) {
565
+ const directPath = resolve(root, "node_modules/vinext/dist/server/app-browser-navigation-controller.js");
566
+ return existsSync(directPath) ? directPath : null;
567
+ }
339
568
  export function isAppPageRouteWiringFile(id) {
340
569
  const cleanId = id.split("?")[0].replace(/\\/g, "/");
341
570
  return cleanId.endsWith("/app-page-route-wiring.js") || cleanId.endsWith("/app-page-route-wiring.tsx") || cleanId.endsWith("/app-page-route-wiring.ts");
@@ -365,14 +594,18 @@ export function resolveVinextOptimisticRoutingPath(root = process.cwd()) {
365
594
  return existsSync(directPath) ? directPath : null;
366
595
  }
367
596
  export function syncPatchVinextOnDisk(root = process.cwd(), options = {}) {
368
- const { routeWiring = true, routeMatching = true, optimisticRouting = true, prefetchLearning = true } = options;
597
+ const { routeWiring = true, routeMatching = true, optimisticRouting = true, prefetchLearning = true, suspenseProbe = true, renderDependency = true, optimisticLearningTimeout = true, pageInvokerSuspensionRelease = true, unblockRenderDependencies = true, unblockPageElementDependencies = false, refreshDeferral = true } = options;
598
+ const optimisticLearningTimeoutMs = typeof optimisticLearningTimeout === "number" ? optimisticLearningTimeout : undefined;
369
599
  let changed = false;
370
600
  const wiringPath = routeWiring ? resolveVinextAppPageRouteWiringPath(root) : null;
371
601
  if (wiringPath) {
372
602
  try {
373
603
  const content = readFileSync(wiringPath, "utf8");
374
- if (!isAppPageRouteWiringAlreadyFixed(content)) {
375
- const patched = patchAppPageRouteWiring(content);
604
+ if (!isAppPageRouteWiringAlreadyFixed(content, { unblockRenderDependencies, unblockPageElementDependencies }) && !hasRequiredSymbols(content, "routeWiring")) {
605
+ warnIncompatible(wiringPath, "routeWiring", content);
606
+ }
607
+ else if (!isAppPageRouteWiringAlreadyFixed(content, { unblockRenderDependencies, unblockPageElementDependencies })) {
608
+ const patched = patchAppPageRouteWiring(content, { unblockRenderDependencies, unblockPageElementDependencies });
376
609
  if (patched !== content) {
377
610
  writeFileSync(wiringPath, patched, "utf8");
378
611
  changed = true;
@@ -389,7 +622,10 @@ export function syncPatchVinextOnDisk(root = process.cwd(), options = {}) {
389
622
  if (matchingPath) {
390
623
  try {
391
624
  const content = readFileSync(matchingPath, "utf8");
392
- if (!isRouteMatchingAlreadyFixed(content)) {
625
+ if (!isRouteMatchingAlreadyFixed(content) && !hasRequiredSymbols(content, "routeMatching")) {
626
+ warnIncompatible(matchingPath, "routeMatching", content);
627
+ }
628
+ else if (!isRouteMatchingAlreadyFixed(content)) {
393
629
  const patched = patchRouteMatching(content);
394
630
  if (patched !== content) {
395
631
  writeFileSync(matchingPath, patched, "utf8");
@@ -407,7 +643,10 @@ export function syncPatchVinextOnDisk(root = process.cwd(), options = {}) {
407
643
  if (optimisticPath) {
408
644
  try {
409
645
  const content = readFileSync(optimisticPath, "utf8");
410
- if (!isOptimisticRoutingAlreadyFixed(content)) {
646
+ if (!isOptimisticRoutingAlreadyFixed(content) && !hasRequiredSymbols(content, "optimisticRouting")) {
647
+ warnIncompatible(optimisticPath, "optimisticRouting", content);
648
+ }
649
+ else if (!isOptimisticRoutingAlreadyFixed(content)) {
411
650
  const patched = patchOptimisticRouting(content);
412
651
  if (patched !== content) {
413
652
  writeFileSync(optimisticPath, patched, "utf8");
@@ -425,7 +664,10 @@ export function syncPatchVinextOnDisk(root = process.cwd(), options = {}) {
425
664
  if (browserEntryPath) {
426
665
  try {
427
666
  const content = readFileSync(browserEntryPath, "utf8");
428
- if (!isPrefetchLearningAlreadyFixed(content)) {
667
+ if (!isPrefetchLearningAlreadyFixed(content) && !hasRequiredSymbols(content, "prefetchLearning")) {
668
+ warnIncompatible(browserEntryPath, "prefetchLearning", content);
669
+ }
670
+ else if (!isPrefetchLearningAlreadyFixed(content)) {
429
671
  const patched = patchPrefetchLearning(content);
430
672
  if (patched !== content) {
431
673
  writeFileSync(browserEntryPath, patched, "utf8");
@@ -439,6 +681,111 @@ export function syncPatchVinextOnDisk(root = process.cwd(), options = {}) {
439
681
  catch {
440
682
  }
441
683
  }
684
+ const probePath = suspenseProbe ? resolveVinextAppPageProbePath(root) : null;
685
+ if (probePath) {
686
+ try {
687
+ const content = readFileSync(probePath, "utf8");
688
+ if (!isAppPageProbeAlreadyFixed(content) && !hasRequiredSymbols(content, "suspenseProbe")) {
689
+ warnIncompatible(probePath, "suspenseProbe", content);
690
+ }
691
+ else if (!isAppPageProbeAlreadyFixed(content)) {
692
+ const patched = patchAppPageProbe(content);
693
+ if (patched !== content) {
694
+ writeFileSync(probePath, patched, "utf8");
695
+ changed = true;
696
+ }
697
+ else {
698
+ console.warn(`[cfni:vinext-route-wiring-fix] ${probePath} does not match the expected shape for patchAppPageProbe — this vinext version may have changed; the suspense-probe fix was NOT applied.`);
699
+ }
700
+ }
701
+ }
702
+ catch {
703
+ }
704
+ }
705
+ const renderDependencyPath = renderDependency ? resolveVinextRenderDependencyPath(root) : null;
706
+ if (renderDependencyPath) {
707
+ try {
708
+ const content = readFileSync(renderDependencyPath, "utf8");
709
+ if (!isRenderDependencyAlreadyFixed(content)) {
710
+ const patched = patchRenderDependency(content);
711
+ if (patched !== content) {
712
+ writeFileSync(renderDependencyPath, patched, "utf8");
713
+ changed = true;
714
+ }
715
+ else {
716
+ console.warn(`[cfni:vinext-route-wiring-fix] ${renderDependencyPath} does not match the expected shape for patchRenderDependency — this vinext version may have changed; the render-dependency fix was NOT applied.`);
717
+ }
718
+ }
719
+ }
720
+ catch {
721
+ }
722
+ }
723
+ const optimisticLearningTimeoutEnabled = optimisticLearningTimeout !== false && optimisticLearningTimeout !== undefined;
724
+ const optimisticLearningTimeoutPath = optimisticLearningTimeoutEnabled ? resolveVinextOptimisticLearningTimeoutPath(root) : null;
725
+ if (optimisticLearningTimeoutPath) {
726
+ try {
727
+ const content = readFileSync(optimisticLearningTimeoutPath, "utf8");
728
+ if (!isOptimisticLearningTimeoutAlreadyFixed(content)) {
729
+ const patched = patchOptimisticLearningTimeout(content, optimisticLearningTimeoutMs);
730
+ if (patched !== content) {
731
+ writeFileSync(optimisticLearningTimeoutPath, patched, "utf8");
732
+ changed = true;
733
+ }
734
+ else {
735
+ console.warn(`[cfni:vinext-route-wiring-fix] ${optimisticLearningTimeoutPath} does not match the expected shape for patchOptimisticLearningTimeout — this vinext version may have changed; the optimistic-learning-timeout fix was NOT applied.`);
736
+ }
737
+ }
738
+ }
739
+ catch {
740
+ }
741
+ }
742
+ const pageInvokerSuspensionReleasePath = pageInvokerSuspensionRelease ? resolveVinextPageInvokerSuspensionReleasePath(root) : null;
743
+ if (pageInvokerSuspensionReleasePath) {
744
+ try {
745
+ const content = readFileSync(pageInvokerSuspensionReleasePath, "utf8");
746
+ if (!isPageInvokerSuspensionReleaseAlreadyFixed(content)) {
747
+ const patched = patchPageInvokerSuspensionRelease(content);
748
+ if (patched !== content) {
749
+ writeFileSync(pageInvokerSuspensionReleasePath, patched, "utf8");
750
+ changed = true;
751
+ }
752
+ else {
753
+ console.warn(`[cfni:vinext-route-wiring-fix] ${pageInvokerSuspensionReleasePath} does not match the expected shape for patchPageInvokerSuspensionRelease — this vinext version may have changed; the page-invoker-suspension-release fix was NOT applied.`);
754
+ }
755
+ }
756
+ }
757
+ catch {
758
+ }
759
+ }
760
+ const navControllerPath = refreshDeferral ? resolveVinextNavControllerPath(root) : null;
761
+ if (navControllerPath) {
762
+ try {
763
+ const content = readFileSync(navControllerPath, "utf8");
764
+ if (!isRefreshDeferralNavControllerAlreadyFixed(content)) {
765
+ const patched = patchRefreshDeferralNavController(content);
766
+ if (patched !== content) {
767
+ writeFileSync(navControllerPath, patched, "utf8");
768
+ changed = true;
769
+ }
770
+ else {
771
+ console.warn(`[cfni:vinext-route-wiring-fix] ${navControllerPath} does not match the expected shape for patchRefreshDeferralNavController — this vinext version may have changed; the refresh-deferral fix was NOT applied.`);
772
+ }
773
+ }
774
+ const entryPath = resolveVinextBrowserEntryPath(root);
775
+ if (entryPath && isRefreshDeferralNavControllerAlreadyFixed(readFileSync(navControllerPath, "utf8"))) {
776
+ const entryContent = readFileSync(entryPath, "utf8");
777
+ if (!isRefreshDeferralEntryAlreadyFixed(entryContent)) {
778
+ const patchedEntry = patchRefreshDeferralEntry(entryContent);
779
+ if (patchedEntry !== entryContent) {
780
+ writeFileSync(entryPath, patchedEntry, "utf8");
781
+ changed = true;
782
+ }
783
+ }
784
+ }
785
+ }
786
+ catch {
787
+ }
788
+ }
442
789
  return changed;
443
790
  }
444
791
  export function bustVinextOptimizeDepsCache(cacheDir) {
@@ -456,20 +803,61 @@ export function bustVinextOptimizeDepsCache(cacheDir) {
456
803
  }
457
804
  return removed;
458
805
  }
806
+ export function isVinextOptimizeDepsCacheStale(root, cacheDir) {
807
+ const patchedFiles = [
808
+ resolveVinextAppPageRouteWiringPath(root),
809
+ resolveVinextRouteMatchingPath(root),
810
+ resolveVinextOptimisticRoutingPath(root),
811
+ resolveVinextBrowserEntryPath(root),
812
+ resolveVinextAppPageProbePath(root),
813
+ resolveVinextRenderDependencyPath(root),
814
+ resolveVinextPageInvokerSuspensionReleasePath(root),
815
+ ].filter((filePath) => filePath !== null);
816
+ let newestPatch = 0;
817
+ for (const filePath of patchedFiles) {
818
+ try {
819
+ newestPatch = Math.max(newestPatch, statSync(filePath).mtimeMs);
820
+ }
821
+ catch {
822
+ }
823
+ }
824
+ if (newestPatch === 0)
825
+ return false;
826
+ for (const sub of ["deps", "deps_ssr", "deps_rsc"]) {
827
+ const dir = resolve(cacheDir, sub);
828
+ if (!existsSync(dir))
829
+ continue;
830
+ try {
831
+ if (statSync(dir).mtimeMs < newestPatch)
832
+ return true;
833
+ }
834
+ catch {
835
+ }
836
+ }
837
+ return false;
838
+ }
459
839
  export function vinextRouteWiringFixPlugin(options = {}) {
460
840
  console.warn("[cloudflare-next-intl] WARNING: vinextRouteWiringFix is enabled. Monkey-patching vinext on disk is dangerous and can break routing or upstream compatibility.");
461
841
  const routeWiring = options.routeWiring !== false;
462
842
  const routeMatching = options.routeMatching !== false;
463
843
  const optimisticRouting = options.optimisticRouting !== false;
464
844
  const prefetchLearning = options.prefetchLearning !== false;
845
+ const suspenseProbe = options.suspenseProbe !== false;
846
+ const renderDependency = options.renderDependency !== false;
847
+ const optimisticLearningTimeout = options.optimisticLearningTimeout !== false;
848
+ const optimisticLearningTimeoutMs = typeof options.optimisticLearningTimeout === "number" ? options.optimisticLearningTimeout : undefined;
849
+ const pageInvokerSuspensionRelease = options.pageInvokerSuspensionRelease !== false;
850
+ const unblockRenderDependencies = options.unblockRenderDependencies !== false;
851
+ const unblockPageElementDependencies = options.unblockPageElementDependencies === true;
852
+ const refreshDeferral = options.refreshDeferral !== false;
465
853
  return {
466
854
  name: "cfni:vinext-route-wiring-fix",
467
855
  enforce: "pre",
468
856
  configResolved(config) {
469
857
  const root = config.root || process.cwd();
470
- const changed = syncPatchVinextOnDisk(root, { routeWiring, routeMatching, optimisticRouting, prefetchLearning });
471
- if (changed) {
472
- const cacheDir = config.cacheDir || resolve(root, "node_modules/.vite");
858
+ const changed = syncPatchVinextOnDisk(root, { routeWiring, routeMatching, optimisticRouting, prefetchLearning, suspenseProbe, renderDependency, optimisticLearningTimeout: options.optimisticLearningTimeout ?? true, pageInvokerSuspensionRelease, unblockRenderDependencies, unblockPageElementDependencies, refreshDeferral });
859
+ const cacheDir = config.cacheDir || resolve(root, "node_modules/.vite");
860
+ if (changed || isVinextOptimizeDepsCacheStale(root, cacheDir)) {
473
861
  const busted = bustVinextOptimizeDepsCache(cacheDir);
474
862
  if (busted) {
475
863
  console.log("[cfni:vinext-route-wiring-fix] patched vinext on disk and cleared its stale Vite optimizeDeps cache — dependencies will re-bundle on next request.");
@@ -478,10 +866,10 @@ export function vinextRouteWiringFixPlugin(options = {}) {
478
866
  },
479
867
  transform(code, id) {
480
868
  if (routeWiring && isAppPageRouteWiringFile(id)) {
481
- if (isAppPageRouteWiringAlreadyFixed(code)) {
869
+ if (isAppPageRouteWiringAlreadyFixed(code, { unblockRenderDependencies, unblockPageElementDependencies }) || !hasRequiredSymbols(code, "routeWiring")) {
482
870
  return;
483
871
  }
484
- const patched = patchAppPageRouteWiring(code);
872
+ const patched = patchAppPageRouteWiring(code, { unblockRenderDependencies, unblockPageElementDependencies });
485
873
  if (patched === code) {
486
874
  return;
487
875
  }
@@ -491,7 +879,7 @@ export function vinextRouteWiringFixPlugin(options = {}) {
491
879
  };
492
880
  }
493
881
  if (routeMatching && isRouteMatchingFile(id)) {
494
- if (isRouteMatchingAlreadyFixed(code)) {
882
+ if (isRouteMatchingAlreadyFixed(code) || !hasRequiredSymbols(code, "routeMatching")) {
495
883
  return;
496
884
  }
497
885
  const patched = patchRouteMatching(code);
@@ -503,11 +891,17 @@ export function vinextRouteWiringFixPlugin(options = {}) {
503
891
  map: null,
504
892
  };
505
893
  }
506
- if (prefetchLearning && isPrefetchLearningFile(id)) {
507
- if (isPrefetchLearningAlreadyFixed(code)) {
508
- return;
894
+ if ((prefetchLearning && isPrefetchLearningFile(id)) || (optimisticLearningTimeout && isOptimisticLearningTimeoutFile(id)) || (refreshDeferral && isPrefetchLearningFile(id))) {
895
+ let patched = code;
896
+ if (prefetchLearning && isPrefetchLearningFile(id) && !isPrefetchLearningAlreadyFixed(patched) && hasRequiredSymbols(patched, "prefetchLearning")) {
897
+ patched = patchPrefetchLearning(patched);
898
+ }
899
+ if (optimisticLearningTimeout && isOptimisticLearningTimeoutFile(id)) {
900
+ patched = patchOptimisticLearningTimeout(patched, optimisticLearningTimeoutMs);
901
+ }
902
+ if (refreshDeferral) {
903
+ patched = patchRefreshDeferralEntry(patched);
509
904
  }
510
- const patched = patchPrefetchLearning(code);
511
905
  if (patched === code) {
512
906
  return;
513
907
  }
@@ -517,7 +911,7 @@ export function vinextRouteWiringFixPlugin(options = {}) {
517
911
  };
518
912
  }
519
913
  if (optimisticRouting && isOptimisticRoutingFile(id)) {
520
- if (isOptimisticRoutingAlreadyFixed(code)) {
914
+ if (isOptimisticRoutingAlreadyFixed(code) || !hasRequiredSymbols(code, "optimisticRouting")) {
521
915
  return;
522
916
  }
523
917
  const patched = patchOptimisticRouting(code);
@@ -529,6 +923,58 @@ export function vinextRouteWiringFixPlugin(options = {}) {
529
923
  map: null,
530
924
  };
531
925
  }
926
+ if (renderDependency && isRenderDependencyFile(id)) {
927
+ if (isRenderDependencyAlreadyFixed(code)) {
928
+ return;
929
+ }
930
+ const patched = patchRenderDependency(code);
931
+ if (patched === code) {
932
+ return;
933
+ }
934
+ return {
935
+ code: patched,
936
+ map: null,
937
+ };
938
+ }
939
+ if (pageInvokerSuspensionRelease && isPageInvokerSuspensionReleaseFile(id)) {
940
+ if (isPageInvokerSuspensionReleaseAlreadyFixed(code)) {
941
+ return;
942
+ }
943
+ const patched = patchPageInvokerSuspensionRelease(code);
944
+ if (patched === code) {
945
+ return;
946
+ }
947
+ return {
948
+ code: patched,
949
+ map: null,
950
+ };
951
+ }
952
+ if (refreshDeferral && isRefreshDeferralNavControllerFile(id)) {
953
+ if (isRefreshDeferralNavControllerAlreadyFixed(code)) {
954
+ return;
955
+ }
956
+ const patched = patchRefreshDeferralNavController(code);
957
+ if (patched === code) {
958
+ return;
959
+ }
960
+ return {
961
+ code: patched,
962
+ map: null,
963
+ };
964
+ }
965
+ if (suspenseProbe && isAppPageProbeFile(id)) {
966
+ if (isAppPageProbeAlreadyFixed(code) || !hasRequiredSymbols(code, "suspenseProbe")) {
967
+ return;
968
+ }
969
+ const patched = patchAppPageProbe(code);
970
+ if (patched === code) {
971
+ return;
972
+ }
973
+ return {
974
+ code: patched,
975
+ map: null,
976
+ };
977
+ }
532
978
  },
533
979
  };
534
980
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.9.50",
3
+ "version": "0.9.52",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +12,8 @@
12
12
  "cfni-image-optimizer": "bin/image_optimizer.mjs",
13
13
  "optimize-images": "bin/image_optimizer.mjs",
14
14
  "cfni-check-dynamic-pages": "bin/check_dynamic_pages.mjs",
15
- "cfni-check-locale-params": "bin/check_locale_params.mjs"
15
+ "cfni-check-locale-params": "bin/check_locale_params.mjs",
16
+ "cfni-check-layout-queries": "bin/check_layout_queries.mjs"
16
17
  },
17
18
  "files": [
18
19
  "dist",
@@ -243,6 +244,10 @@
243
244
  "types": "./dist/src/locale_params_check/index.d.ts",
244
245
  "import": "./dist/src/locale_params_check/index.js"
245
246
  },
247
+ "./checkLayoutQueries": {
248
+ "types": "./dist/src/layout_queries_check/index.d.ts",
249
+ "import": "./dist/src/layout_queries_check/index.js"
250
+ },
246
251
  "./db": {
247
252
  "types": "./dist/src/db/index.d.ts",
248
253
  "import": "./dist/src/db/index.js"