@watchforge/browser 0.1.14 → 0.1.15

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/bin/watchforge.js CHANGED
@@ -344,6 +344,65 @@ export default function GlobalError(${propsSignature}) {
344
344
  log(`wrote ${path.relative(cwd, globalErrorPath)}`);
345
345
  }
346
346
 
347
+ function createNextAppError(cwd, layoutPath) {
348
+ const appDir = path.dirname(layoutPath);
349
+ const usesTs = projectUsesTypeScript(cwd);
350
+ const errorPath = path.join(appDir, usesTs ? "error.tsx" : "error.jsx");
351
+
352
+ if (fileExists(errorPath)) {
353
+ const content = fs.readFileSync(errorPath, "utf8");
354
+ if (content.includes("@watchforge/browser") || content.includes("captureException")) {
355
+ log(`${path.relative(cwd, errorPath)} already contains WatchForge setup`);
356
+ return;
357
+ }
358
+ log(`skipped ${path.relative(cwd, errorPath)} because it already exists`);
359
+ log("add captureException(error) there to report App Router segment render errors");
360
+ return;
361
+ }
362
+
363
+ const propsSignature = usesTs
364
+ ? `{\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}`
365
+ : `{ error, reset }`;
366
+
367
+ const content = `"use client";
368
+
369
+ import { useEffect } from "react";
370
+ import { captureException } from "@watchforge/browser";
371
+
372
+ export default function Error(${propsSignature}) {
373
+ useEffect(() => {
374
+ void captureException(error, {
375
+ tags: {
376
+ framework: "nextjs",
377
+ runtime: "error-boundary",
378
+ },
379
+ extra: {
380
+ digest: error.digest,
381
+ },
382
+ contexts: {
383
+ nextjs: {
384
+ error_boundary: "app-error",
385
+ digest: error.digest || null,
386
+ },
387
+ },
388
+ });
389
+ }, [error]);
390
+
391
+ return (
392
+ <div>
393
+ <h2>Something went wrong.</h2>
394
+ <button type="button" onClick={reset}>
395
+ Try again
396
+ </button>
397
+ </div>
398
+ );
399
+ }
400
+ `;
401
+
402
+ writeIfChanged(errorPath, content);
403
+ log(`wrote ${path.relative(cwd, errorPath)}`);
404
+ }
405
+
347
406
  function createNextInstrumentation(cwd, layoutPath) {
348
407
  const appRoot = path.dirname(layoutPath).includes(`${path.sep}src${path.sep}app`)
349
408
  ? path.join(cwd, "src")
@@ -399,6 +458,41 @@ export const onRequestError = watchForgeOnRequestError;
399
458
  log(`wrote ${path.relative(cwd, instrumentationPath)}`);
400
459
  }
401
460
 
