From 5c21bae9f23406090214b330b9fb45ef398758c3 Mon Sep 17 00:00:00 2001 From: chentsulin Date: Thu, 28 Oct 2021 14:07:33 +0800 Subject: [PATCH] feat(jest-haste-map): support dependencyExtractor written in ESM --- e2e/__tests__/findRelatedFiles.test.ts | 58 ++++ packages/jest-core/src/cli/index.ts | 2 +- .../src/__tests__/dependencyExtractor.mjs | 37 +++ .../src/__tests__/includes_dotfiles.test.ts | 4 +- .../src/__tests__/index.test.js | 249 ++++++++++-------- .../src/__tests__/worker.test.js | 17 ++ packages/jest-haste-map/src/index.ts | 22 +- packages/jest-haste-map/src/worker.ts | 9 +- .../src/__tests__/Runtime-statics.test.js | 8 +- packages/jest-runtime/src/index.ts | 4 +- 10 files changed, 287 insertions(+), 123 deletions(-) create mode 100644 packages/jest-haste-map/src/__tests__/dependencyExtractor.mjs diff --git a/e2e/__tests__/findRelatedFiles.test.ts b/e2e/__tests__/findRelatedFiles.test.ts index 7fc3b678426b..50157fe44e3f 100644 --- a/e2e/__tests__/findRelatedFiles.test.ts +++ b/e2e/__tests__/findRelatedFiles.test.ts @@ -8,6 +8,7 @@ import {tmpdir} from 'os'; import * as path from 'path'; import {wrap} from 'jest-snapshot-serializer-raw'; +import {onNodeVersions} from '@jest/test-utils'; import {cleanup, extractSummary, writeFiles} from '../Utils'; import runJest from '../runJest'; @@ -117,6 +118,63 @@ describe('--findRelatedTests flag', () => { expect(stderr).toMatch(summaryMsg); }); + // The versions where vm.Module exists and commonjs with "exports" is not broken + onNodeVersions('>=12.16.0', () => { + test('runs tests related to filename with a custom dependency extractor written in ESM', () => { + writeFiles(DIR, { + '.watchmanconfig': '', + '__tests__/test-skip-deps.test.js': ` + const dynamicImport = path => Promise.resolve(require(path)); + test('a', () => dynamicImport('../a').then(a => { + expect(a.foo).toBe(5); + })); + `, + '__tests__/test.test.js': ` + const dynamicImport = path => Promise.resolve(require(path)); + test('a', () => dynamicImport('../a').then(a => { + expect(a.foo).toBe(5); + })); + `, + 'a.js': 'module.exports = {foo: 5};', + 'dependencyExtractor.mjs': ` + const DYNAMIC_IMPORT_RE = /(?:^|[^.]\\s*)(\\bdynamicImport\\s*?\\(\\s*?)([\`'"])([^\`'"]+)(\\2\\s*?\\))/g; + export default { + extract(code, filePath) { + const dependencies = new Set(); + if (filePath.includes('skip-deps')) { + return dependencies; + } + + const addDependency = (match, pre, quot, dep, post) => { + dependencies.add(dep); + return match; + }; + code.replace(DYNAMIC_IMPORT_RE, addDependency); + return dependencies; + }, + }; + `, + 'package.json': JSON.stringify({ + jest: { + dependencyExtractor: '/dependencyExtractor.mjs', + testEnvironment: 'node', + }, + }), + }); + + const {stdout} = runJest(DIR, ['a.js']); + expect(stdout).toMatch(''); + + const {stderr} = runJest(DIR, ['--findRelatedTests', 'a.js']); + expect(stderr).toMatch('PASS __tests__/test.test.js'); + expect(stderr).not.toMatch('PASS __tests__/test-skip-deps.test.js'); + + const summaryMsg = + 'Ran all test suites related to files matching /a.js/i.'; + expect(stderr).toMatch(summaryMsg); + }); + }); + test('generates coverage report for filename', () => { writeFiles(DIR, { '.watchmanconfig': '', diff --git a/packages/jest-core/src/cli/index.ts b/packages/jest-core/src/cli/index.ts index 6e74f98b0250..d45ed4b15ef8 100644 --- a/packages/jest-core/src/cli/index.ts +++ b/packages/jest-core/src/cli/index.ts @@ -134,7 +134,7 @@ const buildContextsAndHasteMaps = async ( const contexts = await Promise.all( configs.map(async (config, index) => { createDirectory(config.cacheDirectory); - const hasteMapInstance = Runtime.createHasteMap(config, { + const hasteMapInstance = await Runtime.createHasteMap(config, { console: new CustomConsole(outputStream, outputStream), maxWorkers: Math.max( 1, diff --git a/packages/jest-haste-map/src/__tests__/dependencyExtractor.mjs b/packages/jest-haste-map/src/__tests__/dependencyExtractor.mjs new file mode 100644 index 000000000000..37b8627b523c --- /dev/null +++ b/packages/jest-haste-map/src/__tests__/dependencyExtractor.mjs @@ -0,0 +1,37 @@ +/** + * 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. + */ + +const blockCommentRe = /\/\*[^]*?\*\//g; +const lineCommentRe = /\/\/.*/g; +const LOAD_MODULE_RE = + /(?:^|[^.]\s*)(\bloadModule\s*?\(\s*?)([`'"])([^`'"]+)(\2\s*?\))/g; + +export function extract(code, filePath, defaultDependencyExtractor) { + const dependencies = defaultDependencyExtractor(code); + + const addDependency = (match, pre, quot, dep, post) => { + dependencies.add(dep); + return match; + }; + + code + .replace(blockCommentRe, '') + .replace(lineCommentRe, '') + .replace(LOAD_MODULE_RE, addDependency); + + return dependencies; +} + +let cacheKey; + +export function getCacheKey() { + return cacheKey; +} + +export function setCacheKey(key) { + cacheKey = key; +} diff --git a/packages/jest-haste-map/src/__tests__/includes_dotfiles.test.ts b/packages/jest-haste-map/src/__tests__/includes_dotfiles.test.ts index 990a8d7bf849..ee5868c63c9e 100644 --- a/packages/jest-haste-map/src/__tests__/includes_dotfiles.test.ts +++ b/packages/jest-haste-map/src/__tests__/includes_dotfiles.test.ts @@ -21,13 +21,13 @@ const commonOptions = { }; test('watchman crawler and node crawler both include dotfiles', async () => { - const hasteMapWithWatchman = new HasteMap({ + const hasteMapWithWatchman = await HasteMap.create({ ...commonOptions, name: 'withWatchman', useWatchman: true, }); - const hasteMapWithNode = new HasteMap({ + const hasteMapWithNode = await HasteMap.create({ ...commonOptions, name: 'withNode', useWatchman: false, diff --git a/packages/jest-haste-map/src/__tests__/index.test.js b/packages/jest-haste-map/src/__tests__/index.test.js index 90ca7dccf774..e30a334a678d 100644 --- a/packages/jest-haste-map/src/__tests__/index.test.js +++ b/packages/jest-haste-map/src/__tests__/index.test.js @@ -242,15 +242,21 @@ describe('HasteMap', () => { ); }); - it('creates different cache file paths for different roots', () => { + it('creates different cache file paths for different roots', async () => { jest.resetModules(); const HasteMap = require('../').default; - const hasteMap1 = new HasteMap({...defaultConfig, rootDir: '/root1'}); - const hasteMap2 = new HasteMap({...defaultConfig, rootDir: '/root2'}); + const hasteMap1 = await HasteMap.create({ + ...defaultConfig, + rootDir: '/root1', + }); + const hasteMap2 = await HasteMap.create({ + ...defaultConfig, + rootDir: '/root2', + }); expect(hasteMap1.getCacheFilePath()).not.toBe(hasteMap2.getCacheFilePath()); }); - it('creates different cache file paths for different dependency extractor cache keys', () => { + it('creates different cache file paths for different dependency extractor cache keys', async () => { jest.resetModules(); const HasteMap = require('../').default; const dependencyExtractor = require('./dependencyExtractor'); @@ -259,47 +265,53 @@ describe('HasteMap', () => { dependencyExtractor: require.resolve('./dependencyExtractor'), }; dependencyExtractor.setCacheKey('foo'); - const hasteMap1 = new HasteMap(config); + const hasteMap1 = await HasteMap.create(config); dependencyExtractor.setCacheKey('bar'); - const hasteMap2 = new HasteMap(config); + const hasteMap2 = await HasteMap.create(config); expect(hasteMap1.getCacheFilePath()).not.toBe(hasteMap2.getCacheFilePath()); }); - it('creates different cache file paths for different values of computeDependencies', () => { + it('creates different cache file paths for different values of computeDependencies', async () => { jest.resetModules(); const HasteMap = require('../').default; - const hasteMap1 = new HasteMap({ + const hasteMap1 = await HasteMap.create({ ...defaultConfig, computeDependencies: true, }); - const hasteMap2 = new HasteMap({ + const hasteMap2 = await HasteMap.create({ ...defaultConfig, computeDependencies: false, }); expect(hasteMap1.getCacheFilePath()).not.toBe(hasteMap2.getCacheFilePath()); }); - it('creates different cache file paths for different hasteImplModulePath cache keys', () => { + it('creates different cache file paths for different hasteImplModulePath cache keys', async () => { jest.resetModules(); const HasteMap = require('../').default; const hasteImpl = require('./haste_impl'); hasteImpl.setCacheKey('foo'); - const hasteMap1 = new HasteMap(defaultConfig); + const hasteMap1 = await HasteMap.create(defaultConfig); hasteImpl.setCacheKey('bar'); - const hasteMap2 = new HasteMap(defaultConfig); + const hasteMap2 = await HasteMap.create(defaultConfig); expect(hasteMap1.getCacheFilePath()).not.toBe(hasteMap2.getCacheFilePath()); }); - it('creates different cache file paths for different projects', () => { + it('creates different cache file paths for different projects', async () => { jest.resetModules(); const HasteMap = require('../').default; - const hasteMap1 = new HasteMap({...defaultConfig, name: '@scoped/package'}); - const hasteMap2 = new HasteMap({...defaultConfig, name: '-scoped-package'}); + const hasteMap1 = await HasteMap.create({ + ...defaultConfig, + name: '@scoped/package', + }); + const hasteMap2 = await HasteMap.create({ + ...defaultConfig, + name: '-scoped-package', + }); expect(hasteMap1.getCacheFilePath()).not.toBe(hasteMap2.getCacheFilePath()); }); it('matches files against a pattern', async () => { - const {hasteFS} = await new HasteMap(defaultConfig).build(); + const {hasteFS} = await (await HasteMap.create(defaultConfig)).build(); expect( hasteFS.matchFiles( process.platform === 'win32' ? /project\\fruits/ : /project\/fruits/, @@ -321,7 +333,7 @@ describe('HasteMap', () => { mockFs[path.join('/', 'project', 'fruits', 'Kiwi.js')] = ` // Kiwi! `; - const {hasteFS} = await new HasteMap(config).build(); + const {hasteFS} = await (await HasteMap.create(config)).build(); expect(hasteFS.matchFiles(/Kiwi/)).toEqual([]); }); @@ -329,7 +341,7 @@ describe('HasteMap', () => { mockFs[path.join('/', 'project', 'fruits', '.git', 'fruit-history.js')] = ` // test `; - const {hasteFS} = await new HasteMap(defaultConfig).build(); + const {hasteFS} = await (await HasteMap.create(defaultConfig)).build(); expect(hasteFS.matchFiles('.git')).toEqual([]); }); @@ -342,7 +354,7 @@ describe('HasteMap', () => { mockFs[path.join('/', 'project', 'fruits', '.git', 'fruit-history.js')] = ` // test `; - const {hasteFS} = await new HasteMap(config).build(); + const {hasteFS} = await (await HasteMap.create(config)).build(); expect(hasteFS.matchFiles(/Kiwi/)).toEqual([]); expect(hasteFS.matchFiles('.git')).toEqual([]); }); @@ -354,7 +366,7 @@ describe('HasteMap', () => { `; try { - await new HasteMap(config).build(); + await (await HasteMap.create(config)).build(); } catch (err) { expect(err.message).toBe( 'jest-haste-map: the `ignorePattern` option must be a RegExp', @@ -440,7 +452,7 @@ describe('HasteMap', () => { // fbjs2 `; - const hasteMap = new HasteMap({ + const hasteMap = await HasteMap.create({ ...defaultConfig, mocksPattern: '__mocks__', }); @@ -518,40 +530,37 @@ describe('HasteMap', () => { expect(useBuitinsInContext(hasteMap.read())).toEqual(data); }); - it('throws if both symlinks and watchman is enabled', () => { - expect( - () => new HasteMap({...defaultConfig, enableSymlinks: true}), - ).toThrow( + it('throws if both symlinks and watchman is enabled', async () => { + await expect(() => + HasteMap.create({...defaultConfig, enableSymlinks: true}), + ).rejects.toThrow( 'Set either `enableSymlinks` to false or `useWatchman` to false.', ); - expect( - () => - new HasteMap({ - ...defaultConfig, - enableSymlinks: true, - useWatchman: true, - }), - ).toThrow( + await expect(() => + HasteMap.create({ + ...defaultConfig, + enableSymlinks: true, + useWatchman: true, + }), + ).rejects.toThrow( 'Set either `enableSymlinks` to false or `useWatchman` to false.', ); - expect( - () => - new HasteMap({ - ...defaultConfig, - enableSymlinks: false, - useWatchman: true, - }), - ).not.toThrow(); + await expect(() => + HasteMap.create({ + ...defaultConfig, + enableSymlinks: false, + useWatchman: true, + }), + ).rejects.not.toThrow(); - expect( - () => - new HasteMap({ - ...defaultConfig, - enableSymlinks: true, - useWatchman: false, - }), - ).not.toThrow(); + await expect(() => + HasteMap.create({ + ...defaultConfig, + enableSymlinks: true, + useWatchman: false, + }), + ).rejects.not.toThrow(); }); describe('builds a haste map on a fresh cache with SHA-1s', () => { @@ -604,7 +613,7 @@ describe('HasteMap', () => { }); }); - const hasteMap = new HasteMap({ + const hasteMap = await HasteMap.create({ ...defaultConfig, computeSha1: true, maxWorkers: 1, @@ -667,7 +676,7 @@ describe('HasteMap', () => { module.exports = require("./video.mp4"); `; - const hasteMap = new HasteMap({ + const hasteMap = await HasteMap.create({ ...defaultConfig, extensions: [...defaultConfig.extensions], roots: [...defaultConfig.roots, path.join('/', 'project', 'video')], @@ -690,7 +699,7 @@ describe('HasteMap', () => { // fbjs! `; - const hasteMap = new HasteMap({ + const hasteMap = await HasteMap.create({ ...defaultConfig, mocksPattern: '__mocks__', retainAllFiles: true, @@ -739,11 +748,13 @@ describe('HasteMap', () => { `; try { - await new HasteMap({ - mocksPattern: '__mocks__', - throwOnModuleCollision: true, - ...defaultConfig, - }).build(); + await ( + await HasteMap.create({ + mocksPattern: '__mocks__', + throwOnModuleCollision: true, + ...defaultConfig, + }) + ).build(); } catch { expect( wrap(console.error.mock.calls[0][0].replace(/\\/g, '/')), @@ -756,7 +767,9 @@ describe('HasteMap', () => { const Banana = require("Banana"); `; - const {__hasteMapForTest: data} = await new HasteMap(defaultConfig).build(); + const {__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build(); // Duplicate modules are removed so that it doesn't cause // non-determinism later on. @@ -772,10 +785,10 @@ describe('HasteMap', () => { const Banana = require("Banana"); `; - await new HasteMap(defaultConfig).build(); + await (await HasteMap.create(defaultConfig)).build(); expect(console.warn).toHaveBeenCalledTimes(1); - await new HasteMap(defaultConfig).build(); + await (await HasteMap.create(defaultConfig)).build(); expect(console.warn).toHaveBeenCalledTimes(1); }); @@ -787,10 +800,12 @@ describe('HasteMap', () => { `; try { - await new HasteMap({ - throwOnModuleCollision: true, - ...defaultConfig, - }).build(); + await ( + await HasteMap.create({ + throwOnModuleCollision: true, + ...defaultConfig, + }) + ).build(); } catch (err) { expect(err.message).toBe( 'Duplicated files or mocks. Please check the console for more info', @@ -812,7 +827,9 @@ describe('HasteMap', () => { const Blackberry = require("Blackberry"); `; - const {__hasteMapForTest: data} = await new HasteMap(defaultConfig).build(); + const {__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build(); expect(data.files).toEqual( createMap({ @@ -858,8 +875,8 @@ describe('HasteMap', () => { }); it('does not access the file system on a warm cache with no changes', async () => { - const {__hasteMapForTest: initialData} = await new HasteMap( - defaultConfig, + const {__hasteMapForTest: initialData} = await ( + await HasteMap.create(defaultConfig) ).build(); // The first run should access the file system once for the (empty) @@ -877,7 +894,9 @@ describe('HasteMap', () => { vegetables: 'c:fake-clock:4', }); - const {__hasteMapForTest: data} = await new HasteMap(defaultConfig).build(); + const {__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build(); expect(fs.readFileSync.mock.calls.length).toBe(1); if (require('v8').deserialize) { expect(fs.readFileSync).toBeCalledWith(cacheFilePath); @@ -890,8 +909,8 @@ describe('HasteMap', () => { }); it('only does minimal file system access when files change', async () => { - const {__hasteMapForTest: initialData} = await new HasteMap( - defaultConfig, + const {__hasteMapForTest: initialData} = await ( + await HasteMap.create(defaultConfig) ).build(); fs.readFileSync.mockClear(); @@ -908,7 +927,9 @@ describe('HasteMap', () => { vegetables: 'c:fake-clock:2', }); - const {__hasteMapForTest: data} = await new HasteMap(defaultConfig).build(); + const {__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build(); expect(fs.readFileSync.mock.calls.length).toBe(2); @@ -941,8 +962,8 @@ describe('HasteMap', () => { }); it('correctly handles file deletions', async () => { - const {__hasteMapForTest: initialData} = await new HasteMap( - defaultConfig, + const {__hasteMapForTest: initialData} = await ( + await HasteMap.create(defaultConfig) ).build(); fs.readFileSync.mockClear(); @@ -958,7 +979,9 @@ describe('HasteMap', () => { vegetables: 'c:fake-clock:2', }); - const {__hasteMapForTest: data} = await new HasteMap(defaultConfig).build(); + const {__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build(); const files = new Map(initialData.files); files.delete(path.join('fruits', 'Banana.js')); @@ -975,7 +998,9 @@ describe('HasteMap', () => { const Banana = require("Banana"); `; let data; - ({__hasteMapForTest: data} = await new HasteMap(defaultConfig).build()); + ({__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build()); expect(data.map.get('Strawberry')).toEqual({ g: [path.join('fruits', 'Strawberry.js'), 0], }); @@ -987,7 +1012,9 @@ describe('HasteMap', () => { `, }); mockClocks = createMap({fruits: 'c:fake-clock:3'}); - ({__hasteMapForTest: data} = await new HasteMap(defaultConfig).build()); + ({__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build()); expect(data.map.get('Strawberry')).toEqual({ g: [path.join('fruits', 'Strawberry.js'), 0], ios: [path.join('fruits', 'Strawberry.ios.js'), 0], @@ -1003,7 +1030,9 @@ describe('HasteMap', () => { const Raspberry = require("Raspberry"); `; let data; - ({__hasteMapForTest: data} = await new HasteMap(defaultConfig).build()); + ({__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build()); expect(data.map.get('Strawberry')).toEqual({ g: [path.join('fruits', 'Strawberry.js'), 0], ios: [path.join('fruits', 'Strawberry.ios.js'), 0], @@ -1014,7 +1043,9 @@ describe('HasteMap', () => { [path.join('/', 'project', 'fruits', 'Strawberry.ios.js')]: null, }); mockClocks = createMap({fruits: 'c:fake-clock:3'}); - ({__hasteMapForTest: data} = await new HasteMap(defaultConfig).build()); + ({__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build()); expect(data.map.get('Strawberry')).toEqual({ g: [path.join('fruits', 'Strawberry.js'), 0], }); @@ -1026,7 +1057,9 @@ describe('HasteMap', () => { const Raspberry = require("Raspberry"); `; let data; - ({__hasteMapForTest: data} = await new HasteMap(defaultConfig).build()); + ({__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build()); expect(data.map.get('Strawberry')).toEqual({ ios: [path.join('fruits', 'Strawberry.ios.js'), 0], }); @@ -1039,7 +1072,9 @@ describe('HasteMap', () => { `, }); mockClocks = createMap({fruits: 'c:fake-clock:3'}); - ({__hasteMapForTest: data} = await new HasteMap(defaultConfig).build()); + ({__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build()); expect(data.map.get('Strawberry')).toEqual({ g: [path.join('fruits', 'Strawberry.js'), 0], }); @@ -1053,8 +1088,8 @@ describe('HasteMap', () => { const Blackberry = require("Blackberry"); `; - const {__hasteMapForTest: data} = await new HasteMap( - defaultConfig, + const {__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) ).build(); expect(useBuitinsInContext(data.duplicates)).toEqual( createMap({ @@ -1081,8 +1116,8 @@ describe('HasteMap', () => { vegetables: 'c:fake-clock:2', }); - const {__hasteMapForTest: data} = await new HasteMap( - defaultConfig, + const {__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) ).build(); expect(useBuitinsInContext(data.duplicates)).toEqual(new Map()); expect(data.map.get('Strawberry')).toEqual({ @@ -1101,8 +1136,8 @@ describe('HasteMap', () => { {"name": "Strawberry"} `; - const {__hasteMapForTest: data} = await new HasteMap( - defaultConfig, + const {__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) ).build(); expect(useBuitinsInContext(data.duplicates)).toEqual( @@ -1139,8 +1174,8 @@ describe('HasteMap', () => { fruits: 'c:fake-clock:4', }); - const {__hasteMapForTest: correctData} = await new HasteMap( - defaultConfig, + const {__hasteMapForTest: correctData} = await ( + await HasteMap.create(defaultConfig) ).build(); expect(useBuitinsInContext(correctData.duplicates)).toEqual(new Map()); @@ -1161,8 +1196,8 @@ describe('HasteMap', () => { vegetables: 'c:fake-clock:2', }); - const {__hasteMapForTest: data} = await new HasteMap( - defaultConfig, + const {__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) ).build(); expect(useBuitinsInContext(data.duplicates)).toEqual(new Map()); expect(data.map.get('Strawberry')).toEqual({ @@ -1180,7 +1215,7 @@ describe('HasteMap', () => { it('discards the cache when configuration changes', async () => { HasteMap.getCacheFilePath = getCacheFilePath; - await new HasteMap(defaultConfig).build(); + await (await HasteMap.create(defaultConfig)).build(); fs.readFileSync.mockClear(); // Explicitly mock that no files have changed. @@ -1193,7 +1228,7 @@ describe('HasteMap', () => { }); const config = {...defaultConfig, ignorePattern: /Kiwi|Pear/}; - const {moduleMap} = await new HasteMap(config).build(); + const {moduleMap} = await (await HasteMap.create(config)).build(); expect(moduleMap.getModule('Pear')).toBe(null); }); @@ -1215,7 +1250,9 @@ describe('HasteMap', () => { }), ); - const {__hasteMapForTest: data} = await new HasteMap(defaultConfig).build(); + const {__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build(); expect(data.files.size).toBe(5); // Ensure this file is not part of the file list. @@ -1228,12 +1265,14 @@ describe('HasteMap', () => { const jestWorker = require('jest-worker').Worker; const path = require('path'); const dependencyExtractor = path.join(__dirname, 'dependencyExtractor.js'); - const {__hasteMapForTest: data} = await new HasteMap({ - ...defaultConfig, - dependencyExtractor, - hasteImplModulePath: undefined, - maxWorkers: 4, - }).build(); + const {__hasteMapForTest: data} = await ( + await HasteMap.create({ + ...defaultConfig, + dependencyExtractor, + hasteImplModulePath: undefined, + maxWorkers: 4, + }) + ).build(); expect(jestWorker.mock.calls.length).toBe(1); @@ -1313,7 +1352,9 @@ describe('HasteMap', () => { }); }); - const {__hasteMapForTest: data} = await new HasteMap(defaultConfig).build(); + const {__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build(); expect(watchman).toBeCalled(); expect(node).toBeCalled(); @@ -1351,7 +1392,9 @@ describe('HasteMap', () => { }); }); - const {__hasteMapForTest: data} = await new HasteMap(defaultConfig).build(); + const {__hasteMapForTest: data} = await ( + await HasteMap.create(defaultConfig) + ).build(); expect(watchman).toBeCalled(); expect(node).toBeCalled(); @@ -1384,7 +1427,7 @@ describe('HasteMap', () => { ); try { - await new HasteMap(defaultConfig).build(); + await (await HasteMap.create(defaultConfig)).build(); } catch (error) { expect(error.message).toEqual( 'Crawler retry failed:\n' + @@ -1413,7 +1456,7 @@ describe('HasteMap', () => { mockFs = options.mockFs; } const watchConfig = {...defaultConfig, watch: true}; - const hm = new HasteMap(watchConfig); + const hm = await HasteMap.create(watchConfig); await hm.build(); try { await fn(hm); diff --git a/packages/jest-haste-map/src/__tests__/worker.test.js b/packages/jest-haste-map/src/__tests__/worker.test.js index 20326d12541d..6cea498b5dae 100644 --- a/packages/jest-haste-map/src/__tests__/worker.test.js +++ b/packages/jest-haste-map/src/__tests__/worker.test.js @@ -10,6 +10,7 @@ import * as path from 'path'; import * as fs from 'graceful-fs'; +import {onNodeVersions} from '@jest/test-utils'; import H from '../constants'; import {getSha1, worker} from '../worker'; @@ -92,6 +93,22 @@ describe('worker', () => { }); }); + // The versions where vm.Module exists and commonjs with "exports" is not broken + onNodeVersions('>=12.16.0', () => { + it('accepts a custom dependency extractor written in ESM', async () => { + expect( + await worker({ + computeDependencies: true, + dependencyExtractor: path.join(__dirname, 'dependencyExtractor.mjs'), + filePath: path.join('/project', 'fruits', 'Pear.js'), + rootDir, + }), + ).toEqual({ + dependencies: ['Banana', 'Strawberry', 'Lime'], + }); + }); + }); + it('delegates to hasteImplModulePath for getting the id', async () => { expect( await worker({ diff --git a/packages/jest-haste-map/src/index.ts b/packages/jest-haste-map/src/index.ts index 7d847d0f1b23..a63f9fdb1e73 100644 --- a/packages/jest-haste-map/src/index.ts +++ b/packages/jest-haste-map/src/index.ts @@ -16,6 +16,7 @@ import type {Stats} from 'graceful-fs'; import type {Config} from '@jest/types'; import {escapePathForRegex} from 'jest-regex-util'; import serializer from 'jest-serializer'; +import {requireOrImportModule} from 'jest-util'; import {Worker} from 'jest-worker'; import HasteFS from './HasteFS'; import HasteModuleMap from './ModuleMap'; @@ -230,12 +231,16 @@ export default class HasteMap extends EventEmitter { return HasteMap; } - static create(options: Options): HasteMap { + static async create(options: Options): Promise { if (options.hasteMapModulePath) { const CustomHasteMap = require(options.hasteMapModulePath); return new CustomHasteMap(options); } - return new HasteMap(options); + const hasteMap = new HasteMap(options); + + await hasteMap._setupCachePath(options); + + return hasteMap; } private constructor(options: Options) { @@ -292,6 +297,12 @@ export default class HasteMap extends EventEmitter { ); } + this._buildPromise = null; + this._watchers = []; + this._worker = null; + } + + async _setupCachePath(options: Options): Promise { const rootDirHash = createHash('md5').update(options.rootDir).digest('hex'); let hasteImplHash = ''; let dependencyExtractorHash = ''; @@ -304,7 +315,9 @@ export default class HasteMap extends EventEmitter { } if (options.dependencyExtractor) { - const dependencyExtractor = require(options.dependencyExtractor); + const dependencyExtractor = await requireOrImportModule( + options.dependencyExtractor, + ); if (dependencyExtractor.getCacheKey) { dependencyExtractorHash = String(dependencyExtractor.getCacheKey()); } @@ -327,9 +340,6 @@ export default class HasteMap extends EventEmitter { dependencyExtractorHash, this._options.computeDependencies.toString(), ); - this._buildPromise = null; - this._watchers = []; - this._worker = null; } static getCacheFilePath( diff --git a/packages/jest-haste-map/src/worker.ts b/packages/jest-haste-map/src/worker.ts index 7422bb5643fc..c6865f4ca4cb 100644 --- a/packages/jest-haste-map/src/worker.ts +++ b/packages/jest-haste-map/src/worker.ts @@ -8,6 +8,7 @@ import {createHash} from 'crypto'; import * as path from 'path'; import * as fs from 'graceful-fs'; +import {requireOrImportModule} from 'jest-util'; import blacklist from './blacklist'; import H from './constants'; import * as dependencyExtractor from './lib/dependencyExtractor'; @@ -73,11 +74,9 @@ export async function worker(data: WorkerMessage): Promise { const content = getContent(); dependencies = Array.from( data.dependencyExtractor - ? require(data.dependencyExtractor).extract( - content, - filePath, - dependencyExtractor.extract, - ) + ? ( + await requireOrImportModule(data.dependencyExtractor) + ).extract(content, filePath, dependencyExtractor.extract) : dependencyExtractor.extract(content), ); } diff --git a/packages/jest-runtime/src/__tests__/Runtime-statics.test.js b/packages/jest-runtime/src/__tests__/Runtime-statics.test.js index 5252fc5bd736..849e332a82f7 100644 --- a/packages/jest-runtime/src/__tests__/Runtime-statics.test.js +++ b/packages/jest-runtime/src/__tests__/Runtime-statics.test.js @@ -24,8 +24,8 @@ describe('Runtime statics', () => { jest.clearAllMocks(); }); - test('Runtime.createHasteMap passes correct ignore files to HasteMap', () => { - Runtime.createHasteMap(projectConfig, options); + test('Runtime.createHasteMap passes correct ignore files to HasteMap', async () => { + await Runtime.createHasteMap(projectConfig, options); expect(HasteMap.create).toBeCalledWith( expect.objectContaining({ ignorePattern: /\/root\/ignore-1|\/root\/ignore-2/, @@ -33,8 +33,8 @@ describe('Runtime statics', () => { ); }); - test('Runtime.createHasteMap passes correct ignore files to HasteMap in watch mode', () => { - Runtime.createHasteMap(projectConfig, {...options, watch: true}); + test('Runtime.createHasteMap passes correct ignore files to HasteMap in watch mode', async () => { + await Runtime.createHasteMap(projectConfig, {...options, watch: true}); expect(HasteMap.create).toBeCalledWith( expect.objectContaining({ ignorePattern: diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 8640b45ea405..df2dfcd023d0 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -335,7 +335,7 @@ export default class Runtime { }, ): Promise { createDirectory(config.cacheDirectory); - const instance = Runtime.createHasteMap(config, { + const instance = await Runtime.createHasteMap(config, { console: options.console, maxWorkers: options.maxWorkers, resetCache: !config.cache, @@ -355,7 +355,7 @@ export default class Runtime { static createHasteMap( config: Config.ProjectConfig, options?: HasteMapOptions, - ): HasteMap { + ): Promise { const ignorePatternParts = [ ...config.modulePathIgnorePatterns, ...(options && options.watch ? config.watchPathIgnorePatterns : []),