@watchforge/browser 0.1.6 → 0.1.9
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 +36 -3
- package/bin/watchforge.js +349 -17
- package/package.json +5 -1
- package/src/contexts.js +5 -1
- package/src/next-config.d.ts +6 -0
- package/src/next-config.js +135 -0
package/README.md
CHANGED
|
@@ -7,8 +7,9 @@ Browser, Next.js, React, Node.js, and Express SDK for WatchForge. **One call to
|
|
|
7
7
|
| Runtime / Framework | Import | What it captures |
|
|
8
8
|
| --- | --- | --- |
|
|
9
9
|
| Browser JavaScript | `@watchforge/browser` | Uncaught errors, unhandled promise rejections, breadcrumbs, browser/device/page/performance context |
|
|
10
|
-
| React
|
|
11
|
-
|
|
|
10
|
+
| React / Vite / Vue / Angular | `@watchforge/browser` | Browser errors, unhandled promise rejections, breadcrumbs, replay, browser/device/page/performance context |
|
|
11
|
+
| React | `@watchforge/browser/react` | Optional React `ErrorBoundary` component stack context |
|
|
12
|
+
| Next.js App Router | `@watchforge/browser/next` + `@watchforge/browser/next/server` | Client-side errors, replay, global app render errors, Node runtime unhandled errors, route handler helper |
|
|
12
13
|
| Node.js | `@watchforge/browser` or `@watchforge/browser/node` | `uncaughtException`, `unhandledRejection`, Node runtime/server context |
|
|
13
14
|
| Express.js | `@watchforge/browser/express` | Express request errors, request URL/method/headers/body/query, user/IP, route, duration |
|
|
14
15
|
|
|
@@ -47,7 +48,39 @@ npx @watchforge/browser -i nextjs \
|
|
|
47
48
|
--replays-on-error 1
|
|
48
49
|
```
|
|
49
50
|
|
|
50
|
-
The wizard installs `@watchforge/browser
|
|
51
|
+
The wizard installs `@watchforge/browser` and writes the files needed for a full Next.js setup:
|
|
52
|
+
|
|
53
|
+
- `watchforge.config.ts` or `watchforge.config.js`
|
|
54
|
+
- `app/**/watchforge-init.tsx` / `.jsx` for browser errors and session replay
|
|
55
|
+
- `app/**/global-error.tsx` / `.jsx` for Next.js app render errors surfaced through Next's error boundary
|
|
56
|
+
- `src/instrumentation.ts` / `.js` or `instrumentation.ts` / `.js` for server runtime registration
|
|
57
|
+
- patches `app/layout.tsx` / `.jsx` or `pages/_app.tsx` / `.jsx`
|
|
58
|
+
|
|
59
|
+
For route handlers, wrap handlers with `withWatchForgeRouteHandler` from `@watchforge/browser/next/server`.
|
|
60
|
+
|
|
61
|
+
## One-line setup for other JavaScript projects
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# React
|
|
65
|
+
npx @watchforge/browser -i react --dsn "https://PUBLIC_KEY@your-host/PROJECT_ID" --replays-on-error 1
|
|
66
|
+
|
|
67
|
+
# Vite
|
|
68
|
+
npx @watchforge/browser -i vite --dsn "https://PUBLIC_KEY@your-host/PROJECT_ID" --replays-on-error 1
|
|
69
|
+
|
|
70
|
+
# Vue
|
|
71
|
+
npx @watchforge/browser -i vue --dsn "https://PUBLIC_KEY@your-host/PROJECT_ID" --replays-on-error 1
|
|
72
|
+
|
|
73
|
+
# Angular
|
|
74
|
+
npx @watchforge/browser -i angular --dsn "https://PUBLIC_KEY@your-host/PROJECT_ID" --replays-on-error 1
|
|
75
|
+
|
|
76
|
+
# Express
|
|
77
|
+
npx @watchforge/browser -i express --dsn "https://PUBLIC_KEY@your-host/PROJECT_ID"
|
|
78
|
+
|
|
79
|
+
# Node.js
|
|
80
|
+
npx @watchforge/browser -i node --dsn "https://PUBLIC_KEY@your-host/PROJECT_ID"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For React/Vite/Vue/Angular, the wizard creates a browser init module and imports it from the detected entry file. For Express and Node.js, it creates a server setup file and prints the one or two lines to add to your server entry because middleware placement depends on each app.
|
|
51
84
|
|
|
52
85
|
## Framework Imports
|
|
53
86
|
|
package/bin/watchforge.js
CHANGED
|
@@ -10,10 +10,15 @@ WatchForge setup wizard
|
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
12
|
npx @watchforge/browser -i nextjs --dsn <dsn> [options]
|
|
13
|
-
npx @watchforge/browser
|
|
13
|
+
npx @watchforge/browser -i react --dsn <dsn> [options]
|
|
14
|
+
npx @watchforge/browser -i vite --dsn <dsn> [options]
|
|
15
|
+
npx @watchforge/browser -i vue --dsn <dsn> [options]
|
|
16
|
+
npx @watchforge/browser -i angular --dsn <dsn> [options]
|
|
17
|
+
npx @watchforge/browser -i express --dsn <dsn> [options]
|
|
18
|
+
npx @watchforge/browser -i node --dsn <dsn> [options]
|
|
14
19
|
|
|
15
20
|
Options:
|
|
16
|
-
-i, --integration <name> Framework integration
|
|
21
|
+
-i, --integration <name> Framework integration: nextjs, react, vite, vue, angular, express, node
|
|
17
22
|
--dsn <dsn> WatchForge DSN
|
|
18
23
|
--app-env <env> App environment (default: production)
|
|
19
24
|
--debug Enable SDK debug logging
|
|
@@ -131,6 +136,16 @@ function findPagesApp(cwd) {
|
|
|
131
136
|
return candidates.find(fileExists) || null;
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
function findNextConfig(cwd) {
|
|
140
|
+
const candidates = [
|
|
141
|
+
"next.config.ts",
|
|
142
|
+
"next.config.mts",
|
|
143
|
+
"next.config.js",
|
|
144
|
+
"next.config.mjs",
|
|
145
|
+
];
|
|
146
|
+
return candidates.map((candidate) => path.join(cwd, candidate)).find(fileExists) || null;
|
|
147
|
+
}
|
|
148
|
+
|
|
134
149
|
function writeIfChanged(filePath, content) {
|
|
135
150
|
if (fileExists(filePath) && fs.readFileSync(filePath, "utf8") === content) {
|
|
136
151
|
return false;
|
|
@@ -140,8 +155,58 @@ function writeIfChanged(filePath, content) {
|
|
|
140
155
|
return true;
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
function toImportPath(fromDir, targetPath) {
|
|
159
|
+
let importPath = path.relative(fromDir, targetPath).replace(/\\/g, "/");
|
|
160
|
+
importPath = importPath.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
161
|
+
if (!importPath.startsWith(".")) {
|
|
162
|
+
importPath = `./${importPath}`;
|
|
163
|
+
}
|
|
164
|
+
return importPath;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function toNodeImportPath(fromDir, targetPath) {
|
|
168
|
+
let importPath = path.relative(fromDir, targetPath).replace(/\\/g, "/");
|
|
169
|
+
importPath = importPath.replace(/\.(ts|tsx)$/, "");
|
|
170
|
+
if (!importPath.startsWith(".")) {
|
|
171
|
+
importPath = `./${importPath}`;
|
|
172
|
+
}
|
|
173
|
+
return importPath;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function projectUsesTypeScript(cwd) {
|
|
177
|
+
if (fileExists(path.join(cwd, "tsconfig.json"))) return true;
|
|
178
|
+
|
|
179
|
+
const srcDir = path.join(cwd, "src");
|
|
180
|
+
if (!fs.existsSync(srcDir) || !fs.statSync(srcDir).isDirectory()) return false;
|
|
181
|
+
|
|
182
|
+
const stack = [srcDir];
|
|
183
|
+
while (stack.length > 0) {
|
|
184
|
+
const dir = stack.pop();
|
|
185
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
186
|
+
const entryPath = path.join(dir, entry.name);
|
|
187
|
+
if (entry.isDirectory()) {
|
|
188
|
+
if (entry.name !== "node_modules" && entry.name !== ".next") {
|
|
189
|
+
stack.push(entryPath);
|
|
190
|
+
}
|
|
191
|
+
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getConfigPath(cwd) {
|
|
201
|
+
const tsConfig = path.join(cwd, "watchforge.config.ts");
|
|
202
|
+
const jsConfig = path.join(cwd, "watchforge.config.js");
|
|
203
|
+
if (fileExists(tsConfig)) return tsConfig;
|
|
204
|
+
if (fileExists(jsConfig)) return jsConfig;
|
|
205
|
+
return projectUsesTypeScript(cwd) ? tsConfig : jsConfig;
|
|
206
|
+
}
|
|
207
|
+
|
|
143
208
|
function createConfig(cwd, args) {
|
|
144
|
-
const configPath =
|
|
209
|
+
const configPath = getConfigPath(cwd);
|
|
145
210
|
const content = `export const watchforgeConfig = {
|
|
146
211
|
dsn: ${JSON.stringify(args.dsn)},
|
|
147
212
|
app_env: ${JSON.stringify(args.appEnv)},
|
|
@@ -153,6 +218,7 @@ function createConfig(cwd, args) {
|
|
|
153
218
|
`;
|
|
154
219
|
writeIfChanged(configPath, content);
|
|
155
220
|
log(`wrote ${path.relative(cwd, configPath)}`);
|
|
221
|
+
return configPath;
|
|
156
222
|
}
|
|
157
223
|
|
|
158
224
|
function installPackage(cwd, skipInstall) {
|
|
@@ -179,13 +245,12 @@ function installPackage(cwd, skipInstall) {
|
|
|
179
245
|
|
|
180
246
|
function patchAppRouter(cwd, layoutPath) {
|
|
181
247
|
const appDir = path.dirname(layoutPath);
|
|
182
|
-
const configPath =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const initPath = path.join(appDir, "watchforge-init.tsx");
|
|
248
|
+
const configPath = getConfigPath(cwd);
|
|
249
|
+
const configImport = toImportPath(appDir, configPath);
|
|
250
|
+
const initPath = path.join(
|
|
251
|
+
appDir,
|
|
252
|
+
projectUsesTypeScript(cwd) ? "watchforge-init.tsx" : "watchforge-init.jsx"
|
|
253
|
+
);
|
|
189
254
|
|
|
190
255
|
const initContent = `"use client";
|
|
191
256
|
|
|
@@ -213,10 +278,135 @@ export default function WatchForgeInit() {
|
|
|
213
278
|
log(`patched ${path.relative(cwd, layoutPath)}`);
|
|
214
279
|
}
|
|
215
280
|
|
|
281
|
+
function createNextGlobalError(cwd, layoutPath) {
|
|
282
|
+
const appDir = path.dirname(layoutPath);
|
|
283
|
+
const usesTs = projectUsesTypeScript(cwd);
|
|
284
|
+
const globalErrorPath = path.join(appDir, usesTs ? "global-error.tsx" : "global-error.jsx");
|
|
285
|
+
|
|
286
|
+
if (fileExists(globalErrorPath)) {
|
|
287
|
+
const content = fs.readFileSync(globalErrorPath, "utf8");
|
|
288
|
+
if (content.includes("@watchforge/browser") || content.includes("captureException")) {
|
|
289
|
+
log(`${path.relative(cwd, globalErrorPath)} already contains WatchForge setup`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
log(`skipped ${path.relative(cwd, globalErrorPath)} because it already exists`);
|
|
293
|
+
log("add captureException(error) there manually to report Next.js render errors");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const propsSignature = usesTs
|
|
298
|
+
? `{\n error,\n}: {\n error: Error & { digest?: string };\n}`
|
|
299
|
+
: `{ error }`;
|
|
300
|
+
|
|
301
|
+
const content = `"use client";
|
|
302
|
+
|
|
303
|
+
import { useEffect } from "react";
|
|
304
|
+
import { captureException } from "@watchforge/browser";
|
|
305
|
+
|
|
306
|
+
export default function GlobalError(${propsSignature}) {
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
void captureException(error, {
|
|
309
|
+
tags: {
|
|
310
|
+
framework: "nextjs",
|
|
311
|
+
runtime: "error-boundary",
|
|
312
|
+
},
|
|
313
|
+
extra: {
|
|
314
|
+
digest: error.digest,
|
|
315
|
+
},
|
|
316
|
+
contexts: {
|
|
317
|
+
nextjs: {
|
|
318
|
+
error_boundary: "global-error",
|
|
319
|
+
digest: error.digest || null,
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
}, [error]);
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<html>
|
|
327
|
+
<body>
|
|
328
|
+
<h2>Something went wrong.</h2>
|
|
329
|
+
</body>
|
|
330
|
+
</html>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
`;
|
|
334
|
+
|
|
335
|
+
writeIfChanged(globalErrorPath, content);
|
|
336
|
+
log(`wrote ${path.relative(cwd, globalErrorPath)}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function createNextInstrumentation(cwd, layoutPath) {
|
|
340
|
+
const appRoot = path.dirname(layoutPath).includes(`${path.sep}src${path.sep}app`)
|
|
341
|
+
? path.join(cwd, "src")
|
|
342
|
+
: cwd;
|
|
343
|
+
const instrumentationPath = path.join(
|
|
344
|
+
appRoot,
|
|
345
|
+
projectUsesTypeScript(cwd) ? "instrumentation.ts" : "instrumentation.js"
|
|
346
|
+
);
|
|
347
|
+
const configImport = toImportPath(path.dirname(instrumentationPath), getConfigPath(cwd));
|
|
348
|
+
|
|
349
|
+
if (fileExists(instrumentationPath)) {
|
|
350
|
+
const content = fs.readFileSync(instrumentationPath, "utf8");
|
|
351
|
+
if (content.includes("@watchforge/browser/next/server")) {
|
|
352
|
+
log(`${path.relative(cwd, instrumentationPath)} already contains WatchForge setup`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
log(`skipped ${path.relative(cwd, instrumentationPath)} because it already exists`);
|
|
356
|
+
log("add registerWatchForge(watchforgeConfig) there manually to report Next.js server runtime errors");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const content = `import { register as registerWatchForge } from "@watchforge/browser/next/server";
|
|
361
|
+
import { watchforgeConfig } from "${configImport}";
|
|
362
|
+
|
|
363
|
+
export async function register() {
|
|
364
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
365
|
+
registerWatchForge(watchforgeConfig);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
`;
|
|
369
|
+
|
|
370
|
+
writeIfChanged(instrumentationPath, content);
|
|
371
|
+
log(`wrote ${path.relative(cwd, instrumentationPath)}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function patchNextConfig(cwd) {
|
|
375
|
+
const configPath = findNextConfig(cwd);
|
|
376
|
+
if (!configPath) {
|
|
377
|
+
log("could not find next.config.* to patch for compiler error capture");
|
|
378
|
+
log('wrap your Next config with withWatchForgeConfig(config, watchforgeConfig) from "@watchforge/browser/next/config"');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let content = fs.readFileSync(configPath, "utf8");
|
|
383
|
+
if (content.includes("withWatchForgeConfig")) {
|
|
384
|
+
log(`${path.relative(cwd, configPath)} already contains WatchForge compiler setup`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const configImport = toImportPath(path.dirname(configPath), getConfigPath(cwd));
|
|
389
|
+
content = `import { withWatchForgeConfig } from "@watchforge/browser/next/config";\nimport { watchforgeConfig } from "${configImport}";\n${content}`;
|
|
390
|
+
|
|
391
|
+
const exportDefaultMatch = content.match(/export\s+default\s+([^;]+);/);
|
|
392
|
+
if (!exportDefaultMatch) {
|
|
393
|
+
log(`skipped ${path.relative(cwd, configPath)} because export default could not be patched automatically`);
|
|
394
|
+
log('wrap your exported config with withWatchForgeConfig(config, watchforgeConfig)');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
content = content.replace(
|
|
399
|
+
exportDefaultMatch[0],
|
|
400
|
+
`export default withWatchForgeConfig(${exportDefaultMatch[1]}, watchforgeConfig);`
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
fs.writeFileSync(configPath, content);
|
|
404
|
+
log(`patched ${path.relative(cwd, configPath)} for compiler error capture`);
|
|
405
|
+
}
|
|
406
|
+
|
|
216
407
|
function patchPagesRouter(cwd, appPath) {
|
|
217
408
|
const pagesDir = path.dirname(appPath);
|
|
218
|
-
const
|
|
219
|
-
const configImport = isSrcPages ? "../../watchforge.config" : "../watchforge.config";
|
|
409
|
+
const configImport = toImportPath(pagesDir, getConfigPath(cwd));
|
|
220
410
|
const content = fs.readFileSync(appPath, "utf8");
|
|
221
411
|
|
|
222
412
|
if (content.includes("register(watchforgeConfig)")) {
|
|
@@ -237,6 +427,133 @@ ${content.replace(
|
|
|
237
427
|
log(`patched ${path.relative(cwd, appPath)}`);
|
|
238
428
|
}
|
|
239
429
|
|
|
430
|
+
function findFirstFile(cwd, candidates) {
|
|
431
|
+
return candidates.map((candidate) => path.join(cwd, candidate)).find(fileExists) || null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function prependImportOnce(filePath, importLine) {
|
|
435
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
436
|
+
if (content.includes(importLine)) return false;
|
|
437
|
+
fs.writeFileSync(filePath, `${importLine}\n${content}`);
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function createBrowserInit(cwd, initRelativePath = null) {
|
|
442
|
+
const defaultPath = projectUsesTypeScript(cwd)
|
|
443
|
+
? "src/watchforge-init.ts"
|
|
444
|
+
: "src/watchforge-init.js";
|
|
445
|
+
const initPath = path.join(cwd, initRelativePath || defaultPath);
|
|
446
|
+
const configImport = toImportPath(path.dirname(initPath), getConfigPath(cwd));
|
|
447
|
+
const content = `import { register } from "@watchforge/browser";
|
|
448
|
+
import { watchforgeConfig } from "${configImport}";
|
|
449
|
+
|
|
450
|
+
register(watchforgeConfig);
|
|
451
|
+
`;
|
|
452
|
+
|
|
453
|
+
writeIfChanged(initPath, content);
|
|
454
|
+
log(`wrote ${path.relative(cwd, initPath)}`);
|
|
455
|
+
return initPath;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function initBrowserFramework(args, integration) {
|
|
459
|
+
const cwd = process.cwd();
|
|
460
|
+
if (!fileExists(path.join(cwd, "package.json"))) {
|
|
461
|
+
fail(`run this command from the root of your ${integration} project`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
createConfig(cwd, args);
|
|
465
|
+
installPackage(cwd, args.skipInstall);
|
|
466
|
+
|
|
467
|
+
const entryPath = findFirstFile(cwd, [
|
|
468
|
+
"src/main.tsx",
|
|
469
|
+
"src/main.jsx",
|
|
470
|
+
"src/main.ts",
|
|
471
|
+
"src/main.js",
|
|
472
|
+
"src/index.tsx",
|
|
473
|
+
"src/index.jsx",
|
|
474
|
+
"src/index.ts",
|
|
475
|
+
"src/index.js",
|
|
476
|
+
"main.ts",
|
|
477
|
+
"main.js",
|
|
478
|
+
"index.ts",
|
|
479
|
+
"index.js",
|
|
480
|
+
]);
|
|
481
|
+
|
|
482
|
+
const initPath = createBrowserInit(cwd);
|
|
483
|
+
if (!entryPath) {
|
|
484
|
+
log("could not find a browser entry file to patch");
|
|
485
|
+
log(`import "./${path.relative(cwd, initPath).replace(/\\/g, "/").replace(/\.(ts|js)$/, "")}" from your app entry`);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const initImport = toImportPath(path.dirname(entryPath), initPath);
|
|
490
|
+
if (prependImportOnce(entryPath, `import "${initImport}";`)) {
|
|
491
|
+
log(`patched ${path.relative(cwd, entryPath)}`);
|
|
492
|
+
} else {
|
|
493
|
+
log(`${path.relative(cwd, entryPath)} already imports WatchForge`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function initExpress(args) {
|
|
498
|
+
const cwd = process.cwd();
|
|
499
|
+
if (!fileExists(path.join(cwd, "package.json"))) {
|
|
500
|
+
fail("run this command from the root of your Express project");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
createConfig(cwd, args);
|
|
504
|
+
installPackage(cwd, args.skipInstall);
|
|
505
|
+
|
|
506
|
+
const setupPath = path.join(
|
|
507
|
+
cwd,
|
|
508
|
+
projectUsesTypeScript(cwd) ? "watchforge-express.ts" : "watchforge-express.js"
|
|
509
|
+
);
|
|
510
|
+
const configImport = toNodeImportPath(path.dirname(setupPath), getConfigPath(cwd));
|
|
511
|
+
const content = `import { register } from "@watchforge/browser/node";
|
|
512
|
+
import {
|
|
513
|
+
expressMiddleware,
|
|
514
|
+
expressRequestMiddleware,
|
|
515
|
+
} from "@watchforge/browser/express";
|
|
516
|
+
import { watchforgeConfig } from "${configImport}";
|
|
517
|
+
|
|
518
|
+
register(watchforgeConfig);
|
|
519
|
+
|
|
520
|
+
export { expressMiddleware, expressRequestMiddleware };
|
|
521
|
+
`;
|
|
522
|
+
|
|
523
|
+
writeIfChanged(setupPath, content);
|
|
524
|
+
log(`wrote ${path.relative(cwd, setupPath)}`);
|
|
525
|
+
log("add this near the top of your Express app:");
|
|
526
|
+
log(' import { expressRequestMiddleware, expressMiddleware } from "./watchforge-express";');
|
|
527
|
+
log(" app.use(expressRequestMiddleware());");
|
|
528
|
+
log("and add this after your routes:");
|
|
529
|
+
log(" app.use(expressMiddleware());");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function initNode(args) {
|
|
533
|
+
const cwd = process.cwd();
|
|
534
|
+
if (!fileExists(path.join(cwd, "package.json"))) {
|
|
535
|
+
fail("run this command from the root of your Node.js project");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
createConfig(cwd, args);
|
|
539
|
+
installPackage(cwd, args.skipInstall);
|
|
540
|
+
|
|
541
|
+
const setupPath = path.join(
|
|
542
|
+
cwd,
|
|
543
|
+
projectUsesTypeScript(cwd) ? "watchforge-node.ts" : "watchforge-node.js"
|
|
544
|
+
);
|
|
545
|
+
const configImport = toNodeImportPath(path.dirname(setupPath), getConfigPath(cwd));
|
|
546
|
+
const content = `import { register } from "@watchforge/browser/node";
|
|
547
|
+
import { watchforgeConfig } from "${configImport}";
|
|
548
|
+
|
|
549
|
+
register(watchforgeConfig);
|
|
550
|
+
`;
|
|
551
|
+
|
|
552
|
+
writeIfChanged(setupPath, content);
|
|
553
|
+
log(`wrote ${path.relative(cwd, setupPath)}`);
|
|
554
|
+
log('import "./watchforge-node"; as early as possible in your server entry file');
|
|
555
|
+
}
|
|
556
|
+
|
|
240
557
|
function initNextjs(args) {
|
|
241
558
|
const cwd = process.cwd();
|
|
242
559
|
if (!fileExists(path.join(cwd, "package.json"))) {
|
|
@@ -248,12 +565,16 @@ function initNextjs(args) {
|
|
|
248
565
|
|
|
249
566
|
const layoutPath = findNextLayout(cwd);
|
|
250
567
|
if (layoutPath) {
|
|
568
|
+
patchNextConfig(cwd);
|
|
251
569
|
patchAppRouter(cwd, layoutPath);
|
|
570
|
+
createNextGlobalError(cwd, layoutPath);
|
|
571
|
+
createNextInstrumentation(cwd, layoutPath);
|
|
252
572
|
return;
|
|
253
573
|
}
|
|
254
574
|
|
|
255
575
|
const appPath = findPagesApp(cwd);
|
|
256
576
|
if (appPath) {
|
|
577
|
+
patchNextConfig(cwd);
|
|
257
578
|
patchPagesRouter(cwd, appPath);
|
|
258
579
|
return;
|
|
259
580
|
}
|
|
@@ -272,13 +593,24 @@ if (!args.integration) {
|
|
|
272
593
|
fail("missing integration. Use: -i nextjs");
|
|
273
594
|
}
|
|
274
595
|
|
|
275
|
-
if (args.integration !== "nextjs") {
|
|
276
|
-
fail(`unsupported integration "${args.integration}". Currently supported: nextjs`);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
596
|
if (!args.dsn) {
|
|
280
597
|
fail("missing --dsn");
|
|
281
598
|
}
|
|
282
599
|
|
|
283
|
-
|
|
600
|
+
const integration = args.integration.toLowerCase();
|
|
601
|
+
|
|
602
|
+
if (integration === "nextjs" || integration === "next") {
|
|
603
|
+
initNextjs(args);
|
|
604
|
+
} else if (["react", "vite", "vue", "angular", "browser"].includes(integration)) {
|
|
605
|
+
initBrowserFramework(args, integration);
|
|
606
|
+
} else if (integration === "express") {
|
|
607
|
+
initExpress(args);
|
|
608
|
+
} else if (integration === "node" || integration === "nodejs") {
|
|
609
|
+
initNode(args);
|
|
610
|
+
} else {
|
|
611
|
+
fail(
|
|
612
|
+
`unsupported integration "${args.integration}". Supported: nextjs, react, vite, vue, angular, express, node`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
284
616
|
log("setup complete");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@watchforge/browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"types": "./src/next-server.d.ts",
|
|
34
34
|
"default": "./src/next-server.js"
|
|
35
35
|
},
|
|
36
|
+
"./next/config": {
|
|
37
|
+
"types": "./src/next-config.d.ts",
|
|
38
|
+
"default": "./src/next-config.js"
|
|
39
|
+
},
|
|
36
40
|
"./express": {
|
|
37
41
|
"types": "./src/express.d.ts",
|
|
38
42
|
"default": "./src/express.js"
|
package/src/contexts.js
CHANGED
|
@@ -73,10 +73,14 @@ export function getPerformanceContext() {
|
|
|
73
73
|
|
|
74
74
|
let nodeOsPromise = null;
|
|
75
75
|
|
|
76
|
+
// Hide from bundlers (Next.js/webpack) so client builds don't resolve Node built-ins.
|
|
77
|
+
const importNodeBuiltin = (specifier) =>
|
|
78
|
+
new Function("specifier", "return import(specifier)")(specifier);
|
|
79
|
+
|
|
76
80
|
async function getNodeOs() {
|
|
77
81
|
if (!isNode) return null;
|
|
78
82
|
if (!nodeOsPromise) {
|
|
79
|
-
nodeOsPromise =
|
|
83
|
+
nodeOsPromise = importNodeBuiltin("os").catch(() => null);
|
|
80
84
|
}
|
|
81
85
|
return nodeOsPromise;
|
|
82
86
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { captureException, register } from "./client.js";
|
|
2
|
+
|
|
3
|
+
const PLUGIN_NAME = "WatchForgeNextCompilerPlugin";
|
|
4
|
+
const sentCompilerErrors = new Set();
|
|
5
|
+
let registeredDsn = null;
|
|
6
|
+
|
|
7
|
+
function getLoc(rawError) {
|
|
8
|
+
const loc = rawError?.loc || rawError?.module?.loc;
|
|
9
|
+
|
|
10
|
+
if (typeof loc === "string") {
|
|
11
|
+
const match = loc.match(/(\d+)(?::(\d+))?/);
|
|
12
|
+
if (match) {
|
|
13
|
+
return {
|
|
14
|
+
line: Number(match[1]),
|
|
15
|
+
column: Number(match[2] || 1),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (loc && typeof loc === "object") {
|
|
21
|
+
const start = loc.start || loc;
|
|
22
|
+
return {
|
|
23
|
+
line: Number(start.line || start.lineNumber || 1),
|
|
24
|
+
column: Number(start.column || start.col || 1),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { line: 1, column: 1 };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getModulePath(rawError) {
|
|
32
|
+
return (
|
|
33
|
+
rawError?.module?.resource ||
|
|
34
|
+
rawError?.module?.resourceResolveData?.path ||
|
|
35
|
+
rawError?.file ||
|
|
36
|
+
rawError?.moduleName ||
|
|
37
|
+
null
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createCompilerException(rawError) {
|
|
42
|
+
const message =
|
|
43
|
+
rawError?.message ||
|
|
44
|
+
rawError?.details ||
|
|
45
|
+
String(rawError || "Next.js compiler error");
|
|
46
|
+
const error = new Error(message);
|
|
47
|
+
error.name = rawError?.name || "NextWebpackCompilationError";
|
|
48
|
+
|
|
49
|
+
const filePath = getModulePath(rawError);
|
|
50
|
+
if (filePath) {
|
|
51
|
+
const { line, column } = getLoc(rawError);
|
|
52
|
+
error.stack = `${error.name}: ${message}\n at Next.js compilation (${filePath}:${line}:${column})`;
|
|
53
|
+
if (rawError?.stack) {
|
|
54
|
+
error.stack += `\n${String(rawError.stack).split("\n").slice(1).join("\n")}`;
|
|
55
|
+
}
|
|
56
|
+
} else if (rawError?.stack) {
|
|
57
|
+
error.stack = rawError.stack;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return error;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getCompilerErrorFingerprint(rawError, compilerName, hash) {
|
|
64
|
+
const filePath = getModulePath(rawError) || "";
|
|
65
|
+
const loc = getLoc(rawError);
|
|
66
|
+
const message = rawError?.message || String(rawError || "");
|
|
67
|
+
return [hash || "", compilerName || "", filePath, loc.line, loc.column, message].join("|");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createCompilerPlugin() {
|
|
71
|
+
return {
|
|
72
|
+
name: PLUGIN_NAME,
|
|
73
|
+
apply(compiler) {
|
|
74
|
+
compiler.hooks.done.tapPromise(PLUGIN_NAME, async (stats) => {
|
|
75
|
+
if (!stats?.hasErrors?.()) return;
|
|
76
|
+
|
|
77
|
+
const errors = stats.compilation?.errors || [];
|
|
78
|
+
const compilerName = compiler.name || stats.compilation?.name || null;
|
|
79
|
+
|
|
80
|
+
for (const rawError of errors) {
|
|
81
|
+
const fingerprint = getCompilerErrorFingerprint(rawError, compilerName, stats.hash);
|
|
82
|
+
if (sentCompilerErrors.has(fingerprint)) continue;
|
|
83
|
+
sentCompilerErrors.add(fingerprint);
|
|
84
|
+
|
|
85
|
+
await captureException(createCompilerException(rawError), {
|
|
86
|
+
tags: {
|
|
87
|
+
framework: "nextjs",
|
|
88
|
+
runtime: "compiler",
|
|
89
|
+
compiler: compilerName,
|
|
90
|
+
},
|
|
91
|
+
contexts: {
|
|
92
|
+
nextjs: {
|
|
93
|
+
compiler_error: true,
|
|
94
|
+
module: getModulePath(rawError),
|
|
95
|
+
loc: rawError?.loc || rawError?.module?.loc || null,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
extra: {
|
|
99
|
+
details: rawError?.details || null,
|
|
100
|
+
module_identifier: rawError?.module?.identifier?.() || null,
|
|
101
|
+
module_name: rawError?.module?.readableIdentifier?.({}) || rawError?.moduleName || null,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ensureRegistered(options) {
|
|
111
|
+
if (!options?.dsn || registeredDsn === options.dsn) return;
|
|
112
|
+
registeredDsn = options.dsn;
|
|
113
|
+
register(options);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function withWatchForgeConfig(nextConfig = {}, watchforgeOptions = {}) {
|
|
117
|
+
ensureRegistered(watchforgeOptions);
|
|
118
|
+
|
|
119
|
+
const userWebpack = nextConfig.webpack;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
...nextConfig,
|
|
123
|
+
webpack(config, options) {
|
|
124
|
+
const finalConfig = userWebpack ? userWebpack(config, options) || config : config;
|
|
125
|
+
finalConfig.plugins = finalConfig.plugins || [];
|
|
126
|
+
|
|
127
|
+
const hasPlugin = finalConfig.plugins.some((plugin) => plugin?.name === PLUGIN_NAME);
|
|
128
|
+
if (!hasPlugin) {
|
|
129
|
+
finalConfig.plugins.push(createCompilerPlugin());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return finalConfig;
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|