diff --git a/CHANGELOG.md b/CHANGELOG.md index bedea88fdb91..ead254ff7a78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - `[babel-jest]` Add support for `babel.config.js` added in Babel 7.0.0 ([#6911](https://github.com/facebook/jest/pull/6911)) +- `[jest-haste-map]` [**BREAKING**] Replaced internal data structures to improve performance ([#6960](https://github.com/facebook/jest/pull/6960)) ### Fixes diff --git a/packages/jest-haste-map/src/__tests__/index.test.js b/packages/jest-haste-map/src/__tests__/index.test.js index e5966fa07bf1..8afc717dc2d1 100644 --- a/packages/jest-haste-map/src/__tests__/index.test.js +++ b/packages/jest-haste-map/src/__tests__/index.test.js @@ -47,9 +47,9 @@ jest.mock('../crawlers/watchman', () => if (list[file]) { const hash = computeSha1 ? mockHashContents(list[file]) : null; - data.files[file] = ['', 32, 0, [], hash]; + data.files.set(file, ['', 32, 0, [], hash]); } else { - delete data.files[file]; + data.files.delete(file); } } } @@ -102,6 +102,24 @@ jest.mock('fs', () => require('graceful-fs')); const ConditionalTest = require('../../../../scripts/ConditionalTest'); const cacheFilePath = '/cache-file'; +const object = data => Object.assign(Object.create(null), data); +const createMap = obj => new Map(Object.keys(obj).map(key => [key, obj[key]])); + +// Jest toEqual does not match Map instances from different contexts +const normalizePersisted = hasteMap => ({ + clocks: normalizeMap(hasteMap.clocks), + duplicates: normalizeMap(hasteMap.duplicates), + files: normalizeMap(hasteMap.files), + map: normalizeMap(hasteMap.map), + mocks: normalizeMap(hasteMap.mocks), +}); +const normalizeMap = map => { + if (Object.prototype.toString.call(map) !== '[object Map]') { + throw new TypeError('expected map instance'); + } + return new Map(map); +}; + let consoleWarn; let defaultConfig; let fs; @@ -109,7 +127,6 @@ let H; let HasteMap; let mockClocks; let mockEmitters; -let object; let mockEnd; let mockWorker; let getCacheFilePath; @@ -120,8 +137,6 @@ describe('HasteMap', () => { beforeEach(() => { jest.resetModules(); - object = data => Object.assign(Object.create(null), data); - mockEmitters = Object.create(null); mockFs = object({ '/fruits/__mocks__/Pear.js': ['const Melon = require("Melon");'].join( @@ -151,7 +166,7 @@ describe('HasteMap', () => { ), '/video/video.mp4': Buffer.from([0xfa, 0xce, 0xb0, 0x0c]).toString(), }); - mockClocks = object({ + mockClocks = createMap({ '/fruits': 'c:fake-clock:1', '/vegetables': 'c:fake-clock:2', '/video': 'c:fake-clock:3', @@ -271,58 +286,64 @@ describe('HasteMap', () => { return hasteMap.build().then(({__hasteMapForTest: data}) => { expect(data.clocks).toEqual(mockClocks); - expect(data.files).toEqual({ - '/fruits/__mocks__/Pear.js': ['', 32, 1, ['Melon'], null], - '/fruits/banana.js': ['Banana', 32, 1, ['Strawberry'], null], - // node modules - '/fruits/node_modules/fbjs/lib/flatMap.js': [ - 'flatMap', - 32, - 1, - [], - null, - ], - '/fruits/node_modules/react/react.js': [ - 'React', - 32, - 1, - ['Component'], - null, - ], - - '/fruits/pear.js': ['Pear', 32, 1, ['Banana', 'Strawberry'], null], - '/fruits/strawberry.js': ['Strawberry', 32, 1, [], null], - '/vegetables/melon.js': ['Melon', 32, 1, [], null], - }); - - expect(data.map).toEqual({ - Banana: {[H.GENERIC_PLATFORM]: ['/fruits/banana.js', H.MODULE]}, - Melon: {[H.GENERIC_PLATFORM]: ['/vegetables/melon.js', H.MODULE]}, - Pear: {[H.GENERIC_PLATFORM]: ['/fruits/pear.js', H.MODULE]}, - React: { - [H.GENERIC_PLATFORM]: [ - '/fruits/node_modules/react/react.js', - H.MODULE, + expect(data.files).toEqual( + createMap({ + '/fruits/__mocks__/Pear.js': ['', 32, 1, ['Melon'], null], + '/fruits/banana.js': ['Banana', 32, 1, ['Strawberry'], null], + // node modules + '/fruits/node_modules/fbjs/lib/flatMap.js': [ + 'flatMap', + 32, + 1, + [], + null, ], - }, - Strawberry: { - [H.GENERIC_PLATFORM]: ['/fruits/strawberry.js', H.MODULE], - }, - flatMap: { - [H.GENERIC_PLATFORM]: [ - '/fruits/node_modules/fbjs/lib/flatMap.js', - H.MODULE, + '/fruits/node_modules/react/react.js': [ + 'React', + 32, + 1, + ['Component'], + null, ], - }, - }); - expect(data.mocks).toEqual({ - Pear: '/fruits/__mocks__/Pear.js', - }); + '/fruits/pear.js': ['Pear', 32, 1, ['Banana', 'Strawberry'], null], + '/fruits/strawberry.js': ['Strawberry', 32, 1, [], null], + '/vegetables/melon.js': ['Melon', 32, 1, [], null], + }), + ); + + expect(data.map).toEqual( + createMap({ + Banana: {[H.GENERIC_PLATFORM]: ['/fruits/banana.js', H.MODULE]}, + Melon: {[H.GENERIC_PLATFORM]: ['/vegetables/melon.js', H.MODULE]}, + Pear: {[H.GENERIC_PLATFORM]: ['/fruits/pear.js', H.MODULE]}, + React: { + [H.GENERIC_PLATFORM]: [ + '/fruits/node_modules/react/react.js', + H.MODULE, + ], + }, + Strawberry: { + [H.GENERIC_PLATFORM]: ['/fruits/strawberry.js', H.MODULE], + }, + flatMap: { + [H.GENERIC_PLATFORM]: [ + '/fruits/node_modules/fbjs/lib/flatMap.js', + H.MODULE, + ], + }, + }), + ); + + expect(data.mocks).toEqual( + createMap({ + Pear: '/fruits/__mocks__/Pear.js', + }), + ); // The cache file must exactly mirror the data structure returned from a - // build. - expect(hasteMap.read()).toEqual(data); + // build + expect(normalizePersisted(hasteMap.read())).toEqual(data); }); }); @@ -335,7 +356,7 @@ describe('HasteMap', () => { const {data} = options; // The node crawler returns "null" for the SHA-1. - data.files = object({ + data.files = createMap({ '/fruits/__mocks__/Pear.js': ['', 32, 0, ['Melon'], null], '/fruits/banana.js': ['Banana', 32, 0, ['Strawberry'], null], '/fruits/pear.js': ['Pear', 32, 0, ['Banana', 'Strawberry'], null], @@ -356,45 +377,47 @@ describe('HasteMap', () => { const data = (await hasteMap.build()).__hasteMapForTest; - expect(data.files).toEqual({ - '/fruits/__mocks__/Pear.js': [ - '', - 32, - 1, - ['Melon'], - 'a315b7804be2b124b77c1f107205397f45226964', - ], - '/fruits/banana.js': [ - 'Banana', - 32, - 1, - ['Strawberry'], - 'f24c6984cce6f032f6d55d771d04ab8dbbe63c8c', - ], - '/fruits/pear.js': [ - 'Pear', - 32, - 1, - ['Banana', 'Strawberry'], - '211a8ff1e67007b204727d26943c15cf9fd00031', - ], - '/fruits/strawberry.js': [ - 'Strawberry', - 32, - 1, - [], - 'd55d545ad7d997cb2aa10fb412e0cc287d4fbfb3', - ], - '/vegetables/melon.js': [ - 'Melon', - 32, - 1, - [], - '45c5d30e29313187829dfd5a16db81c3143fbcc7', - ], - }); + expect(data.files).toEqual( + createMap({ + '/fruits/__mocks__/Pear.js': [ + '', + 32, + 1, + ['Melon'], + 'a315b7804be2b124b77c1f107205397f45226964', + ], + '/fruits/banana.js': [ + 'Banana', + 32, + 1, + ['Strawberry'], + 'f24c6984cce6f032f6d55d771d04ab8dbbe63c8c', + ], + '/fruits/pear.js': [ + 'Pear', + 32, + 1, + ['Banana', 'Strawberry'], + '211a8ff1e67007b204727d26943c15cf9fd00031', + ], + '/fruits/strawberry.js': [ + 'Strawberry', + 32, + 1, + [], + 'd55d545ad7d997cb2aa10fb412e0cc287d4fbfb3', + ], + '/vegetables/melon.js': [ + 'Melon', + 32, + 1, + [], + '45c5d30e29313187829dfd5a16db81c3143fbcc7', + ], + }), + ); - expect(hasteMap.read()).toEqual(data); + expect(normalizePersisted(hasteMap.read())).toEqual(data); }); }); }); @@ -416,8 +439,8 @@ describe('HasteMap', () => { const {__hasteMapForTest: data} = await hasteMap.build(); - expect(data.map.IRequireAVideo).toBeDefined(); - expect(data.files['/video/video.mp4']).toBeDefined(); + expect(data.map.get('IRequireAVideo')).toBeDefined(); + expect(data.files.get('/video/video.mp4')).toBeDefined(); expect(fs.readFileSync).not.toBeCalledWith('/video/video.mp4', 'utf8'); }); @@ -438,7 +461,7 @@ describe('HasteMap', () => { return hasteMap.build().then(({__hasteMapForTest: data}) => { // Expect the node module to be part of files but make sure it wasn't // read. - expect(data.files['/fruits/node_modules/fbjs/index.js']).toEqual([ + expect(data.files.get('/fruits/node_modules/fbjs/index.js')).toEqual([ '', 32, 0, @@ -446,7 +469,7 @@ describe('HasteMap', () => { null, ]); - expect(data.map.fbjs).not.toBeDefined(); + expect(data.map.get('fbjs')).not.toBeDefined(); // cache file + 5 modules - the node_module expect(fs.readFileSync.mock.calls.length).toBe(6); @@ -489,7 +512,9 @@ describe('HasteMap', () => { .then(({__hasteMapForTest: data}) => { // Duplicate modules are removed so that it doesn't cause // non-determinism later on. - expect(data.map.Strawberry[H.GENERIC_PLATFORM]).not.toBeDefined(); + expect( + data.map.get('Strawberry')[H.GENERIC_PLATFORM], + ).not.toBeDefined(); expect(console.warn.mock.calls[0][0]).toMatchSnapshot(); }); @@ -539,31 +564,35 @@ describe('HasteMap', () => { return new HasteMap(defaultConfig) .build() .then(({__hasteMapForTest: data}) => { - expect(data.files).toEqual({ - '/fruits/strawberry.android.js': [ - 'Strawberry', - 32, - 1, - ['Blackberry'], - null, - ], - '/fruits/strawberry.ios.js': [ - 'Strawberry', - 32, - 1, - ['Raspberry'], - null, - ], - '/fruits/strawberry.js': ['Strawberry', 32, 1, ['Banana'], null], - }); + expect(data.files).toEqual( + createMap({ + '/fruits/strawberry.android.js': [ + 'Strawberry', + 32, + 1, + ['Blackberry'], + null, + ], + '/fruits/strawberry.ios.js': [ + 'Strawberry', + 32, + 1, + ['Raspberry'], + null, + ], + '/fruits/strawberry.js': ['Strawberry', 32, 1, ['Banana'], null], + }), + ); - expect(data.map).toEqual({ - Strawberry: { - [H.GENERIC_PLATFORM]: ['/fruits/strawberry.js', H.MODULE], - android: ['/fruits/strawberry.android.js', H.MODULE], - ios: ['/fruits/strawberry.ios.js', H.MODULE], - }, - }); + expect(data.map).toEqual( + createMap({ + Strawberry: { + [H.GENERIC_PLATFORM]: ['/fruits/strawberry.js', H.MODULE], + android: ['/fruits/strawberry.android.js', H.MODULE], + ios: ['/fruits/strawberry.ios.js', H.MODULE], + }, + }), + ); }); }); @@ -581,7 +610,7 @@ describe('HasteMap', () => { mockChangedFiles = Object.create(null); // Watchman would give us different clocks. - mockClocks = object({ + mockClocks = createMap({ '/fruits': 'c:fake-clock:3', '/vegetables': 'c:fake-clock:4', }); @@ -595,9 +624,9 @@ describe('HasteMap', () => { } else { expect(fs.readFileSync).toBeCalledWith(cacheFilePath, 'utf8'); } - expect(data.clocks).toEqual(mockClocks); - expect(data.files).toEqual(initialData.files); - expect(data.map).toEqual(initialData.map); + expect(normalizeMap(data.clocks)).toEqual(mockClocks); + expect(normalizeMap(data.files)).toEqual(initialData.files); + expect(normalizeMap(data.map)).toEqual(initialData.map); }); })); @@ -618,7 +647,7 @@ describe('HasteMap', () => { }); // Watchman would give us different clocks for `/fruits`. - mockClocks = object({ + mockClocks = createMap({ '/fruits': 'c:fake-clock:3', '/vegetables': 'c:fake-clock:2', }); @@ -635,18 +664,24 @@ describe('HasteMap', () => { } expect(fs.readFileSync).toBeCalledWith('/fruits/banana.js', 'utf8'); - expect(data.clocks).toEqual(mockClocks); + expect(normalizeMap(data.clocks)).toEqual(mockClocks); - const files = object(initialData.files); - files['/fruits/banana.js'] = ['Kiwi', 32, 1, ['Raspberry'], null]; + const files = new Map(initialData.files); + files.set('/fruits/banana.js', [ + 'Kiwi', + 32, + 1, + ['Raspberry'], + null, + ]); - expect(data.files).toEqual(files); + expect(normalizeMap(data.files)).toEqual(files); - const map = object(initialData.map); + const map = new Map(initialData.map); - map.Kiwi = map.Banana; - delete map.Banana; - expect(data.map).toEqual(map); + map.set('Kiwi', map.get('Banana')); + map.delete('Banana'); + expect(normalizeMap(data.map)).toEqual(map); }); })); @@ -663,7 +698,7 @@ describe('HasteMap', () => { }); // Watchman would give us different clocks for `/fruits`. - mockClocks = object({ + mockClocks = createMap({ '/fruits': 'c:fake-clock:3', '/vegetables': 'c:fake-clock:2', }); @@ -671,13 +706,13 @@ describe('HasteMap', () => { return new HasteMap(defaultConfig) .build() .then(({__hasteMapForTest: data}) => { - const files = object(initialData.files); - delete files['/fruits/banana.js']; - expect(data.files).toEqual(files); + const files = new Map(initialData.files); + files.delete('/fruits/banana.js'); + expect(normalizeMap(data.files)).toEqual(files); - const map = object(initialData.map); - delete map.Banana; - expect(data.map).toEqual(map); + const map = new Map(initialData.map); + map.delete('Banana'); + expect(normalizeMap(data.map)).toEqual(map); }); })); @@ -691,7 +726,7 @@ describe('HasteMap', () => { ].join('\n'); let data; ({__hasteMapForTest: data} = await new HasteMap(defaultConfig).build()); - expect(data.map['Strawberry']).toEqual({ + expect(data.map.get('Strawberry')).toEqual({ g: ['/fruits/strawberry.js', 0], }); @@ -704,9 +739,9 @@ describe('HasteMap', () => { 'const Raspberry = require("Raspberry");', ].join('\n'), }); - mockClocks = object({'/fruits': 'c:fake-clock:3'}); + mockClocks = createMap({'/fruits': 'c:fake-clock:3'}); ({__hasteMapForTest: data} = await new HasteMap(defaultConfig).build()); - expect(data.map['Strawberry']).toEqual({ + expect(data.map.get('Strawberry')).toEqual({ g: ['/fruits/strawberry.js', 0], ios: ['/fruits/strawberry.ios.js', 0], }); @@ -728,16 +763,16 @@ describe('HasteMap', () => { ].join('\n'); let data; ({__hasteMapForTest: data} = await new HasteMap(defaultConfig).build()); - expect(data.map['Strawberry']).toEqual({ + expect(data.map.get('Strawberry')).toEqual({ g: ['/fruits/strawberry.js', 0], ios: ['/fruits/strawberry.ios.js', 0], }); delete mockFs['/fruits/strawberry.ios.js']; mockChangedFiles = object({'/fruits/strawberry.ios.js': null}); - mockClocks = object({'/fruits': 'c:fake-clock:3'}); + mockClocks = createMap({'/fruits': 'c:fake-clock:3'}); ({__hasteMapForTest: data} = await new HasteMap(defaultConfig).build()); - expect(data.map['Strawberry']).toEqual({ + expect(data.map.get('Strawberry')).toEqual({ g: ['/fruits/strawberry.js', 0], }); }); @@ -752,7 +787,7 @@ describe('HasteMap', () => { ].join('\n'); let data; ({__hasteMapForTest: data} = await new HasteMap(defaultConfig).build()); - expect(data.map['Strawberry']).toEqual({ + expect(data.map.get('Strawberry')).toEqual({ ios: ['/fruits/strawberry.ios.js', 0], }); @@ -766,9 +801,9 @@ describe('HasteMap', () => { 'const Banana = require("Banana");', ].join('\n'), }); - mockClocks = object({'/fruits': 'c:fake-clock:3'}); + mockClocks = createMap({'/fruits': 'c:fake-clock:3'}); ({__hasteMapForTest: data} = await new HasteMap(defaultConfig).build()); - expect(data.map['Strawberry']).toEqual({ + expect(data.map.get('Strawberry')).toEqual({ g: ['/fruits/strawberry.js', 0], }); }); @@ -785,12 +820,14 @@ describe('HasteMap', () => { const {__hasteMapForTest: data} = await new HasteMap( defaultConfig, ).build(); - expect(data.duplicates).toEqual({ - Strawberry: { - g: {'/fruits/another_strawberry.js': 0, '/fruits/strawberry.js': 0}, - }, - }); - expect(data.map['Strawberry']).toEqual({}); + expect(normalizeMap(data.duplicates)).toEqual( + createMap({ + Strawberry: { + g: {'/fruits/another_strawberry.js': 0, '/fruits/strawberry.js': 0}, + }, + }), + ); + expect(data.map.get('Strawberry')).toEqual({}); }); it('recovers when a duplicate file is deleted', async () => { @@ -798,7 +835,7 @@ describe('HasteMap', () => { mockChangedFiles = object({ '/fruits/another_strawberry.js': null, }); - mockClocks = object({ + mockClocks = createMap({ '/fruits': 'c:fake-clock:3', '/vegetables': 'c:fake-clock:2', }); @@ -806,10 +843,12 @@ describe('HasteMap', () => { const {__hasteMapForTest: data} = await new HasteMap( defaultConfig, ).build(); - expect(data.duplicates).toEqual({}); - expect(data.map['Strawberry']).toEqual({g: ['/fruits/strawberry.js', 0]}); + expect(normalizeMap(data.duplicates)).toEqual(new Map()); + expect(data.map.get('Strawberry')).toEqual({ + g: ['/fruits/strawberry.js', 0], + }); // Make sure the other files are not affected. - expect(data.map['Banana']).toEqual({g: ['/fruits/banana.js', 0]}); + expect(data.map.get('Banana')).toEqual({g: ['/fruits/banana.js', 0]}); }); it('recovers when a duplicate module is renamed', async () => { @@ -821,7 +860,7 @@ describe('HasteMap', () => { 'const Blackberry = require("Blackberry");', ].join('\n'), }); - mockClocks = object({ + mockClocks = createMap({ '/fruits': 'c:fake-clock:3', '/vegetables': 'c:fake-clock:2', }); @@ -829,13 +868,15 @@ describe('HasteMap', () => { const {__hasteMapForTest: data} = await new HasteMap( defaultConfig, ).build(); - expect(data.duplicates).toEqual({}); - expect(data.map['Strawberry']).toEqual({g: ['/fruits/strawberry.js', 0]}); - expect(data.map['AnotherStrawberry']).toEqual({ + expect(normalizeMap(data.duplicates)).toEqual(new Map()); + expect(data.map.get('Strawberry')).toEqual({ + g: ['/fruits/strawberry.js', 0], + }); + expect(data.map.get('AnotherStrawberry')).toEqual({ g: ['/fruits/another_strawberry.js', 0], }); // Make sure the other files are not affected. - expect(data.map['Banana']).toEqual({g: ['/fruits/banana.js', 0]}); + expect(data.map.get('Banana')).toEqual({g: ['/fruits/banana.js', 0]}); }); }); @@ -848,7 +889,7 @@ describe('HasteMap', () => { mockChangedFiles = Object.create(null); // Watchman would give us different clocks. - mockClocks = object({ + mockClocks = createMap({ '/fruits': 'c:fake-clock:3', '/vegetables': 'c:fake-clock:4', }); @@ -869,17 +910,17 @@ describe('HasteMap', () => { watchman.mockImplementation(options => mockImpl(options).then(() => { const {data} = options; - data.files['/fruits/invalid/file.js'] = ['', 34, 0, []]; + data.files.set('/fruits/invalid/file.js', ['', 34, 0, []]); return data; }), ); return new HasteMap(defaultConfig) .build() .then(({__hasteMapForTest: data}) => { - expect(Object.keys(data.files).length).toBe(5); + expect(data.files.size).toBe(5); // Ensure this file is not part of the file list. - expect(data.files['/fruits/invalid/file.js']).toBe(undefined); + expect(data.files.get('/fruits/invalid/file.js')).toBe(undefined); }); }); @@ -952,7 +993,7 @@ describe('HasteMap', () => { }); node.mockImplementation(options => { const {data} = options; - data.files = object({ + data.files = createMap({ '/fruits/banana.js': ['', 32, 0, [], null], }); return Promise.resolve(data); @@ -964,9 +1005,11 @@ describe('HasteMap', () => { expect(watchman).toBeCalled(); expect(node).toBeCalled(); - expect(data.files).toEqual({ - '/fruits/banana.js': ['Banana', 32, 1, ['Strawberry'], null], - }); + expect(data.files).toEqual( + createMap({ + '/fruits/banana.js': ['Banana', 32, 1, ['Strawberry'], null], + }), + ); expect(console.warn.mock.calls[0][0]).toMatchSnapshot(); }); @@ -981,7 +1024,7 @@ describe('HasteMap', () => { ); node.mockImplementation(options => { const {data} = options; - data.files = object({ + data.files = createMap({ '/fruits/banana.js': ['', 32, 0, [], null], }); return Promise.resolve(data); @@ -993,9 +1036,11 @@ describe('HasteMap', () => { expect(watchman).toBeCalled(); expect(node).toBeCalled(); - expect(data.files).toEqual({ - '/fruits/banana.js': ['Banana', 32, 1, ['Strawberry'], null], - }); + expect(data.files).toEqual( + createMap({ + '/fruits/banana.js': ['Banana', 32, 1, ['Strawberry'], null], + }), + ); }); }); diff --git a/packages/jest-haste-map/src/crawlers/__tests__/node.test.js b/packages/jest-haste-map/src/crawlers/__tests__/node.test.js index 599075bb3269..cd724395ae27 100644 --- a/packages/jest-haste-map/src/crawlers/__tests__/node.test.js +++ b/packages/jest-haste-map/src/crawlers/__tests__/node.test.js @@ -68,6 +68,7 @@ jest.mock('fs', () => { }); const pearMatcher = path => /pear/.test(path); +const createMap = obj => new Map(Object.keys(obj).map(key => [key, obj[key]])); let mockResponse; let nodeCrawl; @@ -104,7 +105,7 @@ describe('node crawler', () => { const promise = nodeCrawl({ data: { - files: Object.create(null), + files: new Map(), }, extensions: ['js', 'json'], ignore: pearMatcher, @@ -126,11 +127,13 @@ describe('node crawler', () => { expect(data.files).not.toBe(null); - expect(data.files).toEqual({ - '/fruits/strawberry.js': ['', 32, 0, [], null], - '/fruits/tomato.js': ['', 33, 0, [], null], - '/vegetables/melon.json': ['', 34, 0, [], null], - }); + expect(data.files).toEqual( + createMap({ + '/fruits/strawberry.js': ['', 32, 0, [], null], + '/fruits/tomato.js': ['', 33, 0, [], null], + '/vegetables/melon.json': ['', 34, 0, [], null], + }), + ); }); return promise; @@ -141,12 +144,12 @@ describe('node crawler', () => { nodeCrawl = require('../node'); - const files = Object.create(null); - // In this test sample, strawberry is changed and tomato is unchanged const tomato = ['', 33, 1, [], null]; - files['/fruits/strawberry.js'] = ['', 30, 1, [], null]; - files['/fruits/tomato.js'] = tomato; + const files = createMap({ + '/fruits/strawberry.js': ['', 30, 1, [], null], + '/fruits/tomato.js': tomato, + }); return nodeCrawl({ data: {files}, @@ -154,13 +157,15 @@ describe('node crawler', () => { ignore: pearMatcher, roots: ['/fruits'], }).then(data => { - expect(data.files).toEqual({ - '/fruits/strawberry.js': ['', 32, 0, [], null], - '/fruits/tomato.js': tomato, - }); + expect(data.files).toEqual( + createMap({ + '/fruits/strawberry.js': ['', 32, 0, [], null], + '/fruits/tomato.js': tomato, + }), + ); // Make sure it is the *same* unchanged object. - expect(data.files['/fruits/tomato.js']).toBe(tomato); + expect(data.files.get('/fruits/tomato.js')).toBe(tomato); }); }); @@ -169,17 +174,20 @@ describe('node crawler', () => { nodeCrawl = require('../node'); - const files = Object.create(null); return nodeCrawl({ - data: {files}, + data: { + files: new Map(), + }, extensions: ['js'], ignore: pearMatcher, roots: ['/fruits'], }).then(data => { - expect(data.files).toEqual({ - '/fruits/directory/strawberry.js': ['', 33, 0, [], null], - '/fruits/tomato.js': ['', 32, 0, [], null], - }); + expect(data.files).toEqual( + createMap({ + '/fruits/directory/strawberry.js': ['', 33, 0, [], null], + '/fruits/tomato.js': ['', 32, 0, [], null], + }), + ); }); }); @@ -188,7 +196,7 @@ describe('node crawler', () => { nodeCrawl = require('../node'); - const files = Object.create(null); + const files = new Map(); return nodeCrawl({ data: {files}, extensions: ['js'], @@ -196,10 +204,12 @@ describe('node crawler', () => { ignore: pearMatcher, roots: ['/fruits'], }).then(data => { - expect(data.files).toEqual({ - '/fruits/directory/strawberry.js': ['', 33, 0, [], null], - '/fruits/tomato.js': ['', 32, 0, [], null], - }); + expect(data.files).toEqual( + createMap({ + '/fruits/directory/strawberry.js': ['', 33, 0, [], null], + '/fruits/tomato.js': ['', 32, 0, [], null], + }), + ); }); }); @@ -208,7 +218,7 @@ describe('node crawler', () => { nodeCrawl = require('../node'); - const files = Object.create(null); + const files = new Map(); return nodeCrawl({ data: {files}, extensions: ['js'], @@ -216,7 +226,7 @@ describe('node crawler', () => { ignore: pearMatcher, roots: [], }).then(data => { - expect(data.files).toEqual({}); + expect(data.files).toEqual(new Map()); }); }); @@ -225,14 +235,14 @@ describe('node crawler', () => { nodeCrawl = require('../node'); - const files = Object.create(null); + const files = new Map(); return nodeCrawl({ data: {files}, extensions: ['js'], ignore: pearMatcher, roots: ['/error'], }).then(data => { - expect(data.files).toEqual({}); + expect(data.files).toEqual(new Map()); }); }); }); diff --git a/packages/jest-haste-map/src/crawlers/__tests__/watchman.test.js b/packages/jest-haste-map/src/crawlers/__tests__/watchman.test.js index 0db22b9abd21..ebb9574e0974 100644 --- a/packages/jest-haste-map/src/crawlers/__tests__/watchman.test.js +++ b/packages/jest-haste-map/src/crawlers/__tests__/watchman.test.js @@ -53,6 +53,8 @@ const WATCH_PROJECT_MOCK = { }, }; +const createMap = obj => new Map(Object.keys(obj).map(key => [key, obj[key]])); + describe('watchman watch', () => { beforeEach(() => { watchmanCrawl = require('../watchman'); @@ -97,7 +99,7 @@ describe('watchman watch', () => { 'watch-project': WATCH_PROJECT_MOCK, }; - mockFiles = Object.assign(Object.create(null), { + mockFiles = createMap({ [MELON]: ['', 33, 0, [], null], [STRAWBERRY]: ['', 30, 0, [], null], [TOMATO]: ['', 31, 0, [], null], @@ -111,8 +113,8 @@ describe('watchman watch', () => { test('returns a list of all files when there are no clocks', () => watchmanCrawl({ data: { - clocks: Object.create(null), - files: Object.create(null), + clocks: new Map(), + files: new Map(), }, extensions: ['js', 'json'], ignore: pearMatcher, @@ -148,9 +150,11 @@ describe('watchman watch', () => { 'vegetables/**/*.json', ]); - expect(data.clocks).toEqual({ - [ROOT_MOCK]: 'c:fake-clock:1', - }); + expect(data.clocks).toEqual( + createMap({ + [ROOT_MOCK]: 'c:fake-clock:1', + }), + ); expect(data.files).toEqual(mockFiles); @@ -186,7 +190,7 @@ describe('watchman watch', () => { 'watch-project': WATCH_PROJECT_MOCK, }; - const clocks = Object.assign(Object.create(null), { + const clocks = createMap({ [ROOT_MOCK]: 'c:fake-clock:1', }); @@ -202,15 +206,19 @@ describe('watchman watch', () => { // The object was reused. expect(data.files).toBe(mockFiles); - expect(data.clocks).toEqual({ - [ROOT_MOCK]: 'c:fake-clock:2', - }); - - expect(data.files).toEqual({ - [KIWI]: ['', 42, 0, [], null], - [MELON]: ['', 33, 0, [], null], - [STRAWBERRY]: ['', 30, 0, [], null], - }); + expect(data.clocks).toEqual( + createMap({ + [ROOT_MOCK]: 'c:fake-clock:2', + }), + ); + + expect(data.files).toEqual( + createMap({ + [KIWI]: ['', 42, 0, [], null], + [MELON]: ['', 33, 0, [], null], + [STRAWBERRY]: ['', 30, 0, [], null], + }), + ); }); }); @@ -249,9 +257,9 @@ describe('watchman watch', () => { }; const mockMetadata = ['Banana', 41, 1, ['Raspberry'], null]; - mockFiles[BANANA] = mockMetadata; + mockFiles.set(BANANA, mockMetadata); - const clocks = Object.assign(Object.create(null), { + const clocks = createMap({ [ROOT_MOCK]: 'c:fake-clock:1', }); @@ -267,22 +275,26 @@ describe('watchman watch', () => { // The file object was *not* reused. expect(data.files).not.toBe(mockFiles); - expect(data.clocks).toEqual({ - [ROOT_MOCK]: 'c:fake-clock:3', - }); + expect(data.clocks).toEqual( + createMap({ + [ROOT_MOCK]: 'c:fake-clock:3', + }), + ); // /fruits/strawberry.js was removed from the file list. - expect(data.files).toEqual({ - [BANANA]: mockMetadata, - [KIWI]: ['', 42, 0, [], null], - [TOMATO]: mockFiles[TOMATO], - }); + expect(data.files).toEqual( + createMap({ + [BANANA]: mockMetadata, + [KIWI]: ['', 42, 0, [], null], + [TOMATO]: mockFiles.get(TOMATO), + }), + ); // Even though the file list was reset, old file objects are still reused // if no changes have been made. - expect(data.files[BANANA]).toBe(mockMetadata); + expect(data.files.get(BANANA)).toBe(mockMetadata); - expect(data.files[TOMATO]).toBe(mockFiles[TOMATO]); + expect(data.files.get(TOMATO)).toBe(mockFiles.get(TOMATO)); }); }); @@ -329,7 +341,7 @@ describe('watchman watch', () => { }, }; - const clocks = Object.assign(Object.create(null), { + const clocks = createMap({ [FRUITS]: 'c:fake-clock:1', [VEGETABLES]: 'c:fake-clock:2', }); @@ -343,15 +355,19 @@ describe('watchman watch', () => { ignore: pearMatcher, roots: ROOTS, }).then(data => { - expect(data.clocks).toEqual({ - [FRUITS]: 'c:fake-clock:3', - [VEGETABLES]: 'c:fake-clock:4', - }); - - expect(data.files).toEqual({ - [KIWI]: ['', 42, 0, [], null], - [MELON]: ['', 33, 0, [], null], - }); + expect(data.clocks).toEqual( + createMap({ + [FRUITS]: 'c:fake-clock:3', + [VEGETABLES]: 'c:fake-clock:4', + }), + ); + + expect(data.files).toEqual( + createMap({ + [KIWI]: ['', 42, 0, [], null], + [MELON]: ['', 33, 0, [], null], + }), + ); }); }); @@ -387,8 +403,8 @@ describe('watchman watch', () => { return watchmanCrawl({ data: { - clocks: Object.create(null), - files: Object.create(null), + clocks: new Map(), + files: new Map(), }, extensions: ['js', 'json'], ignore: pearMatcher, @@ -419,11 +435,13 @@ describe('watchman watch', () => { expect(query[2].glob).toEqual(['**/*.js', '**/*.json']); - expect(data.clocks).toEqual({ - [ROOT_MOCK]: 'c:fake-clock:1', - }); + expect(data.clocks).toEqual( + createMap({ + [ROOT_MOCK]: 'c:fake-clock:1', + }), + ); - expect(data.files).toEqual({}); + expect(data.files).toEqual(createMap({})); expect(client.end).toBeCalled(); }); @@ -454,8 +472,8 @@ describe('watchman watch', () => { await watchmanCrawl({ computeSha1: true, data: { - clocks: Object.create(null), - files: Object.create(null), + clocks: new Map(), + files: new Map(), }, extensions: ['js', 'json'], roots: [ROOT_MOCK], @@ -493,8 +511,8 @@ describe('watchman watch', () => { await watchmanCrawl({ computeSha1: true, data: { - clocks: Object.create(null), - files: Object.create(null), + clocks: new Map(), + files: new Map(), }, extensions: ['js', 'json'], roots: [ROOT_MOCK], diff --git a/packages/jest-haste-map/src/crawlers/node.js b/packages/jest-haste-map/src/crawlers/node.js index 96ce13d6e076..16e1e89114b7 100644 --- a/packages/jest-haste-map/src/crawlers/node.js +++ b/packages/jest-haste-map/src/crawlers/node.js @@ -131,16 +131,16 @@ module.exports = function nodeCrawl( return new Promise(resolve => { const callback = list => { - const files = Object.create(null); + const files = new Map(); list.forEach(fileData => { const name = fileData[0]; const mtime = fileData[1]; - const existingFile = data.files[name]; + const existingFile = data.files.get(name); if (existingFile && existingFile[H.MTIME] === mtime) { - files[name] = existingFile; + files.set(name, existingFile); } else { // See ../constants.js; SHA-1 will always be null and fulfilled later. - files[name] = ['', mtime, 0, [], null]; + files.set(name, ['', mtime, 0, [], null]); } }); data.files = files; diff --git a/packages/jest-haste-map/src/crawlers/watchman.js b/packages/jest-haste-map/src/crawlers/watchman.js index 9e6af68d9a71..684d6eeb55e1 100644 --- a/packages/jest-haste-map/src/crawlers/watchman.js +++ b/packages/jest-haste-map/src/crawlers/watchman.js @@ -112,9 +112,9 @@ module.exports = async function watchmanCrawl( } } - const query = clocks[root] + const query = clocks.has(root) ? // Use the `since` generator if we have a clock available - {expression, fields, since: clocks[root]} + {expression, fields, since: clocks.get(root)} : // Otherwise use the `glob` filter {expression, fields, glob}; @@ -145,7 +145,7 @@ module.exports = async function watchmanCrawl( // Reset the file map if watchman was restarted and sends us a list of // files. if (watchmanFileResults.isFresh) { - files = Object.create(null); + files = new Map(); } watchmanFiles = watchmanFileResults.files; @@ -159,20 +159,19 @@ module.exports = async function watchmanCrawl( for (const [watchRoot, response] of watchmanFiles) { const fsRoot = normalizePathSep(watchRoot); - clocks[fsRoot] = response.clock; + clocks.set(fsRoot, response.clock); for (const fileData of response.files) { const name = fsRoot + path.sep + normalizePathSep(fileData.name); if (!fileData.exists) { - delete files[name]; + files.delete(name); } else if (!ignore(name)) { const mtime = typeof fileData.mtime_ms === 'number' ? fileData.mtime_ms : fileData.mtime_ms.toNumber(); - const existingFileData = data.files[name]; - const isOld = existingFileData && existingFileData[H.MTIME] === mtime; - if (isOld) { - files[name] = existingFileData; + const existingFileData = data.files.get(name); + if (existingFileData && existingFileData[H.MTIME] === mtime) { + files.set(name, existingFileData); } else { let sha1hex = fileData['content.sha1hex']; @@ -181,7 +180,7 @@ module.exports = async function watchmanCrawl( } // See ../constants.js - files[name] = ['', mtime, 0, [], sha1hex]; + files.set(name, ['', mtime, 0, [], sha1hex]); } } } diff --git a/packages/jest-haste-map/src/haste_fs.js b/packages/jest-haste-map/src/haste_fs.js index 5c2bd588bf92..f62d84ab19c1 100644 --- a/packages/jest-haste-map/src/haste_fs.js +++ b/packages/jest-haste-map/src/haste_fs.js @@ -22,23 +22,26 @@ export default class HasteFS { } getModuleName(file: Path): ?string { - return (this._files[file] && this._files[file][H.ID]) || null; + const fileMetadata = this._files.get(file); + return (fileMetadata && fileMetadata[H.ID]) || null; } getDependencies(file: Path): ?Array { - return (this._files[file] && this._files[file][H.DEPENDENCIES]) || null; + const fileMetadata = this._files.get(file); + return (fileMetadata && fileMetadata[H.DEPENDENCIES]) || null; } getSha1(file: Path): ?string { - return (this._files[file] && this._files[file][H.SHA1]) || null; + const fileMetadata = this._files.get(file); + return (fileMetadata && fileMetadata[H.SHA1]) || null; } exists(file: Path): boolean { - return !!this._files[file]; + return this._files.has(file); } getAllFiles(): Array { - return Object.keys(this._files); + return Array.from(this._files.keys()); } matchFiles(pattern: RegExp | string): Array { @@ -46,7 +49,7 @@ export default class HasteFS { pattern = new RegExp(pattern); } const files = []; - for (const file in this._files) { + for (const file of this._files.keys()) { if (pattern.test(file)) { files.push(file); } @@ -56,7 +59,7 @@ export default class HasteFS { matchFilesWithGlob(globs: Array, root: ?Path): Set { const files = new Set(); - for (const file in this._files) { + for (const file of this._files.keys()) { const filePath = root ? path.relative(root, file) : file; if (micromatch([filePath], globs).length) { files.add(file); diff --git a/packages/jest-haste-map/src/index.js b/packages/jest-haste-map/src/index.js index 0d5bad0fcb44..86794bb2a5e0 100644 --- a/packages/jest-haste-map/src/index.js +++ b/packages/jest-haste-map/src/index.js @@ -41,6 +41,7 @@ import type { HasteRegExp, MockData, } from 'types/HasteMap'; +import type {SerializableModuleMap as HasteSerializableModuleMap} from './module_map'; type HType = typeof H; @@ -93,6 +94,7 @@ type Watcher = { type WorkerInterface = {worker: typeof worker, getSha1: typeof getSha1}; export type ModuleMap = HasteModuleMap; +export type SerializableModuleMap = HasteSerializableModuleMap; export type FS = HasteFS; const CHANGE_INTERVAL = 30; @@ -311,10 +313,6 @@ class HasteMap extends EventEmitter { hasteMap = this._createEmptyMap(); } - for (const key in hasteMap) { - Object.setPrototypeOf(hasteMap[key], null); - } - return hasteMap; } @@ -340,13 +338,14 @@ class HasteMap extends EventEmitter { .then(() => read.call(this)) .catch(() => this._createEmptyMap()) .then(cachedHasteMap => { - const cachedFiles = Object.keys(cachedHasteMap.files).map(filePath => { - const moduleName = cachedHasteMap.files[filePath][H.ID]; - return {moduleName, path: filePath}; - }); + const cachedFiles = []; + for (const [filePath, fileMetadata] of cachedHasteMap.files) { + const moduleName = fileMetadata[H.ID]; + cachedFiles.push({moduleName, path: filePath}); + } return this._crawl(cachedHasteMap).then(hasteMap => { const deprecatedFiles = cachedFiles.filter(file => { - const fileData = hasteMap.files[file.path]; + const fileData = hasteMap.files.get(file.path); return fileData == null || file.moduleName !== fileData[H.ID]; }); return {deprecatedFiles, hasteMap}; @@ -365,11 +364,11 @@ class HasteMap extends EventEmitter { workerOptions: ?{forceInBand: boolean}, ): ?Promise { const setModule = (id: string, module: ModuleMetaData) => { - if (!map[id]) { - // $FlowFixMe - map[id] = Object.create(null); + let moduleMap = map.get(id); + if (!moduleMap) { + moduleMap = Object.create(null); + map.set(id, moduleMap); } - const moduleMap = map[id]; const platform = getPlatformExtension(module[H.PATH], this._options.platforms) || H.GENERIC_PLATFORM; @@ -391,19 +390,20 @@ class HasteMap extends EventEmitter { // We do NOT want consumers to use a module that is ambiguous. delete moduleMap[platform]; if (Object.keys(moduleMap).length === 1) { - delete map[id]; + map.delete(id); } - let dupsByPlatform = hasteMap.duplicates[id]; + let dupsByPlatform = hasteMap.duplicates.get(id); if (dupsByPlatform == null) { - dupsByPlatform = hasteMap.duplicates[id] = (Object.create(null): any); + dupsByPlatform = Object.create(null); + hasteMap.duplicates.set(id, dupsByPlatform); } - const dups = (dupsByPlatform[platform] = (Object.create(null): any)); + const dups = (dupsByPlatform[platform] = Object.create(null)); dups[module[H.PATH]] = module[H.TYPE]; dups[existingModule[H.PATH]] = existingModule[H.TYPE]; return; } - const dupsByPlatform = hasteMap.duplicates[id]; + const dupsByPlatform = hasteMap.duplicates.get(id); if (dupsByPlatform != null) { const dups = dupsByPlatform[platform]; if (dups != null) { @@ -415,8 +415,14 @@ class HasteMap extends EventEmitter { moduleMap[platform] = module; }; - const fileMetadata = hasteMap.files[filePath]; - const moduleMetadata = hasteMap.map[fileMetadata[H.ID]]; + const fileMetadata = hasteMap.files.get(filePath); + if (!fileMetadata) { + throw new Error( + 'jest-haste-map: File to process was not found in the haste map.', + ); + } + + const moduleMetadata = hasteMap.map.get(fileMetadata[H.ID]); const computeSha1 = this._options.computeSha1 && !fileMetadata[H.SHA1]; // Callback called when the response from the worker is successful. @@ -453,7 +459,7 @@ class HasteMap extends EventEmitter { // If a file cannot be read we remove it from the file list and // ignore the failure silently. - delete hasteMap.files[filePath]; + hasteMap.files.delete(filePath); }; // If we retain all files in the virtual HasteFS representation, we avoid @@ -478,7 +484,8 @@ class HasteMap extends EventEmitter { this._options.mocksPattern.test(filePath) ) { const mockPath = getMockName(filePath); - if (mocks[mockPath]) { + const existingMockPath = mocks.get(mockPath); + if (existingMockPath) { this._console.warn( `jest-haste-map: duplicate manual mock found:\n` + ` Module name: ${mockPath}\n` + @@ -487,10 +494,10 @@ class HasteMap extends EventEmitter { `Jest will use the mock file found in: \n` + `${filePath}\n` + ` Please delete one of the following two files: \n ` + - `${mocks[mockPath]}\n${filePath}\n\n`, + `${existingMockPath}\n${filePath}\n\n`, ); } - mocks[mockPath] = filePath; + mocks.set(mockPath, filePath); } if (fileMetadata[H.VISITED]) { @@ -509,8 +516,12 @@ class HasteMap extends EventEmitter { return null; } - const modulesByPlatform = - map[fileMetadata[H.ID]] || (map[fileMetadata[H.ID]] = {}); + const moduleId = fileMetadata[H.ID]; + let modulesByPlatform = map.get(moduleId); + if (!modulesByPlatform) { + modulesByPlatform = Object.create(null); + map.set(moduleId, modulesByPlatform); + } modulesByPlatform[platform] = module; return null; @@ -532,8 +543,8 @@ class HasteMap extends EventEmitter { hasteMap: InternalHasteMap, }): Promise { const {deprecatedFiles, hasteMap} = data; - const map = Object.create(null); - const mocks = Object.create(null); + const map = new Map(); + const mocks = new Map(); const promises = []; for (let i = 0; i < deprecatedFiles.length; ++i) { @@ -541,7 +552,7 @@ class HasteMap extends EventEmitter { this._recoverDuplicates(hasteMap, file.path, file.moduleName); } - for (const filePath in hasteMap.files) { + for (const filePath of hasteMap.files.keys()) { // SHA-1, if requested, should already be present thanks to the crawler. const promise = this._processFile(hasteMap, map, mocks, filePath); if (promise) { @@ -752,45 +763,49 @@ class HasteMap extends EventEmitter { if (mustCopy) { mustCopy = false; hasteMap = { - clocks: copy(hasteMap.clocks), - duplicates: copy(hasteMap.duplicates), - files: copy(hasteMap.files), - map: copy(hasteMap.map), - mocks: copy(hasteMap.mocks), + clocks: new Map(hasteMap.clocks), + duplicates: new Map(hasteMap.duplicates), + files: new Map(hasteMap.files), + map: new Map(hasteMap.map), + mocks: new Map(hasteMap.mocks), }; } const add = () => eventsQueue.push({filePath, stat, type}); - // Delete the file and all of its metadata. - const moduleName = - hasteMap.files[filePath] && hasteMap.files[filePath][H.ID]; - const platform: string = - getPlatformExtension(filePath, this._options.platforms) || - H.GENERIC_PLATFORM; - - delete hasteMap.files[filePath]; - let moduleMap = hasteMap.map[moduleName]; - if (moduleMap != null) { - // We are forced to copy the object because jest-haste-map exposes - // the map as an immutable entity. - moduleMap = copy(moduleMap); - delete moduleMap[platform]; - if (Object.keys(moduleMap).length === 0) { - delete hasteMap.map[moduleName]; - } else { - hasteMap.map[moduleName] = moduleMap; + const fileMetadata = hasteMap.files.get(filePath); + + // If it's not an addition, delete the file and all its metadata + if (fileMetadata != null) { + const moduleName = fileMetadata[H.ID]; + const platform = + getPlatformExtension(filePath, this._options.platforms) || + H.GENERIC_PLATFORM; + hasteMap.files.delete(filePath); + + let moduleMap = hasteMap.map.get(moduleName); + if (moduleMap != null) { + // We are forced to copy the object because jest-haste-map exposes + // the map as an immutable entity. + moduleMap = copy(moduleMap); + delete moduleMap[platform]; + if (Object.keys(moduleMap).length === 0) { + hasteMap.map.delete(moduleName); + } else { + hasteMap.map.set(moduleName, moduleMap); + } } - } - if ( - this._options.mocksPattern && - this._options.mocksPattern.test(filePath) - ) { - const mockName = getMockName(filePath); - delete hasteMap.mocks[mockName]; - } - this._recoverDuplicates(hasteMap, filePath, moduleName); + if ( + this._options.mocksPattern && + this._options.mocksPattern.test(filePath) + ) { + const mockName = getMockName(filePath); + hasteMap.mocks.delete(mockName); + } + + this._recoverDuplicates(hasteMap, filePath, moduleName); + } // If the file was added or changed, // parse it and update the haste map. @@ -800,7 +815,7 @@ class HasteMap extends EventEmitter { 'since the file exists or changed, it should have stats', ); const fileMetadata = ['', stat.mtime.getTime(), 0, [], null]; - hasteMap.files[filePath] = fileMetadata; + hasteMap.files.set(filePath, fileMetadata); const promise = this._processFile( hasteMap, hasteMap.map, @@ -850,10 +865,11 @@ class HasteMap extends EventEmitter { filePath: string, moduleName: string, ) { - let dupsByPlatform = hasteMap.duplicates[moduleName]; + let dupsByPlatform = hasteMap.duplicates.get(moduleName); if (dupsByPlatform == null) { return; } + const platform = getPlatformExtension(filePath, this._options.platforms) || H.GENERIC_PLATFORM; @@ -861,24 +877,28 @@ class HasteMap extends EventEmitter { if (dups == null) { return; } - dupsByPlatform = hasteMap.duplicates[moduleName] = (copy( - dupsByPlatform, - ): any); - dups = dupsByPlatform[platform] = (copy(dups): any); + + dupsByPlatform = copy(dupsByPlatform); + hasteMap.duplicates.set(moduleName, dupsByPlatform); + dups = copy(dups); + dupsByPlatform[platform] = dups; + const dedupType = dups[filePath]; delete dups[filePath]; const filePaths = Object.keys(dups); if (filePaths.length > 1) { return; } - let dedupMap = hasteMap.map[moduleName]; + + let dedupMap = hasteMap.map.get(moduleName); if (dedupMap == null) { - dedupMap = hasteMap.map[moduleName] = (Object.create(null): any); + dedupMap = Object.create(null); + hasteMap.map.set(moduleName, dedupMap); } dedupMap[platform] = [filePaths[0], dedupType]; delete dupsByPlatform[platform]; if (Object.keys(dupsByPlatform).length === 0) { - delete hasteMap.duplicates[moduleName]; + hasteMap.duplicates.delete(moduleName); } } @@ -936,13 +956,12 @@ class HasteMap extends EventEmitter { } _createEmptyMap(): InternalHasteMap { - // $FlowFixMe return { - clocks: Object.create(null), - duplicates: Object.create(null), - files: Object.create(null), - map: Object.create(null), - mocks: Object.create(null), + clocks: new Map(), + duplicates: new Map(), + files: new Map(), + map: new Map(), + mocks: new Map(), }; } diff --git a/packages/jest-haste-map/src/module_map.js b/packages/jest-haste-map/src/module_map.js index 19cfedee7457..fdf673d01cd0 100644 --- a/packages/jest-haste-map/src/module_map.js +++ b/packages/jest-haste-map/src/module_map.js @@ -13,12 +13,25 @@ import type { HTypeValue, ModuleMetaData, RawModuleMap, + ModuleMapData, + DuplicatesIndex, + MockData, } from 'types/HasteMap'; import H from './constants'; const EMPTY_MAP = {}; +export opaque type SerializableModuleMap = { + // There is no easier way to extract the type of the entries of a Map + duplicates: $Call< + typeof Array.from, + $Call<$PropertyType>, + >, + map: $Call>>, + mocks: $Call>>, +}; + export default class ModuleMap { _raw: RawModuleMap; static DuplicateHasteCandidatesError: Class; @@ -56,7 +69,7 @@ export default class ModuleMap { } getMockModule(name: string): ?Path { - return this._raw.mocks[name] || this._raw.mocks[name + '/index']; + return this._raw.mocks.get(name) || this._raw.mocks.get(name + '/index'); } getRawModuleMap(): RawModuleMap { @@ -67,6 +80,22 @@ export default class ModuleMap { }; } + toJSON(): SerializableModuleMap { + return { + duplicates: Array.from(this._raw.duplicates), + map: Array.from(this._raw.map), + mocks: Array.from(this._raw.mocks), + }; + } + + static fromJSON(serializableModuleMap: SerializableModuleMap) { + return new ModuleMap({ + duplicates: new Map(serializableModuleMap.duplicates), + map: new Map(serializableModuleMap.map), + mocks: new Map(serializableModuleMap.mocks), + }); + } + /** * When looking up a module's data, we walk through each eligible platform for * the query. For each platform, we want to check if there are known @@ -80,8 +109,8 @@ export default class ModuleMap { platform: ?string, supportsNativePlatform: boolean, ): ?ModuleMetaData { - const map = this._raw.map[name] || EMPTY_MAP; - const dupMap = this._raw.duplicates[name] || EMPTY_MAP; + const map = this._raw.map.get(name) || EMPTY_MAP; + const dupMap = this._raw.duplicates.get(name) || EMPTY_MAP; if (platform != null) { this._assertNoDuplicates( name, @@ -132,6 +161,14 @@ export default class ModuleMap { set, ); } + + static create() { + return new ModuleMap({ + duplicates: new Map(), + map: new Map(), + mocks: new Map(), + }); + } } class DuplicateHasteCandidatesError extends Error { diff --git a/packages/jest-resolve/src/__tests__/resolve.test.js b/packages/jest-resolve/src/__tests__/resolve.test.js index 05586429d763..837f4c4c3c98 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.js +++ b/packages/jest-resolve/src/__tests__/resolve.test.js @@ -23,7 +23,7 @@ beforeEach(() => { describe('isCoreModule', () => { it('returns false if `hasCoreModules` is false.', () => { - const moduleMap = new ModuleMap(); + const moduleMap = ModuleMap.create(); const resolver = new Resolver(moduleMap, { hasCoreModules: false, }); @@ -32,14 +32,14 @@ describe('isCoreModule', () => { }); it('returns true if `hasCoreModules` is true and `moduleName` is a core module.', () => { - const moduleMap = new ModuleMap(); + const moduleMap = ModuleMap.create(); const resolver = new Resolver(moduleMap, {}); const isCore = resolver.isCoreModule('assert'); expect(isCore).toEqual(true); }); it('returns false if `hasCoreModules` is true and `moduleName` is not a core module.', () => { - const moduleMap = new ModuleMap(); + const moduleMap = ModuleMap.create(); const resolver = new Resolver(moduleMap, {}); const isCore = resolver.isCoreModule('not-a-core-module'); expect(isCore).toEqual(false); @@ -82,11 +82,7 @@ describe('findNodeModule', () => { describe('resolveModule', () => { let moduleMap; beforeEach(() => { - moduleMap = new ModuleMap({ - duplicates: [], - map: [], - mocks: [], - }); + moduleMap = ModuleMap.create(); }); it('is possible to resolve node modules', () => { @@ -163,11 +159,7 @@ describe('getMockModule', () => { it('is possible to use custom resolver to resolve deps inside mock modules with moduleNameMapper', () => { userResolver.mockImplementation(() => 'module'); - const moduleMap = new ModuleMap({ - duplicates: [], - map: [], - mocks: [], - }); + const moduleMap = ModuleMap.create(); const resolver = new Resolver(moduleMap, { moduleNameMapper: [ { @@ -204,11 +196,7 @@ describe('Resolver.getModulePaths() -> nodeModulesPaths()', () => { beforeEach(() => { jest.resetModules(); - moduleMap = new ModuleMap({ - duplicates: [], - map: [], - mocks: [], - }); + moduleMap = ModuleMap.create(); // Mocking realpath to function the old way, where it just looks at // pathstrings instead of actually trying to access the physical directory. diff --git a/packages/jest-runner/src/__tests__/test_runner.test.js b/packages/jest-runner/src/__tests__/test_runner.test.js index e1d968f0094e..94ae275e4aa3 100644 --- a/packages/jest-runner/src/__tests__/test_runner.test.js +++ b/packages/jest-runner/src/__tests__/test_runner.test.js @@ -27,13 +27,13 @@ jest.mock('jest-worker', () => jest.mock('../test_worker', () => {}); -test('injects the rawModuleMap into each worker in watch mode', () => { +test('injects the serializable module map into each worker in watch mode', () => { const globalConfig = {maxWorkers: 2, watch: true}; const config = {rootDir: '/path/'}; - const rawModuleMap = jest.fn(); + const serializableModuleMap = jest.fn(); const context = { config, - moduleMap: {getRawModuleMap: () => rawModuleMap}, + moduleMap: {toJSON: () => serializableModuleMap}, }; return new TestRunner(globalConfig) .runTests( @@ -46,13 +46,20 @@ test('injects the rawModuleMap into each worker in watch mode', () => { ) .then(() => { expect(mockWorkerFarm.worker.mock.calls).toEqual([ - [{config, globalConfig, path: './file.test.js', rawModuleMap}], - [{config, globalConfig, path: './file2.test.js', rawModuleMap}], + [{config, globalConfig, path: './file.test.js', serializableModuleMap}], + [ + { + config, + globalConfig, + path: './file2.test.js', + serializableModuleMap, + }, + ], ]); }); }); -test('does not inject the rawModuleMap in serial mode', () => { +test('does not inject the serializable module map in serial mode', () => { const globalConfig = {maxWorkers: 1, watch: false}; const config = {rootDir: '/path/'}; const context = {config}; @@ -73,7 +80,7 @@ test('does not inject the rawModuleMap in serial mode', () => { config, globalConfig, path: './file.test.js', - rawModuleMap: null, + serializableModuleMap: null, }, ], [ @@ -81,7 +88,7 @@ test('does not inject the rawModuleMap in serial mode', () => { config, globalConfig, path: './file2.test.js', - rawModuleMap: null, + serializableModuleMap: null, }, ], ]); diff --git a/packages/jest-runner/src/index.js b/packages/jest-runner/src/index.js index 2f4dd7243697..aa4c55e40b96 100644 --- a/packages/jest-runner/src/index.js +++ b/packages/jest-runner/src/index.js @@ -118,8 +118,8 @@ class TestRunner { config: test.context.config, globalConfig: this._globalConfig, path: test.path, - rawModuleMap: watcher.isWatchMode() - ? test.context.moduleMap.getRawModuleMap() + serializableModuleMap: watcher.isWatchMode() + ? test.context.moduleMap.toJSON() : null, }); }); diff --git a/packages/jest-runner/src/test_worker.js b/packages/jest-runner/src/test_worker.js index 5b178df03a4b..5cca559c8fe9 100644 --- a/packages/jest-runner/src/test_worker.js +++ b/packages/jest-runner/src/test_worker.js @@ -9,7 +9,7 @@ import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; import type {SerializableError, TestResult} from 'types/TestResult'; -import type {RawModuleMap} from 'types/HasteMap'; +import type {SerializableModuleMap} from 'types/HasteMap'; import type {ErrorWithCode} from 'types/Errors'; import exit from 'exit'; @@ -22,7 +22,7 @@ export type WorkerData = {| config: ProjectConfig, globalConfig: GlobalConfig, path: Path, - rawModuleMap: ?RawModuleMap, + serializableModuleMap: ?SerializableModuleMap, |}; // Make sure uncaught errors are logged before we exit. @@ -50,13 +50,13 @@ const formatError = (error: string | ErrorWithCode): SerializableError => { }; const resolvers = Object.create(null); -const getResolver = (config, rawModuleMap) => { +const getResolver = (config, moduleMap) => { // In watch mode, the raw module map with all haste modules is passed from // the test runner to the watch command. This is because jest-haste-map's // watch mode does not persist the haste map on disk after every file change. // To make this fast and consistent, we pass it from the TestRunner. - if (rawModuleMap) { - return Runtime.createResolver(config, new HasteMap.ModuleMap(rawModuleMap)); + if (moduleMap) { + return Runtime.createResolver(config, moduleMap); } else { const name = config.name; if (!resolvers[name]) { @@ -73,14 +73,17 @@ export async function worker({ config, globalConfig, path, - rawModuleMap, + serializableModuleMap, }: WorkerData): Promise { try { + const moduleMap = serializableModuleMap + ? HasteMap.ModuleMap.fromJSON(serializableModuleMap) + : null; return await runTest( path, globalConfig, config, - getResolver(config, rawModuleMap), + getResolver(config, moduleMap), ); } catch (error) { throw formatError(error); diff --git a/types/HasteMap.js b/types/HasteMap.js index d5e3ab6d7f8e..f4ad4b42e2a6 100644 --- a/types/HasteMap.js +++ b/types/HasteMap.js @@ -7,25 +7,31 @@ * @flow */ -import type {ModuleMap as _ModuleMap, FS} from 'jest-haste-map'; +import type { + ModuleMap as _ModuleMap, + SerializableModuleMap as _SerializableModuleMap, + FS, +} from 'jest-haste-map'; import type {Path} from 'types/Config'; export type HasteFS = FS; export type ModuleMap = _ModuleMap; +export type SerializableModuleMap = _SerializableModuleMap; -export type FileData = {[filepath: Path]: FileMetaData, __proto__: null}; -export type MockData = {[id: string]: Path, __proto__: null}; -export type ModuleMapData = {[id: string]: ModuleMapItem, __proto__: null}; -export type WatchmanClocks = {[filepath: Path]: string, __proto__: null}; +export type FileData = Map; +export type MockData = Map; +export type ModuleMapData = Map; +export type WatchmanClocks = Map; export type HasteRegExp = RegExp | ((str: string) => boolean); export type DuplicatesSet = { [filePath: string]: /* type */ number, __proto__: null, }; -export type DuplicatesIndex = { - [id: string]: {[platform: string]: DuplicatesSet, __proto__: null}, -}; +export type DuplicatesIndex = Map< + string, + {[platform: string]: DuplicatesSet, __proto__: null}, +>; export type InternalHasteMap = {| clocks: WatchmanClocks, @@ -55,7 +61,7 @@ export type FileMetaData = [ /* sha1 */ ?string, ]; -type ModuleMapItem = {[platform: string]: ModuleMetaData}; +type ModuleMapItem = {[platform: string]: ModuleMetaData, __proto__: null}; export type ModuleMetaData = [Path, /* type */ number]; export type HType = {|