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 4f678f9daac2..4e79a11419f9 100644 --- a/packages/jest-runtime/src/__tests__/runtime_require_module.test.js +++ b/packages/jest-runtime/src/__tests__/runtime_require_module.test.js @@ -8,9 +8,12 @@ 'use strict'; +import {builtinModules, createRequire} from 'module'; import path from 'path'; +import {pathToFileURL} from 'url'; // eslint-disable-next-line import/default import slash from 'slash'; +import {onNodeVersions} from '@jest/test-utils'; let createRuntime; @@ -347,4 +350,70 @@ describe('Runtime requireModule', () => { ); expect(exports.isJSONModuleEncodedInUTF8WithBOM).toBe(true); })); + + onNodeVersions('>=12.12.0', () => { + it('overrides module.createRequire', () => + createRuntime(__filename).then(runtime => { + const exports = runtime.requireModule(runtime.__mockRootPath, 'module'); + + expect(exports.createRequire).not.toBe(createRequire); + + // createRequire with string + { + const customRequire = exports.createRequire(runtime.__mockRootPath); + expect(customRequire('./create_require_module').foo).toBe('foo'); + } + + // createRequire with URL object + { + const customRequire = exports.createRequire( + pathToFileURL(runtime.__mockRootPath), + ); + expect(customRequire('./create_require_module').foo).toBe('foo'); + } + + // createRequire with file URL string + { + const customRequire = exports.createRequire( + pathToFileURL(runtime.__mockRootPath).toString(), + ); + expect(customRequire('./create_require_module').foo).toBe('foo'); + } + + // createRequire with absolute module path + { + const customRequire = exports.createRequire(runtime.__mockRootPath); + expect(customRequire('./create_require_module').foo).toBe('foo'); + } + + // createRequire with relative module path + expect(() => exports.createRequireFromPath('./relative/path')).toThrow( + new TypeError( + `The argument 'filename' must be a file URL object, file URL string, or absolute path string. Received './relative/path'`, + ), + ); + + // createRequireFromPath with absolute module path + { + const customRequire = exports.createRequireFromPath( + runtime.__mockRootPath, + ); + expect(customRequire('./create_require_module').foo).toBe('foo'); + } + + // createRequireFromPath with file URL object + expect(() => + exports.createRequireFromPath(pathToFileURL(runtime.__mockRootPath)), + ).toThrow( + new TypeError( + `The argument 'filename' must be string. Received '${pathToFileURL( + runtime.__mockRootPath, + )}'. Use createRequire for URL filename.`, + ), + ); + + expect(exports.syncBuiltinESMExports).not.toThrow(); + expect(exports.builtinModules).toEqual(builtinModules); + })); + }); }); diff --git a/packages/jest-runtime/src/__tests__/test_root/create_require_module.js b/packages/jest-runtime/src/__tests__/test_root/create_require_module.js new file mode 100644 index 000000000000..3d4f063e647f --- /dev/null +++ b/packages/jest-runtime/src/__tests__/test_root/create_require_module.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +module.exports.foo = 'foo'; diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 7ff305104da9..cb74daf324af 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -5,9 +5,10 @@ * LICENSE file in the root directory of this source tree. */ +import {URL, fileURLToPath} from 'url'; import * as path from 'path'; import {Script, compileFunction} from 'vm'; -import {fileURLToPath} from 'url'; +import * as nativeModule from 'module'; import {Config} from '@jest/types'; import { Jest, @@ -17,13 +18,9 @@ import { ModuleWrapper, } from '@jest/environment'; import {SourceMapRegistry} from '@jest/source-map'; -import jestMock = require('jest-mock'); -import HasteMap = require('jest-haste-map'); import {formatStackTrace, separateMessageFromStack} from 'jest-message-util'; -import Resolver = require('jest-resolve'); import {createDirectory, deepCyclicCopy} from 'jest-util'; import {escapePathForRegex} from 'jest-regex-util'; -import Snapshot = require('jest-snapshot'); import { ScriptTransformer, ShouldInstrumentOptions, @@ -35,11 +32,15 @@ import { import {V8CoverageResult} from '@jest/test-result'; import {CoverageInstrumenter, V8Coverage} from 'collect-v8-coverage'; import * as fs from 'graceful-fs'; -import stripBOM = require('strip-bom'); import {run as cliRun} from './cli'; import {options as cliOptions} from './cli/args'; import {findSiblingsWithFileExtension} from './helpers'; import {Context as JestContext} from './types'; +import jestMock = require('jest-mock'); +import HasteMap = require('jest-haste-map'); +import Resolver = require('jest-resolve'); +import Snapshot = require('jest-snapshot'); +import stripBOM = require('strip-bom'); type HasteMapOptions = { console?: Console; @@ -887,6 +888,64 @@ class Runtime { return this._environment.global.process; } + 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)) { + 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, + }); + }; + + 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; + } + return require(moduleName); } diff --git a/yarn.lock b/yarn.lock index 2cbfd77e2c0a..e9f45689142b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2244,9 +2244,9 @@ "@types/node" "*" "@types/node@*", "@types/node@>= 8": - version "13.5.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.5.2.tgz#3de53b55fd39efc428a901a0f6db31f761cfa131" - integrity sha512-Fr6a47c84PRLfd7M7u3/hEknyUdQrrBA6VoPmkze0tcflhU5UnpWEX2kn12ktA/lb+MNHSqFlSiPHIHsaErTPA== + version "13.7.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4" + integrity sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ== "@types/prettier@^1.16.1": version "1.19.0"