wolverine-ai 4.1.0 → 4.2.0
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/package.json +1 -1
- package/src/core/error-hook.js +27 -6
- package/src/core/runner.js +66 -30
- package/src/core/verifier.js +21 -2
- package/src/core/wolverine.js +23 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/core/error-hook.js
CHANGED
|
@@ -34,7 +34,15 @@ Module._load = function (request, parent, isMain) {
|
|
|
34
34
|
_hookFastify(instance);
|
|
35
35
|
return instance;
|
|
36
36
|
};
|
|
37
|
-
|
|
37
|
+
// #23: Copy all own properties (including non-enumerable and symbols) to preserve prototype chain
|
|
38
|
+
for (const key of Object.getOwnPropertyNames(originalFastify)) {
|
|
39
|
+
if (key !== "length" && key !== "name" && key !== "prototype") {
|
|
40
|
+
try { wrapped[key] = originalFastify[key]; } catch {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const sym of Object.getOwnPropertySymbols(originalFastify)) {
|
|
44
|
+
try { wrapped[sym] = originalFastify[sym]; } catch {}
|
|
45
|
+
}
|
|
38
46
|
wrapped.default = wrapped; // ESM compat
|
|
39
47
|
return wrapped;
|
|
40
48
|
}
|
|
@@ -48,7 +56,15 @@ Module._load = function (request, parent, isMain) {
|
|
|
48
56
|
_hookExpress(app);
|
|
49
57
|
return app;
|
|
50
58
|
};
|
|
51
|
-
|
|
59
|
+
// #23: Copy all own properties (including non-enumerable and symbols)
|
|
60
|
+
for (const key of Object.getOwnPropertyNames(originalExpress)) {
|
|
61
|
+
if (key !== "length" && key !== "name" && key !== "prototype") {
|
|
62
|
+
try { wrapped[key] = originalExpress[key]; } catch {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const sym of Object.getOwnPropertySymbols(originalExpress)) {
|
|
66
|
+
try { wrapped[sym] = originalExpress[sym]; } catch {}
|
|
67
|
+
}
|
|
52
68
|
return wrapped;
|
|
53
69
|
}
|
|
54
70
|
|
|
@@ -94,9 +110,13 @@ function _hookExpress(app) {
|
|
|
94
110
|
// Wrap app.listen to inject error middleware AFTER all user middleware
|
|
95
111
|
const originalListen = app.listen;
|
|
96
112
|
app.listen = function (...args) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
// #24: Use process.nextTick to ensure our error middleware is added AFTER
|
|
114
|
+
// any middleware registered synchronously after listen() is called
|
|
115
|
+
process.nextTick(() => {
|
|
116
|
+
app.use(function _wolverineErrorHook(err, req, res, next) {
|
|
117
|
+
_reportError(req.originalUrl || req.url, req.method, err);
|
|
118
|
+
next(err);
|
|
119
|
+
});
|
|
100
120
|
});
|
|
101
121
|
return originalListen.apply(this, args);
|
|
102
122
|
};
|
|
@@ -114,7 +134,8 @@ function _reportError(url, method, error) {
|
|
|
114
134
|
let file = null, line = null;
|
|
115
135
|
if (error.stack) {
|
|
116
136
|
for (const frame of error.stack.split("\n")) {
|
|
117
|
-
|
|
137
|
+
// #25: Second regex uses (.+) instead of ([^\s(]+) to handle Windows paths with spaces
|
|
138
|
+
const m = frame.match(/\(([^)]+):(\d+):(\d+)\)/) || frame.match(/at\s+(.+):(\d+):(\d+)/);
|
|
118
139
|
if (m && !m[1].includes("node_modules") && !m[1].includes("node:")) {
|
|
119
140
|
file = m[1]; line = parseInt(m[2], 10); break;
|
|
120
141
|
}
|
package/src/core/runner.js
CHANGED
|
@@ -284,7 +284,9 @@ class WolverineRunner {
|
|
|
284
284
|
this._clearStabilityTimer();
|
|
285
285
|
// Clear any pending heals — restart is a clean slate
|
|
286
286
|
this._pendingErrorHeal = null;
|
|
287
|
-
|
|
287
|
+
// #1: Don't clear _healInProgress here — only the heal function itself should clear it
|
|
288
|
+
// #6: Clear stale heal status so dashboard doesn't show phantom heals
|
|
289
|
+
this._healStatus = null;
|
|
288
290
|
|
|
289
291
|
if (this.child) {
|
|
290
292
|
const oldChild = this.child;
|
|
@@ -295,11 +297,9 @@ class WolverineRunner {
|
|
|
295
297
|
const onExit = () => {
|
|
296
298
|
if (spawned) return; // Prevent double-spawn from exit + force-kill timeout
|
|
297
299
|
spawned = true;
|
|
300
|
+
// #7: Don't call _ensurePortFree() here — _spawn() already calls it
|
|
298
301
|
// Give port time to fully release (TIME_WAIT)
|
|
299
|
-
setTimeout(() =>
|
|
300
|
-
this._ensurePortFree();
|
|
301
|
-
setTimeout(() => this._spawn(), 500);
|
|
302
|
-
}, 500);
|
|
302
|
+
setTimeout(() => this._spawn(), 500);
|
|
303
303
|
};
|
|
304
304
|
|
|
305
305
|
oldChild.removeAllListeners("exit");
|
|
@@ -314,7 +314,7 @@ class WolverineRunner {
|
|
|
314
314
|
}
|
|
315
315
|
}, 3000);
|
|
316
316
|
} else {
|
|
317
|
-
|
|
317
|
+
// #7: Don't call _ensurePortFree() here — _spawn() already calls it
|
|
318
318
|
setTimeout(() => this._spawn(), 500);
|
|
319
319
|
}
|
|
320
320
|
}
|
|
@@ -394,7 +394,12 @@ class WolverineRunner {
|
|
|
394
394
|
process.stderr.write(text);
|
|
395
395
|
});
|
|
396
396
|
|
|
397
|
-
|
|
397
|
+
// #27: Only start stability timer if there's a backup to promote — don't clear
|
|
398
|
+
// an existing timer on every spawn (e.g., auto-update restart shouldn't reset
|
|
399
|
+
// the stability countdown for a previously healed backup)
|
|
400
|
+
if (this._lastBackupId) {
|
|
401
|
+
this._startStabilityTimer();
|
|
402
|
+
}
|
|
398
403
|
|
|
399
404
|
// Start process monitor (memory, CPU, heartbeat)
|
|
400
405
|
if (this.child && this.child.pid) {
|
|
@@ -418,32 +423,46 @@ class WolverineRunner {
|
|
|
418
423
|
this.healthMonitor.stop();
|
|
419
424
|
this.healthMonitor.reset();
|
|
420
425
|
this.healthMonitor.start(async (reason) => {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
this.child
|
|
432
|
-
|
|
426
|
+
try {
|
|
427
|
+
if (this._healInProgress || !this.running) return;
|
|
428
|
+
// #26: Claim the heal lock immediately — prevents exit event from starting
|
|
429
|
+
// a concurrent heal between our check and the child kill below
|
|
430
|
+
this._healInProgress = true;
|
|
431
|
+
console.log(chalk.red(`\n🚨 Health check triggered heal (reason: ${reason})`));
|
|
432
|
+
this.logger.error(EVENT_TYPES.HEALTH_UNRESPONSIVE, `Server unresponsive: ${reason}`, { reason });
|
|
433
|
+
this.healthMonitor.stop();
|
|
434
|
+
|
|
435
|
+
// Kill the hung process — remove exit listener to prevent double-heal
|
|
436
|
+
if (this.child) {
|
|
437
|
+
const pid = this.child.pid;
|
|
438
|
+
this.child.removeAllListeners("exit");
|
|
439
|
+
this._killProcessTree(pid, "SIGKILL");
|
|
440
|
+
this.child = null;
|
|
441
|
+
}
|
|
433
442
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
443
|
+
// Synthesize error context for the heal pipeline
|
|
444
|
+
this._stderrBuffer = `Server became unresponsive. Health check failed: ${reason}\n` +
|
|
445
|
+
`The server was running but stopped responding to HTTP requests.\n` +
|
|
446
|
+
`Possible causes: infinite loop, deadlock, memory exhaustion, blocked event loop.`;
|
|
438
447
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
448
|
+
this.retryCount++;
|
|
449
|
+
if (this.retryCount > this.maxRetries) {
|
|
450
|
+
console.log(chalk.red(`\n🛑 Max retries reached.`));
|
|
451
|
+
this._logRollbackHint();
|
|
452
|
+
this.running = false;
|
|
453
|
+
this._healInProgress = false;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
// Release lock so _healAndRestart can acquire it
|
|
457
|
+
this._healInProgress = false;
|
|
458
|
+
await this._healAndRestart();
|
|
459
|
+
} catch (err) {
|
|
460
|
+
// #5: Prevent unhandled errors in health callback from crashing the parent
|
|
461
|
+
console.log(chalk.red(` ⚠️ Health callback error: ${err.message}`));
|
|
462
|
+
this._healInProgress = false;
|
|
463
|
+
this._healStatus = null;
|
|
464
|
+
if (this.running) this._spawn();
|
|
445
465
|
}
|
|
446
|
-
await this._healAndRestart();
|
|
447
466
|
});
|
|
448
467
|
|
|
449
468
|
this.child.on("exit", async (code, signal) => {
|
|
@@ -541,6 +560,8 @@ class WolverineRunner {
|
|
|
541
560
|
|
|
542
561
|
async _healAndRestart() {
|
|
543
562
|
if (this._healInProgress) return;
|
|
563
|
+
// #9: Bail if stop() was called during the window between crash and heal
|
|
564
|
+
if (this._shuttingDown) return;
|
|
544
565
|
this._healInProgress = true;
|
|
545
566
|
this._healStatus = { active: true, error: this._stderrBuffer.slice(0, 200), phase: "diagnosing", startedAt: Date.now() };
|
|
546
567
|
|
|
@@ -567,6 +588,8 @@ class WolverineRunner {
|
|
|
567
588
|
}
|
|
568
589
|
|
|
569
590
|
try {
|
|
591
|
+
// #9: Check again before expensive heal — stop() may have been called during loop guard
|
|
592
|
+
if (this._shuttingDown) { this._healInProgress = false; return; }
|
|
570
593
|
const result = await heal({
|
|
571
594
|
stderr: this._stderrBuffer,
|
|
572
595
|
cwd: this.cwd,
|
|
@@ -606,6 +629,8 @@ class WolverineRunner {
|
|
|
606
629
|
this._healStatus = null;
|
|
607
630
|
// Clear pending errors — the heal fixed the root cause, stale errors are irrelevant
|
|
608
631
|
this._pendingErrorHeal = null;
|
|
632
|
+
// #9: Don't restart if stop() was called while heal was running
|
|
633
|
+
if (this._shuttingDown) return;
|
|
609
634
|
// Use restart() to properly kill old child before spawning — prevents EADDRINUSE
|
|
610
635
|
this.restart();
|
|
611
636
|
} else {
|
|
@@ -673,6 +698,15 @@ class WolverineRunner {
|
|
|
673
698
|
}
|
|
674
699
|
this._healInProgress = true;
|
|
675
700
|
|
|
701
|
+
// #8: Safety timeout — if heal hangs, force-release the lock after 6 minutes
|
|
702
|
+
const healTimeout = setTimeout(() => {
|
|
703
|
+
if (this._healInProgress) {
|
|
704
|
+
console.log(chalk.red(` ⚠️ _healFromError safety timeout (6min) — releasing heal lock`));
|
|
705
|
+
this._healInProgress = false;
|
|
706
|
+
this._healStatus = null;
|
|
707
|
+
}
|
|
708
|
+
}, 360000);
|
|
709
|
+
|
|
676
710
|
console.log(chalk.yellow(`\n🐺 Wolverine healing caught error on ${routePath}...`));
|
|
677
711
|
this._healStatus = { active: true, route: routePath, error: errorDetails?.message?.slice(0, 200), phase: "diagnosing", startedAt: Date.now() };
|
|
678
712
|
this.logger.info("heal.error_monitor", `Healing caught 500 on ${routePath}`, { route: routePath });
|
|
@@ -727,6 +761,7 @@ class WolverineRunner {
|
|
|
727
761
|
routeContext: { path: routePath, method: errorDetails?.method },
|
|
728
762
|
});
|
|
729
763
|
|
|
764
|
+
clearTimeout(healTimeout);
|
|
730
765
|
if (result.healed) {
|
|
731
766
|
console.log(chalk.green(`\n🐺 Wolverine healed ${routePath} via ${result.mode}! Restarting...\n`));
|
|
732
767
|
this.retryCount = 0; // Fresh start after successful heal
|
|
@@ -748,6 +783,7 @@ class WolverineRunner {
|
|
|
748
783
|
this._healStatus = null;
|
|
749
784
|
}
|
|
750
785
|
} catch (err) {
|
|
786
|
+
clearTimeout(healTimeout);
|
|
751
787
|
console.log(chalk.red(`\n🐺 Error during heal: ${err.message}`));
|
|
752
788
|
this._healInProgress = false;
|
|
753
789
|
this._healStatus = null;
|
package/src/core/verifier.js
CHANGED
|
@@ -23,6 +23,8 @@ const BOOT_PROBE_TIMEOUT_MS = 10000; // 10 seconds
|
|
|
23
23
|
*/
|
|
24
24
|
function syntaxCheck(scriptPath) {
|
|
25
25
|
return new Promise((resolve) => {
|
|
26
|
+
// #21: Guard against double resolve from exit + error firing in sequence
|
|
27
|
+
let settled = false;
|
|
26
28
|
const child = spawn("node", ["--check", scriptPath], {
|
|
27
29
|
stdio: ["ignore", "ignore", "pipe"],
|
|
28
30
|
timeout: 5000,
|
|
@@ -32,6 +34,8 @@ function syntaxCheck(scriptPath) {
|
|
|
32
34
|
child.stderr.on("data", (data) => { stderr += data.toString(); });
|
|
33
35
|
|
|
34
36
|
child.on("exit", (code) => {
|
|
37
|
+
if (settled) return;
|
|
38
|
+
settled = true;
|
|
35
39
|
resolve({
|
|
36
40
|
valid: code === 0,
|
|
37
41
|
error: code !== 0 ? stderr.trim() : undefined,
|
|
@@ -39,6 +43,8 @@ function syntaxCheck(scriptPath) {
|
|
|
39
43
|
});
|
|
40
44
|
|
|
41
45
|
child.on("error", (err) => {
|
|
46
|
+
if (settled) return;
|
|
47
|
+
settled = true;
|
|
42
48
|
resolve({ valid: false, error: err.message });
|
|
43
49
|
});
|
|
44
50
|
});
|
|
@@ -71,6 +77,8 @@ function bootProbe(scriptPath, cwd, originalErrorSignature) {
|
|
|
71
77
|
child.on("exit", (code) => {
|
|
72
78
|
if (settled) return;
|
|
73
79
|
settled = true;
|
|
80
|
+
// #19: Always ensure the child is killed after settling (handles orphan sub-processes)
|
|
81
|
+
try { child.kill("SIGTERM"); } catch {}
|
|
74
82
|
|
|
75
83
|
if (code === 0) {
|
|
76
84
|
resolve({ status: "alive" });
|
|
@@ -101,6 +109,8 @@ function bootProbe(scriptPath, cwd, originalErrorSignature) {
|
|
|
101
109
|
child.on("error", (err) => {
|
|
102
110
|
if (settled) return;
|
|
103
111
|
settled = true;
|
|
112
|
+
// #19: Always ensure the child is killed after settling
|
|
113
|
+
try { child.kill("SIGTERM"); } catch {}
|
|
104
114
|
resolve({ status: "crashed", stderr: err.message, sameError: false, exitCode: null });
|
|
105
115
|
});
|
|
106
116
|
|
|
@@ -192,9 +202,10 @@ async function verifyFix(scriptPath, cwd, originalErrorSignature, routeContext)
|
|
|
192
202
|
const relPath = path.relative(cwd, changedFile).replace(/\\/g, "/");
|
|
193
203
|
if (relPath.startsWith("server/") && relPath.endsWith(".js")) {
|
|
194
204
|
try {
|
|
195
|
-
const {
|
|
205
|
+
const { execFileSync } = require("child_process");
|
|
206
|
+
// #22: Use execFileSync with args array to prevent path injection via relPath
|
|
196
207
|
const testCode = `try{require('./${relPath}');console.log('MODULE_OK')}catch(e){console.error(e.message);process.exit(1)}`;
|
|
197
|
-
const out =
|
|
208
|
+
const out = execFileSync("node", ["-e", testCode], {
|
|
198
209
|
cwd, timeout: 5000, encoding: "utf-8",
|
|
199
210
|
env: { ...process.env, NODE_PATH: path.join(cwd, "node_modules") },
|
|
200
211
|
});
|
|
@@ -237,6 +248,14 @@ function routeProbe(scriptPath, cwd, routeContext) {
|
|
|
237
248
|
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
238
249
|
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
239
250
|
|
|
251
|
+
// #20: Handle spawn errors (e.g., node binary not found)
|
|
252
|
+
child.on("error", (err) => {
|
|
253
|
+
clearInterval(checkPort);
|
|
254
|
+
if (settled) return;
|
|
255
|
+
settled = true;
|
|
256
|
+
resolve({ status: "failed", statusCode: 0, body: err.message });
|
|
257
|
+
});
|
|
258
|
+
|
|
240
259
|
child.on("exit", () => {
|
|
241
260
|
if (settled) return;
|
|
242
261
|
settled = true;
|
package/src/core/wolverine.js
CHANGED
|
@@ -165,7 +165,7 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
165
165
|
// 4c. Pre-heal operational fix — detect common non-code errors
|
|
166
166
|
// Some crashes aren't code bugs (missing npm packages, missing config files).
|
|
167
167
|
// Fix these directly without wasting AI tokens.
|
|
168
|
-
const opsFix = await tryOperationalFix(parsed, cwd, logger);
|
|
168
|
+
const opsFix = await tryOperationalFix(parsed, cwd, logger, sandbox);
|
|
169
169
|
if (opsFix.fixed) {
|
|
170
170
|
console.log(chalk.green(` ⚡ Operational fix applied: ${opsFix.action}`));
|
|
171
171
|
if (logger) logger.info(EVENT_TYPES.HEAL_SUCCESS, `Operational fix: ${opsFix.action}`, { action: opsFix.action });
|
|
@@ -297,8 +297,15 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
297
297
|
goal: `Fix: ${parsed.errorMessage.slice(0, 80)}`,
|
|
298
298
|
|
|
299
299
|
onAttempt: async (iteration, researchCtx, priorAttempts) => {
|
|
300
|
-
// Create backup for this attempt
|
|
301
|
-
|
|
300
|
+
// #12: Create backup for this attempt — if backup fails, skip the attempt entirely
|
|
301
|
+
let bid;
|
|
302
|
+
try {
|
|
303
|
+
bid = backupManager.createBackup(`heal attempt ${iteration}: ${parsed.errorMessage.slice(0, 60)}`);
|
|
304
|
+
} catch (backupErr) {
|
|
305
|
+
console.log(chalk.red(` ⚠️ Backup creation failed: ${backupErr.message} — skipping attempt`));
|
|
306
|
+
if (logger) logger.error(EVENT_TYPES.BACKUP_CREATED, `Backup failed: ${backupErr.message}`);
|
|
307
|
+
return { healed: false, explanation: `Backup creation failed: ${backupErr.message}` };
|
|
308
|
+
}
|
|
302
309
|
backupManager.setErrorSignature(bid, errorSignature);
|
|
303
310
|
if (logger) logger.info(EVENT_TYPES.BACKUP_CREATED, `Backup ${bid} (iteration ${iteration})`, { backupId: bid });
|
|
304
311
|
|
|
@@ -532,7 +539,7 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
532
539
|
* Try to fix common operational errors without AI.
|
|
533
540
|
* Returns { fixed: boolean, action: string }
|
|
534
541
|
*/
|
|
535
|
-
async function tryOperationalFix(parsed, cwd, logger) {
|
|
542
|
+
async function tryOperationalFix(parsed, cwd, logger, sandbox) {
|
|
536
543
|
const { execSync } = require("child_process");
|
|
537
544
|
const msg = parsed.errorMessage || "";
|
|
538
545
|
|
|
@@ -565,9 +572,14 @@ async function tryOperationalFix(parsed, cwd, logger) {
|
|
|
565
572
|
|
|
566
573
|
// Only auto-create if it's inside the project and looks like a config/data file
|
|
567
574
|
const rel = path.relative(cwd, missingFile).replace(/\\/g, "/");
|
|
568
|
-
|
|
575
|
+
// #18: Validate through sandbox to prevent creating files outside allowed paths
|
|
576
|
+
let safePath = missingFile;
|
|
577
|
+
if (sandbox) {
|
|
578
|
+
try { safePath = sandbox.resolve(missingFile); } catch { safePath = null; }
|
|
579
|
+
}
|
|
580
|
+
if (safePath && !rel.startsWith("..") && /\.(json|yaml|yml|toml|ini|conf|cfg|env|log|txt|csv|db|sqlite)$/i.test(missingFile)) {
|
|
569
581
|
try {
|
|
570
|
-
fs.mkdirSync(path.dirname(
|
|
582
|
+
fs.mkdirSync(path.dirname(safePath), { recursive: true });
|
|
571
583
|
const ext = path.extname(missingFile).toLowerCase();
|
|
572
584
|
|
|
573
585
|
// For JSON config files, try to infer expected structure from the code or error message
|
|
@@ -600,7 +612,7 @@ async function tryOperationalFix(parsed, cwd, logger) {
|
|
|
600
612
|
content = defaults[ext] || "";
|
|
601
613
|
}
|
|
602
614
|
|
|
603
|
-
fs.writeFileSync(
|
|
615
|
+
fs.writeFileSync(safePath, content, "utf-8");
|
|
604
616
|
console.log(chalk.blue(` 📄 Created missing file: ${rel}`));
|
|
605
617
|
return { fixed: true, action: `Created missing file: ${rel} with ${content === "{}" ? "empty" : "inferred"} config` };
|
|
606
618
|
} catch {}
|
|
@@ -659,10 +671,13 @@ function _inferJsonConfig(missingFile, cwd, parsed) {
|
|
|
659
671
|
const sourceFile = parsed.filePath;
|
|
660
672
|
if (!sourceFile) return null;
|
|
661
673
|
|
|
674
|
+
// #17: Escape all regex special characters in basename to prevent regex injection
|
|
675
|
+
const escapedBasename = basename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
676
|
+
|
|
662
677
|
try {
|
|
663
678
|
const source = fs.readFileSync(sourceFile, "utf-8");
|
|
664
679
|
// Look for property accesses on the loaded config: config.apiUrl, config.timeout, etc.
|
|
665
|
-
const configVarMatch = source.match(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:require|JSON\\.parse).*${
|
|
680
|
+
const configVarMatch = source.match(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:require|JSON\\.parse).*${escapedBasename}`));
|
|
666
681
|
if (!configVarMatch) return null;
|
|
667
682
|
|
|
668
683
|
const varName = configVarMatch[1];
|