@watchforge/browser 0.1.14 → 0.1.16

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.16",
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,10 @@ 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;
28
+ let CAPTURE_NODE_MULTIPLE_RESOLVES = false;
25
29
 
26
30
  // Detect environment
27
31
  const isBrowser = typeof window !== "undefined";
@@ -34,6 +38,8 @@ let breadcrumbs = [];
34
38
  let browserInstrumentationInstalled = false;
35
39
  let nodeInstrumentationInstalled = false;
36
40
  let replayInitPromise = null;
41
+ let capturingGlobalError = false;
42
+ let browserRegisterKey = null;
37
43
 
38
44
  export function addBreadcrumb(breadcrumb) {
39
45
  const entry = {
@@ -58,6 +64,59 @@ function isWatchForgeInternalMessage(message) {
58
64
  return String(message || "").includes("WatchForge SDK");
59
65
  }
60
66
 
67
+ function normalizeException(error, fallbackMessage = "Unknown error") {
68
+ if (error instanceof Error) return error;
69
+
70
+ const message =
71
+ typeof error === "string"
72
+ ? error
73
+ : error?.message ||
74
+ error?.reason?.message ||
75
+ String(error || fallbackMessage);
76
+ const normalized = new Error(message || fallbackMessage);
77
+
78
+ if (error && typeof error === "object") {
79
+ if (typeof error.stack === "string") {
80
+ normalized.stack = error.stack;
81
+ }
82
+ if (typeof error.name === "string") {
83
+ normalized.name = error.name;
84
+ }
85
+ }
86
+
87
+ return normalized;
88
+ }
89
+
90
+ function captureGlobalException(error, context = {}) {
91
+ if (capturingGlobalError) return;
92
+ capturingGlobalError = true;
93
+ setTimeout(() => {
94
+ capturingGlobalError = false;
95
+ }, 0);
96
+
97
+ void captureException(normalizeException(error, context.message), context);
98
+ }
99
+
100
+ function getElementDescriptor(element) {
101
+ if (!element || typeof element !== "object") return null;
102
+ const tag = element.tagName ? String(element.tagName).toLowerCase() : "unknown";
103
+ const src =
104
+ element.currentSrc ||
105
+ element.src ||
106
+ element.href ||
107
+ element.action ||
108
+ null;
109
+ return {
110
+ tag,
111
+ url: src ? String(src) : null,
112
+ id: element.id || null,
113
+ className:
114
+ typeof element.className === "string"
115
+ ? element.className
116
+ : null,
117
+ };
118
+ }
119
+
61
120
  // Simple browser environment detectors (best-effort, not 100% accurate)
62
121
  function getBrowserContext() {
63
122
  if (!isBrowser) return null;
@@ -163,8 +222,7 @@ function setupBrowserInstrumentation() {
163
222
  if (browserInstrumentationInstalled) return;
164
223
  browserInstrumentationInstalled = true;
165
224
 
166
- // Global error handlers (already existed but keep here with breadcrumbs)
167
- window.onerror = function (msg, url, line, col, error) {
225
+ const handleWindowError = function (msg, url, line, col, error) {
168
226
  addBreadcrumb({
169
227
  type: "error",
170
228
  level: "error",
@@ -172,20 +230,94 @@ function setupBrowserInstrumentation() {
172
230
  message: String(msg),
173
231
  data: { url, line, col },
174
232
  });
175
- void captureException(error || msg);
233
+ captureGlobalException(error || msg, {
234
+ tags: {
235
+ handled: false,
236
+ mechanism: "window.onerror",
237
+ },
238
+ extra: { url, line, col },
239
+ message: String(msg),
240
+ });
176
241
  };
177
242
 
178
- window.onunhandledrejection = function (event) {
243
+ const handleUnhandledRejection = function (event) {
244
+ const reason = event?.reason;
179
245
  addBreadcrumb({
180
246
  type: "error",
181
247
  level: "error",
182
248
  category: "unhandledrejection",
183
- message: String(event.reason),
249
+ message: String(reason),
184
250
  data: {},
185
251
  });
186
- void captureException(event.reason);
252
+ captureGlobalException(reason, {
253
+ tags: {
254
+ handled: false,
255
+ mechanism: "unhandledrejection",
256
+ },
257
+ });
187
258
  };
188
259
 
260
+ const previousOnError = window.onerror;
261
+ window.onerror = function (msg, url, line, col, error) {
262
+ handleWindowError(msg, url, line, col, error);
263
+ if (typeof previousOnError === "function") {
264
+ return previousOnError.call(window, msg, url, line, col, error);
265
+ }
266
+ return false;
267
+ };
268
+
269
+ const previousUnhandledRejection = window.onunhandledrejection;
270
+ window.onunhandledrejection = function (event) {
271
+ handleUnhandledRejection(event);
272
+ if (typeof previousUnhandledRejection === "function") {
273
+ return previousUnhandledRejection.call(window, event);
274
+ }
275
+ return false;
276
+ };
277
+
278
+ window.addEventListener(
279
+ "error",
280
+ (event) => {
281
+ if (event?.error || event?.message) {
282
+ handleWindowError(
283
+ event.message,
284
+ event.filename,
285
+ event.lineno,
286
+ event.colno,
287
+ event.error
288
+ );
289
+ return;
290
+ }
291
+
292
+ const target = getElementDescriptor(event?.target);
293
+ if (!target) return;
294
+
295
+ addBreadcrumb({
296
+ type: "error",
297
+ level: "error",
298
+ category: "resource",
299
+ message: `Failed to load <${target.tag}> resource`,
300
+ data: target,
301
+ });
302
+
303
+ if (!CAPTURE_RESOURCE_ERRORS) return;
304
+
305
+ captureGlobalException(
306
+ new Error(`Failed to load <${target.tag}> resource${target.url ? `: ${target.url}` : ""}`),
307
+ {
308
+ tags: {
309
+ handled: false,
310
+ mechanism: "resource.error",
311
+ resource_tag: target.tag,
312
+ },
313
+ extra: target,
314
+ }
315
+ );
316
+ },
317
+ true
318
+ );
319
+ window.addEventListener("unhandledrejection", handleUnhandledRejection);
320
+
189
321
  // Console breadcrumbs
190
322
  ["log", "info", "warn", "error", "debug"].forEach((level) => {
191
323
  if (!console[level]) return;
@@ -348,6 +480,20 @@ function setupBrowserInstrumentation() {
348
480
  duration_ms: duration,
349
481
  },
350
482
  });
483
+ if (CAPTURE_FAILED_REQUESTS) {
484
+ void captureException(error, {
485
+ tags: {
486
+ handled: true,
487
+ mechanism: "fetch",
488
+ operation: "http.client",
489
+ },
490
+ extra: {
491
+ url,
492
+ method,
493
+ duration_ms: duration,
494
+ },
495
+ });
496
+ }
351
497
  throw error;
352
498
  }
353
499
  };
@@ -373,9 +519,10 @@ function setupBrowserInstrumentation() {
373
519
  const start = Date.now();
374
520
  xhr.addEventListener("loadend", () => {
375
521
  const duration = Date.now() - start;
522
+ const level = xhr.status >= 500 || xhr.status === 0 ? "error" : "info";
376
523
  addBreadcrumb({
377
524
  type: "http",
378
- level: "info",
525
+ level,
379
526
  category: "http",
380
527
  message: `${method} ${url}`,
381
528
  data: {
@@ -386,6 +533,20 @@ function setupBrowserInstrumentation() {
386
533
  },
387
534
  });
388
535
  });
536
+ xhr.addEventListener("error", () => {
537
+ if (!CAPTURE_FAILED_REQUESTS) return;
538
+ void captureException(new Error(`XMLHttpRequest failed: ${method || "GET"} ${url || ""}`), {
539
+ tags: {
540
+ handled: true,
541
+ mechanism: "xhr",
542
+ operation: "http.client",
543
+ },
544
+ extra: {
545
+ url,
546
+ method,
547
+ },
548
+ });
549
+ });
389
550
  origSend.apply(xhr, sendArgs);
390
551
  };
391
552
 
@@ -431,13 +592,43 @@ export function register({
431
592
  ignoreClass = "rr-ignore",
432
593
  maskTextClass = "rr-mask",
433
594
  captureConsoleErrors = true,
595
+ captureResourceErrors = true,
596
+ captureFailedRequests = true,
597
+ captureNodeWarnings = false,
598
+ captureNodeMultipleResolves = false,
434
599
  projectRoot = null,
435
600
  }) {
601
+ const nextBrowserRegisterKey = isBrowser
602
+ ? JSON.stringify({
603
+ dsn,
604
+ app_env,
605
+ release,
606
+ debug,
607
+ replaysSessionSampleRate,
608
+ replaysOnErrorSampleRate,
609
+ maskAllInputs,
610
+ blockClass,
611
+ ignoreClass,
612
+ maskTextClass,
613
+ captureConsoleErrors,
614
+ captureResourceErrors,
615
+ captureFailedRequests,
616
+ })
617
+ : null;
618
+
619
+ if (isBrowser && browserRegisterKey === nextBrowserRegisterKey) {
620
+ return;
621
+ }
622
+
436
623
  DSN = dsn;
437
624
  APP_ENV = app_env;
438
625
  RELEASE = release;
439
626
  DEBUG = debug;
440
627
  CAPTURE_CONSOLE_ERRORS = Boolean(captureConsoleErrors);
628
+ CAPTURE_RESOURCE_ERRORS = Boolean(captureResourceErrors);
629
+ CAPTURE_FAILED_REQUESTS = Boolean(captureFailedRequests);
630
+ CAPTURE_NODE_WARNINGS = Boolean(captureNodeWarnings);
631
+ CAPTURE_NODE_MULTIPLE_RESOLVES = Boolean(captureNodeMultipleResolves);
441
632
  setProjectRoot(
442
633
  projectRoot ||
443
634
  (isNode ? process.env.WATCHFORGE_PROJECT_ROOT || process.env.INIT_CWD : null)
@@ -467,6 +658,7 @@ export function register({
467
658
 
468
659
  // Browser: Set up full instrumentation (errors, breadcrumbs, HTTP, navigation)
469
660
  if (isBrowser) {
661
+ browserRegisterKey = nextBrowserRegisterKey;
470
662
  setupBrowserInstrumentation();
471
663
  replayInitPromise = initReplay({
472
664
  replaysSessionSampleRate,
@@ -491,6 +683,52 @@ export function register({
491
683
  process.on("unhandledRejection", (reason) => {
492
684
  void captureException(reason);
493
685
  });
686
+
687
+ process.on("multipleResolves", (type, promise, reason) => {
688
+ addBreadcrumb({
689
+ type: "error",
690
+ level: "warning",
691
+ category: "process.multipleResolves",
692
+ message: `Node.js promise multipleResolves: ${type}`,
693
+ data: {
694
+ type,
695
+ reason: reason?.message || String(reason || ""),
696
+ },
697
+ });
698
+
699
+ if (!CAPTURE_NODE_MULTIPLE_RESOLVES) return;
700
+ void captureException(
701
+ normalizeException(reason, `Node.js promise multipleResolves: ${type}`),
702
+ {
703
+ tags: {
704
+ handled: true,
705
+ mechanism: "process.multipleResolves",
706
+ type,
707
+ },
708
+ }
709
+ );
710
+ });
711
+
712
+ process.on("warning", (warning) => {
713
+ addBreadcrumb({
714
+ type: "warning",
715
+ level: "warning",
716
+ category: "process.warning",
717
+ message: warning?.message || String(warning),
718
+ data: {
719
+ name: warning?.name || null,
720
+ code: warning?.code || null,
721
+ },
722
+ });
723
+
724
+ if (!CAPTURE_NODE_WARNINGS) return;
725
+ void captureException(normalizeException(warning, "Node.js process warning"), {
726
+ tags: {
727
+ handled: true,
728
+ mechanism: "process.warning",
729
+ },
730
+ });
731
+ });
494
732
  }
495
733
  }
496
734
 
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,10 @@ export interface WatchForgeRegisterOptions {
10
10
  ignoreClass?: string;
11
11
  maskTextClass?: string;
12
12
  captureConsoleErrors?: boolean;
13
+ captureResourceErrors?: boolean;
14
+ captureFailedRequests?: boolean;
15
+ captureNodeWarnings?: boolean;
16
+ captureNodeMultipleResolves?: boolean;
13
17
  projectRoot?: string | null;
14
18
  }
15
19
 
@@ -41,3 +45,10 @@ export {
41
45
  Transaction,
42
46
  Span,
43
47
  } from "./tracing.js";
48
+
49
+ export {
50
+ expressMiddleware,
51
+ expressRequestMiddleware,
52
+ withWatchForgeExpressHandler,
53
+ wrapExpressHandler,
54
+ } 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;