cloudflare-next-intl 0.10.5 → 0.10.7

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.
@@ -12,6 +12,7 @@ export interface DynamicApiCheck {
12
12
  name: string;
13
13
  pattern: RegExp;
14
14
  }
15
+ export declare function hasSetLocaleCall(sourceText: string): boolean;
15
16
  export declare const USE_CLIENT_DIRECTIVE: RegExp;
16
17
  export declare function detectDynamicUsage(sourceText: string, extraChecks?: readonly DynamicApiCheck[]): DynamicDetectionResult;
17
18
  export declare function readExplicitDynamicValue(sourceText: string): 'force-static' | 'force-dynamic' | 'auto' | 'error' | null;
@@ -32,7 +32,6 @@ export function stripComments(sourceText) {
32
32
  const DYNAMIC_API_CHECKS = [
33
33
  { name: 'cookies()', pattern: /\bcookies\s*\(/ },
34
34
  { name: 'headers()', pattern: /\bheaders\s*\(\s*\)/ },
35
- { name: 'searchParams', pattern: /\bsearchParams\b/ },
36
35
  { name: 'unstable_noStore()', pattern: /\bunstable_noStore\s*\(/ },
37
36
  { name: 'connection()', pattern: /\bconnection\s*\(\s*\)/ },
38
37
  { name: 'cache: "no-store"', pattern: /cache:\s*['"]no-store['"]/ },
@@ -41,8 +40,12 @@ const DYNAMIC_API_CHECKS = [
41
40
  { name: 'withUserDb()', pattern: /\bwithUserDb\s*\(/ },
42
41
  ];
43
42
  const USE_AUTH_USER_CALL = /\buseAuthUser\s*\(/;
43
+ const SEARCH_PARAMS_IDENTIFIER = /\bsearchParams\b/;
44
44
  const TRANSLATIONS_CALL_NO_LOCALE = /\b(?:getTranslations|useTranslations)\s*\(\s*(?:['"][^'"]*['"]|[A-Za-z_$][\w$]*)\s*\)/;
45
45
  const SET_LOCALE_CALL = /\bsetLocale(?:Async)?\s*\(/;
46
+ export function hasSetLocaleCall(sourceText) {
47
+ return SET_LOCALE_CALL.test(stripComments(sourceText));
48
+ }
46
49
  export const USE_CLIENT_DIRECTIVE = /^(?:\s*['"]use \w[\w-]*['"]\s*;?\s*)*['"]use client['"]\s*;?/;
47
50
  const EXPLICIT_DYNAMIC_EXPORT = /export\s+const\s+dynamic\s*=/;
48
51
  export function detectDynamicUsage(sourceText, extraChecks = []) {
@@ -59,7 +62,12 @@ export function detectDynamicUsage(sourceText, extraChecks = []) {
59
62
  if (found !== null)
60
63
  matches.push({ name: 'useAuthUser()', line: lineOf(sourceText, found.index) });
61
64
  }
62
- if (!SET_LOCALE_CALL.test(code)) {
65
+ if (!USE_CLIENT_DIRECTIVE.test(sourceText)) {
66
+ const found = SEARCH_PARAMS_IDENTIFIER.exec(code);
67
+ if (found !== null)
68
+ matches.push({ name: 'searchParams', line: lineOf(sourceText, found.index) });
69
+ }
70
+ if (!USE_CLIENT_DIRECTIVE.test(sourceText) && !SET_LOCALE_CALL.test(code)) {
63
71
  const found = TRANSLATIONS_CALL_NO_LOCALE.exec(code);
64
72
  if (found !== null)
65
73
  matches.push({ name: 'getTranslations()/useTranslations() (cookie-derived locale)', line: lineOf(sourceText, found.index) });
@@ -1,7 +1,8 @@
1
- import { detectDynamicUsage } from './detect_dynamic_usage.js';
1
+ import { detectDynamicUsage, hasSetLocaleCall } from './detect_dynamic_usage.js';
2
2
  import { collectReachableFiles } from './collect_reachable_files.js';
3
3
  export function traceDynamicUsage(entryFile, entrySource, aliases, io, extraChecks = []) {
4
4
  const files = collectReachableFiles(entryFile, entrySource, aliases, io);
5
+ const entryHasSetLocale = hasSetLocaleCall(entrySource);
5
6
  let hasExplicitDynamicExport = false;
6
7
  const detectedApis = new Set();
7
8
  const signals = [];
@@ -13,6 +14,9 @@ export function traceDynamicUsage(entryFile, entrySource, aliases, io, extraChec
13
14
  first = false;
14
15
  }
15
16
  detection.matches.forEach(({ name, line }) => {
17
+ if (entryHasSetLocale && file !== entryFile && name === 'getTranslations()/useTranslations() (cookie-derived locale)') {
18
+ return;
19
+ }
16
20
  detectedApis.add(name);
17
21
  signals.push({ api: name, file, line });
18
22
  });
@@ -27,3 +27,8 @@ export declare function extractFieldValue(body: string, key: string): {
27
27
  } | null;
28
28
  export declare function formatFirebaseAuthConfigMessage(issues: FirebaseAuthConfigIssue[], intlConfigPath?: string): string;
29
29
  export declare function checkFirebaseAuthConfig(options?: CheckFirebaseAuthConfigOptions): CheckFirebaseAuthConfigReport;
30
+ export interface ValidateFirebaseAuthConfigValuesOptions {
31
+ firebaseAuth: Record<string, unknown> | undefined;
32
+ intlConfigPath?: string;
33
+ }
34
+ export declare function validateFirebaseAuthConfigValues(options: ValidateFirebaseAuthConfigValuesOptions): CheckFirebaseAuthConfigReport;
@@ -337,7 +337,29 @@ function resolveSpreadBodies(mainBody, source, fromFile, cache) {
337
337
  return { bodies, hasUnresolvedSpread };
338
338
  }
339
339
  export function extractFieldValue(body, key) {
340
- const keyMatch = new RegExp(`(^|[\\s{,])${key}\\s*:`).exec(maskCommentsAndStrings(body));
340
+ const masked = maskCommentsAndStrings(body);
341
+ const keyPattern = new RegExp(`^${key}\\s*:`);
342
+ let searchDepth = 0;
343
+ let scanIndex = 0;
344
+ let keyMatch = null;
345
+ while (scanIndex < masked.length) {
346
+ const char = masked[scanIndex];
347
+ if (char === "{" || char === "[" || char === "(") {
348
+ searchDepth += 1;
349
+ }
350
+ else if (char === "}" || char === "]" || char === ")") {
351
+ searchDepth -= 1;
352
+ }
353
+ else if (searchDepth === 0 && (scanIndex === 0 || /[\s{,]/.test(masked[scanIndex - 1]))) {
354
+ const candidate = keyPattern.exec(masked.slice(scanIndex));
355
+ if (candidate) {
356
+ keyMatch = candidate;
357
+ keyMatch.index = scanIndex;
358
+ break;
359
+ }
360
+ }
361
+ scanIndex += 1;
362
+ }
341
363
  if (!keyMatch)
342
364
  return null;
343
365
  const start = keyMatch.index + keyMatch[0].length;
@@ -531,3 +553,54 @@ export function checkFirebaseAuthConfig(options = {}) {
531
553
  }
532
554
  return { valid, checked: true, issues, formattedMessage };
533
555
  }
556
+ function isUsableFieldValue(value) {
557
+ if (value === undefined || value === null)
558
+ return false;
559
+ if (typeof value === "string")
560
+ return value.trim() !== "";
561
+ return true;
562
+ }
563
+ const UNUSABLE_REASON = "resolved to an empty or missing value";
564
+ export function validateFirebaseAuthConfigValues(options) {
565
+ const { firebaseAuth } = options;
566
+ if (!firebaseAuth) {
567
+ return { valid: true, checked: false, issues: [], formattedMessage: "" };
568
+ }
569
+ const issues = [];
570
+ for (const key of REQUIRED_AUTH_FIELDS) {
571
+ if (!isUsableFieldValue(firebaseAuth[key])) {
572
+ issues.push({ field: `firebaseAuth.${key}`, severity: "error", reason: UNUSABLE_REASON });
573
+ }
574
+ }
575
+ const appCheck = firebaseAuth.appCheck;
576
+ if (appCheck && appCheck.reportMissingServerCredentials !== false) {
577
+ for (const key of REQUIRED_APP_CHECK_FIELDS) {
578
+ if (!isUsableFieldValue(appCheck[key])) {
579
+ issues.push({ field: `firebaseAuth.appCheck.${key}`, severity: "warning", reason: UNUSABLE_REASON });
580
+ }
581
+ }
582
+ const hasPrivateKey = isUsableFieldValue(appCheck.privateKey);
583
+ const tripleUsable = OAUTH_TRIPLE.map((key) => isUsableFieldValue(appCheck[key]));
584
+ const hasTriple = tripleUsable.every(Boolean);
585
+ if (!hasPrivateKey && !hasTriple) {
586
+ const hasPartialTriple = tripleUsable.some(Boolean);
587
+ if (hasPartialTriple) {
588
+ OAUTH_TRIPLE.forEach((key, index) => {
589
+ if (!tripleUsable[index]) {
590
+ issues.push({ field: `firebaseAuth.appCheck.${key}`, severity: "warning", reason: UNUSABLE_REASON });
591
+ }
592
+ });
593
+ }
594
+ else {
595
+ issues.push({
596
+ field: "firebaseAuth.appCheck.privateKey",
597
+ severity: "warning",
598
+ reason: UNUSABLE_REASON + " — set it, or the full oauthClientId/oauthClientSecret/oauthRefreshToken triple",
599
+ });
600
+ }
601
+ }
602
+ }
603
+ const valid = !issues.some((issue) => issue.severity === "error");
604
+ const formattedMessage = formatFirebaseAuthConfigMessage(issues, options.intlConfigPath);
605
+ return { valid, checked: true, issues, formattedMessage };
606
+ }
@@ -1 +1,2 @@
1
- export { checkFirebaseAuthConfig, formatFirebaseAuthConfigMessage, extractObjectLiteral, extractFieldValue, type FirebaseAuthConfigIssue, type CheckFirebaseAuthConfigOptions, type CheckFirebaseAuthConfigReport, } from "./check_firebase_auth_config.js";
1
+ export { checkFirebaseAuthConfig, validateFirebaseAuthConfigValues, formatFirebaseAuthConfigMessage, extractObjectLiteral, extractFieldValue, type FirebaseAuthConfigIssue, type CheckFirebaseAuthConfigOptions, type CheckFirebaseAuthConfigReport, type ValidateFirebaseAuthConfigValuesOptions, } from "./check_firebase_auth_config.js";
2
+ export { loadResolvedFirebaseAuth, type LoadResolvedFirebaseAuthOptions } from "./load_resolved_firebase_auth.js";
@@ -1 +1,2 @@
1
- export { checkFirebaseAuthConfig, formatFirebaseAuthConfigMessage, extractObjectLiteral, extractFieldValue, } from "./check_firebase_auth_config.js";
1
+ export { checkFirebaseAuthConfig, validateFirebaseAuthConfigValues, formatFirebaseAuthConfigMessage, extractObjectLiteral, extractFieldValue, } from "./check_firebase_auth_config.js";
2
+ export { loadResolvedFirebaseAuth } from "./load_resolved_firebase_auth.js";
@@ -0,0 +1,8 @@
1
+ import type { ResolvedConfig } from "vite";
2
+ export interface LoadResolvedFirebaseAuthOptions {
3
+ intlConfigPath: string;
4
+ viteConfig: Pick<ResolvedConfig, "root" | "envDir" | "mode"> & {
5
+ resolve: Pick<ResolvedConfig["resolve"], "alias">;
6
+ };
7
+ }
8
+ export declare function loadResolvedFirebaseAuth(options: LoadResolvedFirebaseAuthOptions): Promise<Record<string, unknown> | undefined>;
@@ -0,0 +1,51 @@
1
+ export async function loadResolvedFirebaseAuth(options) {
2
+ let server;
3
+ try {
4
+ const { createServer } = await import("vite");
5
+ server = await createServer({
6
+ configFile: false,
7
+ root: options.viteConfig.root,
8
+ envDir: options.viteConfig.envDir,
9
+ mode: options.viteConfig.mode,
10
+ resolve: { alias: options.viteConfig.resolve.alias },
11
+ server: { middlewareMode: true, hmr: false, watch: null },
12
+ optimizeDeps: { noDiscovery: true },
13
+ logLevel: "silent",
14
+ clearScreen: false,
15
+ ssr: { noExternal: ["cloudflare-next-intl", /^cloudflare:/] },
16
+ plugins: [
17
+ {
18
+ name: "cfni:firebase-auth-check-intl-config-alias",
19
+ enforce: "pre",
20
+ resolveId(id) {
21
+ if (id === "@intl-config")
22
+ return options.intlConfigPath;
23
+ if (id === "cloudflare:workers" || id.startsWith("cloudflare:")) {
24
+ return "\0cfni:cloudflare-workers-stub";
25
+ }
26
+ },
27
+ load(id) {
28
+ if (id === "\0cfni:cloudflare-workers-stub") {
29
+ return ("export class WorkerEntrypoint {}\n" +
30
+ "export class DurableObject {}\n" +
31
+ "export const env = {};\n" +
32
+ "export default {};\n");
33
+ }
34
+ },
35
+ },
36
+ ],
37
+ });
38
+ const mod = await server.ssrLoadModule(options.intlConfigPath);
39
+ const exported = mod.default;
40
+ const firebaseAuth = exported?.firebaseAuth;
41
+ return firebaseAuth && typeof firebaseAuth === "object"
42
+ ? firebaseAuth
43
+ : undefined;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ finally {
49
+ await server?.close();
50
+ }
51
+ }
@@ -1,4 +1,4 @@
1
- import { checkFirebaseAuthConfig } from "../firebase_auth_check/index.js";
1
+ import { checkFirebaseAuthConfig, validateFirebaseAuthConfigValues, loadResolvedFirebaseAuth, } from "../firebase_auth_check/index.js";
2
2
  import { resolveDefaultIntlConfigPath } from "./locale_file_plugin.js";
3
3
  export function firebaseAuthCheckPlugin(options = {}) {
4
4
  let ran = false;
@@ -23,7 +23,21 @@ export function firebaseAuthCheckPlugin(options = {}) {
23
23
  ...process.env,
24
24
  };
25
25
  }
26
- const report = checkFirebaseAuthConfig({ intlConfigPath, env, throwOnError: false });
26
+ const previousEnv = { ...process.env };
27
+ Object.assign(process.env, env);
28
+ let firebaseAuth;
29
+ try {
30
+ firebaseAuth = await loadResolvedFirebaseAuth({
31
+ intlConfigPath,
32
+ viteConfig: { root: config.root, envDir: config.envDir, mode: config.mode, resolve: { alias: config.resolve?.alias } },
33
+ });
34
+ }
35
+ finally {
36
+ process.env = previousEnv;
37
+ }
38
+ const report = firebaseAuth !== undefined
39
+ ? validateFirebaseAuthConfigValues({ firebaseAuth, intlConfigPath })
40
+ : checkFirebaseAuthConfig({ intlConfigPath, env, throwOnError: false });
27
41
  if (report.issues.length === 0)
28
42
  return;
29
43
  console.warn(report.formattedMessage);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
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",