diff --git a/CHANGELOG.md b/CHANGELOG.md index 36dab227162f..b086303c9286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,12 +20,15 @@ - `[jest-environment-node]` [**BREAKING**] Second argument `context` to constructor is mandatory ([#12469](https://github.com/facebook/jest/pull/12469)) - `[@jest/expect]` New module which extends `expect` with `jest-snapshot` matchers ([#12404](https://github.com/facebook/jest/pull/12404), [#12410](https://github.com/facebook/jest/pull/12410), [#12418](https://github.com/facebook/jest/pull/12418)) - `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323)) +- `[jest-haste-map]` [**BREAKING**] `HasteMap.create` now returns a promise ([#12008](https://github.com/facebook/jest/pull/12008)) +- `[jest-haste-map]` Add support for `dependencyExtractor` written in ESM ([#12008](https://github.com/facebook/jest/pull/12008)) - `[jest-mock]` [**BREAKING**] Rename exported utility types `ConstructorLike`, `MethodLike`, `ConstructorLikeKeys`, `MethodLikeKeys`, `PropertyLikeKeys`; remove exports of utility types `ArgumentsOf`, `ArgsType`, `ConstructorArgumentsOf` - TS builtin utility types `ConstructorParameters` and `Parameters` should be used instead ([#12435](https://github.com/facebook/jest/pull/12435)) - `[jest-mock]` Improve `isMockFunction` to infer types of passed function ([#12442](https://github.com/facebook/jest/pull/12442)) - `[jest-resolve]` [**BREAKING**] Add support for `package.json` `exports` ([#11961](https://github.com/facebook/jest/pull/11961), [#12373](https://github.com/facebook/jest/pull/12373)) - `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392)) - `[jest-resolve, jest-runtime]` Add support for async resolver ([#11540](https://github.com/facebook/jest/pull/11540)) - `[jest-runner]` Allow passing `testEnvironmentOptions` via docblocks ([#12470](https://github.com/facebook/jest/pull/12470)) +- `[jest-runtime]` [**BREAKING**] `Runtime.createHasteMap` now returns a promise ([#12008](https://github.com/facebook/jest/pull/12008)) - `[@jest/schemas]` New module for JSON schemas for Jest's config ([#12384](https://github.com/facebook/jest/pull/12384)) - `[jest-worker]` [**BREAKING**] Allow only absolute `workerPath` ([#12343](https://github.com/facebook/jest/pull/12343)) - `[pretty-format]` New `maxWidth` parameter ([#12402](https://github.com/facebook/jest/pull/12402)) diff --git a/e2e/__tests__/findRelatedFiles.test.ts b/e2e/__tests__/findRelatedFiles.test.ts index 5d298ce2e9f3..a5999d5d4536 100644 --- a/e2e/__tests__/findRelatedFiles.test.ts +++ b/e2e/__tests__/findRelatedFiles.test.ts @@ -116,6 +116,56 @@ describe('--findRelatedTests flag', () => { expect(stderr).toMatch(summaryMsg); }); + 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 function 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/e2e/__tests__/hasteMapMockChanged.test.ts b/e2e/__tests__/hasteMapMockChanged.test.ts index 1e1427b0aacd..bfda6f0d6ebe 100644 --- a/e2e/__tests__/hasteMapMockChanged.test.ts +++ b/e2e/__tests__/hasteMapMockChanged.test.ts @@ -37,11 +37,11 @@ test('should not warn when a mock file changes', async () => { writeFiles(DIR, { '__mocks__/fs.js': '"foo fs"', }); - await new JestHasteMap(hasteConfig).build(); + await (await JestHasteMap.create(hasteConfig)).build(); // This will throw if the mock file being updated triggers a warning. writeFiles(DIR, { '__mocks__/fs.js': '"foo fs!"', }); - await new JestHasteMap(hasteConfig).build(); + await (await JestHasteMap.create(hasteConfig)).build(); }); diff --git a/e2e/__tests__/hasteMapSha1.test.ts b/e2e/__tests__/hasteMapSha1.test.ts index c611da6172ba..bba942943671 100644 --- a/e2e/__tests__/hasteMapSha1.test.ts +++ b/e2e/__tests__/hasteMapSha1.test.ts @@ -26,7 +26,7 @@ test('exits the process after test are done but before timers complete', async ( 'node_modules/bar/index.js': '"node modules bar"', }); - const haste = new JestHasteMap({ + const haste = await JestHasteMap.create({ computeSha1: true, extensions: ['js', 'json', 'png'], forceNodeFilesystemAPI: true, diff --git a/e2e/__tests__/hasteMapSize.test.ts b/e2e/__tests__/hasteMapSize.test.ts index 290cb571652b..e21246b55ad9 100644 --- a/e2e/__tests__/hasteMapSize.test.ts +++ b/e2e/__tests__/hasteMapSize.test.ts @@ -37,13 +37,13 @@ const options = { }; test('reports the correct file size', async () => { - const hasteMap = new HasteMap(options); + const hasteMap = await HasteMap.create(options); const hasteFS = (await hasteMap.build()).hasteFS; expect(hasteFS.getSize(path.join(DIR, 'file.js'))).toBe(5); }); test('updates the file size when a file changes', async () => { - const hasteMap = new HasteMap({...options, watch: true}); + const hasteMap = await HasteMap.create({...options, watch: true}); await hasteMap.build(); writeFiles(DIR, { diff --git a/packages/jest-core/src/cli/index.ts b/packages/jest-core/src/cli/index.ts index b6e6e5d77681..61bdb12d8d8f 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__/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 824f6f292639..454995640051 100644 --- a/packages/jest-haste-map/src/__tests__/index.test.js +++ b/packages/jest-haste-map/src/__tests__/index.test.js @@ -241,15 +241,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'); @@ -258,47 +264,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/, @@ -320,7 +332,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([]); }); @@ -328,7 +340,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([]); }); @@ -341,7 +353,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([]); }); @@ -353,7 +365,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', @@ -439,7 +451,7 @@ describe('HasteMap', () => { // fbjs2 `; - const hasteMap = new HasteMap({ + const hasteMap = await HasteMap.create({ ...defaultConfig, mocksPattern: '__mocks__', }); @@ -517,40 +529,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, + }), + ).resolves.not.toThrow(); - expect( - () => - new HasteMap({ - ...defaultConfig, - enableSymlinks: true, - useWatchman: false, - }), - ).not.toThrow(); + await expect( + HasteMap.create({ + ...defaultConfig, + enableSymlinks: true, + useWatchman: false, + }), + ).resolves.not.toThrow(); }); describe('builds a haste map on a fresh cache with SHA-1s', () => { @@ -603,7 +612,7 @@ describe('HasteMap', () => { }); }); - const hasteMap = new HasteMap({ + const hasteMap = await HasteMap.create({ ...defaultConfig, computeSha1: true, maxWorkers: 1, @@ -666,7 +675,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')], @@ -689,7 +698,7 @@ describe('HasteMap', () => { // fbjs! `; - const hasteMap = new HasteMap({ + const hasteMap = await HasteMap.create({ ...defaultConfig, mocksPattern: '__mocks__', retainAllFiles: true, @@ -738,11 +747,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( console.error.mock.calls[0][0].replace(/\\/g, '/'), @@ -755,7 +766,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. @@ -769,10 +782,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); }); @@ -784,10 +797,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', @@ -809,7 +824,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({ @@ -855,8 +872,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) @@ -874,7 +891,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); @@ -887,8 +906,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(); @@ -905,7 +924,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); @@ -938,8 +959,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(); @@ -955,7 +976,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')); @@ -972,7 +995,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], }); @@ -984,7 +1009,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], @@ -1000,7 +1027,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], @@ -1011,7 +1040,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], }); @@ -1023,7 +1054,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], }); @@ -1036,7 +1069,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], }); @@ -1050,8 +1085,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({ @@ -1078,8 +1113,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({ @@ -1098,8 +1133,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( @@ -1136,8 +1171,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()); @@ -1158,8 +1193,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({ @@ -1177,7 +1212,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. @@ -1190,7 +1225,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); }); @@ -1212,7 +1247,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. @@ -1225,12 +1262,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); @@ -1310,7 +1349,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(); @@ -1348,7 +1389,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(); @@ -1381,7 +1424,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' + @@ -1410,7 +1453,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/index.ts b/packages/jest-haste-map/src/index.ts index e538d631702a..f901964064bb 100644 --- a/packages/jest-haste-map/src/index.ts +++ b/packages/jest-haste-map/src/index.ts @@ -16,6 +16,7 @@ import {deserialize, serialize} from 'v8'; import {Stats, readFileSync, writeFileSync} from 'graceful-fs'; import type {Config} from '@jest/types'; import {escapePathForRegex} from 'jest-regex-util'; +import {requireOrImportModule} from 'jest-util'; import {Worker} from 'jest-worker'; import HasteFS from './HasteFS'; import HasteModuleMap from './ModuleMap'; @@ -29,6 +30,7 @@ import normalizePathSep from './lib/normalizePathSep'; import type { ChangeEvent, CrawlerOptions, + DependencyExtractor, EventsQueue, FileData, FileMetaData, @@ -230,12 +232,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 +298,13 @@ export default class HasteMap extends EventEmitter { ); } + this._cachePath = ''; + this._buildPromise = null; + this._watchers = []; + this._worker = null; + } + + private async setupCachePath(options: Options): Promise { const rootDirHash = createHash('md5').update(options.rootDir).digest('hex'); let hasteImplHash = ''; let dependencyExtractorHash = ''; @@ -304,7 +317,11 @@ export default class HasteMap extends EventEmitter { } if (options.dependencyExtractor) { - const dependencyExtractor = require(options.dependencyExtractor); + const dependencyExtractor = + await requireOrImportModule( + options.dependencyExtractor, + false, + ); if (dependencyExtractor.getCacheKey) { dependencyExtractorHash = String(dependencyExtractor.getCacheKey()); } @@ -327,9 +344,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/lib/__tests__/dependencyExtractor.test.js b/packages/jest-haste-map/src/lib/__tests__/dependencyExtractor.test.js index f2196a19c4b5..f2b142dfc675 100644 --- a/packages/jest-haste-map/src/lib/__tests__/dependencyExtractor.test.js +++ b/packages/jest-haste-map/src/lib/__tests__/dependencyExtractor.test.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {extract} from '../dependencyExtractor'; +import {extractor} from '../dependencyExtractor'; import isRegExpSupported from '../isRegExpSupported'; const COMMENT_NO_NEG_LB = isRegExpSupported('(? { * require('ignore-block-comment'); */ `; - expect(extract(code)).toEqual(new Set()); + expect(extractor.extract(code)).toEqual(new Set()); }); it('should not extract dependencies inside comments (windows line endings)', () => { @@ -35,7 +35,7 @@ describe('dependencyExtractor', () => { ' */', ].join('\r\n'); - expect(extract(code)).toEqual(new Set([])); + expect(extractor.extract(code)).toEqual(new Set([])); }); it('should not extract dependencies inside comments (unicode line endings)', () => { @@ -47,7 +47,7 @@ describe('dependencyExtractor', () => { ' */', ].join(''); - expect(extract(code)).toEqual(new Set([])); + expect(extractor.extract(code)).toEqual(new Set([])); }); it('should extract dependencies from `import` statements', () => { @@ -68,7 +68,9 @@ describe('dependencyExtractor', () => { ${COMMENT_NO_NEG_LB} foo . import ('inv1'); ${COMMENT_NO_NEG_LB} foo . export ('inv2'); `; - expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + expect(extractor.extract(code)).toEqual( + new Set(['dep1', 'dep2', 'dep3', 'dep4']), + ); }); // https://github.com/facebook/jest/issues/8547 @@ -83,7 +85,7 @@ describe('dependencyExtractor', () => { import ./inv1; import inv2 `; - expect(extract(code)).toEqual( + expect(extractor.extract(code)).toEqual( new Set(['./side-effect-dep1', 'side-effect-dep2']), ); }); @@ -94,7 +96,7 @@ describe('dependencyExtractor', () => { import typeof {foo} from 'inv1'; import type {foo} from 'inv2'; `; - expect(extract(code)).toEqual(new Set([])); + expect(extractor.extract(code)).toEqual(new Set([])); }); it('should extract dependencies from `export` statements', () => { @@ -115,7 +117,9 @@ describe('dependencyExtractor', () => { ${COMMENT_NO_NEG_LB} foo . export ('inv1'); ${COMMENT_NO_NEG_LB} foo . export ('inv2'); `; - expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + expect(extractor.extract(code)).toEqual( + new Set(['dep1', 'dep2', 'dep3', 'dep4']), + ); }); it('should extract dependencies from `export-from` statements', () => { @@ -136,7 +140,9 @@ describe('dependencyExtractor', () => { ${COMMENT_NO_NEG_LB} foo . export ('inv1'); ${COMMENT_NO_NEG_LB} foo . export ('inv2'); `; - expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + expect(extractor.extract(code)).toEqual( + new Set(['dep1', 'dep2', 'dep3', 'dep4']), + ); }); it('should not extract dependencies from `export type/typeof` statements', () => { @@ -145,7 +151,7 @@ describe('dependencyExtractor', () => { export typeof {foo} from 'inv1'; export type {foo} from 'inv2'; `; - expect(extract(code)).toEqual(new Set([])); + expect(extractor.extract(code)).toEqual(new Set([])); }); it('should extract dependencies from dynamic `import` calls', () => { @@ -163,7 +169,7 @@ describe('dependencyExtractor', () => { importx('inv3'); import('inv4', 'inv5'); `; - expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3'])); + expect(extractor.extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3'])); }); it('should extract dependencies from `require` calls', () => { @@ -181,7 +187,7 @@ describe('dependencyExtractor', () => { requirex('inv3'); require('inv4', 'inv5'); `; - expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3'])); + expect(extractor.extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3'])); }); it('should extract dependencies from `jest.requireActual` calls', () => { @@ -201,7 +207,9 @@ describe('dependencyExtractor', () => { jest.requireActualx('inv3'); jest.requireActual('inv4', 'inv5'); `; - expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + expect(extractor.extract(code)).toEqual( + new Set(['dep1', 'dep2', 'dep3', 'dep4']), + ); }); it('should extract dependencies from `jest.requireMock` calls', () => { @@ -221,7 +229,9 @@ describe('dependencyExtractor', () => { jest.requireMockx('inv3'); jest.requireMock('inv4', 'inv5'); `; - expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + expect(extractor.extract(code)).toEqual( + new Set(['dep1', 'dep2', 'dep3', 'dep4']), + ); }); it('should extract dependencies from `jest.genMockFromModule` calls', () => { @@ -241,7 +251,9 @@ describe('dependencyExtractor', () => { jest.genMockFromModulex('inv3'); jest.genMockFromModule('inv4', 'inv5'); `; - expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + expect(extractor.extract(code)).toEqual( + new Set(['dep1', 'dep2', 'dep3', 'dep4']), + ); }); it('should extract dependencies from `jest.createMockFromModule` calls', () => { @@ -261,6 +273,8 @@ describe('dependencyExtractor', () => { jest.createMockFromModulex('inv3'); jest.createMockFromModule('inv4', 'inv5'); `; - expect(extract(code)).toEqual(new Set(['dep1', 'dep2', 'dep3', 'dep4'])); + expect(extractor.extract(code)).toEqual( + new Set(['dep1', 'dep2', 'dep3', 'dep4']), + ); }); }); diff --git a/packages/jest-haste-map/src/lib/dependencyExtractor.ts b/packages/jest-haste-map/src/lib/dependencyExtractor.ts index 56dce5ebfd81..8a54303346dd 100644 --- a/packages/jest-haste-map/src/lib/dependencyExtractor.ts +++ b/packages/jest-haste-map/src/lib/dependencyExtractor.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import type {DependencyExtractor} from '../types'; import isRegExpSupported from './isRegExpSupported'; // Negative look behind is only supported in Node 9+ @@ -73,20 +74,22 @@ const JEST_EXTENSIONS_RE = createRegExp( 'g', ); -export function extract(code: string): Set { - const dependencies = new Set(); +export const extractor: DependencyExtractor = { + extract(code) { + const dependencies = new Set(); - const addDependency = (match: string, _: string, dep: string) => { - dependencies.add(dep); - return match; - }; + const addDependency = (match: string, _: string, dep: string) => { + dependencies.add(dep); + return match; + }; - code - .replace(BLOCK_COMMENT_RE, '') - .replace(LINE_COMMENT_RE, '') - .replace(IMPORT_OR_EXPORT_RE, addDependency) - .replace(REQUIRE_OR_DYNAMIC_IMPORT_RE, addDependency) - .replace(JEST_EXTENSIONS_RE, addDependency); + code + .replace(BLOCK_COMMENT_RE, '') + .replace(LINE_COMMENT_RE, '') + .replace(IMPORT_OR_EXPORT_RE, addDependency) + .replace(REQUIRE_OR_DYNAMIC_IMPORT_RE, addDependency) + .replace(JEST_EXTENSIONS_RE, addDependency); - return dependencies; -} + return dependencies; + }, +}; diff --git a/packages/jest-haste-map/src/types.ts b/packages/jest-haste-map/src/types.ts index 314348955655..62d0c2e64d0b 100644 --- a/packages/jest-haste-map/src/types.ts +++ b/packages/jest-haste-map/src/types.ts @@ -154,3 +154,12 @@ export type ChangeEvent = { hasteFS: HasteFS; moduleMap: ModuleMap; }; + +export type DependencyExtractor = { + extract: ( + code: string, + filePath: string, + defaultExtract: DependencyExtractor['extract'], + ) => Iterable; + getCacheKey?: () => string; +}; diff --git a/packages/jest-haste-map/src/worker.ts b/packages/jest-haste-map/src/worker.ts index 4a53879e6ae4..9790f719d58e 100644 --- a/packages/jest-haste-map/src/worker.ts +++ b/packages/jest-haste-map/src/worker.ts @@ -8,10 +8,16 @@ 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'; -import type {HasteImpl, WorkerMessage, WorkerMetadata} from './types'; +import {extractor as defaultDependencyExtractor} from './lib/dependencyExtractor'; +import type { + DependencyExtractor, + HasteImpl, + WorkerMessage, + WorkerMetadata, +} from './types'; const PACKAGE_JSON = path.sep + 'package.json'; @@ -71,14 +77,18 @@ export async function worker(data: WorkerMessage): Promise { if (computeDependencies) { const content = getContent(); + const extractor = data.dependencyExtractor + ? await requireOrImportModule( + data.dependencyExtractor, + false, + ) + : defaultDependencyExtractor; dependencies = Array.from( - data.dependencyExtractor - ? require(data.dependencyExtractor).extract( - content, - filePath, - dependencyExtractor.extract, - ) - : dependencyExtractor.extract(content), + extractor.extract( + content, + filePath, + defaultDependencyExtractor.extract, + ), ); } diff --git a/packages/jest-runtime/src/__mocks__/createRuntime.js b/packages/jest-runtime/src/__mocks__/createRuntime.js index 17d1d2c9f93b..becfd68eafc5 100644 --- a/packages/jest-runtime/src/__mocks__/createRuntime.js +++ b/packages/jest-runtime/src/__mocks__/createRuntime.js @@ -88,10 +88,12 @@ module.exports = async function createRuntime(filename, projectConfig) { }); environment.global.console = console; - const hasteMap = await Runtime.createHasteMap(projectConfig, { - maxWorkers: 1, - resetCache: false, - }).build(); + const hasteMap = await ( + await Runtime.createHasteMap(projectConfig, { + maxWorkers: 1, + resetCache: false, + }) + ).build(); const cacheFS = new Map(); const scriptTransformer = await createScriptTransformer( 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 710a6a9b747b..4154d3b43d7d 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -338,7 +338,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, @@ -358,7 +358,7 @@ export default class Runtime { static createHasteMap( config: Config.ProjectConfig, options?: HasteMapOptions, - ): HasteMap { + ): Promise { const ignorePatternParts = [ ...config.modulePathIgnorePatterns, ...(options && options.watch ? config.watchPathIgnorePatterns : []),