Skip to content

Commit 07a31da

Browse files
authored
fix: Use pirates for proper load extensions install/uninstall handling (#219)
This fixes a probably rare problem with a recursive loop in `restoreExtensions`, when `ts` extension installed by one `rewire` instance (and not yet restored) is saved to `originalExtensions.ts` by another one, leading to recursion in `registerExtensions -> tsExtension -> restoreExtensions` and eventual `RangeError: Maximum call stack size exceeded` error. Fixes #97 The problem can be easily reproduced if ts-node is omitted to run tests, for example. ``` > mocha .... 65 passing (277ms) 1 failing 1) rewire should work with TypeScript: RangeError: Maximum call stack size exceeded at Object.tsExtension [as ts] (lib/moduleEnv.js:136:21) at Object.tsExtension [as ts] (lib/moduleEnv.js:159:24) at Object.tsExtension [as ts] (lib/moduleEnv.js:159:24) ... ... few more thousand of the same text ... at Object.tsExtension [as .ts] (lib/moduleEnv.js:159:24) at Module.load (node:internal/modules/cjs/loader:1203:32) at Object.load (lib/moduleEnv.js:54:18) at internalRewire (lib/rewire.js:49:15) at rewire (lib/index.js:11:12) at Context.<anonymous> (test/rewire.test.js:23:20) at process.processImmediate (node:internal/timers:476:21) ``` It also happens if you use `require(esm)` in some cases, though it is way harder to reproduce it reliably. With this patch it never happens :) ("should work with TypeScript" will still fail w/o ts-node, yet it will be way more trivial reason :) ) Also, this is a groundwork to make rewire ESM-compatible (thanks to require(esm), esbuild, and some AST magic). Though, esm-compatibility is a subject for another PR and probably a major version bump. Anyway, it is not yet ready to be shared, so i'll get back to you with esm support ready to merge abit later, i hope :")
2 parents 7141d1b + 2745de2 commit 07a31da

File tree

3 files changed

+31
-80
lines changed

3 files changed

+31
-80
lines changed

lib/moduleEnv.js

Lines changed: 16 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
"use strict";
22

3-
// TODO: Use https://www.npmjs.com/package/pirates here?
4-
53
var Module = require("module"),
4+
pirates = require("pirates"),
65
eslint = require("eslint");
76

87
var moduleWrapper0 = Module.wrapper[0],
98
moduleWrapper1 = Module.wrapper[1],
10-
originalExtensions = {},
119
linter = new eslint.Linter(),
1210
eslintOptions = {
1311
languageOptions: {
@@ -50,9 +48,11 @@ function load(targetModule) {
5048
targetModule.require = requireProxy;
5149
currentModule = targetModule;
5250

53-
registerExtensions();
51+
var restoreExtensions = pirates.addHook(patchSources, { extensions: ['.js', '.cjs', '.mjs', '.ts']});
52+
5453
targetModule.load(targetModule.id);
5554

55+
restoreExtensions();
5656
// This is only necessary if nothing has been required within the module
5757
reset();
5858
}
@@ -79,84 +79,24 @@ function requireProxy(path) {
7979
return nodeRequire.call(currentModule, path); // node's require only works when "this" points to the module
8080
}
8181

82-
function registerExtensions() {
83-
var originalJsExtension = require.extensions[".js"];
84-
var originalTsExtension = require.extensions[".ts"];
85-
86-
if (originalJsExtension) {
87-
originalExtensions.js = originalJsExtension;
88-
}
89-
if (originalTsExtension) {
90-
originalExtensions.ts = originalTsExtension;
91-
}
92-
require.extensions[".js"] = jsExtension;
93-
require.extensions[".ts"] = tsExtension;
94-
}
95-
96-
function restoreExtensions() {
97-
if ("js" in originalExtensions) {
98-
require.extensions[".js"] = originalExtensions.js;
99-
}
100-
if ("ts" in originalExtensions) {
101-
require.extensions[".ts"] = originalExtensions.ts;
102-
}
103-
}
104-
10582
function isNoConstAssignMessage(message) {
10683
return message.ruleId === "no-const-assign";
10784
}
10885

109-
function jsExtension(module, filename) {
110-
var _compile = module._compile;
86+
function patchSources(content, filename) {
87+
var noConstAssignMessage = linter.verify(content, eslintOptions).find(isNoConstAssignMessage);
88+
var line;
89+
var column;
11190

112-
module._compile = function (content, filename) {
113-
var noConstAssignMessage = linter.verify(content, eslintOptions).find(isNoConstAssignMessage);
114-
var line;
115-
var column;
116-
117-
if (noConstAssignMessage !== undefined) {
118-
line = noConstAssignMessage.line;
119-
column = noConstAssignMessage.column;
120-
throw new TypeError(`Assignment to constant variable at ${filename}:${line}:${column}`);
121-
}
122-
123-
_compile.call(
124-
module,
125-
content
126-
.replace(shebang, '') // Remove shebang declarations
127-
.replace(matchConst, "$1let $2"), // replace const with let, while maintaining the column width
128-
filename
129-
);
130-
};
131-
132-
restoreExtensions();
133-
originalExtensions.js(module, filename);
134-
}
135-
136-
function tsExtension(module, filename) {
137-
var _compile = module._compile;
138-
139-
module._compile = function rewireCompile(content, filename) {
140-
var noConstAssignMessage = linter.verify(content, eslintOptions).find(isNoConstAssignMessage);
141-
var line;
142-
var column;
143-
144-
if (noConstAssignMessage !== undefined) {
145-
line = noConstAssignMessage.line;
146-
column = noConstAssignMessage.column;
147-
throw new TypeError(`Assignment to constant variable at ${filename}:${line}:${column}`);
148-
}
149-
_compile.call(
150-
this,
151-
content
152-
.replace(shebang, '') // Remove shebang declarations
153-
.replace(matchConst, "$1let $2"), // replace const with let, while maintaining the column width
154-
filename
155-
);
156-
};
91+
if (noConstAssignMessage !== undefined) {
92+
line = noConstAssignMessage.line;
93+
column = noConstAssignMessage.column;
94+
throw new TypeError(`Assignment to constant variable at ${filename}:${line}:${column}`);
95+
}
15796

158-
restoreExtensions();
159-
originalExtensions.ts(module, filename);
97+
return content
98+
.replace(shebang, '') // Remove shebang declarations
99+
.replace(matchConst, "$1let $2"); // replace const with let, while maintaining the column width
160100
}
161101

162102
exports.load = load;

package-lock.json

Lines changed: 13 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"test": "nyc --reporter=html --reporter=lcov mocha -r ts-node/register -R spec"
4444
},
4545
"dependencies": {
46-
"eslint": "^9.30"
46+
"eslint": "^9.30",
47+
"pirates": "^4.0.7"
4748
},
4849
"files": [
4950
"lib"

0 commit comments

Comments
 (0)