From 56d872b8f7b17e8aebb0765fd8617a45167e8fa0 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 4 Apr 2020 15:43:14 +0200 Subject: [PATCH 01/10] feat: experimental ES Modules support --- CHANGELOG.md | 1 + .../moduleNameMapper.test.ts.snap | 4 +- .../__snapshots__/nativeEsm.test.ts.snap | 9 ++ .../resolveNoFileExtensions.test.ts.snap | 2 +- e2e/__tests__/nativeEsm.test.ts | 36 +++++ e2e/native-esm/__tests__/native-esm.test.js | 23 ++++ e2e/native-esm/index.js | 10 ++ e2e/native-esm/package.json | 7 + .../legacy-code-todo-rewrite/jestAdapter.ts | 19 ++- packages/jest-jasmine2/src/index.ts | 19 ++- packages/jest-resolve/package.json | 1 + packages/jest-resolve/src/index.ts | 5 + packages/jest-resolve/src/shouldLoadAsEsm.ts | 65 +++++++++ packages/jest-runner/src/runTest.ts | 10 +- .../src/__mocks__/createRuntime.js | 10 +- packages/jest-runtime/src/cli/index.ts | 17 ++- packages/jest-runtime/src/index.ts | 128 +++++++++++++++--- yarn.lock | 44 ++++++ 18 files changed, 381 insertions(+), 29 deletions(-) create mode 100644 e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap create mode 100644 e2e/__tests__/nativeEsm.test.ts create mode 100644 e2e/native-esm/__tests__/native-esm.test.js create mode 100644 e2e/native-esm/index.js create mode 100644 e2e/native-esm/package.json create mode 100644 packages/jest-resolve/src/shouldLoadAsEsm.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index dbed475bddec..d51df0dbc63f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[jest-console]` Add code frame to `console.error` and `console.warn` ([#9741](https://github.com/facebook/jest/pull/9741)) - `[@jest/globals]` New package so Jest's globals can be explicitly imported ([#9801](https://github.com/facebook/jest/pull/9801)) +- `[jest-runtime, jest-jasmine2, jest-circus]` Experimental, limited ECMAScript Modules support ([#9772](https://github.com/facebook/jest/pull/9772)) ### Fixes diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index d43ffe5dce4f..ed248a26f289 100644 --- a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap +++ b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap @@ -36,7 +36,7 @@ FAIL __tests__/index.js 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:540:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:545:17) at Object.require (index.js:10:1) `; @@ -65,6 +65,6 @@ FAIL __tests__/index.js 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:540:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:545:17) at Object.require (index.js:10:1) `; diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap new file mode 100644 index 000000000000..d3b49ee4f1c8 --- /dev/null +++ b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`on node ^12.16.0 || >=13.0.0 runs test with native ESM 1`] = ` +Test Suites: 1 passed, 1 total +Tests: 2 passed, 2 total +Snapshots: 0 total +Time: <> +Ran all test suites. +`; diff --git a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap index a62a66e69f80..062640b2406a 100644 --- a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap +++ b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap @@ -37,6 +37,6 @@ FAIL __tests__/test.js | ^ 9 | - at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:296:11) + at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:299:11) at Object.require (index.js:8:18) `; diff --git a/e2e/__tests__/nativeEsm.test.ts b/e2e/__tests__/nativeEsm.test.ts new file mode 100644 index 000000000000..a36c24c1e72d --- /dev/null +++ b/e2e/__tests__/nativeEsm.test.ts @@ -0,0 +1,36 @@ +/** + * 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. + */ + +import {resolve} from 'path'; +import wrap from 'jest-snapshot-serializer-raw'; +import {onNodeVersions} from '@jest/test-utils'; +import runJest, {getConfig} from '../runJest'; +import {extractSummary} from '../Utils'; + +const DIR = resolve(__dirname, '../native-esm'); + +test('test config is without transform', () => { + const {configs} = getConfig(DIR); + + expect(configs).toHaveLength(1); + expect(configs[0].transform).toEqual([]); +}); + +// The versions vm.Module was introduced +onNodeVersions('^12.16.0 || >=13.0.0', () => { + test('runs test with native ESM', () => { + const {exitCode, stderr, stdout} = runJest(DIR, [], { + nodeOptions: '--experimental-vm-modules', + }); + + const {summary} = extractSummary(stderr); + + expect(wrap(summary)).toMatchSnapshot(); + expect(stdout).toBe(''); + expect(exitCode).toBe(0); + }); +}); diff --git a/e2e/native-esm/__tests__/native-esm.test.js b/e2e/native-esm/__tests__/native-esm.test.js new file mode 100644 index 000000000000..222c038fbc52 --- /dev/null +++ b/e2e/native-esm/__tests__/native-esm.test.js @@ -0,0 +1,23 @@ +/** + * 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. + */ + +import {double} from '../index'; + +test('should have correct import.meta', () => { + expect(typeof jest).toBe('undefined'); + expect(import.meta).toEqual({ + jest: expect.anything(), + url: expect.any(String), + }); + expect( + import.meta.url.endsWith('/e2e/native-esm/__tests__/native-esm.test.js') + ).toBe(true); +}); + +test('should double stuff', () => { + expect(double(1)).toBe(2); +}); diff --git a/e2e/native-esm/index.js b/e2e/native-esm/index.js new file mode 100644 index 000000000000..19bd49f5543e --- /dev/null +++ b/e2e/native-esm/index.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. + */ + +export function double(num) { + return num * 2; +} diff --git a/e2e/native-esm/package.json b/e2e/native-esm/package.json new file mode 100644 index 000000000000..5a90624bc1fe --- /dev/null +++ b/e2e/native-esm/package.json @@ -0,0 +1,7 @@ +{ + "type": "module", + "jest": { + "testEnvironment": "node", + "transform": {} + } +} diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts index 026ba3f54c7c..a7fdf68cb0d9 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts @@ -76,9 +76,24 @@ const jestAdapter = async ( } }); - config.setupFilesAfterEnv.forEach(path => runtime.requireModule(path)); + for (const path of config.setupFilesAfterEnv) { + const esm = runtime.unstable_shouldLoadAsEsm(path); + + if (esm) { + await runtime.unstable_importModule(path); + } else { + runtime.requireModule(path); + } + } + + const esm = runtime.unstable_shouldLoadAsEsm(testPath); + + if (esm) { + await runtime.unstable_importModule(testPath); + } else { + runtime.requireModule(testPath); + } - runtime.requireModule(testPath); const results = await runAndTransformResultsToJestFormat({ config, globalConfig, diff --git a/packages/jest-jasmine2/src/index.ts b/packages/jest-jasmine2/src/index.ts index f8ab81713895..03d2eebdc851 100644 --- a/packages/jest-jasmine2/src/index.ts +++ b/packages/jest-jasmine2/src/index.ts @@ -155,7 +155,15 @@ async function jasmine2( testPath, }); - config.setupFilesAfterEnv.forEach(path => runtime.requireModule(path)); + for (const path of config.setupFilesAfterEnv) { + const esm = runtime.unstable_shouldLoadAsEsm(path); + + if (esm) { + await runtime.unstable_importModule(path); + } else { + runtime.requireModule(path); + } + } if (globalConfig.enabledTestsMap) { env.specFilter = (spec: Spec) => { @@ -169,7 +177,14 @@ async function jasmine2( env.specFilter = (spec: Spec) => testNameRegex.test(spec.getFullName()); } - runtime.requireModule(testPath); + const esm = runtime.unstable_shouldLoadAsEsm(testPath); + + if (esm) { + await runtime.unstable_importModule(testPath); + } else { + runtime.requireModule(testPath); + } + await env.execute(); const results = await reporter.getResults(); diff --git a/packages/jest-resolve/package.json b/packages/jest-resolve/package.json index fe0e8e5de75e..e4056c273d91 100644 --- a/packages/jest-resolve/package.json +++ b/packages/jest-resolve/package.json @@ -21,6 +21,7 @@ "browser-resolve": "^1.11.3", "chalk": "^3.0.0", "jest-pnp-resolver": "^1.2.1", + "read-pkg-up": "^7.0.1", "realpath-native": "^2.0.0", "resolve": "^1.15.1", "slash": "^3.0.0" diff --git a/packages/jest-resolve/src/index.ts b/packages/jest-resolve/src/index.ts index 15856fb5aeb2..5cdbbd8a3ef3 100644 --- a/packages/jest-resolve/src/index.ts +++ b/packages/jest-resolve/src/index.ts @@ -15,6 +15,7 @@ import isBuiltinModule from './isBuiltinModule'; import defaultResolver, {clearDefaultResolverCache} from './defaultResolver'; import type {ResolverConfig} from './types'; import ModuleNotFoundError from './ModuleNotFoundError'; +import shouldLoadAsEsm, {clearCachedLookups} from './shouldLoadAsEsm'; type FindNodeModuleConfig = { basedir: Config.Path; @@ -100,6 +101,7 @@ class Resolver { static clearDefaultResolverCache(): void { clearDefaultResolverCache(); + clearCachedLookups(); } static findNodeModule( @@ -129,6 +131,9 @@ class Resolver { return null; } + // unstable as it should be replaced by https://github.com/nodejs/modules/issues/393, and we don't want people to use it + static unstable_shouldLoadAsEsm = shouldLoadAsEsm; + resolveModuleFromDirIfExists( dirname: Config.Path, moduleName: string, diff --git a/packages/jest-resolve/src/shouldLoadAsEsm.ts b/packages/jest-resolve/src/shouldLoadAsEsm.ts new file mode 100644 index 000000000000..73312d0cf0b5 --- /dev/null +++ b/packages/jest-resolve/src/shouldLoadAsEsm.ts @@ -0,0 +1,65 @@ +/** + * 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. + */ + +import {dirname, extname} from 'path'; +// @ts-ignore: experimental, not added to the types +import {SourceTextModule} from 'vm'; +import type {Config} from '@jest/types'; +import readPkgUp = require('read-pkg-up'); + +const runtimeSupportsVmModules = typeof SourceTextModule === 'function'; + +const cachedLookups = new Map(); + +export function clearCachedLookups(): void { + cachedLookups.clear(); +} + +export default function cachedShouldLoadAsEsm(path: Config.Path): boolean { + let cachedLookup = cachedLookups.get(path); + + if (cachedLookup === undefined) { + cachedLookup = shouldLoadAsEsm(path); + cachedLookups.set(path, cachedLookup); + } + + return cachedLookup; +} + +// this is a bad version of what https://github.com/nodejs/modules/issues/393 would provide +function shouldLoadAsEsm(path: Config.Path): boolean { + if (!runtimeSupportsVmModules) { + return false; + } + + const extension = extname(path); + + if (extension === '.mjs') { + return true; + } + + if (extension === '.cjs') { + return false; + } + + // this isn't correct - we might wanna load any file as a module (using synthetic module) + // do we need an option to Jest so people can opt in to ESM for non-js? + if (extension !== '.js') { + return false; + } + + const cwd = dirname(path); + + // TODO: can we cache lookups somehow? + const pkg = readPkgUp.sync({cwd, normalize: false}); + + if (!pkg) { + return false; + } + + return pkg.packageJson.type === 'module'; +} diff --git a/packages/jest-runner/src/runTest.ts b/packages/jest-runner/src/runTest.ts index 87b9e809720c..36754d3f086e 100644 --- a/packages/jest-runner/src/runTest.ts +++ b/packages/jest-runner/src/runTest.ts @@ -156,7 +156,15 @@ async function runTestInternal( const start = Date.now(); - config.setupFiles.forEach(path => runtime.requireModule(path)); + for (const path of config.setupFiles) { + const esm = runtime.unstable_shouldLoadAsEsm(path); + + if (esm) { + await runtime.unstable_importModule(path); + } else { + runtime.requireModule(path); + } + } const sourcemapOptions: sourcemapSupport.Options = { environment: 'node', diff --git a/packages/jest-runtime/src/__mocks__/createRuntime.js b/packages/jest-runtime/src/__mocks__/createRuntime.js index 1efc081eabfa..1512da0cb3eb 100644 --- a/packages/jest-runtime/src/__mocks__/createRuntime.js +++ b/packages/jest-runtime/src/__mocks__/createRuntime.js @@ -49,7 +49,15 @@ module.exports = async function createRuntime(filename, config) { Runtime.createResolver(config, hasteMap.moduleMap), ); - config.setupFiles.forEach(path => runtime.requireModule(path)); + for (const path of config.setupFiles) { + const esm = runtime.unstable_shouldLoadAsEsm(path); + + if (esm) { + await runtime.unstable_importModule(path); + } else { + runtime.requireModule(path); + } + } runtime.__mockRootPath = path.join(config.rootDir, 'root.js'); runtime.__mockSubdirPath = path.join( diff --git a/packages/jest-runtime/src/cli/index.ts b/packages/jest-runtime/src/cli/index.ts index e7c8e1dd3750..54857611e5c2 100644 --- a/packages/jest-runtime/src/cli/index.ts +++ b/packages/jest-runtime/src/cli/index.ts @@ -93,9 +93,22 @@ export async function run( const runtime = new Runtime(config, environment, hasteMap.resolver); - config.setupFiles.forEach(path => runtime.requireModule(path)); + for (const path of config.setupFiles) { + const esm = runtime.unstable_shouldLoadAsEsm(path); - runtime.requireModule(filePath); + if (esm) { + await runtime.unstable_importModule(path); + } else { + runtime.requireModule(path); + } + } + const esm = runtime.unstable_shouldLoadAsEsm(filePath); + + if (esm) { + await runtime.unstable_importModule(filePath); + } else { + runtime.requireModule(filePath); + } } catch (e) { console.error(chalk.red(e.stack || e)); process.on('exit', () => (process.exitCode = 1)); diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index d80cfd780b8e..62802e44e61f 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -5,9 +5,16 @@ * LICENSE file in the root directory of this source tree. */ -import {URL, fileURLToPath} from 'url'; +import {URL, fileURLToPath, pathToFileURL} from 'url'; import * as path from 'path'; -import {Script, compileFunction} from 'vm'; +import { + Script, + // @ts-ignore: experimental, not added to the types + SourceTextModule, + // @ts-ignore: experimental, not added to the types + Module as VMModule, + compileFunction, +} from 'vm'; import * as nativeModule from 'module'; import type {Config, Global} from '@jest/types'; import type { @@ -48,6 +55,10 @@ interface JestGlobalsValues extends Global.TestFrameworkGlobals { expect: JestGlobals.expect; } +interface JestImportMeta extends ImportMeta { + jest: Jest; +} + type HasteMapOptions = { console?: Console; maxWorkers: number; @@ -106,6 +117,8 @@ const EVAL_RESULT_VARIABLE = 'Object.'; type RunScriptEvalResult = {[EVAL_RESULT_VARIABLE]: ModuleWrapper}; +const runtimeSupportsVmModules = typeof SourceTextModule === 'function'; + /* eslint-disable-next-line no-redeclare */ class Runtime { private _cacheFS: CacheFS; @@ -125,6 +138,7 @@ class Runtime { private _moduleMocker: typeof jestMock; private _isolatedModuleRegistry: ModuleRegistry | null; private _moduleRegistry: ModuleRegistry; + private _esmoduleRegistry: Map; private _needsCoverageMapped: Set; private _resolver: Resolver; private _shouldAutoMock: boolean; @@ -169,6 +183,7 @@ class Runtime { this._isolatedModuleRegistry = null; this._isolatedMockRegistry = null; this._moduleRegistry = new Map(); + this._esmoduleRegistry = new Map(); this._needsCoverageMapped = new Set(); this._resolver = resolver; this._scriptTransformer = new ScriptTransformer(config); @@ -302,6 +317,77 @@ class Runtime { return cliOptions; } + // unstable as it should be replaced by https://github.com/nodejs/modules/issues/393, and we don't want people to use it + unstable_shouldLoadAsEsm = Resolver.unstable_shouldLoadAsEsm; + + private async loadEsmModule( + modulePath: Config.Path, + query = '', + ): Promise { + const cacheKey = modulePath + query; + + if (!this._esmoduleRegistry.has(cacheKey)) { + const context = this._environment.getVmContext!(); + + invariant(context); + + const transformedFile = this.transformFile(modulePath, { + isInternalModule: false, + supportsDynamicImport: true, + supportsStaticESM: true, + }); + + const module = new SourceTextModule(transformedFile.code, { + context, + identifier: modulePath, + importModuleDynamically: ( + specifier: string, + referencingModule: VMModule, + ) => + this.loadEsmModule( + this._resolveModule(referencingModule.identifier, specifier), + ), + initializeImportMeta(meta: JestImportMeta) { + meta.url = pathToFileURL(modulePath).href; + // @ts-ignore TODO: fill this + meta.jest = {}; + }, + }); + + this._esmoduleRegistry.set(cacheKey, module); + } + + const module = this._esmoduleRegistry.get(cacheKey); + + invariant(module); + + return module; + } + + async unstable_importModule( + from: Config.Path, + moduleName?: string, + ): Promise { + invariant( + runtimeSupportsVmModules, + 'You need to run with a version of node that supports ES Modules in the VM API.', + ); + invariant( + typeof this._environment.getVmContext === 'function', + 'ES Modules are only supported if your test environment has the `getVmContext` function', + ); + + const modulePath = this._resolveModule(from, moduleName); + + const module = await this.loadEsmModule(modulePath); + await module.link((specifier: string, referencingModule: VMModule) => + this.loadEsmModule( + this._resolveModule(referencingModule.identifier, specifier), + ), + ); + await module.evaluate(); + } + requireModule( from: Config.Path, moduleName?: string, @@ -795,23 +881,8 @@ class Runtime { Object.defineProperty(localModule, 'require', { value: this._createRequireImplementation(localModule, options), }); - const transformedFile = this._scriptTransformer.transform( - filename, - this._getFullTransformationOptions(options), - this._cacheFS[filename], - ); - - // we only care about non-internal modules - if (!options || !options.isInternalModule) { - this._fileTransforms.set(filename, transformedFile); - } - if (transformedFile.sourceMapPath) { - this._sourceMapRegistry[filename] = transformedFile.sourceMapPath; - if (transformedFile.mapCoverage) { - this._needsCoverageMapped.add(filename); - } - } + const transformedFile = this.transformFile(filename, options); let compiledFunction: ModuleWrapper | null = null; @@ -907,6 +978,27 @@ class Runtime { this._currentlyExecutingModulePath = lastExecutingModulePath; } + private transformFile(filename: string, options?: InternalModuleOptions) { + const transformedFile = this._scriptTransformer.transform( + filename, + this._getFullTransformationOptions(options), + this._cacheFS[filename], + ); + + // we only care about non-internal modules + if (!options || !options.isInternalModule) { + this._fileTransforms.set(filename, transformedFile); + } + + if (transformedFile.sourceMapPath) { + this._sourceMapRegistry[filename] = transformedFile.sourceMapPath; + if (transformedFile.mapCoverage) { + this._needsCoverageMapped.add(filename); + } + } + return transformedFile; + } + private createScriptFromCode(scriptSource: string, filename: string) { try { return new Script(this.wrapCodeInModuleWrapper(scriptSource), { diff --git a/yarn.lock b/yarn.lock index 7a6349aa8a03..5263a63044e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2435,6 +2435,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7" integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g== +"@types/normalize-package-data@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" + integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + "@types/prettier@^1.19.0": version "1.19.1" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f" @@ -9178,6 +9183,11 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + list-item@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/list-item/-/list-item-1.1.1.tgz#0c65d00e287cb663ccb3cb3849a77e89ec268a56" @@ -11200,6 +11210,16 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" +parse-json@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f" + integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + lines-and-columns "^1.1.6" + parse-node-version@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" @@ -12272,6 +12292,15 @@ read-pkg-up@^3.0.0: find-up "^2.0.0" read-pkg "^3.0.0" +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + read-pkg@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" @@ -12299,6 +12328,16 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + read@1, read@~1.0.1: version "1.0.7" resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" @@ -14293,6 +14332,11 @@ type-fest@^0.3.0, type-fest@^0.3.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + type-fest@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" From a9c1bfcaecad034a727c7ff78248fb270b1c1db3 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 10 Apr 2020 13:32:36 +0200 Subject: [PATCH 02/10] support importing node core modules --- .../__snapshots__/nativeEsm.test.ts.snap | 2 +- e2e/native-esm/__tests__/native-esm.test.js | 17 +++++++++ packages/jest-runtime/src/index.ts | 38 ++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap index d3b49ee4f1c8..4a6a74329208 100644 --- a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap +++ b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap @@ -2,7 +2,7 @@ exports[`on node ^12.16.0 || >=13.0.0 runs test with native ESM 1`] = ` Test Suites: 1 passed, 1 total -Tests: 2 passed, 2 total +Tests: 3 passed, 3 total Snapshots: 0 total Time: <> Ran all test suites. diff --git a/e2e/native-esm/__tests__/native-esm.test.js b/e2e/native-esm/__tests__/native-esm.test.js index 222c038fbc52..0f7e54319151 100644 --- a/e2e/native-esm/__tests__/native-esm.test.js +++ b/e2e/native-esm/__tests__/native-esm.test.js @@ -5,9 +5,13 @@ * LICENSE file in the root directory of this source tree. */ +import {readFileSync} from 'fs'; +import {dirname, resolve} from 'path'; +import {fileURLToPath} from 'url'; import {double} from '../index'; test('should have correct import.meta', () => { + expect(typeof require).toBe('undefined'); expect(typeof jest).toBe('undefined'); expect(import.meta).toEqual({ jest: expect.anything(), @@ -21,3 +25,16 @@ test('should have correct import.meta', () => { test('should double stuff', () => { expect(double(1)).toBe(2); }); + +test('should support importing node core modules', () => { + const dir = dirname(fileURLToPath(import.meta.url)); + const packageJsonPath = resolve(dir, '../package.json'); + + expect(JSON.parse(readFileSync(packageJsonPath, 'utf8'))).toEqual({ + jest: { + testEnvironment: 'node', + transform: {}, + }, + type: 'module', + }); +}); diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 62802e44e61f..c7dda4b008c7 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -12,6 +12,9 @@ import { // @ts-ignore: experimental, not added to the types SourceTextModule, // @ts-ignore: experimental, not added to the types + SyntheticModule, + Context as VMContext, + // @ts-ignore: experimental, not added to the types Module as VMModule, compileFunction, } from 'vm'; @@ -331,6 +334,12 @@ class Runtime { invariant(context); + if (this._resolver.isCoreModule(modulePath)) { + const core = await this._importCoreModule(modulePath, context); + this._esmoduleRegistry.set(cacheKey, core); + return core; + } + const transformedFile = this.transformFile(modulePath, { isInternalModule: false, supportsDynamicImport: true, @@ -1024,6 +1033,24 @@ class Runtime { return require(moduleName); } + private _importCoreModule(moduleName: string, context: VMContext) { + const required = this._requireCoreModule(moduleName); + + return new SyntheticModule( + ['default', ...Object.keys(required)], + function () { + // @ts-ignore: TS doesn't know what `this` is + this.setExport('default', required); + Object.entries(required).forEach(([key, value]) => { + // @ts-ignore: TS doesn't know what `this` is + this.setExport(key, value); + }); + }, + // should identifier be `node://${moduleName}`? + {context, identifier: moduleName}, + ); + } + private _getMockedNativeModule(): typeof nativeModule.Module { if (this._moduleImplementation) { return this._moduleImplementation; @@ -1058,13 +1085,20 @@ class Runtime { // should we implement the class ourselves? class Module extends nativeModule.Module {} + Object.entries(nativeModule.Module).forEach(([key, value]) => { + // @ts-ignore + Module[key] = value; + }); + Module.Module = Module; if ('createRequire' in nativeModule) { Module.createRequire = createRequire; } if ('createRequireFromPath' in nativeModule) { - Module.createRequireFromPath = (filename: string | URL) => { + Module.createRequireFromPath = function createRequireFromPath( + filename: string | URL, + ) { if (typeof filename !== 'string') { const error = new TypeError( `The argument 'filename' must be string. Received '${filename}'.${ @@ -1081,7 +1115,7 @@ class Runtime { }; } if ('syncBuiltinESMExports' in nativeModule) { - Module.syncBuiltinESMExports = () => {}; + Module.syncBuiltinESMExports = function syncBuiltinESMExports() {}; } this._moduleImplementation = Module; From ad64ade6deae66891d02dd76a2fefdef0f14f59c Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 10 Apr 2020 13:43:24 +0200 Subject: [PATCH 03/10] add test for dynamic import --- e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap | 2 +- e2e/native-esm/__tests__/native-esm.test.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap index 4a6a74329208..7aa2b751b2f4 100644 --- a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap +++ b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap @@ -2,7 +2,7 @@ exports[`on node ^12.16.0 || >=13.0.0 runs test with native ESM 1`] = ` Test Suites: 1 passed, 1 total -Tests: 3 passed, 3 total +Tests: 4 passed, 4 total Snapshots: 0 total Time: <> Ran all test suites. diff --git a/e2e/native-esm/__tests__/native-esm.test.js b/e2e/native-esm/__tests__/native-esm.test.js index 0f7e54319151..4fde434ab166 100644 --- a/e2e/native-esm/__tests__/native-esm.test.js +++ b/e2e/native-esm/__tests__/native-esm.test.js @@ -38,3 +38,10 @@ test('should support importing node core modules', () => { type: 'module', }); }); + +test('dynamic import should work', async () => { + const {double: importedDouble} = await import('../index'); + + expect(importedDouble).toBe(double); + expect(importedDouble(1)).toBe(2); +}); From d4b5d1df829640b4c92d1ab5340e824a73908f17 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 13 Apr 2020 11:22:00 +0200 Subject: [PATCH 04/10] Update e2e/__tests__/nativeEsm.test.ts Co-Authored-By: Tim Seckinger --- e2e/__tests__/nativeEsm.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/__tests__/nativeEsm.test.ts b/e2e/__tests__/nativeEsm.test.ts index a36c24c1e72d..405547a39743 100644 --- a/e2e/__tests__/nativeEsm.test.ts +++ b/e2e/__tests__/nativeEsm.test.ts @@ -21,7 +21,7 @@ test('test config is without transform', () => { }); // The versions vm.Module was introduced -onNodeVersions('^12.16.0 || >=13.0.0', () => { +onNodeVersions('>=12.16.0', () => { test('runs test with native ESM', () => { const {exitCode, stderr, stdout} = runJest(DIR, [], { nodeOptions: '--experimental-vm-modules', From 988e791595247af93b55cce944a6ff2eb0519f02 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 13 Apr 2020 11:22:53 +0200 Subject: [PATCH 05/10] renamed snap --- e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap index 7aa2b751b2f4..1ec158742c61 100644 --- a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap +++ b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`on node ^12.16.0 || >=13.0.0 runs test with native ESM 1`] = ` +exports[`on node >=12.16.0 runs test with native ESM 1`] = ` Test Suites: 1 passed, 1 total Tests: 4 passed, 4 total Snapshots: 0 total From 7bca6a21a5ef33492c7023766556cc39cb701108 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 13 Apr 2020 11:24:37 +0200 Subject: [PATCH 06/10] clear registry on module reset --- packages/jest-runtime/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index c7dda4b008c7..6d641b37ed36 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -672,6 +672,7 @@ class Runtime { this._isolatedMockRegistry = null; this._mockRegistry.clear(); this._moduleRegistry.clear(); + this._esmoduleRegistry.clear(); if (this._environment) { if (this._environment.global) { From a40e22d4121c98dd22a4dc7f5de016babfa201be Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 13 Apr 2020 12:03:59 +0200 Subject: [PATCH 07/10] invariant over ! --- packages/jest-runtime/src/index.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 6d641b37ed36..d822b5301f60 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -59,7 +59,7 @@ interface JestGlobalsValues extends Global.TestFrameworkGlobals { } interface JestImportMeta extends ImportMeta { - jest: Jest; + jest: JestGlobals.jest; } type HasteMapOptions = { @@ -330,7 +330,12 @@ class Runtime { const cacheKey = modulePath + query; if (!this._esmoduleRegistry.has(cacheKey)) { - const context = this._environment.getVmContext!(); + invariant( + typeof this._environment.getVmContext === 'function', + 'ES Modules are only supported if your test environment has the `getVmContext` function', + ); + + const context = this._environment.getVmContext(); invariant(context); @@ -381,10 +386,6 @@ class Runtime { runtimeSupportsVmModules, 'You need to run with a version of node that supports ES Modules in the VM API.', ); - invariant( - typeof this._environment.getVmContext === 'function', - 'ES Modules are only supported if your test environment has the `getVmContext` function', - ); const modulePath = this._resolveModule(from, moduleName); From dc365a6e970913432bdc20fee01bc07862a274da Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 13 Apr 2020 12:10:56 +0200 Subject: [PATCH 08/10] cache directory lookups as well --- packages/jest-resolve/src/shouldLoadAsEsm.ts | 23 +++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/jest-resolve/src/shouldLoadAsEsm.ts b/packages/jest-resolve/src/shouldLoadAsEsm.ts index 73312d0cf0b5..1e52d9419028 100644 --- a/packages/jest-resolve/src/shouldLoadAsEsm.ts +++ b/packages/jest-resolve/src/shouldLoadAsEsm.ts @@ -13,18 +13,20 @@ import readPkgUp = require('read-pkg-up'); const runtimeSupportsVmModules = typeof SourceTextModule === 'function'; -const cachedLookups = new Map(); +const cachedFileLookups = new Map(); +const cachedDirLookups = new Map(); export function clearCachedLookups(): void { - cachedLookups.clear(); + cachedFileLookups.clear(); + cachedDirLookups.clear(); } export default function cachedShouldLoadAsEsm(path: Config.Path): boolean { - let cachedLookup = cachedLookups.get(path); + let cachedLookup = cachedFileLookups.get(path); if (cachedLookup === undefined) { cachedLookup = shouldLoadAsEsm(path); - cachedLookups.set(path, cachedLookup); + cachedFileLookups.set(path, cachedLookup); } return cachedLookup; @@ -54,12 +56,23 @@ function shouldLoadAsEsm(path: Config.Path): boolean { const cwd = dirname(path); + const cachedLookup = cachedDirLookups.get(cwd); + + if (cachedLookup !== undefined) { + return cachedLookup; + } + // TODO: can we cache lookups somehow? const pkg = readPkgUp.sync({cwd, normalize: false}); if (!pkg) { + cachedDirLookups.set(cwd, false); return false; } - return pkg.packageJson.type === 'module'; + const isTypeModule = pkg.packageJson.type === 'module'; + + cachedDirLookups.set(cwd, isTypeModule); + + return isTypeModule; } From bb5c50a36b2736f7dd45a712a0abf68a184a33d7 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 13 Apr 2020 12:21:42 +0200 Subject: [PATCH 09/10] remove import.meta.jest --- e2e/native-esm/__tests__/native-esm.test.js | 1 - packages/jest-runtime/src/index.ts | 16 +++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/e2e/native-esm/__tests__/native-esm.test.js b/e2e/native-esm/__tests__/native-esm.test.js index 4fde434ab166..3b8508cd77cf 100644 --- a/e2e/native-esm/__tests__/native-esm.test.js +++ b/e2e/native-esm/__tests__/native-esm.test.js @@ -14,7 +14,6 @@ test('should have correct import.meta', () => { expect(typeof require).toBe('undefined'); expect(typeof jest).toBe('undefined'); expect(import.meta).toEqual({ - jest: expect.anything(), url: expect.any(String), }); expect( diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index d822b5301f60..1a5cff8361a7 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -58,10 +58,6 @@ interface JestGlobalsValues extends Global.TestFrameworkGlobals { expect: JestGlobals.expect; } -interface JestImportMeta extends ImportMeta { - jest: JestGlobals.jest; -} - type HasteMapOptions = { console?: Console; maxWorkers: number; @@ -327,6 +323,13 @@ class Runtime { modulePath: Config.Path, query = '', ): Promise { + if (modulePath === '@jest/globals') { + // TODO: create a Synthetic Module for this. Will need to create a `jest` object without a `LocalModuleRequire` + throw new Error( + 'Importing `@jest/globals` is not supported from ESM yet', + ); + } + const cacheKey = modulePath + query; if (!this._esmoduleRegistry.has(cacheKey)) { @@ -361,10 +364,8 @@ class Runtime { this.loadEsmModule( this._resolveModule(referencingModule.identifier, specifier), ), - initializeImportMeta(meta: JestImportMeta) { + initializeImportMeta(meta: ImportMeta) { meta.url = pathToFileURL(modulePath).href; - // @ts-ignore TODO: fill this - meta.jest = {}; }, }); @@ -1486,6 +1487,7 @@ class Runtime { private getGlobalsForFile(from: Config.Path): JestGlobalsValues { const jest = this.jestObjectCaches.get(from); + // This won't exist in ESM invariant(jest, 'There should always be a Jest object already'); return { From 4e2630c839c6eb7233448b5aedb1c18731c239f4 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 14 Apr 2020 09:18:58 +0200 Subject: [PATCH 10/10] split out function --- packages/jest-resolve/src/shouldLoadAsEsm.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/jest-resolve/src/shouldLoadAsEsm.ts b/packages/jest-resolve/src/shouldLoadAsEsm.ts index 1e52d9419028..ca49e3fdda75 100644 --- a/packages/jest-resolve/src/shouldLoadAsEsm.ts +++ b/packages/jest-resolve/src/shouldLoadAsEsm.ts @@ -56,23 +56,23 @@ function shouldLoadAsEsm(path: Config.Path): boolean { const cwd = dirname(path); - const cachedLookup = cachedDirLookups.get(cwd); + let cachedLookup = cachedDirLookups.get(cwd); - if (cachedLookup !== undefined) { - return cachedLookup; + if (cachedLookup === undefined) { + cachedLookup = cachedPkgCheck(cwd); + cachedFileLookups.set(cwd, cachedLookup); } + return cachedLookup; +} + +function cachedPkgCheck(cwd: Config.Path): boolean { // TODO: can we cache lookups somehow? const pkg = readPkgUp.sync({cwd, normalize: false}); if (!pkg) { - cachedDirLookups.set(cwd, false); return false; } - const isTypeModule = pkg.packageJson.type === 'module'; - - cachedDirLookups.set(cwd, isTypeModule); - - return isTypeModule; + return pkg.packageJson.type === 'module'; }