@watchforge/browser 0.1.7 → 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/bin/watchforge.js CHANGED
@@ -136,6 +136,16 @@ function findPagesApp(cwd) {
136
136
  return candidates.find(fileExists) || null;
137
137
  }
138
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
+
139
149
  function writeIfChanged(filePath, content) {
140
150
  if (fileExists(filePath) && fs.readFileSync(filePath, "utf8") === content) {
141
151
  return false;
@@ -361,6 +371,39 @@ export async function register() {
361
371
  log(`wrote ${path.relative(cwd, instrumentationPath)}`);
362
372
  }
363
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
+
364
407
  function patchPagesRouter(cwd, appPath) {
365
408
  const pagesDir = path.dirname(appPath);
366
409
  const configImport = toImportPath(pagesDir, getConfigPath(cwd));
@@ -522,6 +565,7 @@ function initNextjs(args) {
522
565
 
523
566
  const layoutPath = findNextLayout(cwd);
524
567
  if (layoutPath) {
568
+ patchNextConfig(cwd);
525
569
  patchAppRouter(cwd, layoutPath);
526
570
  createNextGlobalError(cwd, layoutPath);
527
571
  createNextInstrumentation(cwd, layoutPath);
@@ -530,6 +574,7 @@ function initNextjs(args) {
530
574
 
531
575
  const appPath = findPagesApp(cwd);
532
576
  if (appPath) {
577
+ patchNextConfig(cwd);
533
578
  patchPagesRouter(cwd, appPath);
534
579
  return;
535
580
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@watchforge/browser",
3
- "version": "0.1.7",
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 = import("os").catch(() => null);
83
+ nodeOsPromise = importNodeBuiltin("os").catch(() => null);
80
84
  }
81
85
  return nodeOsPromise;
82
86
  }
@@ -0,0 +1,6 @@
1
+ import type { WatchForgeRegisterOptions } from "./index";
2
+
3
+ export function withWatchForgeConfig<T extends Record<string, unknown>>(
4
+ nextConfig?: T,
5
+ watchforgeOptions?: WatchForgeRegisterOptions
6
+ ): T;
@@ -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
+ }