461
+ function createNextClientInstrumentation(cwd, layoutPath) {
462
+ const appRoot = path.dirname(layoutPath).includes(`${path.sep}src${path.sep}app`)
463
+ ? path.join(cwd, "src")
464
+ : cwd;
465
+ const instrumentationClientPath = path.join(
466
+ appRoot,
467
+ projectUsesTypeScript(cwd) ? "instrumentation-client.ts" : "instrumentation-client.js"
468
+ );
469
+ const configImport = toImportPath(
470
+ path.dirname(instrumentationClientPath),
471
+ getConfigPath(cwd)
472
+ );
473
+
474
+ if (fileExists(instrumentationClientPath)) {
475
+ const content = fs.readFileSync(instrumentationClientPath, "utf8");
476
+ if (content.includes("@watchforge/browser") && content.includes("register(")) {
477
+ log(`${path.relative(cwd, instrumentationClientPath)} already contains WatchForge client setup`);
478
+ return;
479
+ }
480
+
481
+ log(`skipped ${path.relative(cwd, instrumentationClientPath)} because it already exists`);
482
+ log("add register(watchforgeConfig) there to install browser error handlers before hydration");
483
+ return;
484
+ }
485
+
486
+ const content = `import { register } from "@watchforge/browser";
487
+ import { watchforgeConfig } from "${configImport}";
488
+
489
+ register(watchforgeConfig);
490
+ `;
491
+
492
+ writeIfChanged(instrumentationClientPath, content);
493
+ log(`wrote ${path.relative(cwd, instrumentationClientPath)}`);
494
+ }
495
+
402
496
  function patchNextConfig(cwd) {
403
497
  const configPath = findNextConfig(cwd);
404
498
  if (!configPath) {
@@ -595,8 +689,10 @@ function initNextjs(args) {
595
689
  if (layoutPath) {
596
690
  patchNextConfig(cwd);
597
691
  patchAppRouter(cwd, layoutPath);
692
+ createNextAppError(cwd, layoutPath);
598
693
  createNextGlobalError(cwd, layoutPath);
599
694
  createNextInstrumentation(cwd, layoutPath);
695
+ createNextClientInstrumentation(cwd, layoutPath);
600
696
  return;
601
697
  }
602
698
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@watchforge/browser",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "main": "./src/index.js",
5
5
  "types": "./src/index.d.ts",
6
6
  "description": "WatchForge JavaScript SDK for browser JavaScript, Next.js, React, Node.js, and Express.js",
package/src/client.js CHANGED
@@ -22,6 +22,9 @@ let APP_ENV = "production";
22
22
  let RELEASE = null;
23
23
  let DEBUG = false;
24
24
  let CAPTURE_CONSOLE_ERRORS = true;
25
+ let CAPTURE_RESOURCE_ERRORS = true;
26
+ let CAPTURE_FAILED_REQUESTS = true;
27
+ let CAPTURE_NODE_WARNINGS = false;
25
28
 
26
29
  // Detect environment
27
30
  const isBrowser = typeof window !== "undefined";
@@ -34,6 +37,8 @@ let breadcrumbs = [];
34
37
  let browserInstrumentationInstalled = false;
35
38
  let nodeInstrumentationInstalled = false;
36
39
  let replayInitPromise = null;
40
+ let capturingGlobalError = false;
41
+ let browserRegisterKey = null;
37
42
 
38
43
  export function addBreadcrumb(breadcrumb) {
39
44
  const entry = {
@@ -58,6 +63,59 @@ function isWatchForgeInternalMessage(message) {
58
63
  return String(message || "").includes("WatchForge SDK");
59
64
  }
60
65
 
66
+ function normalizeException(error, fallbackMessage = "Unknown error") {
67
+ if (error instanceof Error) return error;
68
+
69
+ const message =
70
+ typeof error === "string"
71
+ ? error
72
+ : error?.message ||
73
+ error?.reason?.message ||
74
+ String(error || fallbackMessage);
75
+ const normalized = new Error(message || fallbackMessage);
76
+
77
+ if (error && typeof error === "object") {
78
+ if (typeof error.stack === "string") {
79
+ normalized.stack = error.stack;
80
+ }
81
+ if (typeof error.name === "string") {
82
+ normalized.name = error.name;
83
+ }
84
+ }
85
+
86
+ return normalized;
87
+ }
88
+
89
+ function captureGlobalException(error, context = {}) {
90
+ if (capturingGlobalError) return;
91
+ capturingGlobalError = true;
92
+ setTimeout(() => {
93
+ capturingGlobalError = false;
94
+ }, 0);
95
+
96
+ void captureException(normalizeException(error, context.message), context);
97
+ }
98
+
99
+ function getElementDescriptor(element) {
100
+ if (!element || typeof element !== "object") return null;
101
+ const tag = element.tagName ? String(element.tagName).toLowerCase() : "unknown";
102
+ const src =
103
+ element.currentSrc ||
104
+ element.src ||
105
+ element.href ||
106
+ element.action ||
107
+ null;
108
+ return {
109
+ tag,
110
+ url: src ? String(src) : null,
111
+ id: element.id || null,
112
+ className:
113
+ typeof element.className === "string"
114
+ ? element.className
115
+ : null,
116
+ };
117
+ }
118
+
61
119
  // Simple browser environment detectors (best-effort, not 100% accurate)
62
120
  function getBrowserContext() {
63
121
  if (!isBrowser) return null;
@@ -163,8 +221,7 @@ function setupBrowserInstrumentation() {
163
221
  if (browserInstrumentationInstalled) return;
164
222
  browserInstrumentationInstalled = true;
165
223
 
166
- // Global error handlers (already existed but keep here with breadcrumbs)
167
- window.onerror = function (msg, url, line, col, error) {
224
+ const handleWindowError = function (msg, url, line, col, error) {
168
225
  addBreadcrumb({
169
226
  type: "error",
170
227
  level: "error",
@@ -172,20 +229,94 @@ function setupBrowserInstrumentation() {
172
229
  message: String(msg),
173
230
  data: { url, line, col },
174
231
  });
175
- void captureException(error || msg);
232
+ captureGlobalException(error || msg, {
233
+ tags: {
234
+ handled: false,
235
+ mechanism: "window.onerror",
236
+ },
237
+ extra: { url, line, col },
238
+ message: String(msg),
239
+ });
176
240
  };
177
241
 
178
- window.onunhandledrejection = function (event) {
242
+ const handleUnhandledRejection = function (event) {
243
+ const reason = event?.reason;
179
244
  addBreadcrumb({
180
245
  type: "error",
181
246
  level: "error",
182
247
  category: "unhandledrejection",
183
- message: String(event.reason),
248
+ message: String(reason),
184
249
  data: {},
185
250
  });
186
- void captureException(event.reason);
251
+ captureGlobalException(reason, {
252
+ tags: {
253
+ handled: false,
254
+ mechanism: "unhandledrejection",
255
+ },
256
+ });
187
257
  };
188
258
 
259
+ const previousOnError = window.onerror;
260
+ window.onerror = function (msg, url, line, col, error) {
261
+ handleWindowError(msg, url, line, col, error);
262
+ if (typeof previousOnError === "function") {
263
+ return previousOnError.call(window, msg, url, line, col, error);
264
+ }
265
+ return false;
266
+ };
267
+
268
+ const previousUnhandledRejection = window.onunhandledrejection;
269
+ window.onunhandledrejection = function (event) {
270
+ handleUnhandledRejection(event);
271
+ if (typeof previousUnhandledRejection === "function") {
272
+ return previousUnhandledRejection.call(window, event);
273
+ }
274
+ return false;
275
+ };
276
+
277
+ window.addEventListener(
278
+ "error",
279
+ (event) => {
280
+ if (event?.error || event?.message) {
281
+ handleWindowError(
282
+ event.message,
283
+ event.filename,
284
+ event.lineno,
285
+ event.colno,
286
+ event.error
287
+ );
288
+ return;
289
+ }
290
+
291
+ const target = getElementDescriptor(event?.target);
292
+ if (!target) return;
293
+
294
+ addBreadcrumb({
295
+ type: "error",
296
+ level: "error",
297
+ category: "resource",
298
+ message: `Failed to load <${target.tag}> resource`,
299
+ data: target,
300
+ });
301
+
302
+ if (!CAPTURE_RESOURCE_ERRORS) return;
303
+
304
+ captureGlobalException(
305
+ new Error(`Failed to load <${target.tag}> resource${target.url ? `: ${target.url}` : ""}`),
306
+ {
307
+ tags: {
308
+ handled: false,
309
+ mechanism: "resource.error",
310
+ resource_tag: target.tag,
311
+ },
312
+ extra: target,
313
+ }
314
+ );
315
+ },
316
+ true
317
+ );
318
+ window.addEventListener("unhandledrejection", handleUnhandledRejection);
319
+
189
320
  // Console breadcrumbs
190
321
  ["log", "info", "warn", "error", "debug"].forEach((level) => {
191
322
  if (!console[level]) return;
@@ -348,6 +479,20 @@ function setupBrowserInstrumentation() {
348
479
  duration_ms: duration,
349
480
  },
350
481
  });
482
+ if (CAPTURE_FAILED_REQUESTS) {
483
+ void captureException(error, {
484
+ tags: {
485
+ handled: true,
486
+ mechanism: "fetch",
487
+ operation: "http.client",
488
+ },
489
+ extra: {
490
+ url,
491
+ method,
492
+ duration_ms: duration,
493
+ },
494
+ });
495
+ }
351
496
  throw error;
352
497
  }
353
498
  };
@@ -373,9 +518,10 @@ function setupBrowserInstrumentation() {
373
518
  const start = Date.now();
374
519
  xhr.addEventListener("loadend", () => {
375
520
  const duration = Date.now() - start;
521
+ const level = xhr.status >= 500 || xhr.status === 0 ? "error" : "info";
376
522
  addBreadcrumb({
377
523
  type: "http",
378
- level: "info",
524
+ level,
379
525
  category: "http",
380
526
  message: `${method} ${url}`,
381
527
  data: {
@@ -386,6 +532,20 @@ function setupBrowserInstrumentation() {
386
532
  },
387
533
  });
388
534
  });
535
+ xhr.addEventListener("error", () => {
536
+ if (!CAPTURE_FAILED_REQUESTS) return;
537
+ void captureException(new Error(`XMLHttpRequest failed: ${method || "GET"} ${url || ""}`), {
538
+ tags: {
539
+ handled: true,
540
+ mechanism: "xhr",
541
+ operation: "http.client",
542
+ },
543
+ extra: {
544
+ url,
545
+ method,
546
+ },
547
+ });
548
+ });
389
549
  origSend.apply(xhr, sendArgs);
390
550
  };
391
551
 
@@ -431,13 +591,41 @@ export function register({
431
591
  ignoreClass = "rr-ignore",
432
592
  maskTextClass = "rr-mask",
433
593
  captureConsoleErrors = true,
594
+ captureResourceErrors = true,
595
+ captureFailedRequests = true,
596
+ captureNodeWarnings = false,
434
597
  projectRoot = null,
435
598
  }) {
599
+ const nextBrowserRegisterKey = isBrowser
600
+ ? JSON.stringify({
601
+ dsn,
602
+ app_env,
603
+ release,
604
+ debug,
605
+ replaysSessionSampleRate,
606
+ replaysOnErrorSampleRate,
607
+ maskAllInputs,
608
+ blockClass,
609
+ ignoreClass,
610
+ maskTextClass,
611
+ captureConsoleErrors,
612
+ captureResourceErrors,
613
+ captureFailedRequests,
614
+ })
615
+ : null;
616
+
617
+ if (isBrowser && browserRegisterKey === nextBrowserRegisterKey) {
618
+ return;
619
+ }
620
+
436
621
  DSN = dsn;
437
622
  APP_ENV = app_env;
438
623
  RELEASE = release;
439
624
  DEBUG = debug;
440
625
  CAPTURE_CONSOLE_ERRORS = Boolean(captureConsoleErrors);
626
+ CAPTURE_RESOURCE_ERRORS = Boolean(captureResourceErrors);
627
+ CAPTURE_FAILED_REQUESTS = Boolean(captureFailedRequests);
628
+ CAPTURE_NODE_WARNINGS = Boolean(captureNodeWarnings);
441
629
  setProjectRoot(
442
630
  projectRoot ||
443
631
  (isNode ? process.env.WATCHFORGE_PROJECT_ROOT || process.env.INIT_CWD : null)
@@ -467,6 +655,7 @@ export function register({
467
655
 
468
656
  // Browser: Set up full instrumentation (errors, breadcrumbs, HTTP, navigation)
469
657
  if (isBrowser) {
658
+ browserRegisterKey = nextBrowserRegisterKey;
470
659
  setupBrowserInstrumentation();
471
660
  replayInitPromise = initReplay({
472
661
  replaysSessionSampleRate,
@@ -491,6 +680,40 @@ export function register({
491
680
  process.on("unhandledRejection", (reason) => {
492
681
  void captureException(reason);
493
682
  });
683
+
684
+ process.on("multipleResolves", (type, promise, reason) => {
685
+ void captureException(
686
+ normalizeException(reason, `Node.js promise multipleResolves: ${type}`),
687
+ {
688
+ tags: {
689
+ handled: false,
690
+ mechanism: "process.multipleResolves",
691
+ type,
692
+ },
693
+ }
694
+ );
695
+ });
696
+
697
+ process.on("warning", (warning) => {
698
+ addBreadcrumb({
699
+ type: "warning",
700
+ level: "warning",
701
+ category: "process.warning",
702
+ message: warning?.message || String(warning),
703
+ data: {
704
+ name: warning?.name || null,
705
+ code: warning?.code || null,
706
+ },
707
+ });
708
+
709
+ if (!CAPTURE_NODE_WARNINGS) return;
710
+ void captureException(normalizeException(warning, "Node.js process warning"), {
711
+ tags: {
712
+ handled: true,
713
+ mechanism: "process.warning",
714
+ },
715
+ });
716
+ });
494
717
  }
495
718
  }
496
719
 
package/src/express.d.ts CHANGED
@@ -10,3 +10,15 @@ export function expressMiddleware(): (
10
10
  res: any,
11
11
  next: (error?: unknown) => void
12
12
  ) => void;
13
+
14
+ export type ExpressHandler = (
15
+ req: any,
16
+ res: any,
17
+ next?: (error?: unknown) => void
18
+ ) => unknown | Promise<unknown>;
19
+
20
+ export function withWatchForgeExpressHandler<T extends ExpressHandler>(
21
+ handler: T
22
+ ): T;
23
+
24
+ export const wrapExpressHandler: typeof withWatchForgeExpressHandler;
package/src/express.js CHANGED
@@ -87,6 +87,75 @@ export function expressMiddleware() {
87
87
  };
88
88
  }
89
89
 
90
+ export function withWatchForgeExpressHandler(handler) {
91
+ return async function watchForgeExpressHandler(req, res, next) {
92
+ try {
93
+ return await handler(req, res, next);
94
+ } catch (error) {
95
+ await captureException(error, {
96
+ user: getUserContext(req),
97
+ request: getExpressRequestContext(req),
98
+ tags: {
99
+ framework: "express",
100
+ route: req.route?.path || req.path,
101
+ runtime: "server",
102
+ },
103
+ extra: {
104
+ params: req.params || {},
105
+ },
106
+ contexts: {
107
+ express: {
108
+ route: req.route?.path || req.path,
109
+ base_url: req.baseUrl || null,
110
+ handler_wrapper: true,
111
+ },
112
+ },
113
+ });
114
+ if (typeof next === "function") {
115
+ return next(error);
116
+ }
117
+ throw error;
118
+ }
119
+ };
120
+ }
121
+
122
+ export const wrapExpressHandler = withWatchForgeExpressHandler;
123
+
124
+ function getRequestIp(req) {
125
+ return (
126
+ req.ip ||
127
+ req.connection?.remoteAddress ||
128
+ req.socket?.remoteAddress ||
129
+ (req.connection?.socket ? req.connection.socket.remoteAddress : null) ||
130
+ req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
131
+ req.headers["x-real-ip"] ||
132
+ null
133
+ );
134
+ }
135
+
136
+ function getUserContext(req) {
137
+ const ip = getRequestIp(req);
138
+ return req.user
139
+ ? {
140
+ id: String(req.user.id),
141
+ email: req.user.email || null,
142
+ username: req.user.username || null,
143
+ ip_address: ip,
144
+ }
145
+ : null;
146
+ }
147
+
148
+ function getExpressRequestContext(req) {
149
+ return {
150
+ url: `${req.protocol}://${req.get("host")}${req.originalUrl || req.url}`,
151
+ method: req.method,
152
+ headers: sanitizeHeaders(req.headers),
153
+ query: req.query || {},
154
+ body: req.body || {},
155
+ ip: getRequestIp(req),
156
+ };
157
+ }
158
+
90
159
  function sanitizeHeaders(headers) {
91
160
  const sanitized = {};
92
161
  for (const [key, value] of Object.entries(headers)) {
package/src/index.d.ts CHANGED
@@ -10,6 +10,9 @@ export interface WatchForgeRegisterOptions {
10
10
  ignoreClass?: string;
11
11
  maskTextClass?: string;
12
12
  captureConsoleErrors?: boolean;
13
+ captureResourceErrors?: boolean;
14
+ captureFailedRequests?: boolean;
15
+ captureNodeWarnings?: boolean;
13
16
  projectRoot?: string | null;
14
17
  }
15
18
 
@@ -41,3 +44,10 @@ export {
41
44
  Transaction,
42
45
  Span,
43
46
  } from "./tracing.js";
47
+
48
+ export {
49
+ expressMiddleware,
50
+ expressRequestMiddleware,
51
+ withWatchForgeExpressHandler,
52
+ wrapExpressHandler,
53
+ } from "./express.js";
package/src/index.js CHANGED
@@ -13,4 +13,9 @@ export { init } from "./client.js";
13
13
  export { startTransaction, getCurrentTransaction, finishTransaction, Transaction, Span } from "./tracing.js";
14
14
 
15
15
  // Export framework integrations
16
- export { expressMiddleware, expressRequestMiddleware } from "./express.js";
16
+ export {
17
+ expressMiddleware,
18
+ expressRequestMiddleware,
19
+ withWatchForgeExpressHandler,
20
+ wrapExpressHandler,
21
+ } from "./express.js";
@@ -45,3 +45,18 @@ export type NextRouteHandler<TContext = unknown> = (
45
45
  export function withWatchForgeRouteHandler<TContext = unknown>(
46
46
  handler: NextRouteHandler<TContext>
47
47
  ): NextRouteHandler<TContext>;
48
+
49
+ export function withWatchForgeServerAction<TArgs extends unknown[], TResult>(
50
+ action: (...args: TArgs) => TResult | Promise<TResult>,
51
+ actionName?: string
52
+ ): (...args: TArgs) => Promise<TResult>;
53
+
54
+ export type NextApiHandler = (req: any, res: any) => unknown | Promise<unknown>;
55
+
56
+ export function withWatchForgeApiHandler<T extends NextApiHandler>(
57
+ handler: T
58
+ ): T;
59
+
60
+ export const wrapRouteHandler: typeof withWatchForgeRouteHandler;
61
+ export const wrapServerAction: typeof withWatchForgeServerAction;
62
+ export const wrapApiHandler: typeof withWatchForgeApiHandler;
@@ -116,3 +116,63 @@ export function withWatchForgeRouteHandler(handler) {
116
116
  }
117
117
  };
118
118
  }
119
+
120
+ export function withWatchForgeServerAction(action, actionName = action?.name || "serverAction") {
121
+ return async function watchForgeServerAction(...args) {
122
+ try {
123
+ return await action(...args);
124
+ } catch (error) {
125
+ await captureException(error, {
126
+ tags: {
127
+ framework: "nextjs",
128
+ runtime: "server",
129
+ mechanism: "server-action",
130
+ action: actionName,
131
+ },
132
+ contexts: {
133
+ nextjs: {
134
+ router: "app",
135
+ server_action: true,
136
+ action: actionName,
137
+ },
138
+ },
139
+ });
140
+ throw error;
141
+ }
142
+ };
143
+ }
144
+
145
+ export function withWatchForgeApiHandler(handler) {
146
+ return async function watchForgeApiHandler(req, res) {
147
+ try {
148
+ return await handler(req, res);
149
+ } catch (error) {
150
+ await captureException(error, {
151
+ request: {
152
+ url: req?.url || "",
153
+ method: req?.method || "",
154
+ headers: sanitizeRequestHeaders(req?.headers),
155
+ query: req?.query || {},
156
+ body: req?.body,
157
+ },
158
+ tags: {
159
+ framework: "nextjs",
160
+ runtime: "server",
161
+ router: "pages",
162
+ route_type: "api",
163
+ },
164
+ contexts: {
165
+ nextjs: {
166
+ router: "pages",
167
+ api_route: true,
168
+ },
169
+ },
170
+ });
171
+ throw error;
172
+ }
173
+ };
174
+ }
175
+
176
+ export const wrapRouteHandler = withWatchForgeRouteHandler;
177
+ export const wrapServerAction = withWatchForgeServerAction;
178
+ export const wrapApiHandler = withWatchForgeApiHandler;
package/src/next.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { WatchForgeRegisterOptions } from "./index.js";
4
4
  export interface WatchForgeProviderProps {
5
5
  options: WatchForgeRegisterOptions;
6
6
  children?: ReactNode;
7
+ fallback?: ReactNode;
7
8
  }
8
9
 
9
10
  export function WatchForgeProvider(
package/src/next.js CHANGED
@@ -2,11 +2,14 @@
2
2
 
3
3
  import React, { useEffect } from "react";
4
4
  import { register } from "./client.js";
5
+ import { ErrorBoundary } from "./react.js";
5
6
 
6
- export function WatchForgeProvider({ options, children = null }) {
7
+ export function WatchForgeProvider({ options, children = null, fallback = null }) {
7
8
  useEffect(() => {
8
9
  register(options);
9
10
  }, [options]);
10
11
 
11
- return React.createElement(React.Fragment, null, children);
12
+ if (!children) return null;
13
+
14
+ return React.createElement(ErrorBoundary, { fallback }, children);
12
15
  }
package/src/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Component, ReactNode } from "react";
1
+ import type { Component, ComponentType, ReactNode } from "react";
2
2
 
3
3
  export interface ErrorBoundaryProps {
4
4
  children?: ReactNode;
@@ -6,3 +6,10 @@ export interface ErrorBoundaryProps {
6
6
  }
7
7
 
8
8
  export class ErrorBoundary extends Component<ErrorBoundaryProps> {}
9
+
10
+ export function withWatchForgeErrorBoundary<P>(
11
+ component: ComponentType<P>,
12
+ options?: Pick<ErrorBoundaryProps, "fallback">
13
+ ): ComponentType<P>;
14
+
15
+ export const withErrorBoundary: typeof withWatchForgeErrorBoundary;
package/src/react.js CHANGED
@@ -50,3 +50,21 @@ export class ErrorBoundary extends React.Component {
50
50
  return this.props.children;
51
51
  }
52
52
  }
53
+
54
+ export function withWatchForgeErrorBoundary(Component, options = {}) {
55
+ const Wrapped = function WatchForgeErrorBoundaryWrapper(props) {
56
+ return React.createElement(
57
+ ErrorBoundary,
58
+ { fallback: options.fallback },
59
+ React.createElement(Component, props)
60
+ );
61
+ };
62
+
63
+ Wrapped.displayName = `withWatchForgeErrorBoundary(${
64
+ Component.displayName || Component.name || "Component"
65
+ })`;
66
+
67
+ return Wrapped;
68
+ }
69
+
70
+ export const withErrorBoundary = withWatchForgeErrorBoundary;