cloudflare-next-intl 0.9.57 → 0.9.59

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
@@ -269,6 +269,7 @@ export default defineConfig({
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
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
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
+ 12. **Firebase Auth Config Check (`firebaseAuthCheck`)**: Statically validates the `firebaseAuth` block of your `@intl-config` file on both `vite dev` and `vite build` (also usable standalone from `cloudflare-next-intl/checkFirebaseAuthConfig`). Errors on the fields nothing works without (`apiKey`, `authDomain`, `projectId`, `appId`, `redirectAuthPath`, `homePath`); warns when `appCheck` is present but server-side App Check minting can't run — `clientEmail`/`appId` plus **one** of `privateKey` or the full `oauthClientId`/`oauthClientSecret`/`oauthRefreshToken` triple — the misconfiguration whose only other symptom is a signed-in user rendering as signed-out on a cold navigation. Env-var-backed fields are resolved against `.env*` (Vite `loadEnv`, empty prefix) + `process.env`, so an unset `FIREBASE_SERVICE_ACCOUNT_*` secret is caught before deploy. Deliberately conservative: a value it can't evaluate (a call, an imported constant, a `??` fallback) counts as present. Prints a warning by default; pass `{ strict: true }` to fail the build, `false` to disable, or `appCheck.reportMissingServerCredentials: false` in your config to silence just the App Check warnings. No-op when there is no `firebaseAuth` block.
272
273
  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.
273
274
 
274
275
  ##### Plugin Options
@@ -304,6 +305,10 @@ export default defineConfig({
304
305
  strict: false, // Fail the build on violations instead of just warning (default: false)
305
306
  runOnDev: true, // Also run on `vite dev`, not just build (default: true)
306
307
  },
308
+ firebaseAuthCheck: { // Validate the `firebaseAuth` config block (or `false` to disable)
309
+ strict: false, // Fail the build on missing required fields instead of warning (default: false)
310
+ runOnDev: true, // Also run on `vite dev`, not just build (default: true)
311
+ },
307
312
  messagesDir: "./messages", // Path to locale JSON files (default: './messages')
308
313
  intlConfigPath: "./src/l18n/intl_config.ts", // Path to intl config (auto-detected if omitted)
309
314
  buildIdAsset: true, // Emit BUILD_ID asset (or custom string filename, default: true)
@@ -333,7 +338,7 @@ export default defineConfig({
333
338
  > **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
339
 
335
340
  Individual standalone plugins are also exported if you only need a specific feature:
336
- `imageOptimizerPlugin` (or `imageOptimizer`), `autoLocaleParamsPlugin`, `layoutQueriesPlugin` (or `layoutQueriesCheck`), `buildIdAsset`, `localeFilePlugin`, `userAgentStubPlugin`, `cfWorkersClientStubPlugin`, `vinextRouteWiringFixPlugin`, `lucideOptimizerPlugin`.
341
+ `imageOptimizerPlugin` (or `imageOptimizer`), `autoLocaleParamsPlugin`, `layoutQueriesPlugin` (or `layoutQueriesCheck`), `firebaseAuthCheckPlugin` (or `firebaseAuthCheck`), `buildIdAsset`, `localeFilePlugin`, `userAgentStubPlugin`, `cfWorkersClientStubPlugin`, `vinextRouteWiringFixPlugin`, `lucideOptimizerPlugin`.
337
342
 
338
343
  ##### Per-Image Optimizer Settings
339
344
 
@@ -9,7 +9,7 @@ const MAX_RECOVERY_ATTEMPTS = 2;
9
9
  const BUILD_ID_KEY = 'buildId';
10
10
  const BUILD_ID_SET_AT_KEY = 'buildIdSetAt';
11
11
  const RECENT_BUILD_WINDOW_MS = 60000;
12
- const RELOAD_THROTTLE_MS = 15000;
12
+ const RELOAD_THROTTLE_MS = 1000;
13
13
  function currentBuildId() {
14
14
  try {
15
15
  return localStorage.getItem(BUILD_ID_KEY) ?? 'unknown';
@@ -0,0 +1,29 @@
1
+ export interface FirebaseAuthConfigIssue {
2
+ field: string;
3
+ severity: "error" | "warning";
4
+ reason: string;
5
+ envVar?: string;
6
+ lineNumber?: number;
7
+ }
8
+ export interface CheckFirebaseAuthConfigOptions {
9
+ intlConfigPath?: string;
10
+ source?: string;
11
+ env?: Record<string, string | undefined>;
12
+ throwOnError?: boolean;
13
+ }
14
+ export interface CheckFirebaseAuthConfigReport {
15
+ valid: boolean;
16
+ checked: boolean;
17
+ issues: FirebaseAuthConfigIssue[];
18
+ formattedMessage: string;
19
+ }
20
+ export declare function extractObjectLiteral(source: string, key: string): {
21
+ body: string;
22
+ start: number;
23
+ } | null;
24
+ export declare function extractFieldValue(body: string, key: string): {
25
+ value: string;
26
+ lineNumber: number;
27
+ } | null;
28
+ export declare function formatFirebaseAuthConfigMessage(issues: FirebaseAuthConfigIssue[], intlConfigPath?: string): string;
29
+ export declare function checkFirebaseAuthConfig(options?: CheckFirebaseAuthConfigOptions): CheckFirebaseAuthConfigReport;
@@ -0,0 +1,250 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ const REQUIRED_AUTH_FIELDS = [
3
+ "apiKey",
4
+ "authDomain",
5
+ "projectId",
6
+ "appId",
7
+ "redirectAuthPath",
8
+ "homePath",
9
+ ];
10
+ const REQUIRED_APP_CHECK_FIELDS = ["clientEmail", "appId"];
11
+ const OAUTH_TRIPLE = ["oauthClientId", "oauthClientSecret", "oauthRefreshToken"];
12
+ export function extractObjectLiteral(source, key) {
13
+ const keyMatch = new RegExp(`(^|[\\s{,])${key}\\s*:`).exec(source);
14
+ if (!keyMatch)
15
+ return null;
16
+ const afterKey = keyMatch.index + keyMatch[0].length;
17
+ const open = source.indexOf("{", afterKey);
18
+ if (open === -1)
19
+ return null;
20
+ if (/[,;]/.test(source.slice(afterKey, open)))
21
+ return null;
22
+ let depth = 0;
23
+ let index = open;
24
+ let quote = null;
25
+ let comment = null;
26
+ while (index < source.length) {
27
+ const char = source[index];
28
+ const next = source[index + 1];
29
+ if (comment === "line") {
30
+ if (char === "\n")
31
+ comment = null;
32
+ }
33
+ else if (comment === "block") {
34
+ if (char === "*" && next === "/") {
35
+ comment = null;
36
+ index += 1;
37
+ }
38
+ }
39
+ else if (quote) {
40
+ if (char === "\\")
41
+ index += 1;
42
+ else if (char === quote)
43
+ quote = null;
44
+ }
45
+ else if (char === "/" && next === "/") {
46
+ comment = "line";
47
+ index += 1;
48
+ }
49
+ else if (char === "/" && next === "*") {
50
+ comment = "block";
51
+ index += 1;
52
+ }
53
+ else if (char === '"' || char === "'" || char === "`") {
54
+ quote = char;
55
+ }
56
+ else if (char === "{") {
57
+ depth += 1;
58
+ }
59
+ else if (char === "}") {
60
+ depth -= 1;
61
+ if (depth === 0) {
62
+ return { body: source.slice(open + 1, index), start: open };
63
+ }
64
+ }
65
+ index += 1;
66
+ }
67
+ return null;
68
+ }
69
+ export function extractFieldValue(body, key) {
70
+ const keyMatch = new RegExp(`(^|[\\s{,])${key}\\s*:`).exec(body);
71
+ if (!keyMatch)
72
+ return null;
73
+ const start = keyMatch.index + keyMatch[0].length;
74
+ let index = start;
75
+ let depth = 0;
76
+ let quote = null;
77
+ while (index < body.length) {
78
+ const char = body[index];
79
+ if (quote) {
80
+ if (char === "\\")
81
+ index += 1;
82
+ else if (char === quote)
83
+ quote = null;
84
+ }
85
+ else if (char === '"' || char === "'" || char === "`") {
86
+ quote = char;
87
+ }
88
+ else if (char === "{" || char === "[" || char === "(") {
89
+ depth += 1;
90
+ }
91
+ else if (char === "}" || char === "]" || char === ")") {
92
+ if (depth === 0)
93
+ break;
94
+ depth -= 1;
95
+ }
96
+ else if (char === "," && depth === 0) {
97
+ break;
98
+ }
99
+ index += 1;
100
+ }
101
+ return {
102
+ value: body.slice(start, index).trim(),
103
+ lineNumber: body.slice(0, start).split("\n").length,
104
+ };
105
+ }
106
+ function envVarsIn(value) {
107
+ const names = [];
108
+ const pattern = /process\.env\s*(?:\.\s*([A-Za-z_$][\w$]*)|\[\s*["'`]([^"'`]+)["'`]\s*\])/g;
109
+ let match;
110
+ while ((match = pattern.exec(value)) !== null) {
111
+ const name = match[1] ?? match[2];
112
+ if (name)
113
+ names.push(name);
114
+ }
115
+ return names;
116
+ }
117
+ function evaluateValue(value, env) {
118
+ if (value === "" || value === "undefined" || value === "null") {
119
+ return { ok: false, reason: "set to `" + (value || "nothing") + "`" };
120
+ }
121
+ const literal = /^(["'`])(.*)\1$/s.exec(value);
122
+ if (literal && literal[2].trim() === "") {
123
+ return { ok: false, reason: "an empty string literal" };
124
+ }
125
+ if (literal)
126
+ return { ok: true };
127
+ const envVars = envVarsIn(value);
128
+ if (envVars.length === 0)
129
+ return { ok: true };
130
+ const withoutEnv = value.replace(/process\.env\s*(?:\.\s*[A-Za-z_$][\w$]*|\[\s*["'`][^"'`]+["'`]\s*\])/g, "");
131
+ const hasFallback = /[A-Za-z0-9_$"'`]/.test(withoutEnv.replace(/\bas\b|\bstring\b|\bundefined\b|\bnull\b/g, ""));
132
+ const unset = envVars.filter((name) => !env[name] || env[name].trim() === "");
133
+ if (unset.length === envVars.length && !hasFallback) {
134
+ return {
135
+ ok: false,
136
+ reason: "read from `process.env." + unset[0] + "`, which is not set",
137
+ envVar: unset[0],
138
+ };
139
+ }
140
+ return { ok: true };
141
+ }
142
+ function checkField(body, path, key, severity, env, baseLine) {
143
+ const field = `${path}.${key}`;
144
+ const found = extractFieldValue(body, key);
145
+ if (!found) {
146
+ return { field, severity, reason: "missing from the config" };
147
+ }
148
+ const verdict = evaluateValue(found.value, env);
149
+ if (verdict.ok)
150
+ return null;
151
+ return {
152
+ field,
153
+ severity,
154
+ reason: verdict.reason,
155
+ ...(verdict.envVar ? { envVar: verdict.envVar } : {}),
156
+ lineNumber: baseLine + found.lineNumber - 1,
157
+ };
158
+ }
159
+ export function formatFirebaseAuthConfigMessage(issues, intlConfigPath) {
160
+ if (issues.length === 0)
161
+ return "";
162
+ const separator = "=".repeat(84);
163
+ const thinSeparator = "-".repeat(84);
164
+ const errors = issues.filter((issue) => issue.severity === "error");
165
+ const lines = [
166
+ "",
167
+ separator,
168
+ errors.length > 0
169
+ ? "🚨 [cloudflare-next-intl] INCOMPLETE `firebaseAuth` CONFIG"
170
+ : "⚠️ [cloudflare-next-intl] INCOMPLETE `firebaseAuth` CONFIG",
171
+ separator,
172
+ "",
173
+ ];
174
+ if (intlConfigPath)
175
+ lines.push("📄 Config: " + intlConfigPath, "");
176
+ lines.push("📋 ISSUES FOUND (" + issues.length + "):", thinSeparator);
177
+ issues.forEach((issue, index) => {
178
+ const icon = issue.severity === "error" ? "❌" : "⚠️ ";
179
+ lines.push(` [${index + 1}] ${icon} ${issue.field} — ${issue.reason}`);
180
+ if (issue.envVar) {
181
+ lines.push(` Set ${issue.envVar} in your .env / Cloudflare secrets.`);
182
+ }
183
+ if (issue.lineNumber) {
184
+ lines.push(` Around line ${issue.lineNumber}.`);
185
+ }
186
+ lines.push(thinSeparator);
187
+ });
188
+ lines.push("", "💡 WHAT BREAKS OTHERWISE:", " • Missing core fields (apiKey / authDomain / projectId / appId): the Firebase SDK", " rejects init, so every sign-in and every server-side session lookup fails.", " • Missing redirectAuthPath / homePath: the middleware's guest and auth-page", " redirects never match a pathname and silently do nothing.", " • Incomplete appCheck server credentials (clientEmail + appId, plus privateKey OR the", " full oauthClientId/oauthClientSecret/oauthRefreshToken triple): a signed-in user", " renders as signed-out on any cold navigation that beats the client's App Check cookie.", " Silence just this one with `appCheck.reportMissingServerCredentials: false`.", "", separator, "");
189
+ return lines.join("\n");
190
+ }
191
+ export function checkFirebaseAuthConfig(options = {}) {
192
+ const env = options.env ?? process.env;
193
+ let source = options.source;
194
+ if (source === undefined) {
195
+ const path = options.intlConfigPath;
196
+ if (!path || !existsSync(path)) {
197
+ return { valid: true, checked: false, issues: [], formattedMessage: "" };
198
+ }
199
+ try {
200
+ source = readFileSync(path, "utf8");
201
+ }
202
+ catch {
203
+ return { valid: true, checked: false, issues: [], formattedMessage: "" };
204
+ }
205
+ }
206
+ const firebaseAuth = extractObjectLiteral(source, "firebaseAuth");
207
+ if (!firebaseAuth) {
208
+ return { valid: true, checked: false, issues: [], formattedMessage: "" };
209
+ }
210
+ const baseLine = source.slice(0, firebaseAuth.start).split("\n").length;
211
+ const issues = [];
212
+ for (const key of REQUIRED_AUTH_FIELDS) {
213
+ const issue = checkField(firebaseAuth.body, "firebaseAuth", key, "error", env, baseLine);
214
+ if (issue)
215
+ issues.push(issue);
216
+ }
217
+ const appCheck = extractObjectLiteral(firebaseAuth.body, "appCheck");
218
+ if (appCheck) {
219
+ const appCheckLine = baseLine + firebaseAuth.body.slice(0, appCheck.start).split("\n").length - 1;
220
+ const optedOut = /reportMissingServerCredentials\s*:\s*false/.test(appCheck.body);
221
+ if (!optedOut) {
222
+ for (const key of REQUIRED_APP_CHECK_FIELDS) {
223
+ const issue = checkField(appCheck.body, "firebaseAuth.appCheck", key, "warning", env, appCheckLine);
224
+ if (issue)
225
+ issues.push(issue);
226
+ }
227
+ const privateKey = checkField(appCheck.body, "firebaseAuth.appCheck", "privateKey", "warning", env, appCheckLine);
228
+ const triple = OAUTH_TRIPLE.map((key) => checkField(appCheck.body, "firebaseAuth.appCheck", key, "warning", env, appCheckLine));
229
+ if (privateKey && triple.some((issue) => issue !== null)) {
230
+ const partialTriple = triple.some((issue) => issue === null);
231
+ if (partialTriple) {
232
+ issues.push(...triple.filter((issue) => issue !== null));
233
+ }
234
+ else {
235
+ issues.push({
236
+ ...privateKey,
237
+ reason: privateKey.reason
238
+ + " — set it, or the full oauthClientId/oauthClientSecret/oauthRefreshToken triple",
239
+ });
240
+ }
241
+ }
242
+ }
243
+ }
244
+ const valid = !issues.some((issue) => issue.severity === "error");
245
+ const formattedMessage = formatFirebaseAuthConfigMessage(issues, options.intlConfigPath);
246
+ if (!valid && options.throwOnError) {
247
+ throw new Error(formattedMessage);
248
+ }
249
+ return { valid, checked: true, issues, formattedMessage };
250
+ }
@@ -0,0 +1 @@
1
+ export { checkFirebaseAuthConfig, formatFirebaseAuthConfigMessage, extractObjectLiteral, extractFieldValue, type FirebaseAuthConfigIssue, type CheckFirebaseAuthConfigOptions, type CheckFirebaseAuthConfigReport, } from "./check_firebase_auth_config.js";
@@ -0,0 +1 @@
1
+ export { checkFirebaseAuthConfig, formatFirebaseAuthConfigMessage, extractObjectLiteral, extractFieldValue, } from "./check_firebase_auth_config.js";
@@ -20,9 +20,7 @@ export default function HelperScript() {
20
20
  var timeKey = 'stale-deploy-recovery-time';
21
21
  var countKey = 'stale-deploy-recovery-count';
22
22
  var maxAttempts = 3;
23
- var throttleMs = 15000;
24
23
  var attemptedThisLoad = false;
25
- var retryScheduled = false;
26
24
  // Set by the resource-error listener: the first same-origin
27
25
  // chunk that failed. Reloading is pointless until that URL
28
26
  // answers with real JavaScript again, so it doubles as the
@@ -43,15 +41,27 @@ export default function HelperScript() {
43
41
  // against the tree and every removeChild/insertBefore throws,
44
42
  // which crashes it into the very error UI this is hiding.
45
43
  var overlayId = 'cfni-stale-deploy-overlay';
44
+ var overlayWanted = false;
46
45
  function showOverlay() {
46
+ overlayWanted = true;
47
47
  try {
48
- var root = document.documentElement;
49
- if (!root || document.getElementById(overlayId)) return;
48
+ if (document.getElementById(overlayId)) return;
49
+ // This script runs from <head>, and a chunk can fail
50
+ // before the parser has opened <body>. An element
51
+ // appended to <html> at that point is discarded, so the
52
+ // error UI would paint uncovered until the reload.
53
+ if (!document.body) {
54
+ document.addEventListener('DOMContentLoaded', function() {
55
+ if (overlayWanted) showOverlay();
56
+ }, { once: true });
57
+ setTimeout(function() { if (overlayWanted) showOverlay(); }, 0);
58
+ return;
59
+ }
50
60
  var el = document.createElement('div');
51
61
  el.id = overlayId;
52
62
  el.setAttribute('style', 'position:fixed;inset:0;z-index:2147483647;background:#ffffff;');
53
63
  el.innerHTML = ${JSON.stringify(reloadHtml)};
54
- (document.body || root).appendChild(el);
64
+ document.body.appendChild(el);
55
65
  } catch (e) {}
56
66
  }
57
67
  function doReload() {
@@ -94,11 +104,8 @@ export default function HelperScript() {
94
104
  if (!stale) return;
95
105
  var buildId = localStorage.getItem('buildId') || 'unknown';
96
106
  var marker = sessionStorage.getItem(key);
97
- var lastRaw = sessionStorage.getItem(timeKey);
98
- var last = lastRaw ? Number(lastRaw) : null;
99
- var throttled = last !== null && (Date.now() - last) < throttleMs;
100
- // Attempts are counted per build id: the 1st and 2nd
101
- // page load may each recover, the 3rd falls through to
107
+ // Attempts are counted per build id: the first few
108
+ // page loads may each recover, then it falls through to
102
109
  // the error UI. A new deploy resets the count.
103
110
  var sameBuild = marker === buildId;
104
111
  var attempts = 0;
@@ -111,22 +118,12 @@ export default function HelperScript() {
111
118
  console.warn('[StaleDeploy early-catch] Skipping reload, attempts exhausted for buildId:', buildId, attempts);
112
119
  return;
113
120
  }
114
- // A reload that lands inside the same throttle window
115
- // means the previous recovery did not help: the assets
116
- // are momentarily unreadable at the edge rather than
117
- // stale. Waiting out the window and retrying lets the
118
- // page heal itself instead of stranding the visitor on
119
- // the error UI.
120
- if (sameBuild && throttled) {
121
- if (retryScheduled) return;
122
- retryScheduled = true;
123
- var wait = throttleMs - (Date.now() - last);
124
- if (!(wait > 0)) wait = 0;
125
- console.warn('[StaleDeploy early-catch] Throttled for buildId:', buildId, '- retrying in', wait, 'ms');
126
- showOverlay();
127
- setTimeout(function() { retryScheduled = false; recover(msg, source); }, wait + 250);
128
- return;
129
- }
121
+ // No time-based throttle: the probe below already
122
+ // paces the retry and refuses to reload until the asset
123
+ // is readable, so a clock-based wait only strands the
124
+ // visitor on the overlay for the rest of the window.
125
+ // attemptedThisLoad stops a burst within one load and
126
+ // maxAttempts stops a reload loop across loads.
130
127
  attemptedThisLoad = true;
131
128
  sessionStorage.setItem(key, buildId);
132
129
  sessionStorage.setItem(countKey, String(attempts + 1));
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from "vite";
2
+ import { type CheckFirebaseAuthConfigOptions } from "../firebase_auth_check/index.js";
3
+ export interface FirebaseAuthCheckPluginOptions extends Pick<CheckFirebaseAuthConfigOptions, "intlConfigPath" | "env"> {
4
+ strict?: boolean;
5
+ runOnDev?: boolean;
6
+ }
7
+ export declare function firebaseAuthCheckPlugin(options?: FirebaseAuthCheckPluginOptions): Plugin;
@@ -0,0 +1,35 @@
1
+ import { checkFirebaseAuthConfig } from "../firebase_auth_check/index.js";
2
+ import { resolveDefaultIntlConfigPath } from "./locale_file_plugin.js";
3
+ export function firebaseAuthCheckPlugin(options = {}) {
4
+ let ran = false;
5
+ return {
6
+ name: "cloudflare-next-intl-firebase-auth-check",
7
+ enforce: "pre",
8
+ async configResolved(config) {
9
+ if (ran)
10
+ return;
11
+ const isBuild = config.command === "build";
12
+ const isDev = config.command === "serve";
13
+ if (!isBuild && !(isDev && (options.runOnDev ?? true)))
14
+ return;
15
+ ran = true;
16
+ const root = config.root || process.cwd();
17
+ const intlConfigPath = options.intlConfigPath ?? resolveDefaultIntlConfigPath(root);
18
+ let env = options.env;
19
+ if (!env) {
20
+ const { loadEnv } = await import("vite");
21
+ env = {
22
+ ...loadEnv(config.mode, config.envDir || root, ""),
23
+ ...process.env,
24
+ };
25
+ }
26
+ const report = checkFirebaseAuthConfig({ intlConfigPath, env, throwOnError: false });
27
+ if (report.issues.length === 0)
28
+ return;
29
+ console.warn(report.formattedMessage);
30
+ if (!report.valid && options.strict) {
31
+ throw new Error("[cloudflare-next-intl] Build failed: `firebaseAuth` config is incomplete. See details above.");
32
+ }
33
+ },
34
+ };
35
+ }
@@ -1,6 +1,7 @@
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
3
  export { layoutQueriesPlugin, type LayoutQueriesPluginOptions } from "./layout_queries_plugin.js";
4
+ export { firebaseAuthCheckPlugin, type FirebaseAuthCheckPluginOptions } from "./firebase_auth_check_plugin.js";
4
5
  export { buildIdAsset } from "./build_id_asset.js";
5
6
  export { userAgentStubPlugin, USER_AGENT_STUB_ID, USER_AGENT_STUB_CODE } from "./user_agent_stub.js";
6
7
  export { cfWorkersClientStubPlugin, CF_WORKERS_CLIENT_STUB_ID, CF_WORKERS_CLIENT_STUB_CODE } from "./cf_workers_client_stub.js";
@@ -1,6 +1,7 @@
1
1
  export { autoDynamicPagesPlugin } from "./auto_dynamic_pages_plugin.js";
2
2
  export { autoLocaleParamsPlugin } from "./auto_locale_params_plugin.js";
3
3
  export { layoutQueriesPlugin } from "./layout_queries_plugin.js";
4
+ export { firebaseAuthCheckPlugin } from "./firebase_auth_check_plugin.js";
4
5
  export { buildIdAsset } from "./build_id_asset.js";
5
6
  export { userAgentStubPlugin, USER_AGENT_STUB_ID, USER_AGENT_STUB_CODE } from "./user_agent_stub.js";
6
7
  export { cfWorkersClientStubPlugin, CF_WORKERS_CLIENT_STUB_ID, CF_WORKERS_CLIENT_STUB_CODE } from "./cf_workers_client_stub.js";
@@ -4,6 +4,7 @@ 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
6
  import { type LayoutQueriesPluginOptions } from "./layout_queries_plugin.js";
7
+ import { type FirebaseAuthCheckPluginOptions } from "./firebase_auth_check_plugin.js";
7
8
  import { type VinextRouteWiringFixPluginOptions } from "./vinext_route_wiring_fix.js";
8
9
  import { type LucideOptimizerPluginOptions } from "./lucide_optimizer_plugin.js";
9
10
  export interface CloudflareNextIntlOptions extends LocaleFilePluginOptions {
@@ -17,6 +18,7 @@ export interface CloudflareNextIntlOptions extends LocaleFilePluginOptions {
17
18
  vinextRouteWiringFix?: boolean | VinextRouteWiringFixPluginOptions;
18
19
  autoLocaleParams?: boolean | AutoLocaleParamsPluginOptions;
19
20
  layoutQueriesCheck?: boolean | LayoutQueriesPluginOptions;
21
+ firebaseAuthCheck?: boolean | FirebaseAuthCheckPluginOptions;
20
22
  experimentalRouteLoadingFixes?: boolean;
21
23
  }
22
24
  export declare function cloudflareNextIntl(options?: CloudflareNextIntlOptions): Plugin[];
@@ -6,6 +6,7 @@ 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
8
  import { layoutQueriesPlugin } from "./layout_queries_plugin.js";
9
+ import { firebaseAuthCheckPlugin } from "./firebase_auth_check_plugin.js";
9
10
  import { vinextRouteWiringFixPlugin } from "./vinext_route_wiring_fix.js";
10
11
  import { lucideOptimizerPlugin } from "./lucide_optimizer_plugin.js";
11
12
  export function cloudflareNextIntl(options = {}) {
@@ -15,6 +16,11 @@ export function cloudflareNextIntl(options = {}) {
15
16
  ? options.layoutQueriesCheck
16
17
  : undefined));
17
18
  }
19
+ if (options.firebaseAuthCheck !== false) {
20
+ plugins.push(firebaseAuthCheckPlugin(typeof options.firebaseAuthCheck === "object"
21
+ ? options.firebaseAuthCheck
22
+ : { intlConfigPath: options.intlConfigPath }));
23
+ }
18
24
  if (options.lucideOptimizer !== false) {
19
25
  plugins.push(lucideOptimizerPlugin(typeof options.lucideOptimizer === "object"
20
26
  ? options.lucideOptimizer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.9.57",
3
+ "version": "0.9.59",
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",
@@ -248,6 +248,10 @@
248
248
  "types": "./dist/src/layout_queries_check/index.d.ts",
249
249
  "import": "./dist/src/layout_queries_check/index.js"
250
250
  },
251
+ "./checkFirebaseAuthConfig": {
252
+ "types": "./dist/src/firebase_auth_check/index.d.ts",
253
+ "import": "./dist/src/firebase_auth_check/index.js"
254
+ },
251
255
  "./db": {
252
256
  "types": "./dist/src/db/index.d.ts",
253
257
  "import": "./dist/src/db/index.js"