diff --git a/packages/metro-file-map/src/__tests__/index-test.js b/packages/metro-file-map/src/__tests__/index-test.js index 6db43b0238..1df6420416 100644 --- a/packages/metro-file-map/src/__tests__/index-test.js +++ b/packages/metro-file-map/src/__tests__/index-test.js @@ -94,7 +94,7 @@ jest.mock('../crawlers/watchman', () => ({ '', // dependencies hash, typeof contentOrLink !== 'string' ? 1 : 0, - '', // Haste name + null, // Haste name ]); } } else { @@ -524,7 +524,7 @@ describe('FileMap', () => { 'Melon', null, 0, - '', + null, ], [path.join('vegetables', 'Melon.js')]: [ 32, @@ -582,7 +582,7 @@ describe('FileMap', () => { // $FlowFixMe[missing-local-annot] node.mockImplementation(options => { // The node crawler returns "null" for the SHA-1. - const changedFiles = createMap({ + const changedFiles = createMap({ [path.join('fruits', 'Banana.js')]: [ 32, 42, @@ -617,7 +617,7 @@ describe('FileMap', () => { 'Melon', null, 0, - '', + null, ], [path.join('vegetables', 'Melon.js')]: [ 32, @@ -637,7 +637,7 @@ describe('FileMap', () => { '', null, 1, - '', + null, ], } : null), @@ -695,7 +695,7 @@ describe('FileMap', () => { 'Melon', '8d40afbb6e2dc78e1ba383b6d02cafad35cceef2', 0, - '', + null, ], [path.join('vegetables', 'Melon.js')]: [ 32, @@ -714,7 +714,7 @@ describe('FileMap', () => { 1, '', null, - '', + null, ], } : null), @@ -1422,7 +1422,7 @@ describe('FileMap', () => { // $FlowFixMe[missing-local-annot] watchman.mockImplementation(async options => { const {changedFiles} = await mockImpl(options); - changedFiles.set(invalidFilePath, [34, 44, 0, '', null, 0, '']); + changedFiles.set(invalidFilePath, [34, 44, 0, '', null, 0, null]); return { changedFiles, removedFiles: new Set(), @@ -1466,9 +1466,8 @@ describe('FileMap', () => { computeDependencies: true, computeSha1: false, dependencyExtractor, - enableHastePackages: true, filePath: path.join('/', 'project', 'fruits', 'Banana.js'), - hasteImplModulePath: undefined, + isNodeModules: false, maybeReturnContent: false, }, ], @@ -1477,9 +1476,8 @@ describe('FileMap', () => { computeDependencies: true, computeSha1: false, dependencyExtractor, - enableHastePackages: true, filePath: path.join('/', 'project', 'fruits', 'Pear.js'), - hasteImplModulePath: undefined, + isNodeModules: false, maybeReturnContent: false, }, ], @@ -1488,9 +1486,8 @@ describe('FileMap', () => { computeDependencies: true, computeSha1: false, dependencyExtractor, - enableHastePackages: true, filePath: path.join('/', 'project', 'fruits', 'Strawberry.js'), - hasteImplModulePath: undefined, + isNodeModules: false, maybeReturnContent: false, }, ], @@ -1499,9 +1496,8 @@ describe('FileMap', () => { computeDependencies: true, computeSha1: false, dependencyExtractor, - enableHastePackages: true, filePath: path.join('/', 'project', 'fruits', '__mocks__', 'Pear.js'), - hasteImplModulePath: undefined, + isNodeModules: false, maybeReturnContent: false, }, ], @@ -1510,9 +1506,8 @@ describe('FileMap', () => { computeDependencies: true, computeSha1: false, dependencyExtractor, - enableHastePackages: true, filePath: path.join('/', 'project', 'vegetables', 'Melon.js'), - hasteImplModulePath: undefined, + isNodeModules: false, maybeReturnContent: false, }, ], @@ -1533,7 +1528,7 @@ describe('FileMap', () => { node.mockImplementation((() => { return Promise.resolve({ changedFiles: createMap({ - [path.join('fruits', 'Banana.js')]: [32, 42, 0, '', null, 0, ''], + [path.join('fruits', 'Banana.js')]: [32, 42, 0, '', null, 0, null], }), removedFiles: new Set(), }); @@ -1575,7 +1570,7 @@ describe('FileMap', () => { node.mockImplementation(() => { return Promise.resolve({ changedFiles: createMap({ - [path.join('fruits', 'Banana.js')]: [32, 42, 0, '', null, 0, ''], + [path.join('fruits', 'Banana.js')]: [32, 42, 0, '', null, 0, null], }), removedFiles: new Set(), }); @@ -1681,7 +1676,8 @@ describe('FileMap', () => { fm_it('build returns a "live" fileSystem and hasteMap', async hm => { const {fileSystem, hasteMap} = await hm.build(); const filePath = path.join('/', 'project', 'fruits', 'Banana.js'); - expect(fileSystem.getModuleName(filePath)).toBeDefined(); + expect(fileSystem.exists(filePath)).toBe(true); + expect(hasteMap.getModuleNameByPath(filePath)).toBe('Banana'); expect(hasteMap.getModule('Banana')).toBe(filePath); mockDeleteFile(path.join('/', 'project', 'fruits'), 'Banana.js'); mockDeleteFile(path.join('/', 'project', 'fruits'), 'Banana.js'); @@ -1698,7 +1694,8 @@ describe('FileMap', () => { }; expect(eventsQueue).toEqual([deletedBanana]); // Verify that the initial result has been updated - expect(fileSystem.getModuleName(filePath)).toBeNull(); + expect(fileSystem.exists(filePath)).toBe(false); + expect(hasteMap.getModuleNameByPath(filePath)).toBeNull(); expect(hasteMap.getModule('Banana')).toBeNull(); }); @@ -1765,10 +1762,8 @@ describe('FileMap', () => { }, ]); expect( - fileSystem.getModuleName( - path.join('/', 'project', 'fruits', 'Tomato.js'), - ), - ).not.toBeNull(); + fileSystem.exists(path.join('/', 'project', 'fruits', 'Tomato.js')), + ).toBe(true); expect(hasteMap.getModule('Tomato')).toBeDefined(); expect(hasteMap.getModule('Pear')).toBe( path.join('/', 'project', 'fruits', 'Pear.js'), @@ -1940,7 +1935,7 @@ describe('FileMap', () => { expect(eventsQueue).toEqual([ {filePath, metadata: MOCK_CHANGE_FILE, type: 'add'}, ]); - expect(fileSystem.getModuleName(filePath)).toBeDefined(); + expect(fileSystem.exists(filePath)).toBe(true); }, ); @@ -1967,7 +1962,7 @@ describe('FileMap', () => { expect(eventsQueue).toEqual([ {filePath, metadata: MOCK_CHANGE_FILE, type: 'change'}, ]); - expect(fileSystem.getModuleName(filePath)).toBeDefined(); + expect(fileSystem.exists(filePath)).toBe(true); }, ); @@ -1990,7 +1985,7 @@ describe('FileMap', () => { expect(eventsQueue).toEqual([ {filePath, metadata: MOCK_DELETE_FILE, type: 'delete'}, ]); - expect(fileSystem.getModuleName(filePath)).toBeDefined(); + expect(fileSystem.exists(filePath)).toBe(false); expect(console.warn).not.toHaveBeenCalled(); expect(console.error).not.toHaveBeenCalled(); }); @@ -2025,8 +2020,13 @@ describe('FileMap', () => { modifiedTime: 46, size: 5, }); - // getModuleName traverses the symlink, verifying the link is read. - expect(fileSystem.getModuleName(filePath)).toEqual('Strawberry'); + // lookup traverses the symlink, verifying the link is read. + expect(fileSystem.lookup(filePath)).toEqual( + expect.objectContaining({ + exists: true, + realPath: expect.stringMatching(/Strawberry\.js$/), + }), + ); }, {config: {enableSymlinks: true}}, ); @@ -2044,8 +2044,8 @@ describe('FileMap', () => { ); const realPath = path.join('/', 'project', 'fruits', 'Strawberry.js'); - expect(fileSystem.getModuleName(symlinkPath)).toEqual('Strawberry'); - expect(fileSystem.getModuleName(realPath)).toEqual('Strawberry'); + expect(hasteMap.getModuleNameByPath(symlinkPath)).toEqual('Strawberry'); + expect(hasteMap.getModuleNameByPath(realPath)).toEqual('Strawberry'); expect(hasteMap.getModule('Strawberry', 'g')).toEqual(realPath); // Delete the symlink @@ -2065,8 +2065,8 @@ describe('FileMap', () => { // Symlink is deleted without affecting the Haste module or real file. expect(fileSystem.exists(symlinkPath)).toBe(false); expect(fileSystem.exists(realPath)).toBe(true); - expect(fileSystem.getModuleName(symlinkPath)).toEqual(null); - expect(fileSystem.getModuleName(realPath)).toEqual('Strawberry'); + expect(hasteMap.getModuleNameByPath(symlinkPath)).toEqual(null); + expect(hasteMap.getModuleNameByPath(realPath)).toEqual('Strawberry'); expect(hasteMap.getModule('Strawberry', 'g')).toEqual(realPath); }, {config: {enableSymlinks: true}}, @@ -2075,7 +2075,7 @@ describe('FileMap', () => { fm_it( 'correctly tracks changes to both platform-specific versions of a single module name', async hm => { - const {hasteMap, fileSystem} = await hm.build(); + const {hasteMap} = await hm.build(); expect(hasteMap.getModule('Orange', 'ios')).toBeTruthy(); expect(hasteMap.getModule('Orange', 'android')).toBeTruthy(); const e = mockEmitters[path.join('/', 'project', 'fruits')]; @@ -2104,12 +2104,12 @@ describe('FileMap', () => { }, ]); expect( - fileSystem.getModuleName( + hasteMap.getModuleNameByPath( path.join('/', 'project', 'fruits', 'Orange.ios.js'), ), ).toBeTruthy(); expect( - fileSystem.getModuleName( + hasteMap.getModuleNameByPath( path.join('/', 'project', 'fruits', 'Orange.android.js'), ), ).toBeTruthy(); diff --git a/packages/metro-file-map/src/__tests__/worker-test.js b/packages/metro-file-map/src/__tests__/worker-test.js index 899dc654af..45375ee386 100644 --- a/packages/metro-file-map/src/__tests__/worker-test.js +++ b/packages/metro-file-map/src/__tests__/worker-test.js @@ -13,6 +13,7 @@ import type {WorkerMessage, WorkerMetadata} from '../flow-types'; import typeof TWorker from '../worker'; import typeof FS from 'fs'; +import {HastePlugin} from '..'; import {Worker} from '../worker'; import * as fs from 'fs'; import * as path from 'path'; @@ -65,13 +66,35 @@ jest.mock('fs', () => { }); const defaults: WorkerMessage = { + isNodeModules: false, computeDependencies: false, computeSha1: false, - enableHastePackages: false, filePath: path.join('/project', 'notexist.js'), maybeReturnContent: false, }; +const defaultHasteConfig = { + enableHastePackages: true, + failValidationOnConflicts: false, + hasteImplModulePath: require.resolve('./haste_impl.js'), + platforms: new Set(['ios', 'android']), + rootDir: path.normalize('/project'), +}; + +function workerWithHaste( + message: WorkerMessage, + hasteOverrides: Partial = {}, +) { + return new Worker({ + plugins: [ + new HastePlugin({ + ...defaultHasteConfig, + ...hasteOverrides, + }).getWorker(), + ], + }).processFile(message); +} + describe('worker', () => { let worker: (message: WorkerMessage) => Promise; @@ -84,7 +107,7 @@ describe('worker', () => { const defaults: WorkerMessage = { computeDependencies: false, computeSha1: false, - enableHastePackages: false, + isNodeModules: false, filePath: path.join('/project', 'notexist.js'), maybeReturnContent: false, }; @@ -98,6 +121,7 @@ describe('worker', () => { }), ).toEqual({ dependencies: ['Banana', 'Strawberry'], + pluginData: [], }); expect( @@ -108,12 +132,13 @@ describe('worker', () => { }), ).toEqual({ dependencies: [], + pluginData: [], }); }); test('accepts a custom dependency extractor', async () => { expect( - new Worker({}).processFile({ + await worker({ ...defaults, computeDependencies: true, dependencyExtractor: path.join(__dirname, 'dependencyExtractor.js'), @@ -121,62 +146,63 @@ describe('worker', () => { }), ).toEqual({ dependencies: ['Banana', 'Strawberry', 'Lime'], + pluginData: [], }); }); test('delegates to hasteImplModulePath for getting the id', async () => { expect( - await worker({ + await workerWithHaste({ ...defaults, computeDependencies: true, filePath: path.join('/project', 'fruits', 'Pear.js'), - hasteImplModulePath: require.resolve('./haste_impl.js'), }), ).toEqual({ dependencies: ['Banana', 'Strawberry'], - id: 'Pear', + pluginData: ['Pear'], }); expect( - await worker({ + await workerWithHaste({ ...defaults, computeDependencies: true, filePath: path.join('/project', 'fruits', 'Strawberry.js'), - hasteImplModulePath: require.resolve('./haste_impl.js'), }), ).toEqual({ dependencies: [], - id: 'Strawberry', + pluginData: ['Strawberry'], }); }); test('parses package.json files as haste packages when enableHastePackages=true', async () => { - const worker = new Worker({}); expect( - worker.processFile({ - ...defaults, - computeDependencies: true, - enableHastePackages: true, - filePath: path.join('/project', 'package.json'), - }), + await workerWithHaste( + { + ...defaults, + computeDependencies: true, + filePath: path.join('/project', 'package.json'), + }, + {enableHastePackages: true}, + ), ).toEqual({ dependencies: undefined, - id: 'haste-package', + pluginData: ['haste-package'], }); }); test('does not parse package.json files as haste packages when enableHastePackages=false', async () => { - const worker = new Worker({}); expect( - worker.processFile({ - ...defaults, - computeDependencies: true, - enableHastePackages: false, - filePath: path.join('/project', 'package.json'), - }), + await workerWithHaste( + { + ...defaults, + computeDependencies: true, + filePath: path.join('/project', 'package.json'), + }, + {enableHastePackages: false}, + ), ).toEqual({ dependencies: undefined, - id: undefined, + pluginData: [null], }); }); @@ -203,7 +229,10 @@ describe('worker', () => { computeSha1: true, filePath: path.join('/project', 'fruits', 'apple.png'), }), - ).toEqual({sha1: '4caece539b039b16e16206ea2478f8c5ffb2ca05'}); + ).toEqual({ + pluginData: [], + sha1: '4caece539b039b16e16206ea2478f8c5ffb2ca05', + }); expect( await worker({ @@ -211,7 +240,7 @@ describe('worker', () => { computeSha1: false, filePath: path.join('/project', 'fruits', 'Banana.js'), }), - ).toEqual({sha1: undefined}); + ).toEqual({pluginData: [], sha1: undefined}); expect( await worker({ @@ -219,7 +248,10 @@ describe('worker', () => { computeSha1: true, filePath: path.join('/project', 'fruits', 'Banana.js'), }), - ).toEqual({sha1: '7772b628e422e8cf59c526be4bb9f44c0898e3d1'}); + ).toEqual({ + pluginData: [], + sha1: '7772b628e422e8cf59c526be4bb9f44c0898e3d1', + }); expect( await worker({ @@ -227,7 +259,10 @@ describe('worker', () => { computeSha1: true, filePath: path.join('/project', 'fruits', 'Pear.js'), }), - ).toEqual({sha1: 'c7a7a68a1c8aaf452669dd2ca52ac4a434d25552'}); + ).toEqual({ + pluginData: [], + sha1: 'c7a7a68a1c8aaf452669dd2ca52ac4a434d25552', + }); await expect(() => worker({...defaults, computeSha1: true, filePath: '/i/dont/exist.js'}), @@ -236,15 +271,14 @@ describe('worker', () => { test('avoids computing dependencies if not requested and Haste does not need it', async () => { expect( - await worker({ + await workerWithHaste({ ...defaults, computeDependencies: false, filePath: path.join('/project', 'fruits', 'Pear.js'), - hasteImplModulePath: path.resolve(__dirname, 'haste_impl.js'), }), ).toEqual({ dependencies: undefined, - id: 'Pear', + pluginData: ['Pear'], sha1: undefined, }); @@ -255,7 +289,7 @@ describe('worker', () => { test('returns content if requested and content is read', async () => { expect( - await worker({ + await workerWithHaste({ ...defaults, computeSha1: true, filePath: path.join('/project', 'fruits', 'Pear.js'), @@ -263,23 +297,23 @@ describe('worker', () => { }), ).toEqual({ content: expect.any(Buffer), + pluginData: ['Pear'], sha1: 'c7a7a68a1c8aaf452669dd2ca52ac4a434d25552', }); }); test('does not return content if maybeReturnContent but content is not read', async () => { expect( - await worker({ + await workerWithHaste({ ...defaults, computeSha1: false, filePath: path.join('/project', 'fruits', 'Pear.js'), - hasteImplModulePath: path.resolve(__dirname, 'haste_impl.js'), maybeReturnContent: true, }), ).toEqual({ content: undefined, dependencies: undefined, - id: 'Pear', + pluginData: ['Pear'], sha1: undefined, }); }); diff --git a/packages/metro-file-map/src/cache/__tests__/DiskCacheManager-test.js b/packages/metro-file-map/src/cache/__tests__/DiskCacheManager-test.js index 6fe313dd2c..d03d7ca127 100644 --- a/packages/metro-file-map/src/cache/__tests__/DiskCacheManager-test.js +++ b/packages/metro-file-map/src/cache/__tests__/DiskCacheManager-test.js @@ -138,6 +138,9 @@ describe('cacheManager', () => { getSerializableSnapshot() { return {}; }, + getWorker() { + return null; + }, onNewOrModifiedFile() {}, onRemovedFile() {}, getCacheKey() { diff --git a/packages/metro-file-map/src/constants.js b/packages/metro-file-map/src/constants.js index f2c11f4488..78fd7a58a8 100644 --- a/packages/metro-file-map/src/constants.js +++ b/packages/metro-file-map/src/constants.js @@ -36,7 +36,7 @@ const constants/*: HType */ = { DEPENDENCIES: 3, SHA1: 4, SYMLINK: 5, - ID: 6, + PLUGINDATA: 6, /* module map attributes */ PATH: 0, diff --git a/packages/metro-file-map/src/crawlers/__tests__/integration-test.js b/packages/metro-file-map/src/crawlers/__tests__/integration-test.js index 876ba633fc..775aaaed88 100644 --- a/packages/metro-file-map/src/crawlers/__tests__/integration-test.js +++ b/packages/metro-file-map/src/crawlers/__tests__/integration-test.js @@ -84,18 +84,26 @@ const CASES = [ [ true, new Map([ - ['foo.js', [expect.any(Number), 245, 0, '', null, 0, '']], + ['foo.js', [expect.any(Number), 245, 0, '', null, 0, null]], [ join('directory', 'bar.js'), - [expect.any(Number), 245, 0, '', null, 0, ''], + [expect.any(Number), 245, 0, '', null, 0, null], ], [ 'link-to-directory', - [expect.any(Number), 9, 0, '', null, expect.oneOf(1, 'directory'), ''], + [ + expect.any(Number), + 9, + 0, + '', + null, + expect.oneOf(1, 'directory'), + null, + ], ], [ 'link-to-foo.js', - [expect.any(Number), 6, 0, '', null, expect.oneOf(1, 'foo.js'), ''], + [expect.any(Number), 6, 0, '', null, expect.oneOf(1, 'foo.js'), null], ], ]), ], @@ -104,9 +112,9 @@ const CASES = [ new Map([ [ join('directory', 'bar.js'), - [expect.any(Number), 245, 0, '', null, 0, ''], + [expect.any(Number), 245, 0, '', null, 0, null], ], - ['foo.js', [expect.any(Number), 245, 0, '', null, 0, '']], + ['foo.js', [expect.any(Number), 245, 0, '', null, 0, null]], ]), ], ]; @@ -126,7 +134,9 @@ describe.each(Object.keys(CRAWLERS))( previousState: { fileSystem: new TreeFS({ rootDir: FIXTURES_DIR, - files: new Map([['removed.js', [123, 234, 0, '', null, 0, '']]]), + files: new Map([ + ['removed.js', [123, 234, 0, '', null, 0, null]], + ]), processFile: () => { throw new Error('Not implemented'); }, diff --git a/packages/metro-file-map/src/crawlers/__tests__/node-test.js b/packages/metro-file-map/src/crawlers/__tests__/node-test.js index 1bc720543b..6fbba2fa70 100644 --- a/packages/metro-file-map/src/crawlers/__tests__/node-test.js +++ b/packages/metro-file-map/src/crawlers/__tests__/node-test.js @@ -185,9 +185,9 @@ describe('node crawler', () => { expect(changedFiles).toEqual( createMap({ - 'fruits/strawberry.js': [32, 42, 0, '', null, 0, ''], - 'fruits/tomato.js': [33, 42, 0, '', null, 0, ''], - 'vegetables/melon.json': [34, 42, 0, '', null, 0, ''], + 'fruits/strawberry.js': [32, 42, 0, '', null, 0, null], + 'fruits/tomato.js': [33, 42, 0, '', null, 0, null], + 'vegetables/melon.json': [34, 42, 0, '', null, 0, null], }), ); @@ -198,9 +198,9 @@ describe('node crawler', () => { nodeCrawl = require('../node').default; // In this test sample, strawberry is changed and tomato is unchanged - const tomato = [33, 42, 1, '', null, 0, '']; + const tomato = [33, 42, 1, '', null, 0, null]; const files = createMap({ - 'fruits/strawberry.js': [30, 40, 1, '', null, 0, ''], + 'fruits/strawberry.js': [30, 40, 1, '', null, 0, null], 'fruits/tomato.js': tomato, }); @@ -215,7 +215,7 @@ describe('node crawler', () => { // Tomato is not included because it is unchanged expect(changedFiles).toEqual( createMap({ - 'fruits/strawberry.js': [32, 42, 0, '', null, 0, ''], + 'fruits/strawberry.js': [32, 42, 0, '', null, 0, null], }), ); @@ -228,9 +228,9 @@ describe('node crawler', () => { // In this test sample, previouslyExisted was present before and will not be // when crawling this directory. const files = createMap({ - 'fruits/previouslyExisted.js': [30, 40, 1, '', null, 0, ''], - 'fruits/strawberry.js': [33, 42, 0, '', null, 0, ''], - 'fruits/tomato.js': [32, 42, 0, '', null, 0, ''], + 'fruits/previouslyExisted.js': [30, 40, 1, '', null, 0, null], + 'fruits/strawberry.js': [33, 42, 0, '', null, 0, null], + 'fruits/tomato.js': [32, 42, 0, '', null, 0, null], }); const {changedFiles, removedFiles} = await nodeCrawl({ @@ -243,8 +243,8 @@ describe('node crawler', () => { expect(changedFiles).toEqual( createMap({ - 'fruits/strawberry.js': [32, 42, 0, '', null, 0, ''], - 'fruits/tomato.js': [33, 42, 0, '', null, 0, ''], + 'fruits/strawberry.js': [32, 42, 0, '', null, 0, null], + 'fruits/tomato.js': [33, 42, 0, '', null, 0, null], }), ); expect(removedFiles).toEqual(new Set(['fruits/previouslyExisted.js'])); @@ -272,8 +272,8 @@ describe('node crawler', () => { ); expect(changedFiles).toEqual( createMap({ - 'fruits/directory/strawberry.js': [33, 42, 0, '', null, 0, ''], - 'fruits/tomato.js': [32, 42, 0, '', null, 0, ''], + 'fruits/directory/strawberry.js': [33, 42, 0, '', null, 0, null], + 'fruits/tomato.js': [32, 42, 0, '', null, 0, null], }), ); expect(removedFiles).toEqual(new Set()); @@ -297,8 +297,8 @@ describe('node crawler', () => { expect(changedFiles).toEqual( createMap({ - 'fruits/directory/strawberry.js': [33, 42, 0, '', null, 0, ''], - 'fruits/tomato.js': [32, 42, 0, '', null, 0, ''], + 'fruits/directory/strawberry.js': [33, 42, 0, '', null, 0, null], + 'fruits/tomato.js': [32, 42, 0, '', null, 0, null], }), ); expect(removedFiles).toEqual(new Set()); @@ -321,8 +321,8 @@ describe('node crawler', () => { expect(childProcess.spawn).toHaveBeenCalledTimes(0); expect(changedFiles).toEqual( createMap({ - 'fruits/directory/strawberry.js': [33, 42, 0, '', null, 0, ''], - 'fruits/tomato.js': [32, 42, 0, '', null, 0, ''], + 'fruits/directory/strawberry.js': [33, 42, 0, '', null, 0, null], + 'fruits/tomato.js': [32, 42, 0, '', null, 0, null], }), ); expect(removedFiles).toEqual(new Set()); @@ -386,8 +386,8 @@ describe('node crawler', () => { expect(changedFiles).toEqual( createMap({ - 'fruits/directory/strawberry.js': [33, 42, 0, '', null, 0, ''], - 'fruits/tomato.js': [32, 42, 0, '', null, 0, ''], + 'fruits/directory/strawberry.js': [33, 42, 0, '', null, 0, null], + 'fruits/tomato.js': [32, 42, 0, '', null, 0, null], }), ); expect(removedFiles).toEqual(new Set()); diff --git a/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js b/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js index 2d971458cd..ee4b25363f 100644 --- a/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js +++ b/packages/metro-file-map/src/crawlers/__tests__/watchman-test.js @@ -118,9 +118,9 @@ describe('watchman watch', () => { }; mockFiles = createMap({ - [MELON_RELATIVE]: [33, 43, 0, '', null, 0, ''], - [STRAWBERRY_RELATIVE]: [30, 40, 0, '', null, 0, ''], - [TOMATO_RELATIVE]: [31, 41, 0, '', null, 0, ''], + [MELON_RELATIVE]: [33, 43, 0, '', null, 0, null], + [STRAWBERRY_RELATIVE]: [30, 40, 0, '', null, 0, null], + [TOMATO_RELATIVE]: [31, 41, 0, '', null, 0, null], }); }); @@ -223,7 +223,7 @@ describe('watchman watch', () => { expect(changedFiles).toEqual( createMap({ - [KIWI_RELATIVE]: [42, 40, 0, '', null, 0, ''], + [KIWI_RELATIVE]: [42, 40, 0, '', null, 0, null], }), ); @@ -296,7 +296,7 @@ describe('watchman watch', () => { // banana is not included because it is unchanged expect(changedFiles).toEqual( createMap({ - [KIWI_RELATIVE]: [42, 52, 0, '', null, 0, ''], + [KIWI_RELATIVE]: [42, 52, 0, '', null, 0, null], [TOMATO_RELATIVE]: [76, 41, 1, '', mockTomatoSha1, 0, 'Tomato'], }), ); @@ -373,7 +373,7 @@ describe('watchman watch', () => { // Melon is not included because it is unchanged. expect(changedFiles).toEqual( createMap({ - [KIWI_RELATIVE]: [42, 52, 0, '', null, 0, ''], + [KIWI_RELATIVE]: [42, 52, 0, '', null, 0, null], }), ); @@ -542,7 +542,7 @@ describe('watchman watch', () => { expect(changedFiles).toEqual( createMap({ - [KIWI_RELATIVE]: [42, 40, 0, '', null, 0, ''], + [KIWI_RELATIVE]: [42, 40, 0, '', null, 0, null], }), ); diff --git a/packages/metro-file-map/src/crawlers/node/index.js b/packages/metro-file-map/src/crawlers/node/index.js index a21fb02b1e..075a87516c 100644 --- a/packages/metro-file-map/src/crawlers/node/index.js +++ b/packages/metro-file-map/src/crawlers/node/index.js @@ -82,7 +82,7 @@ function find( '', null, stat.isSymbolicLink() ? 1 : 0, - '', + null, ]); } } @@ -160,7 +160,7 @@ function findNative( '', null, stat.isSymbolicLink() ? 1 : 0, - '', + null, ]); } if (--count === 0) { diff --git a/packages/metro-file-map/src/crawlers/watchman/index.js b/packages/metro-file-map/src/crawlers/watchman/index.js index 5a3d15868e..68baacc658 100644 --- a/packages/metro-file-map/src/crawlers/watchman/index.js +++ b/packages/metro-file-map/src/crawlers/watchman/index.js @@ -339,7 +339,7 @@ export default async function watchmanCrawl({ '', sha1hex ?? null, symlinkInfo, - '', + null, ]; // If watchman is fresh, the removed files map starts with all files diff --git a/packages/metro-file-map/src/flow-types.js b/packages/metro-file-map/src/flow-types.js index 228b43de79..cbf2253425 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -165,45 +165,68 @@ export type EventsQueue = Array<{ type: string, }>; -export type FileMapDelta = $ReadOnly<{ - removed: Iterable<[CanonicalPath, FileMetadata]>, - addedOrModified: Iterable<[CanonicalPath, FileMetadata]>, +export type FileMapDelta = $ReadOnly<{ + removed: Iterable<[CanonicalPath, T]>, + addedOrModified: Iterable<[CanonicalPath, T]>, }>; -interface FileSystemState { - metadataIterator( - opts: $ReadOnly<{ - includeNodeModules: boolean, - includeSymlinks: boolean, +export type FileMapPluginInitOptions< + SerializableState, + PerFileData = null | void, +> = $ReadOnly<{ + files: $ReadOnly<{ + metadataIterator( + opts: $ReadOnly<{ + includeNodeModules: boolean, + includeSymlinks: boolean, + }>, + ): Iterable<{ + baseName: string, + canonicalPath: string, + data: PerFileData, }>, - ): Iterable<{ - baseName: string, - canonicalPath: string, - metadata: FileMetadata, - }>; -} - -export type FileMapPluginInitOptions = $ReadOnly<{ - files: FileSystemState, + getFilePluginData(mixedPath: string): ?PerFileData, + }>, pluginState: ?SerializableState, }>; -type V8Serializable = interface {}; +export type FileMapPluginWorker = $ReadOnly<{ + workerModulePath: string, + workerSetupArgs: JsonData, +}>; -export interface FileMapPlugin { +export type V8Serializable = + | string + | number + | boolean + | null + | $ReadOnlyArray + | $ReadOnlySet + | $ReadOnlyMap + | {[key: string]: V8Serializable}; + +export interface FileMapPlugin< + SerializableState = V8Serializable, + PerFileData = null | void, +> { +name: string; initialize( - initOptions: FileMapPluginInitOptions, + initOptions: FileMapPluginInitOptions, ): Promise; assertValid(): void; - bulkUpdate(delta: FileMapDelta): Promise; + bulkUpdate(delta: FileMapDelta): Promise; getSerializableSnapshot(): SerializableState; - onRemovedFile(relativeFilePath: string, fileMetadata: FileMetadata): void; - onNewOrModifiedFile( - relativeFilePath: string, - fileMetadata: FileMetadata, - ): void; + onRemovedFile(relativeFilePath: string, pluginData: PerFileData): void; + onNewOrModifiedFile(relativeFilePath: string, pluginData: PerFileData): void; getCacheKey(): string; + getWorker(): ?FileMapPluginWorker; +} + +export interface MetadataWorker { + processFile( + WorkerMessage, + $ReadOnly<{getContent: () => Buffer}>, + ): V8Serializable; } export type HType = { @@ -213,7 +236,7 @@ export type HType = { DEPENDENCIES: 3, SHA1: 4, SYMLINK: 5, - ID: 6, + PLUGINDATA: number, PATH: 0, TYPE: 1, MODULE: 0, @@ -236,7 +259,8 @@ export type FileMetadata = [ /* dependencies */ string, /* sha1 */ ?string, /* symlink */ 0 | 1 | string, // string specifies target, if known - /* id */ string, + /* plugindata */ + ... ]; export type FileStats = $ReadOnly<{ @@ -253,7 +277,6 @@ export interface FileSystem { changedFiles: FileData, removedFiles: Set, }; - getModuleName(file: Path): ?string; getSerializableSnapshot(): CacheData['fileSystemData']; getSha1(file: Path): ?string; getOrComputeSha1(file: Path): Promise; @@ -324,6 +347,14 @@ export interface FileSystem { export type Glob = string; +export type JsonData = + | string + | number + | boolean + | null + | Array + | {[key: string]: JsonData}; + export type LookupResult = | { // The node is missing from the FileSystem implementation (note this @@ -366,6 +397,8 @@ export interface HasteMap { type?: ?HTypeValue, ): ?Path; + getModuleNameByPath(file: Path): ?string; + getPackage( name: string, platform: ?string, @@ -460,17 +493,18 @@ export type WorkerMessage = $ReadOnly<{ computeDependencies: boolean, computeSha1: boolean, dependencyExtractor?: ?string, - enableHastePackages: boolean, + isNodeModules: boolean, filePath: string, - hasteImplModulePath?: ?string, maybeReturnContent: boolean, }>; export type WorkerMetadata = $ReadOnly<{ dependencies?: ?$ReadOnlyArray, - id?: ?string, sha1?: ?string, content?: ?Buffer, + pluginData?: $ReadOnlyArray, }>; -export type WorkerSetupArgs = $ReadOnly<{}>; +export type WorkerSetupArgs = $ReadOnly<{ + plugins?: $ReadOnlyArray, +}>; diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index 7430775a89..556b2ac34f 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -123,6 +123,13 @@ type InternalOptions = $ReadOnly<{ watchmanDeferStates: $ReadOnlyArray, }>; +// $FlowFixMe[unclear-type] Plugin types cannot be known statically +type AnyFileMapPlugin = FileMapPlugin; +type IndexedPlugin = $ReadOnly<{ + plugin: AnyFileMapPlugin, + dataIdx: ?number, +}>; + export {DiskCacheManager} from './cache/DiskCacheManager'; export {DuplicateHasteCandidatesError} from './plugins/haste/DuplicateHasteCandidatesError'; export {HasteConflictsError} from './plugins/haste/HasteConflictsError'; @@ -143,7 +150,7 @@ export type { // This should be bumped whenever a code change to `metro-file-map` itself // would cause a change to the cache data structure and/or content (for a given // filesystem state and build parameters). -const CACHE_BREAKER = '10'; +const CACHE_BREAKER = '11'; const CHANGE_INTERVAL = 30; @@ -252,7 +259,7 @@ export default class FileMap extends EventEmitter { #hastePlugin: HastePlugin; #mockPlugin: ?MockPlugin = null; - #plugins: $ReadOnlyArray>; + #plugins: $ReadOnlyArray; static create(options: InputOptions): FileMap { return new FileMap(options); @@ -293,13 +300,14 @@ export default class FileMap extends EventEmitter { this.#hastePlugin = new HastePlugin({ console: this._console, enableHastePackages, + hasteImplModulePath: options.hasteImplModulePath, perfLogger: this._startupPerfLogger, platforms: new Set(options.platforms), rootDir: options.rootDir, failValidationOnConflicts: throwOnModuleCollision, }); - const plugins: Array> = [this.#hastePlugin]; + const plugins: Array = [this.#hastePlugin]; if (options.mocksPattern != null && options.mocksPattern !== '') { this.#mockPlugin = new MockPlugin({ @@ -311,7 +319,11 @@ export default class FileMap extends EventEmitter { plugins.push(this.#mockPlugin); } - this.#plugins = plugins; + let dataSlot: number = H.PLUGINDATA; + this.#plugins = plugins.map(plugin => ({ + plugin, + dataIdx: isDataPlugin(plugin) ? dataSlot++ : null, + })); const buildParameters: BuildParameters = { computeDependencies: @@ -353,12 +365,11 @@ export default class FileMap extends EventEmitter { this._fileProcessor = new FileProcessor({ dependencyExtractor: buildParameters.dependencyExtractor, - enableHastePackages: buildParameters.enableHastePackages, enableWorkerThreads: options.enableWorkerThreads ?? false, - hasteImplModulePath: buildParameters.hasteImplModulePath, maxFilesPerWorker: options.maxFilesPerWorker, maxWorkers: options.maxWorkers, perfLogger: this._startupPerfLogger, + pluginWorkers: plugins.map(plugin => plugin.getWorker()).filter(Boolean), }); this._buildPromise = null; @@ -428,9 +439,28 @@ export default class FileMap extends EventEmitter { clocks: initialData?.clocks ?? new Map(), }), Promise.all( - plugins.map(plugin => + plugins.map(({plugin, dataIdx}) => plugin.initialize({ - files: fileSystem, + files: { + getFilePluginData: + dataIdx != null + ? (filePath: string) => + fileSystem.getFileMetadata(filePath)?.[dataIdx] + : () => { + throw new Error( + 'Plugin does not store file metadata', + ); + }, + metadataIterator: opts => + mapIterator( + fileSystem.metadataIterator(opts), + ({baseName, canonicalPath, metadata}) => ({ + baseName, + canonicalPath, + data: dataIdx != null ? metadata[dataIdx] : null, + }), + ), + }, pluginState: initialData?.plugins.get(plugin.name), }), ), @@ -441,7 +471,7 @@ export default class FileMap extends EventEmitter { await this._applyFileDelta(fileSystem, plugins, fileDelta); // Validate the mock and Haste maps before persisting them. - plugins.forEach(plugin => plugin.assertValid()); + plugins.forEach(({plugin}) => plugin.assertValid()); const watchmanClocks = new Map(fileDelta.clocks ?? []); await this._takeSnapshotAndPersist( @@ -565,7 +595,7 @@ export default class FileMap extends EventEmitter { async _applyFileDelta( fileSystem: MutableFileSystem, - plugins: $ReadOnlyArray>, + plugins: $ReadOnlyArray, delta: $ReadOnly<{ changedFiles: FileData, removedFiles: $ReadOnlySet, @@ -695,13 +725,18 @@ export default class FileMap extends EventEmitter { this._startupPerfLogger?.point('applyFileDelta_add_end'); this._startupPerfLogger?.point('applyFileDelta_updatePlugins_start'); + await Promise.all([ - plugins.map(plugin => - plugin.bulkUpdate({ - addedOrModified: changedFiles, - removed, - }), - ), + plugins.map(({plugin, dataIdx}) => { + const mapFn: ([CanonicalPath, FileMetadata]) => [CanonicalPath, mixed] = + dataIdx != null + ? ([relativePath, fileData]) => [relativePath, fileData[dataIdx]] + : ([relativePath, fileData]) => [relativePath, null]; + return plugin.bulkUpdate({ + addedOrModified: mapIterator(changedFiles.entries(), mapFn), + removed: mapIterator(removed.values(), mapFn), + }); + }), ]); this._startupPerfLogger?.point('applyFileDelta_updatePlugins_end'); this._startupPerfLogger?.point('applyFileDelta_end'); @@ -713,7 +748,7 @@ export default class FileMap extends EventEmitter { async _takeSnapshotAndPersist( fileSystem: FileSystem, clocks: WatchmanClocks, - plugins: $ReadOnlyArray>, + plugins: $ReadOnlyArray, changed: FileData, removed: Set, ) { @@ -723,7 +758,7 @@ export default class FileMap extends EventEmitter { fileSystemData: fileSystem.getSerializableSnapshot(), clocks: new Map(clocks), plugins: new Map( - plugins.map(plugin => [ + plugins.map(({plugin}) => [ plugin.name, plugin.getSerializableSnapshot(), ]), @@ -758,7 +793,7 @@ export default class FileMap extends EventEmitter { async _watch( fileSystem: MutableFileSystem, clocks: WatchmanClocks, - plugins: $ReadOnlyArray>, + plugins: $ReadOnlyArray, ): Promise { this._startupPerfLogger?.point('watch_start'); if (!this._options.watch) { @@ -913,7 +948,7 @@ export default class FileMap extends EventEmitter { '', null, change.metadata.type === 'l' ? 1 : 0, - '', + null, ]; try { @@ -932,8 +967,13 @@ export default class FileMap extends EventEmitter { } fileSystem.addOrModify(relativeFilePath, fileMetadata); this._updateClock(clocks, change.clock); - plugins.forEach(plugin => - plugin.onNewOrModifiedFile(relativeFilePath, fileMetadata), + plugins.forEach(({plugin, dataIdx}) => + dataIdx != null + ? plugin.onNewOrModifiedFile( + relativeFilePath, + fileMetadata[dataIdx], + ) + : plugin.onNewOrModifiedFile(relativeFilePath), ); enqueueEvent(change.metadata); } catch (e) { @@ -957,8 +997,10 @@ export default class FileMap extends EventEmitter { // exists in the file map and remove should always return metadata. const metadata = nullthrows(fileSystem.remove(relativeFilePath)); this._updateClock(clocks, change.clock); - plugins.forEach(plugin => - plugin.onRemovedFile(relativeFilePath, metadata), + plugins.forEach(({plugin, dataIdx}) => + dataIdx != null + ? plugin.onRemovedFile(relativeFilePath, metadata[dataIdx]) + : plugin.onRemovedFile(relativeFilePath), ); enqueueEvent({ @@ -1074,3 +1116,19 @@ export default class FileMap extends EventEmitter { static H: HType = H; } + +// TODO: Replace with it.map() from Node 22+ +const mapIterator: (Iterator, (T) => S) => Iterable = (it, fn) => + 'map' in it + ? it.map(fn) + : (function* mapped() { + for (const item of it) { + yield fn(item); + } + })(); + +function isDataPlugin(plugin: AnyFileMapPlugin): boolean { + // TODO: Allow plugins to declare whether they store per-file data, + // remove this special-casing of HastePlugin. + return plugin instanceof HastePlugin; +} diff --git a/packages/metro-file-map/src/lib/FileProcessor.js b/packages/metro-file-map/src/lib/FileProcessor.js index d22d4e9cd5..9ea252a118 100644 --- a/packages/metro-file-map/src/lib/FileProcessor.js +++ b/packages/metro-file-map/src/lib/FileProcessor.js @@ -10,6 +10,7 @@ */ import type { + FileMapPluginWorker, FileMetadata, PerfLogger, WorkerMessage, @@ -68,21 +69,20 @@ export class FileProcessor { constructor( opts: $ReadOnly<{ dependencyExtractor: ?string, - enableHastePackages: boolean, enableWorkerThreads: boolean, - hasteImplModulePath: ?string, maxFilesPerWorker?: ?number, maxWorkers: number, + pluginWorkers: ?$ReadOnlyArray, perfLogger: ?PerfLogger, }>, ) { this.#dependencyExtractor = opts.dependencyExtractor; - this.#enableHastePackages = opts.enableHastePackages; this.#enableWorkerThreads = opts.enableWorkerThreads; - this.#hasteImplModulePath = opts.hasteImplModulePath; this.#maxFilesPerWorker = opts.maxFilesPerWorker ?? MAX_FILES_PER_WORKER; this.#maxWorkers = opts.maxWorkers; - this.#workerArgs = {}; + this.#workerArgs = { + plugins: [...(opts.pluginWorkers ?? [])], + }; this.#inBandWorker = new Worker(this.#workerArgs); this.#perfLogger = opts.perfLogger; } @@ -153,7 +153,7 @@ export class FileProcessor { req: ProcessFileRequest, ): ?WorkerMessage { const computeSha1 = req.computeSha1 && fileMetadata[H.SHA1] == null; - + const isNodeModules = absolutePath.includes(NODE_MODULES); const {computeDependencies, maybeReturnContent} = req; // Use a cheaper worker configuration for node_modules files, because we @@ -168,9 +168,8 @@ export class FileProcessor { computeDependencies: false, computeSha1: true, dependencyExtractor: null, - enableHastePackages: false, + isNodeModules, filePath: absolutePath, - hasteImplModulePath: null, maybeReturnContent, }; } @@ -181,9 +180,8 @@ export class FileProcessor { computeDependencies, computeSha1, dependencyExtractor: this.#dependencyExtractor, - enableHastePackages: this.#enableHastePackages, + isNodeModules, filePath: absolutePath, - hasteImplModulePath: this.#hasteImplModulePath, maybeReturnContent, }; } @@ -235,11 +233,13 @@ function processWorkerReply( fileMetadata: FileMetadata, ) { fileMetadata[H.VISITED] = 1; - - const metadataId = metadata.id; - - if (metadataId != null) { - fileMetadata[H.ID] = metadataId; + if (metadata.pluginData) { + // $FlowFixMe[incompatible-type] - treat inexact tuple as array to set tail entries + (fileMetadata as Array).splice( + H.PLUGINDATA, + metadata.pluginData.length, + ...metadata.pluginData, + ); } fileMetadata[H.DEPENDENCIES] = metadata.dependencies diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index 76a653e6b7..8c6507d715 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -134,9 +134,8 @@ export default class TreeFS implements MutableFileSystem { return tfs; } - getModuleName(mixedPath: Path): ?string { - const fileMetadata = this._getFileData(mixedPath); - return (fileMetadata && fileMetadata[H.ID]) ?? null; + getFileMetadata(mixedPath: Path): ?FileMetadata { + return this._getFileData(mixedPath, {followLeaf: true}); } getSize(mixedPath: Path): ?number { @@ -1006,7 +1005,7 @@ export default class TreeFS implements MutableFileSystem { includeSymlinks: boolean, includeNodeModules: boolean, }>, - ): Iterable<{ + ): Iterator<{ baseName: string, canonicalPath: string, metadata: FileMetadata, diff --git a/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js b/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js index 16c66bcc45..3d632da108 100644 --- a/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js +++ b/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js @@ -10,6 +10,7 @@ */ import type { + FileMapPluginWorker, FileMetadata, WorkerMessage, WorkerMetadata, @@ -25,11 +26,10 @@ const mockWorkerFn = jest.fn().mockReturnValue({}); const defaultOptions = { dependencyExtractor: null, - enableHastePackages: false, enableWorkerThreads: true, - hasteImplModulePath: null, maxWorkers: 5, perfLogger: null, + pluginWorkers: [] as $ReadOnlyArray, }; describe('processBatch', () => { @@ -133,6 +133,6 @@ function getNMockFiles(numFiles: number): Array<[string, FileMetadata]> { .fill(null) .map((_, i) => [ `file${i}.js`, - [123, 234, 0, '', null, 0, ''] as FileMetadata, + [123, 234, 0, '', null, 0, null] as FileMetadata, ]); } diff --git a/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js b/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js index de81077a7a..49734decdc 100644 --- a/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js +++ b/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js @@ -36,18 +36,18 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { rootDir: p('/project'), files: new Map([ [p('foo/another.js'), [123, 2, 0, '', '', 0, 'another']], - [p('foo/owndir'), [0, 0, 0, '', '', '.', '']], - [p('foo/link-to-bar.js'), [0, 0, 0, '', '', p('../bar.js'), '']], - [p('foo/link-to-another.js'), [0, 0, 0, '', '', p('another.js'), '']], - [p('../outside/external.js'), [0, 0, 0, '', '', 0, '']], + [p('foo/owndir'), [0, 0, 0, '', '', '.', null]], + [p('foo/link-to-bar.js'), [0, 0, 0, '', '', p('../bar.js'), null]], + [p('foo/link-to-another.js'), [0, 0, 0, '', '', p('another.js'), null]], + [p('../outside/external.js'), [0, 0, 0, '', '', 0, null]], [p('bar.js'), [234, 3, 0, '', '', 0, 'bar']], - [p('link-to-foo'), [456, 0, 0, '', '', p('./../project/foo'), '']], - [p('abs-link-out'), [456, 0, 0, '', '', p('/outside/./baz/..'), '']], - [p('root'), [0, 0, 0, '', '', '..', '']], - [p('link-to-nowhere'), [123, 0, 0, '', '', p('./nowhere'), '']], - [p('link-to-self'), [123, 0, 0, '', '', p('./link-to-self'), '']], - [p('link-cycle-1'), [123, 0, 0, '', '', p('./link-cycle-2'), '']], - [p('link-cycle-2'), [123, 0, 0, '', '', p('./link-cycle-1'), '']], + [p('link-to-foo'), [456, 0, 0, '', '', p('./../project/foo'), null]], + [p('abs-link-out'), [456, 0, 0, '', '', p('/outside/./baz/..'), null]], + [p('root'), [0, 0, 0, '', '', '..', null]], + [p('link-to-nowhere'), [123, 0, 0, '', '', p('./nowhere'), null]], + [p('link-to-self'), [123, 0, 0, '', '', p('./link-to-self'), null]], + [p('link-cycle-1'), [123, 0, 0, '', '', p('./link-cycle-2'), null]], + [p('link-cycle-2'), [123, 0, 0, '', '', p('./link-cycle-1'), null]], [p('node_modules/pkg/a.js'), [123, 0, 0, '', '', 0, 'a']], [p('node_modules/pkg/package.json'), [123, 0, 0, '', '', 0, 'pkg']], ]), @@ -187,8 +187,8 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { const tfs = new TreeFS({ rootDir: p('/deep/project/root'), files: new Map([ - [p('foo/index.js'), [123, 0, 0, '', '', 0, '']], - [p('link-up'), [123, 0, 0, '', '', p('..'), '']], + [p('foo/index.js'), [123, 0, 0, '', '', 0, null]], + [p('link-up'), [123, 0, 0, '', '', p('..'), null]], ]), processFile: () => { throw new Error('Not implemented'); @@ -215,7 +215,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { describe('symlinks to an ancestor of the project root', () => { beforeEach(() => { - tfs.addOrModify(p('foo/link-up-2'), [0, 0, 0, '', '', p('../..'), '']); + tfs.addOrModify(p('foo/link-up-2'), [0, 0, 0, '', '', p('../..'), null]); }); test.each([ @@ -269,23 +269,23 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { describe('getDifference', () => { test('returns changed (inc. new) and removed files in given FileData', () => { const newFiles: FileData = new Map([ - [p('new-file'), [789, 0, 0, '', '', 0, '']], - [p('link-to-foo'), [456, 0, 0, '', '', p('./foo'), '']], + [p('new-file'), [789, 0, 0, '', '', 0, null]], + [p('link-to-foo'), [456, 0, 0, '', '', p('./foo'), null]], // Different modified time, expect new mtime in changedFiles - [p('foo/another.js'), [124, 0, 0, '', '', 0, '']], - [p('link-cycle-1'), [123, 0, 0, '', '', p('./link-cycle-2'), '']], - [p('link-cycle-2'), [123, 0, 0, '', '', p('./link-cycle-1'), '']], + [p('foo/another.js'), [124, 0, 0, '', '', 0, null]], + [p('link-cycle-1'), [123, 0, 0, '', '', p('./link-cycle-2'), null]], + [p('link-cycle-2'), [123, 0, 0, '', '', p('./link-cycle-1'), null]], // Was a symlink, now a regular file - [p('link-to-self'), [123, 0, 0, '', '', 0, '']], - [p('link-to-nowhere'), [123, 0, 0, '', '', p('./nowhere'), '']], + [p('link-to-self'), [123, 0, 0, '', '', 0, null]], + [p('link-to-nowhere'), [123, 0, 0, '', '', p('./nowhere'), null]], [p('node_modules/pkg/a.js'), [123, 0, 0, '', '', 0, 'a']], [p('node_modules/pkg/package.json'), [123, 0, 0, '', '', 0, 'pkg']], ]); expect(tfs.getDifference(newFiles)).toEqual({ changedFiles: new Map([ - [p('new-file'), [789, 0, 0, '', '', 0, '']], - [p('foo/another.js'), [124, 0, 0, '', '', 0, '']], - [p('link-to-self'), [123, 0, 0, '', '', 0, '']], + [p('new-file'), [789, 0, 0, '', '', 0, null]], + [p('foo/another.js'), [124, 0, 0, '', '', 0, null]], + [p('link-to-self'), [123, 0, 0, '', '', 0, null]], ]), removedFiles: new Set([ p('foo/owndir'), @@ -310,24 +310,24 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { ([ [ p('a/1/package.json'), - [0, 0, 0, '', '', './real-package.json', ''], + [0, 0, 0, '', '', './real-package.json', null], ], [ p('a/2/package.json'), - [0, 0, 0, '', '', './notexist-package.json', ''], + [0, 0, 0, '', '', './notexist-package.json', null], ], - [p('a/b/c/d/link-to-C'), [0, 0, 0, '', '', p('../../../..'), '']], + [p('a/b/c/d/link-to-C'), [0, 0, 0, '', '', p('../../../..'), null]], [ p('a/b/c/d/link-to-B'), - [0, 0, 0, '', '', p('../../../../..'), ''], + [0, 0, 0, '', '', p('../../../../..'), null], ], [ p('a/b/c/d/link-to-A'), - [0, 0, 0, '', '', p('../../../../../..'), ''], + [0, 0, 0, '', '', p('../../../../../..'), null], ], [ p('n_m/workspace/link-to-pkg'), - [0, 0, 0, '', '', p('../../../workspace-pkg'), ''], + [0, 0, 0, '', '', p('../../../workspace-pkg'), null], ], ]: Array<[CanonicalPath, FileMetadata]>).concat( [ @@ -347,7 +347,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { '../../package.json', '../../../a/b/package.json', '../workspace-pkg/package.json', - ].map(posixPath => [p(posixPath), [0, 0, 0, '', '', 0, '']]), + ].map(posixPath => [p(posixPath), [0, 0, 0, '', '', 0, null]]), ), ), processFile: () => { @@ -711,8 +711,16 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { describe('mutation', () => { describe('addOrModify', () => { test('accepts non-real and absolute paths', () => { - tfs.addOrModify(p('link-to-foo/new.js'), [0, 1, 0, '', '', 0, '']); - tfs.addOrModify(p('/project/fileatroot.js'), [0, 2, 0, '', '', 0, '']); + tfs.addOrModify(p('link-to-foo/new.js'), [0, 1, 0, '', '', 0, null]); + tfs.addOrModify(p('/project/fileatroot.js'), [ + 0, + 2, + 0, + '', + '', + 0, + null, + ]); expect(tfs.getAllFiles().sort()).toEqual([ p('/outside/external.js'), p('/project/bar.js'), @@ -733,10 +741,10 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { new Map([ [ p('newdir/link-to-link-to-bar.js'), - [0, 0, 0, '', '', p('../foo/link-to-bar.js'), ''], + [0, 0, 0, '', '', p('../foo/link-to-bar.js'), null], ], - [p('foo/baz.js'), [0, 0, 0, '', '', 0, '']], - [p('bar.js'), [999, 1, 0, '', '', 0, '']], + [p('foo/baz.js'), [0, 0, 0, '', '', 0, null]], + [p('bar.js'), [999, 1, 0, '', '', 0, null]], ]), ); @@ -830,7 +838,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { { baseName: 'external.js', canonicalPath: p('../outside/external.js'), - metadata: [0, 0, 0, '', '', 0, ''], + metadata: [0, 0, 0, '', '', 0, null], }, { baseName: 'bar.js', @@ -868,7 +876,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { { baseName: 'link-to-bar.js', canonicalPath: p('foo/link-to-bar.js'), - metadata: [0, 0, 0, '', '', p('../bar.js'), ''], + metadata: [0, 0, 0, '', '', p('../bar.js'), null], }, ]), ); @@ -882,9 +890,9 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { tfs = new TreeFS({ rootDir: p('/project'), files: new Map([ - [p('foo.js'), [123, 0, 0, '', 'def456', 0, '']], - [p('bar.js'), [123, 0, 0, '', '', 0, '']], - [p('link-to-bar'), [456, 0, 0, '', '', p('./bar.js'), '']], + [p('foo.js'), [123, 0, 0, '', 'def456', 0, null]], + [p('bar.js'), [123, 0, 0, '', '', 0, null]], + [p('link-to-bar'), [456, 0, 0, '', '', p('./bar.js'), null]], ]), processFile: mockProcessFile, }); @@ -958,7 +966,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { {computeSha1: true}, ); // Simulate the file being modified while we're waiting for the SHA1. - tfs.addOrModify(p('bar.js'), [123, 0, 0, '', '', 0, '']); + tfs.addOrModify(p('bar.js'), [123, 0, 0, '', '', 0, null]); resolve?.('newsha1'); expect(await getOrComputePromise).toEqual({sha1: 'newsha1'}); // A second call re-computes diff --git a/packages/metro-file-map/src/plugins/HastePlugin.js b/packages/metro-file-map/src/plugins/HastePlugin.js index e63f4cf27c..6666f3b24e 100644 --- a/packages/metro-file-map/src/plugins/HastePlugin.js +++ b/packages/metro-file-map/src/plugins/HastePlugin.js @@ -16,7 +16,7 @@ import type { FileMapDelta, FileMapPlugin, FileMapPluginInitOptions, - FileMetadata, + FileMapPluginWorker, HasteConflict, HasteMap, HasteMapItem, @@ -44,13 +44,16 @@ const YIELD_EVERY_NUM_HASTE_FILES = 10000; type HasteMapOptions = $ReadOnly<{ console?: ?Console, enableHastePackages: boolean, - perfLogger: ?PerfLogger, + hasteImplModulePath?: ?string, + perfLogger?: ?PerfLogger, platforms: $ReadOnlySet, rootDir: Path, - failValidationOnConflicts: boolean, + failValidationOnConflicts?: boolean, }>; -export default class HastePlugin implements HasteMap, FileMapPlugin { +export default class HastePlugin + implements HasteMap, FileMapPlugin +{ +name = 'haste'; +#rootDir: Path; @@ -59,41 +62,67 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { +#console: ?Console; +#enableHastePackages: boolean; + +#hasteImplCacheKey: ?string; + +#hasteImplModulePath: ?string; +#perfLogger: ?PerfLogger; +#pathUtils: RootPathUtils; +#platforms: $ReadOnlySet; +#failValidationOnConflicts: boolean; + #getModuleNameByPath: string => ?string; constructor(options: HasteMapOptions) { this.#console = options.console ?? null; this.#enableHastePackages = options.enableHastePackages; + const hasteImplPath = options.hasteImplModulePath; + + if (hasteImplPath != null) { + // $FlowFixMe[unsupported-syntax] - dynamic require + const hasteImpl = require(hasteImplPath); + if (typeof hasteImpl.getCacheKey !== 'function') { + throw new Error( + `HasteImpl module ${hasteImplPath} must export a function named "getCacheKey"`, + ); + } + this.#hasteImplCacheKey = hasteImpl.getCacheKey(); + this.#hasteImplModulePath = hasteImplPath; + } + this.#perfLogger = options.perfLogger; this.#platforms = options.platforms; this.#rootDir = options.rootDir; this.#pathUtils = new RootPathUtils(options.rootDir); - this.#failValidationOnConflicts = options.failValidationOnConflicts; + this.#failValidationOnConflicts = + options.failValidationOnConflicts ?? false; } - async initialize({files}: FileMapPluginInitOptions): Promise { + async initialize({ + files, + }: FileMapPluginInitOptions): Promise { this.#perfLogger?.point('constructHasteMap_start'); let hasteFiles = 0; - for (const {baseName, canonicalPath, metadata} of files.metadataIterator({ + for (const { + baseName, + canonicalPath, + data: hasteId, + } of files.metadataIterator({ // Symlinks and node_modules are never Haste modules or packages. includeNodeModules: false, includeSymlinks: false, })) { - if (metadata[H.ID]) { - this.setModule(metadata[H.ID], [ - canonicalPath, - this.#enableHastePackages && baseName === 'package.json' - ? H.PACKAGE - : H.MODULE, - ]); - if (++hasteFiles % YIELD_EVERY_NUM_HASTE_FILES === 0) { - await new Promise(setImmediate); - } + if (hasteId == null) { + continue; + } + this.setModule(hasteId, [ + canonicalPath, + this.#enableHastePackages && baseName === 'package.json' + ? H.PACKAGE + : H.MODULE, + ]); + if (++hasteFiles % YIELD_EVERY_NUM_HASTE_FILES === 0) { + await new Promise(setImmediate); } } + this.#getModuleNameByPath = files.getFilePluginData; this.#perfLogger?.point('constructHasteMap_end'); this.#perfLogger?.annotate({int: {hasteFiles}}); } @@ -126,6 +155,15 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { return null; } + getModuleNameByPath(mixedPath: Path): ?string { + if (this.#getModuleNameByPath == null) { + throw new Error( + 'HastePlugin has not been initialized before getModuleNameByPath', + ); + } + return this.#getModuleNameByPath(mixedPath) ?? null; + } + getPackage( name: string, platform: ?string, @@ -207,19 +245,19 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { ); } - async bulkUpdate(delta: FileMapDelta): Promise { + async bulkUpdate(delta: FileMapDelta): Promise { // Process removals first so that moves aren't treated as duplicates. - for (const [normalPath, metadata] of delta.removed) { - this.onRemovedFile(normalPath, metadata); + for (const [normalPath, maybeHasteId] of delta.removed) { + this.onRemovedFile(normalPath, maybeHasteId); } - for (const [normalPath, metadata] of delta.addedOrModified) { - this.onNewOrModifiedFile(normalPath, metadata); + for (const [normalPath, maybeHasteId] of delta.addedOrModified) { + this.onNewOrModifiedFile(normalPath, maybeHasteId); } } - onNewOrModifiedFile(relativeFilePath: string, fileMetadata: FileMetadata) { - const id = fileMetadata[H.ID] || null; // Empty string indicates no module + onNewOrModifiedFile(relativeFilePath: string, id: ?string) { if (id == null) { + // Not a Haste module or package return; } @@ -294,9 +332,9 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { hasteMapItem[platform] = module; } - onRemovedFile(relativeFilePath: string, fileMetadata: FileMetadata) { - const moduleName = fileMetadata[H.ID] || null; // Empty string indicates no module + onRemovedFile(relativeFilePath: string, moduleName: ?string) { if (moduleName == null) { + // Not a Haste module or package return; } @@ -454,7 +492,18 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { getCacheKey(): string { return JSON.stringify([ this.#enableHastePackages, + this.#hasteImplCacheKey, [...this.#platforms].sort(), ]); } + + getWorker(): FileMapPluginWorker { + return { + workerModulePath: require.resolve('./haste/worker.js'), + workerSetupArgs: { + enableHastePackages: this.#enableHastePackages, + hasteImplModulePath: this.#hasteImplModulePath ?? null, + }, + }; + } } diff --git a/packages/metro-file-map/src/plugins/MockPlugin.js b/packages/metro-file-map/src/plugins/MockPlugin.js index 43ad69f589..d0b34f0e3e 100644 --- a/packages/metro-file-map/src/plugins/MockPlugin.js +++ b/packages/metro-file-map/src/plugins/MockPlugin.js @@ -13,6 +13,7 @@ import type { FileMapDelta, FileMapPlugin, FileMapPluginInitOptions, + FileMapPluginWorker, MockMap as IMockMap, Path, RawMockMap, @@ -80,7 +81,7 @@ export default class MockPlugin implements FileMapPlugin, IMockMap { includeNodeModules: false, includeSymlinks: false, }), - ].map(({canonicalPath, metadata}) => [canonicalPath, metadata]), + ].map(({canonicalPath}) => [canonicalPath, null]), removed: [], }); } @@ -97,7 +98,7 @@ export default class MockPlugin implements FileMapPlugin, IMockMap { ); } - async bulkUpdate(delta: FileMapDelta): Promise { + async bulkUpdate(delta: FileMapDelta<>): Promise { // Process removals first so that moves aren't treated as duplicates. for (const [relativeFilePath] of delta.removed) { this.onRemovedFile(relativeFilePath); @@ -213,4 +214,8 @@ export default class MockPlugin implements FileMapPlugin, IMockMap { this.#mocksPattern.flags ); } + + getWorker(): ?FileMapPluginWorker { + return null; + } } diff --git a/packages/metro-file-map/src/plugins/haste/__tests__/HastePlugin-test.js b/packages/metro-file-map/src/plugins/haste/__tests__/HastePlugin-test.js index 513994be01..a0783e1c5c 100644 --- a/packages/metro-file-map/src/plugins/haste/__tests__/HastePlugin-test.js +++ b/packages/metro-file-map/src/plugins/haste/__tests__/HastePlugin-test.js @@ -9,7 +9,6 @@ * @oncall react_native */ -import type {FileMetadata} from '../../../flow-types'; import type HasteMapType from '../../HastePlugin'; let mockPathModule; @@ -25,22 +24,22 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { { canonicalPath: p('project/Foo.js'), baseName: 'Foo.js', - metadata: hasteMetadata('NameForFoo'), + data: 'NameForFoo', }, { canonicalPath: p('project/Bar.js'), baseName: 'Bar.js', - metadata: hasteMetadata('Bar'), + data: 'Bar', }, { canonicalPath: p('project/Duplicate.js'), baseName: 'Duplicate.js', - metadata: hasteMetadata('Duplicate'), + data: 'Duplicate', }, { canonicalPath: p('project/other/Duplicate.js'), baseName: 'Duplicate.js', - metadata: hasteMetadata('Duplicate'), + data: 'Duplicate', }, ]; @@ -73,9 +72,10 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { { canonicalPath: p('project/Foo.js'), baseName: 'Foo.js', - metadata: hasteMetadata('NameForFoo'), + data: 'NameForFoo', }, ]), + getFilePluginData: jest.fn(), }, pluginState: null, }; @@ -93,14 +93,17 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { beforeEach(async () => { hasteMap = new HasteMap(opts); await hasteMap.initialize({ - files: {metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES)}, + files: { + metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES), + getFilePluginData: jest.fn(), + }, pluginState: null, }); }); test('removes a module, without affecting others', () => { expect(hasteMap.getModule('NameForFoo')).not.toBeNull(); - hasteMap.onRemovedFile(p('project/Foo.js'), hasteMetadata('NameForFoo')); + hasteMap.onRemovedFile(p('project/Foo.js'), 'NameForFoo'); expect(hasteMap.getModule('NameForFoo')).toBeNull(); expect(hasteMap.getModule('Bar')).not.toBeNull(); }); @@ -109,10 +112,7 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { expect(() => hasteMap.getModule('Duplicate')).toThrow( DuplicateHasteCandidatesError, ); - hasteMap.onRemovedFile( - p('project/Duplicate.js'), - hasteMetadata('Duplicate'), - ); + hasteMap.onRemovedFile(p('project/Duplicate.js'), 'Duplicate'); expect(hasteMap.getModule('Duplicate')).toBe( p('/root/project/other/Duplicate.js'), ); @@ -125,14 +125,17 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { beforeEach(async () => { hasteMap = new HasteMap(opts); await hasteMap.initialize({ - files: {metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES)}, + files: { + metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES), + getFilePluginData: jest.fn(), + }, pluginState: null, }); }); test('removes a module, without affecting others', () => { expect(hasteMap.getModule('NameForFoo')).not.toBeNull(); - hasteMap.onRemovedFile(p('project/Foo.js'), hasteMetadata('NameForFoo')); + hasteMap.onRemovedFile(p('project/Foo.js'), 'NameForFoo'); expect(hasteMap.getModule('NameForFoo')).toBeNull(); expect(hasteMap.getModule('Bar')).not.toBeNull(); }); @@ -143,12 +146,12 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { ); await hasteMap.bulkUpdate({ removed: [ - [p('project/Duplicate.js'), hasteMetadata('Duplicate')], - [p('project/Foo.js'), hasteMetadata('NameForFoo')], + [p('project/Duplicate.js'), 'Duplicate'], + [p('project/Foo.js'), 'NameForFoo'], ], addedOrModified: [ - [p('project/Baz.js'), hasteMetadata('Baz')], // New - [p('project/other/Bar.js'), hasteMetadata('Bar')], // New duplicate + [p('project/Baz.js'), 'Baz'], // New + [p('project/other/Bar.js'), 'Bar'], // New duplicate ], }); expect(hasteMap.getModule('Duplicate')).toBe( @@ -161,8 +164,35 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { ); }); }); -}); -function hasteMetadata(hasteName: string): FileMetadata { - return [0, 0, 0, '', '', 0, hasteName]; -} + describe('getModuleNameByPath', () => { + let hasteMap: HasteMapType; + let getFilePluginData; + + beforeEach(async () => { + hasteMap = new HasteMap(opts); + getFilePluginData = jest.fn().mockReturnValue(null); + + await hasteMap.initialize({ + files: { + metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES), + getFilePluginData, + }, + pluginState: null, + }); + }); + + test('returns the correct module name', () => { + getFilePluginData.mockImplementation( + filePath => + ({ + [p('/root/Foo.js')]: 'Foo' as ?string, + [p('/root/not-haste.js')]: null, + })[filePath] ?? null, + ); + expect(hasteMap.getModuleNameByPath(p('/root/Foo.js'))).toBe('Foo'); + expect(hasteMap.getModuleNameByPath(p('/root/not-haste.js'))).toBe(null); + expect(hasteMap.getModuleNameByPath(p('/root/not-exists.js'))).toBe(null); + }); + }); +}); diff --git a/packages/metro-file-map/src/plugins/haste/types.js b/packages/metro-file-map/src/plugins/haste/types.js new file mode 100644 index 0000000000..044ab781c2 --- /dev/null +++ b/packages/metro-file-map/src/plugins/haste/types.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +export type WorkerSetupArgs = $ReadOnly<{ + hasteImplModulePath: string, + enableHastePackages: boolean, +}>; diff --git a/packages/metro-file-map/src/plugins/haste/worker.js b/packages/metro-file-map/src/plugins/haste/worker.js new file mode 100644 index 0000000000..2a4779e13b --- /dev/null +++ b/packages/metro-file-map/src/plugins/haste/worker.js @@ -0,0 +1,63 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +/* eslint-disable import/no-commonjs */ + +'use strict'; + +const excludedExtensions = require('../../workerExclusionList'); +const path = require('path'); + +/*:: +import type {WorkerSetupArgs} from './types'; +import type {MetadataWorker, WorkerMessage, V8Serializable} from '../../flow-types'; +*/ + +const PACKAGE_JSON = path.sep + 'package.json'; + +module.exports = class Worker /*:: implements MetadataWorker */ { + #enableHastePackages /*: boolean */; + #hasteImpl /*: ?$ReadOnly<{getHasteName: string => ?string}> */; + #hasteImplModulePath /*: ?string */ = null; + + constructor(setupArgs /*: WorkerSetupArgs */) { + this.#enableHastePackages = setupArgs.enableHastePackages; + if (setupArgs.hasteImplModulePath) { + // $FlowFixMe[unsupported-syntax] - dynamic require + this.#hasteImpl = require(setupArgs.hasteImplModulePath); + } + } + + processFile( + data /*: WorkerMessage */, + utils /*: $ReadOnly<{getContent: () => Buffer }> */, + ) /*: V8Serializable */ { + let hasteName /*: string | null */ = null; + const {filePath} = data; + if (this.#enableHastePackages && filePath.endsWith(PACKAGE_JSON)) { + // Process a package.json that is returned as a PACKAGE type with its name. + try { + const fileData = JSON.parse(utils.getContent().toString()); + if (fileData.name) { + hasteName = fileData.name; + } + } catch (err) { + throw new Error(`Cannot parse ${filePath} as JSON: ${err.message}`); + } + } else if ( + this.#hasteImpl != null && + !excludedExtensions.has(filePath.substr(filePath.lastIndexOf('.'))) + ) { + // Process a random file that is returned as a MODULE. + hasteName = this.#hasteImpl?.getHasteName(filePath) || null; + } + return hasteName; + } +}; diff --git a/packages/metro-file-map/src/plugins/mocks/__tests__/MockPlugin-test.js b/packages/metro-file-map/src/plugins/mocks/__tests__/MockPlugin-test.js index 33fbbd3ef7..2f81febc80 100644 --- a/packages/metro-file-map/src/plugins/mocks/__tests__/MockPlugin-test.js +++ b/packages/metro-file-map/src/plugins/mocks/__tests__/MockPlugin-test.js @@ -100,6 +100,9 @@ Duplicate manual mock found for \`foo\`: metadataIterator: () => { throw new Error('should not be used'); }, + getFilePluginData: () => { + throw new Error('should not be used'); + }, }, pluginState: { mocks: new Map([ diff --git a/packages/metro-file-map/src/worker.js b/packages/metro-file-map/src/worker.js index 5c135dd397..34d3a479cb 100644 --- a/packages/metro-file-map/src/worker.js +++ b/packages/metro-file-map/src/worker.js @@ -13,6 +13,8 @@ /*:: import type { DependencyExtractor, + FileMapPluginWorker, + MetadataWorker, WorkerMessage, WorkerMetadata, WorkerSetupArgs, @@ -25,43 +27,28 @@ const defaultDependencyExtractor = require('./lib/dependencyExtractor'); const excludedExtensions = require('./workerExclusionList'); const {createHash} = require('crypto'); const fs = require('graceful-fs'); -const path = require('path'); - -const PACKAGE_JSON = path.sep + 'package.json'; - -let hasteImpl /*: ?{getHasteName: string => ?string} */ = null; -let hasteImplModulePath /*: ?string */ = null; - -function getHasteImpl( - requestedModulePath /*: string */, -) /*: {getHasteName: string => ?string} */ { - if (hasteImpl) { - if (requestedModulePath !== hasteImplModulePath) { - throw new Error('metro-file-map: hasteImplModulePath changed'); - } - return hasteImpl; - } - hasteImplModulePath = requestedModulePath; - // $FlowFixMe[unsupported-syntax] - dynamic require - hasteImpl = require(hasteImplModulePath); - return hasteImpl; -} function sha1hex(content /*: string | Buffer */) /*: string */ { return createHash('sha1').update(content).digest('hex'); } class Worker { - constructor(args /*: WorkerSetupArgs */) {} + #plugins /*: $ReadOnlyArray */; + + constructor({plugins = []} /*: WorkerSetupArgs */) { + this.#plugins = plugins.map(({workerModulePath, workerSetupArgs}) => { + // $FlowFixMe[unsupported-syntax] - dynamic require + const PluginWorker = require(workerModulePath); + return new PluginWorker(workerSetupArgs); + }); + } processFile(data /*: WorkerMessage */) /*: WorkerMetadata */ { let content /*: ?Buffer */; let dependencies /*: WorkerMetadata['dependencies'] */; - let id /*: WorkerMetadata['id'] */; let sha1 /*: WorkerMetadata['sha1'] */; - const {computeDependencies, computeSha1, enableHastePackages, filePath} = - data; + const {computeDependencies, computeSha1, filePath} = data; const getContent = () /*: Buffer */ => { if (content == null) { @@ -71,43 +58,30 @@ class Worker { return content; }; - if (enableHastePackages && filePath.endsWith(PACKAGE_JSON)) { - // Process a package.json that is returned as a PACKAGE type with its name. - try { - const fileData = JSON.parse(getContent().toString()); + const workerUtils = {getContent}; + const pluginData = this.#plugins.map(plugin => + plugin.processFile(data, workerUtils), + ); - if (fileData.name) { - id = fileData.name; - } - } catch (err) { - throw new Error(`Cannot parse ${filePath} as JSON: ${err.message}`); - } - } else if ( - (data.hasteImplModulePath != null || computeDependencies) && + if ( + computeDependencies && !excludedExtensions.has(filePath.substr(filePath.lastIndexOf('.'))) ) { - // Process a random file that is returned as a MODULE. - if (data.hasteImplModulePath != null) { - id = getHasteImpl(data.hasteImplModulePath).getHasteName(filePath); - } - - if (computeDependencies) { - const dependencyExtractor /*: ?DependencyExtractor */ = - data.dependencyExtractor != null - ? // $FlowFixMe[unsupported-syntax] - dynamic require - require(data.dependencyExtractor) - : null; - - dependencies = Array.from( - dependencyExtractor != null - ? dependencyExtractor.extract( - getContent().toString(), - filePath, - defaultDependencyExtractor.extract, - ) - : defaultDependencyExtractor.extract(getContent().toString()), - ); - } + const dependencyExtractor /*: ?DependencyExtractor */ = + data.dependencyExtractor != null + ? // $FlowFixMe[unsupported-syntax] - dynamic require + require(data.dependencyExtractor) + : null; + + dependencies = Array.from( + dependencyExtractor != null + ? dependencyExtractor.extract( + getContent().toString(), + filePath, + defaultDependencyExtractor.extract, + ) + : defaultDependencyExtractor.extract(getContent().toString()), + ); } // If a SHA-1 is requested on update, compute it. @@ -116,8 +90,8 @@ class Worker { } return content && data.maybeReturnContent - ? {content, dependencies, id, sha1} - : {dependencies, id, sha1}; + ? {content, dependencies, pluginData, sha1} + : {dependencies, pluginData, sha1}; } } diff --git a/packages/metro/src/node-haste/DependencyGraph.js b/packages/metro/src/node-haste/DependencyGraph.js index c905dc9140..7f517d1653 100644 --- a/packages/metro/src/node-haste/DependencyGraph.js +++ b/packages/metro/src/node-haste/DependencyGraph.js @@ -381,7 +381,7 @@ export default class DependencyGraph extends EventEmitter { }; getHasteName(filePath: string): string { - const hasteName = this._fileSystem.getModuleName(filePath); + const hasteName = this._hasteMap.getModuleNameByPath(filePath); if (hasteName) { return hasteName;