@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 +45 -0
- 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/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.
|
|
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
|
+
}
|