cloudflare-next-intl 0.6.6 → 0.6.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.
@@ -2,11 +2,29 @@ import { type ReportErrorConfig } from './report_error';
2
2
  /**
3
3
  * Replaces the global `console.error` so every `console.error(...)` call is
4
4
  * also routed through `config.errorHandling.onError`/`reportError` — the
5
- * original `console.error` still runs afterwards, nothing is swallowed.
6
- * Safe to call more than once (a no-op after the first call in this JS
7
- * realm — call it separately on the server and on the client, each has its
8
- * own `console`). Only takes effect when `config.errorHandling.overrideConsoleError`
9
- * is `true`.
5
+ * original `console.error` still runs afterwards, nothing is swallowed by
6
+ * default. Safe to call more than once (a no-op after the first call in
7
+ * this JS realm — call it separately on the server and on the client, each
8
+ * has its own `console`). Only takes effect when
9
+ * `config.errorHandling.overrideConsoleError` is `true`.
10
+ *
11
+ * On the client ONLY (`isClient: true`), passing
12
+ * `config.errorHandling.suppressClientConsoleError: true` skips the
13
+ * browser's own `console.error` output entirely once a call has been
14
+ * routed to `onError`/`reportError` — the error is still reported, it just
15
+ * never shows up in browser devtools. Has no effect server-side.
16
+ *
17
+ * Sets `consoleOverrideState.active = true` in `report_error.ts` — once
18
+ * installed, THIS override becomes the sole place that ever calls the real
19
+ * console for a report (it already logs the raw message below, before
20
+ * calling `reportError`), and `reportError`'s own console-logging step
21
+ * stays out of the loop entirely. Without that, `reportError`'s own
22
+ * fallback log would call `console.error` again — landing right back on
23
+ * this override and recursing. (An "original console.error, captured once
24
+ * at module load" reference does NOT reliably dodge this: Next.js's own
25
+ * dev-mode console interception forwards through whatever `console.error`
26
+ * is CURRENT at call time, not the function it originally wrapped, so a
27
+ * stale capture can still loop back into a patched `console.error`.)
10
28
  *
11
29
  * A component stuck in a render-error loop calls `console.error` on every
12
30
  * render — `reportError`'s own dedup/cap (on by default, see
@@ -1,14 +1,32 @@
1
- import reportError from './report_error';
1
+ import reportError, { consoleOverrideState } from './report_error';
2
2
  import stringifyUnknown from './stringify_unknown';
3
3
  import { defaultIgnoredConsoleErrors } from './default_ignored_console_errors';
4
4
  /**
5
5
  * Replaces the global `console.error` so every `console.error(...)` call is
6
6
  * also routed through `config.errorHandling.onError`/`reportError` — the
7
- * original `console.error` still runs afterwards, nothing is swallowed.
8
- * Safe to call more than once (a no-op after the first call in this JS
9
- * realm — call it separately on the server and on the client, each has its
10
- * own `console`). Only takes effect when `config.errorHandling.overrideConsoleError`
11
- * is `true`.
7
+ * original `console.error` still runs afterwards, nothing is swallowed by
8
+ * default. Safe to call more than once (a no-op after the first call in
9
+ * this JS realm — call it separately on the server and on the client, each
10
+ * has its own `console`). Only takes effect when
11
+ * `config.errorHandling.overrideConsoleError` is `true`.
12
+ *
13
+ * On the client ONLY (`isClient: true`), passing
14
+ * `config.errorHandling.suppressClientConsoleError: true` skips the
15
+ * browser's own `console.error` output entirely once a call has been
16
+ * routed to `onError`/`reportError` — the error is still reported, it just
17
+ * never shows up in browser devtools. Has no effect server-side.
18
+ *
19
+ * Sets `consoleOverrideState.active = true` in `report_error.ts` — once
20
+ * installed, THIS override becomes the sole place that ever calls the real
21
+ * console for a report (it already logs the raw message below, before
22
+ * calling `reportError`), and `reportError`'s own console-logging step
23
+ * stays out of the loop entirely. Without that, `reportError`'s own
24
+ * fallback log would call `console.error` again — landing right back on
25
+ * this override and recursing. (An "original console.error, captured once
26
+ * at module load" reference does NOT reliably dodge this: Next.js's own
27
+ * dev-mode console interception forwards through whatever `console.error`
28
+ * is CURRENT at call time, not the function it originally wrapped, so a
29
+ * stale capture can still loop back into a patched `console.error`.)
12
30
  *
13
31
  * A component stuck in a render-error loop calls `console.error` on every
14
32
  * render — `reportError`'s own dedup/cap (on by default, see
@@ -34,8 +52,12 @@ export default function installConsoleErrorOverride(config, isClient) {
34
52
  if (console.error.__isErrorHandlingOverride)
35
53
  return;
36
54
  const originalConsoleError = console.error.bind(console);
55
+ consoleOverrideState.active = true;
56
+ const suppressOnClient = isClient === true && config.errorHandling?.suppressClientConsoleError === true;
37
57
  const override = (message, ...optionalParams) => {
38
- originalConsoleError(message, ...optionalParams);
58
+ if (!suppressOnClient) {
59
+ originalConsoleError(message, ...optionalParams);
60
+ }
39
61
  const stringified = stringifyUnknown(message, isClient);
40
62
  const ignoreList = config.errorHandling?.ignoreConsoleErrors ?? defaultIgnoredConsoleErrors;
41
63
  if (ignoreList.some((ignored) => stringified.includes(ignored)))
@@ -3,18 +3,23 @@ export interface ReportErrorConfig {
3
3
  errorHandling?: ErrorHandlingRoutingConfig;
4
4
  generate?: GenerateRoutingConfig;
5
5
  }
6
+ export declare const consoleOverrideState: {
7
+ active: boolean;
8
+ };
6
9
  /**
7
- * Reports `params`: logs `params.formattedMessage` via the real
8
- * `console.error` (unless `config.errorHandling.logToConsole` is `false`)
9
- * AND calls `config.errorHandling.onError` when setboth run, not one
10
- * instead of the other, so wiring `onError` (Sentry, Telegram, etc) never
11
- * silently loses the console output. Skips reporting entirely when
10
+ * Reports `params`: logs `params.formattedMessage` via `console.error`
11
+ * (unless `config.errorHandling.logToConsole` is `false`, or
12
+ * `installConsoleErrorOverride` is activein which case IT already did
13
+ * the console logging before calling this) AND calls
14
+ * `config.errorHandling.onError` when set both run, not one instead of
15
+ * the other, so wiring `onError` (Sentry, Telegram, etc) never silently
16
+ * loses the console output. Skips reporting entirely when
12
17
  * `config.errorHandling.enable === false`, `params.consent` is set and not
13
18
  * `true` (reporting to a third party without cookie consent can itself be
14
19
  * GDPR-relevant), or dedup throttles it (on by default — see
15
20
  * `errorHandling.dedup`/`throttleMs`/`resetDedup`). Never throws — a broken
16
- * `onError` must not mask the original error (falls back to logging via the
17
- * real console instead).
21
+ * `onError` must not mask the original error (falls back to logging via
22
+ * `console.error` instead, when the override isn't already handling that).
18
23
  *
19
24
  * Always overwrites `params.formattedMessage` with a fresh
20
25
  * `formatErrorMessage(params)` before reporting — a human-readable one-line
@@ -22,15 +27,20 @@ export interface ReportErrorConfig {
22
27
  * instead of the raw `error`/`params` object, for a default reporter (or a
23
28
  * simple `onError`) to print directly.
24
29
  *
25
- * When `config.generate?.getCloudflareContext` is set, `waitUntil` is called
26
- * SYNCHRONOUSLY, in the same tick, with the `callOnError(...)` promise —
27
- * Cloudflare Workers only extends the request's lifetime for work already
28
- * registered with `waitUntil` by the time the handler returns; deferring
29
- * that call through an extra microtask (e.g. `Promise.resolve().then(...)`)
30
- * risks the isolate tearing down the request before `waitUntil` is ever
31
- * actually invoked, silently dropping the report. Falls back to awaiting
32
- * `onError` directly when `getCloudflareContext`/`ctx.waitUntil` is unset or
33
- * unavailable (e.g. outside a Cloudflare Worker).
30
+ * When `config.generate?.getCloudflareContext` is set AND `params.isClient`
31
+ * is not `true`, `waitUntil` is called SYNCHRONOUSLY, in the same tick, with
32
+ * the `callOnError(...)` promise — Cloudflare Workers only extends the
33
+ * request's lifetime for work already registered with `waitUntil` by the
34
+ * time the handler returns; deferring that call through an extra microtask
35
+ * (e.g. `Promise.resolve().then(...)`) risks the isolate tearing down the
36
+ * request before `waitUntil` is ever actually invoked, silently dropping
37
+ * the report. `getCloudflareContext` is never called at all for a
38
+ * client-originated report (`params.isClient: true`) it only exists
39
+ * server-side inside a Cloudflare Worker and throws synchronously (not a
40
+ * rejected promise) when called anywhere else, including the browser.
41
+ * Falls back to awaiting `onError` directly when `getCloudflareContext`/
42
+ * `ctx.waitUntil` is unset, unavailable (e.g. outside a Cloudflare Worker),
43
+ * or skipped for a client report.
34
44
  *
35
45
  * Passing `params.error` as `null`/`undefined` with `errorHandling.resetDedup: true`
36
46
  * and nothing else is a valid "reset-only" call: the dedup state clears and
@@ -1,13 +1,22 @@
1
1
  import formatErrorMessage from './format_error_message';
2
2
  import stringifyUnknown from './stringify_unknown';
3
3
  const DEFAULT_THROTTLE_MS = 5000;
4
- // Captured once at module load BEFORE `installConsoleErrorOverride` can
5
- // ever patch `console.error` so `callOnError`'s own fallback/always-log
6
- // path calls the real console, never the override. Calling a possibly-
7
- // patched `console.error` here would loop straight back into
8
- // `reportError` (the override's job is to call `reportError`), producing
9
- // a duplicate (or, if `onError` itself throws, an infinite) report.
10
- const originalConsoleError = console.error.bind(console);
4
+ // Set by `installConsoleErrorOverride` once it patches `console.error`.
5
+ // When active, THAT override is the sole place that ever calls the real
6
+ // console for a report it already logs the raw message itself, before
7
+ // calling `reportError` so `callOnError`'s own console-logging step must
8
+ // stay OUT of the loop entirely rather than trying to detect and skip a
9
+ // recursive call after the fact. Attempting the latter (capturing "the
10
+ // original console.error" at module load, or tagging messages with a
11
+ // marker) is unreliable: Next.js's own dev-mode console interception
12
+ // forwards through whatever `console.error` is CURRENT at call time, not
13
+ // the function it originally wrapped, so any capture-then-call-through
14
+ // strategy can still loop back into a patched `console.error`. Removing
15
+ // the second caller removes the race entirely — not a per-module boolean
16
+ // (that would only cover one bundle chunk's module instance of this file;
17
+ // this constant is exported so `installConsoleErrorOverride` can mutate it
18
+ // via a live binding regardless of chunk).
19
+ export const consoleOverrideState = { active: false };
11
20
  // Module-scope dedup state — safe by default only because a fresh JS realm
12
21
  // (isolate/Worker instance) starts with it cleared. In a long-lived server
13
22
  // process reused across many requests, pass `resetDedup: true` on the first
@@ -20,30 +29,39 @@ function buildDedupKey(params) {
20
29
  }
21
30
  async function callOnError(config, params) {
22
31
  const paramsWithFormattedMessage = { ...params, formattedMessage: formatErrorMessage(params) };
23
- if (config?.logToConsole !== false) {
24
- originalConsoleError(paramsWithFormattedMessage.formattedMessage);
32
+ // When `installConsoleErrorOverride` is active, it already logged the
33
+ // raw message to the real console BEFORE calling `reportError` — logging
34
+ // `formattedMessage` here too would be a second, redundant console
35
+ // write (differently formatted) for the exact same call, not a fix for
36
+ // a missing one.
37
+ if (config?.logToConsole !== false && !consoleOverrideState.active) {
38
+ console.error(paramsWithFormattedMessage.formattedMessage);
25
39
  }
26
40
  if (config?.onError) {
27
41
  try {
28
42
  await config.onError(paramsWithFormattedMessage);
29
43
  }
30
44
  catch {
31
- originalConsoleError(paramsWithFormattedMessage.formattedMessage);
45
+ if (!consoleOverrideState.active) {
46
+ console.error(paramsWithFormattedMessage.formattedMessage);
47
+ }
32
48
  }
33
49
  }
34
50
  }
35
51
  /**
36
- * Reports `params`: logs `params.formattedMessage` via the real
37
- * `console.error` (unless `config.errorHandling.logToConsole` is `false`)
38
- * AND calls `config.errorHandling.onError` when setboth run, not one
39
- * instead of the other, so wiring `onError` (Sentry, Telegram, etc) never
40
- * silently loses the console output. Skips reporting entirely when
52
+ * Reports `params`: logs `params.formattedMessage` via `console.error`
53
+ * (unless `config.errorHandling.logToConsole` is `false`, or
54
+ * `installConsoleErrorOverride` is activein which case IT already did
55
+ * the console logging before calling this) AND calls
56
+ * `config.errorHandling.onError` when set both run, not one instead of
57
+ * the other, so wiring `onError` (Sentry, Telegram, etc) never silently
58
+ * loses the console output. Skips reporting entirely when
41
59
  * `config.errorHandling.enable === false`, `params.consent` is set and not
42
60
  * `true` (reporting to a third party without cookie consent can itself be
43
61
  * GDPR-relevant), or dedup throttles it (on by default — see
44
62
  * `errorHandling.dedup`/`throttleMs`/`resetDedup`). Never throws — a broken
45
- * `onError` must not mask the original error (falls back to logging via the
46
- * real console instead).
63
+ * `onError` must not mask the original error (falls back to logging via
64
+ * `console.error` instead, when the override isn't already handling that).
47
65
  *
48
66
  * Always overwrites `params.formattedMessage` with a fresh
49
67
  * `formatErrorMessage(params)` before reporting — a human-readable one-line
@@ -51,15 +69,20 @@ async function callOnError(config, params) {
51
69
  * instead of the raw `error`/`params` object, for a default reporter (or a
52
70
  * simple `onError`) to print directly.
53
71
  *
54
- * When `config.generate?.getCloudflareContext` is set, `waitUntil` is called
55
- * SYNCHRONOUSLY, in the same tick, with the `callOnError(...)` promise —
56
- * Cloudflare Workers only extends the request's lifetime for work already
57
- * registered with `waitUntil` by the time the handler returns; deferring
58
- * that call through an extra microtask (e.g. `Promise.resolve().then(...)`)
59
- * risks the isolate tearing down the request before `waitUntil` is ever
60
- * actually invoked, silently dropping the report. Falls back to awaiting
61
- * `onError` directly when `getCloudflareContext`/`ctx.waitUntil` is unset or
62
- * unavailable (e.g. outside a Cloudflare Worker).
72
+ * When `config.generate?.getCloudflareContext` is set AND `params.isClient`
73
+ * is not `true`, `waitUntil` is called SYNCHRONOUSLY, in the same tick, with
74
+ * the `callOnError(...)` promise — Cloudflare Workers only extends the
75
+ * request's lifetime for work already registered with `waitUntil` by the
76
+ * time the handler returns; deferring that call through an extra microtask
77
+ * (e.g. `Promise.resolve().then(...)`) risks the isolate tearing down the
78
+ * request before `waitUntil` is ever actually invoked, silently dropping
79
+ * the report. `getCloudflareContext` is never called at all for a
80
+ * client-originated report (`params.isClient: true`) it only exists
81
+ * server-side inside a Cloudflare Worker and throws synchronously (not a
82
+ * rejected promise) when called anywhere else, including the browser.
83
+ * Falls back to awaiting `onError` directly when `getCloudflareContext`/
84
+ * `ctx.waitUntil` is unset, unavailable (e.g. outside a Cloudflare Worker),
85
+ * or skipped for a client report.
63
86
  *
64
87
  * Passing `params.error` as `null`/`undefined` with `errorHandling.resetDedup: true`
65
88
  * and nothing else is a valid "reset-only" call: the dedup state clears and
@@ -91,9 +114,13 @@ export default async function reportError(config, params) {
91
114
  lastDedupKey = dedupKey;
92
115
  lastReportedAt = now;
93
116
  }
94
- const waitUntil = config?.generate?.getCloudflareContext?.({ async: false })?.ctx?.waitUntil;
95
- if (waitUntil) {
96
- waitUntil(callOnError(errorHandling, params));
117
+ // `getCloudflareContext` only exists server-side inside a Cloudflare
118
+ // Worker (with `initOpenNextCloudflareForDev` set up in dev) — calling
119
+ // it at all for a client-originated report throws synchronously, before
120
+ // any "is it available" check can run.
121
+ const ctx = params.isClient ? undefined : config?.generate?.getCloudflareContext?.({ async: false })?.ctx;
122
+ if (ctx?.waitUntil) {
123
+ ctx.waitUntil(callOnError(errorHandling, params));
97
124
  return;
98
125
  }
99
126
  await callOnError(errorHandling, params);
@@ -1,4 +1,10 @@
1
1
  const MAX_FUNCTION_RESOLUTION_ATTEMPTS = 5;
2
+ // eslint-disable-next-line no-control-regex
3
+ const ANSI_ESCAPE_CODE_PATTERN = /\x1b\[[0-9;]*m/g;
4
+ /** Strips ANSI color/style escape codes (e.g. from Next.js's own pretty-printed terminal errors) — unreadable once JSON-escaped into a report. */
5
+ function stripAnsiCodes(value) {
6
+ return value.replace(ANSI_ESCAPE_CODE_PATTERN, '');
7
+ }
2
8
  function resolveFunctionError(value) {
3
9
  let result = value;
4
10
  try {
@@ -24,9 +30,9 @@ function resolveFunctionError(value) {
24
30
  */
25
31
  export default function stringifyUnknown(value, isClient, isNested = false) {
26
32
  if (typeof value === 'string')
27
- return value;
33
+ return stripAnsiCodes(value);
28
34
  if (value instanceof Error)
29
- return `${value.name}: ${value.message}\n\n${value.stack ?? ''}`;
35
+ return stripAnsiCodes(`${value.name}: ${value.message}\n\n${value.stack ?? ''}`);
30
36
  if (typeof value === 'function') {
31
37
  if (isClient)
32
38
  return '[Function]';
@@ -34,7 +40,7 @@ export default function stringifyUnknown(value, isClient, isNested = false) {
34
40
  return typeof resolved !== 'function' ? stringifyUnknown(resolved, isClient) : '[Function]';
35
41
  }
36
42
  try {
37
- return isNested ? JSON.stringify(value) : JSON.stringify(value, null, 2);
43
+ return stripAnsiCodes(isNested ? JSON.stringify(value) : JSON.stringify(value, null, 2));
38
44
  }
39
45
  catch {
40
46
  return '[Unserializable value]';
@@ -194,6 +194,18 @@ export interface ErrorHandlingRoutingConfig {
194
194
  * bigger behavior change than a plain function call.
195
195
  */
196
196
  overrideConsoleError?: boolean;
197
+ /**
198
+ * On the CLIENT only (`installConsoleErrorOverride(config, true)`),
199
+ * suppresses the browser's own `console.error(...)` output for a call
200
+ * once it's been routed to `onError`/`reportError` — the error is still
201
+ * reported (server-side logging, Sentry, etc), it just never shows up
202
+ * in browser devtools. Has no effect server-side (the server override
203
+ * always keeps logging normally — there's no "hide it from the
204
+ * terminal" use case). Only consulted when `overrideConsoleError` is
205
+ * `true`. Defaults to `false` (nothing is swallowed anywhere, matching
206
+ * `overrideConsoleError`'s own doc).
207
+ */
208
+ suppressClientConsoleError?: boolean;
197
209
  /**
198
210
  * Substrings matched against the stringified message of each
199
211
  * `console.error(...)` call (only consulted when `overrideConsoleError`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.6.6",
3
+ "version": "0.6.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",