From 65e46a0227f4332fbc2cfaea4e0692bf01645244 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 26 Mar 2020 14:21:55 +0100 Subject: [PATCH 1/2] fix: return constructable class from `require('module')` --- CHANGELOG.md | 1 + .../__tests__/runtime_require_module.test.js | 7 ++ packages/jest-runtime/src/index.ts | 110 ++++++++++-------- 3 files changed, 69 insertions(+), 49 deletions(-) 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..a20a522a3bf8 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,13 @@ describe('Runtime requireModule', () => { expect(exports.isJSONModuleEncodedInUTF8WithBOM).toBe(true); })); + it('should export a constructible Module class', () => + createRuntime(__filename).then(runtime => { + const Module = runtime.requireModule(runtime.__mockRootPath, 'module'); + + expect(() => new Module()).not.toThrow(); + })); + 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) { From 57a2909834ea300e17d95b28f0f15e98aadf11e7 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 26 Mar 2020 14:29:01 +0100 Subject: [PATCH 2/2] add caching test --- .../src/__tests__/runtime_require_module.test.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 a20a522a3bf8..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,13 +351,21 @@ describe('Runtime requireModule', () => { expect(exports.isJSONModuleEncodedInUTF8WithBOM).toBe(true); })); - it('should export a constructible Module class', () => + 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 => {