diff --git a/CHANGELOG.md b/CHANGELOG.md index fbdfbb91be1c..634eaee5d5d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - `[jest-environment-node]` Remove `getVmContext` from Node env on older versions of Node ([#9706](https://github.com/facebook/jest/pull/9706)) +- `[jest-runtime]` Return constructable class from `require('module')` ([#9711](https://github.com/facebook/jest/pull/9711)) ### Chore & Maintenance diff --git a/packages/jest-runtime/src/__tests__/runtime_require_module.test.js b/packages/jest-runtime/src/__tests__/runtime_require_module.test.js index 4e79a11419f9..685667dea150 100644 --- a/packages/jest-runtime/src/__tests__/runtime_require_module.test.js +++ b/packages/jest-runtime/src/__tests__/runtime_require_module.test.js @@ -351,6 +351,21 @@ describe('Runtime requireModule', () => { expect(exports.isJSONModuleEncodedInUTF8WithBOM).toBe(true); })); + it('should export a constructable Module class', () => + createRuntime(__filename).then(runtime => { + const Module = runtime.requireModule(runtime.__mockRootPath, 'module'); + + expect(() => new Module()).not.toThrow(); + })); + + it('caches Module correctly', () => + createRuntime(__filename).then(runtime => { + const Module1 = runtime.requireModule(runtime.__mockRootPath, 'module'); + const Module2 = runtime.requireModule(runtime.__mockRootPath, 'module'); + + expect(Module1).toBe(Module2); + })); + onNodeVersions('>=12.12.0', () => { it('overrides module.createRequire', () => createRuntime(__filename).then(runtime => { diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 39a43443c26c..11bb29aa2850 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -122,6 +122,7 @@ class Runtime { private _transitiveShouldMock: BooleanObject; private _unmockList: RegExp | undefined; private _virtualMocks: BooleanObject; + private _moduleImplementation?: typeof nativeModule.Module; constructor( config: Config.ProjectConfig, @@ -898,64 +899,75 @@ class Runtime { } if (moduleName === 'module') { - const createRequire = (modulePath: string | URL) => { - const filename = - typeof modulePath === 'string' - ? modulePath.startsWith('file:///') - ? fileURLToPath(new URL(modulePath)) - : modulePath - : fileURLToPath(modulePath); - - if (!path.isAbsolute(filename)) { + return this._getMockedNativeModule(); + } + + return require(moduleName); + } + + private _getMockedNativeModule(): typeof nativeModule.Module { + if (this._moduleImplementation) { + return this._moduleImplementation; + } + + const createRequire = (modulePath: string | URL) => { + const filename = + typeof modulePath === 'string' + ? modulePath.startsWith('file:///') + ? fileURLToPath(new URL(modulePath)) + : modulePath + : fileURLToPath(modulePath); + + if (!path.isAbsolute(filename)) { + const error = new TypeError( + `The argument 'filename' must be a file URL object, file URL string, or absolute path string. Received '${filename}'`, + ); + // @ts-ignore + error.code = 'ERR_INVALID_ARG_TYPE'; + throw error; + } + + return this._createRequireImplementation({ + children: [], + exports: {}, + filename, + id: filename, + loaded: false, + }); + }; + + // should we implement the class ourselves? + class Module extends nativeModule.Module {} + + Module.Module = Module; + + if ('createRequire' in nativeModule) { + Module.createRequire = createRequire; + } + if ('createRequireFromPath' in nativeModule) { + Module.createRequireFromPath = (filename: string | URL) => { + if (typeof filename !== 'string') { const error = new TypeError( - `The argument 'filename' must be a file URL object, file URL string, or absolute path string. Received '${filename}'`, + `The argument 'filename' must be string. Received '${filename}'.${ + filename instanceof URL + ? ' Use createRequire for URL filename.' + : '' + }`, ); // @ts-ignore error.code = 'ERR_INVALID_ARG_TYPE'; throw error; } - - return this._createRequireImplementation({ - children: [], - exports: {}, - filename, - id: filename, - loaded: false, - }); + return createRequire(filename); }; - - const overriddenModules: Partial = {}; - - if ('createRequire' in nativeModule) { - overriddenModules.createRequire = createRequire; - } - if ('createRequireFromPath' in nativeModule) { - overriddenModules.createRequireFromPath = (filename: string | URL) => { - if (typeof filename !== 'string') { - const error = new TypeError( - `The argument 'filename' must be string. Received '${filename}'.${ - filename instanceof URL - ? ' Use createRequire for URL filename.' - : '' - }`, - ); - // @ts-ignore - error.code = 'ERR_INVALID_ARG_TYPE'; - throw error; - } - return createRequire(filename); - }; - } - if ('syncBuiltinESMExports' in nativeModule) { - overriddenModules.syncBuiltinESMExports = () => {}; - } - - return Object.keys(overriddenModules).length > 0 - ? {...nativeModule, ...overriddenModules} - : nativeModule; + } + if ('syncBuiltinESMExports' in nativeModule) { + Module.syncBuiltinESMExports = () => {}; } - return require(moduleName); + this._moduleImplementation = Module; + + return Module; } private _generateMock(from: Config.Path, moduleName: string) {