Skip to content

Commit 2c5751f

Browse files
authored
fix: return constructable class from require('module') (#9711)
1 parent d1c81fd commit 2c5751f

3 files changed

Lines changed: 77 additions & 49 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Fixes
66

77
- `[jest-environment-node]` Remove `getVmContext` from Node env on older versions of Node ([#9706](https://github.com/facebook/jest/pull/9706))
8+
- `[jest-runtime]` Return constructable class from `require('module')` ([#9711](https://github.com/facebook/jest/pull/9711))
89

910
### Chore & Maintenance
1011

packages/jest-runtime/src/__tests__/runtime_require_module.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,21 @@ describe('Runtime requireModule', () => {
351351
expect(exports.isJSONModuleEncodedInUTF8WithBOM).toBe(true);
352352
}));
353353

354+
it('should export a constructable Module class', () =>
355+
createRuntime(__filename).then(runtime => {
356+
const Module = runtime.requireModule(runtime.__mockRootPath, 'module');
357+
358+
expect(() => new Module()).not.toThrow();
359+
}));
360+
361+
it('caches Module correctly', () =>
362+
createRuntime(__filename).then(runtime => {
363+
const Module1 = runtime.requireModule(runtime.__mockRootPath, 'module');
364+
const Module2 = runtime.requireModule(runtime.__mockRootPath, 'module');
365+
366+
expect(Module1).toBe(Module2);
367+
}));
368+
354369
onNodeVersions('>=12.12.0', () => {
355370
it('overrides module.createRequire', () =>
356371
createRuntime(__filename).then(runtime => {

packages/jest-runtime/src/index.ts

Lines changed: 61 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class Runtime {
122122
private _transitiveShouldMock: BooleanObject;
123123
private _unmockList: RegExp | undefined;
124124
private _virtualMocks: BooleanObject;
125+
private _moduleImplementation?: typeof nativeModule.Module;
125126

126127
constructor(
127128
config: Config.ProjectConfig,
@@ -898,64 +899,75 @@ class Runtime {
898899
}
899900

900901
if (moduleName === 'module') {
901-
const createRequire = (modulePath: string | URL) => {
902-
const filename =
903-
typeof modulePath === 'string'
904-
? modulePath.startsWith('file:///')
905-
? fileURLToPath(new URL(modulePath))
906-
: modulePath
907-
: fileURLToPath(modulePath);
908-
909-
if (!path.isAbsolute(filename)) {
902+
return this._getMockedNativeModule();
903+
}
904+
905+
return require(moduleName);
906+
}
907+
908+
private _getMockedNativeModule(): typeof nativeModule.Module {
909+
if (this._moduleImplementation) {
910+
return this._moduleImplementation;
911+
}
912+
913+
const createRequire = (modulePath: string | URL) => {
914+
const filename =
915+
typeof modulePath === 'string'
916+
? modulePath.startsWith('file:///')
917+
? fileURLToPath(new URL(modulePath))
918+
: modulePath
919+
: fileURLToPath(modulePath);
920+
921+
if (!path.isAbsolute(filename)) {
922+
const error = new TypeError(
923+
`The argument 'filename' must be a file URL object, file URL string, or absolute path string. Received '${filename}'`,
924+
);
925+
// @ts-ignore
926+
error.code = 'ERR_INVALID_ARG_TYPE';
927+
throw error;
928+
}
929+
930+
return this._createRequireImplementation({
931+
children: [],
932+
exports: {},
933+
filename,
934+
id: filename,
935+
loaded: false,
936+
});
937+
};
938+
939+
// should we implement the class ourselves?
940+
class Module extends nativeModule.Module {}
941+
942+
Module.Module = Module;
943+
944+
if ('createRequire' in nativeModule) {
945+
Module.createRequire = createRequire;
946+
}
947+
if ('createRequireFromPath' in nativeModule) {
948+
Module.createRequireFromPath = (filename: string | URL) => {
949+
if (typeof filename !== 'string') {
910950
const error = new TypeError(
911-
`The argument 'filename' must be a file URL object, file URL string, or absolute path string. Received '${filename}'`,
951+
`The argument 'filename' must be string. Received '${filename}'.${
952+
filename instanceof URL
953+
? ' Use createRequire for URL filename.'
954+
: ''
955+
}`,
912956
);
913957
// @ts-ignore
914958
error.code = 'ERR_INVALID_ARG_TYPE';
915959
throw error;
916960
}
917-
918-
return this._createRequireImplementation({
919-
children: [],
920-
exports: {},
921-
filename,
922-
id: filename,
923-
loaded: false,
924-
});
961+
return createRequire(filename);
925962
};
926-
927-
const overriddenModules: Partial<typeof nativeModule> = {};
928-
929-
if ('createRequire' in nativeModule) {
930-
overriddenModules.createRequire = createRequire;
931-
}
932-
if ('createRequireFromPath' in nativeModule) {
933-
overriddenModules.createRequireFromPath = (filename: string | URL) => {
934-
if (typeof filename !== 'string') {
935-
const error = new TypeError(
936-
`The argument 'filename' must be string. Received '${filename}'.${
937-
filename instanceof URL
938-
? ' Use createRequire for URL filename.'
939-
: ''
940-
}`,
941-
);
942-
// @ts-ignore
943-
error.code = 'ERR_INVALID_ARG_TYPE';
944-
throw error;
945-
}
946-
return createRequire(filename);
947-
};
948-
}
949-
if ('syncBuiltinESMExports' in nativeModule) {
950-
overriddenModules.syncBuiltinESMExports = () => {};
951-
}
952-
953-
return Object.keys(overriddenModules).length > 0
954-
? {...nativeModule, ...overriddenModules}
955-
: nativeModule;
963+
}
964+
if ('syncBuiltinESMExports' in nativeModule) {
965+
Module.syncBuiltinESMExports = () => {};
956966
}
957967

958-
return require(moduleName);
968+
this._moduleImplementation = Module;
969+
970+
return Module;
959971
}
960972

961973
private _generateMock(from: Config.Path, moduleName: string) {

0 commit comments

Comments
 (0)