From 7c4398f67a1e0dded94fe9e4af23db266bfef585 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Tue, 30 Sep 2025 07:49:09 -0700 Subject: [PATCH 1/6] Move getModuleName from FileSystem to HastePlugin Differential Revision: D76735892 --- .../src/__tests__/index-test.js | 41 ++++++++++-------- packages/metro-file-map/src/flow-types.js | 4 +- packages/metro-file-map/src/lib/TreeFS.js | 5 +-- .../metro-file-map/src/plugins/HastePlugin.js | 17 ++++++++ .../haste/__tests__/HastePlugin-test.js | 42 ++++++++++++++++++- .../mocks/__tests__/MockPlugin-test.js | 3 ++ .../metro/src/node-haste/DependencyGraph.js | 2 +- 7 files changed, 89 insertions(+), 25 deletions(-) diff --git a/packages/metro-file-map/src/__tests__/index-test.js b/packages/metro-file-map/src/__tests__/index-test.js index 6db43b0238..4c5c89b394 100644 --- a/packages/metro-file-map/src/__tests__/index-test.js +++ b/packages/metro-file-map/src/__tests__/index-test.js @@ -1681,7 +1681,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 +1699,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 +1767,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 +1940,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 +1967,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 +1990,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 +2025,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 +2049,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 +2070,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 +2080,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 +2109,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/flow-types.js b/packages/metro-file-map/src/flow-types.js index 228b43de79..89928010cb 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -181,6 +181,7 @@ interface FileSystemState { canonicalPath: string, metadata: FileMetadata, }>; + getFileMetadata(mixedPath: string): ?FileMetadata; } export type FileMapPluginInitOptions = $ReadOnly<{ @@ -253,7 +254,6 @@ export interface FileSystem { changedFiles: FileData, removedFiles: Set, }; - getModuleName(file: Path): ?string; getSerializableSnapshot(): CacheData['fileSystemData']; getSha1(file: Path): ?string; getOrComputeSha1(file: Path): Promise; @@ -366,6 +366,8 @@ export interface HasteMap { type?: ?HTypeValue, ): ?Path; + getModuleNameByPath(file: Path): ?string; + getPackage( name: string, platform: ?string, diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index 76a653e6b7..def4a60cd4 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 { diff --git a/packages/metro-file-map/src/plugins/HastePlugin.js b/packages/metro-file-map/src/plugins/HastePlugin.js index e63f4cf27c..cafe22a613 100644 --- a/packages/metro-file-map/src/plugins/HastePlugin.js +++ b/packages/metro-file-map/src/plugins/HastePlugin.js @@ -63,6 +63,7 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { +#pathUtils: RootPathUtils; +#platforms: $ReadOnlySet; +#failValidationOnConflicts: boolean; + #getModuleNameByPath: string => ?string; constructor(options: HasteMapOptions) { this.#console = options.console ?? null; @@ -94,6 +95,13 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { } } } + this.#getModuleNameByPath = mixedPath => { + const metadata = files.getFileMetadata(mixedPath); + if (metadata == null || metadata[H.ID] === '' || metadata[H.ID] == null) { + return null; + } + return metadata[H.ID]; + }; this.#perfLogger?.point('constructHasteMap_end'); this.#perfLogger?.annotate({int: {hasteFiles}}); } @@ -126,6 +134,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); + } + getPackage( name: string, platform: ?string, 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..002e985f86 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 @@ -76,6 +76,7 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { metadata: hasteMetadata('NameForFoo'), }, ]), + getFileMetadata: jest.fn(), }, pluginState: null, }; @@ -93,7 +94,10 @@ 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), + getFileMetadata: jest.fn(), + }, pluginState: null, }); }); @@ -125,7 +129,10 @@ 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), + getFileMetadata: jest.fn(), + }, pluginState: null, }); }); @@ -161,6 +168,37 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { ); }); }); + + describe('getModuleNameByPath', () => { + let hasteMap: HasteMapType; + let getFileMetadata; + + beforeEach(async () => { + hasteMap = new HasteMap(opts); + getFileMetadata = jest.fn().mockReturnValue(null); + + await hasteMap.initialize({ + files: { + metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES), + getFileMetadata, + }, + pluginState: null, + }); + }); + + test('returns the correct module name', () => { + getFileMetadata.mockImplementation( + filePath => + ({ + [p('/root/Foo.js')]: hasteMetadata('Foo'), + [p('/root/not-haste.js')]: hasteMetadata(''), + })[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); + }); + }); }); function hasteMetadata(hasteName: string): FileMetadata { 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..85d0d8ba04 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'); }, + getFileMetadata: () => { + throw new Error('should not be used'); + }, }, pluginState: { mocks: new Map([ 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; From 9c003502b4037f3f5dab451a3948d8d8f6eb7438 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Tue, 30 Sep 2025 07:49:09 -0700 Subject: [PATCH 2/6] File map: Use nulls instead of empty strings in non-Haste metadata storage Differential Revision: D83361951 --- .../src/__tests__/index-test.js | 20 ++-- .../crawlers/__tests__/integration-test.js | 24 +++-- .../src/crawlers/__tests__/node-test.js | 38 ++++---- .../src/crawlers/__tests__/watchman-test.js | 14 +-- .../metro-file-map/src/crawlers/node/index.js | 4 +- .../src/crawlers/watchman/index.js | 2 +- packages/metro-file-map/src/flow-types.js | 2 +- packages/metro-file-map/src/index.js | 4 +- .../src/lib/__tests__/FileProcessor-test.js | 2 +- .../src/lib/__tests__/TreeFS-test.js | 92 ++++++++++--------- .../metro-file-map/src/plugins/HastePlugin.js | 10 +- .../haste/__tests__/HastePlugin-test.js | 4 +- packages/metro-file-map/src/worker.js | 3 +- 13 files changed, 120 insertions(+), 99 deletions(-) diff --git a/packages/metro-file-map/src/__tests__/index-test.js b/packages/metro-file-map/src/__tests__/index-test.js index 4c5c89b394..9fae00d317 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(), @@ -1533,7 +1533,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 +1575,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(), }); 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 89928010cb..1085fd7a77 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -237,7 +237,7 @@ export type FileMetadata = [ /* dependencies */ string, /* sha1 */ ?string, /* symlink */ 0 | 1 | string, // string specifies target, if known - /* id */ string, + /* id */ ?string, // Haste module/package name, or null for non-Haste ]; export type FileStats = $ReadOnly<{ diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index 7430775a89..ddcc92d126 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -143,7 +143,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; @@ -913,7 +913,7 @@ export default class FileMap extends EventEmitter { '', null, change.metadata.type === 'l' ? 1 : 0, - '', + null, ]; try { 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..b37aa8aa80 100644 --- a/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js +++ b/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js @@ -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 cafe22a613..a683699ec2 100644 --- a/packages/metro-file-map/src/plugins/HastePlugin.js +++ b/packages/metro-file-map/src/plugins/HastePlugin.js @@ -83,7 +83,7 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { includeNodeModules: false, includeSymlinks: false, })) { - if (metadata[H.ID]) { + if (metadata[H.ID] != null) { this.setModule(metadata[H.ID], [ canonicalPath, this.#enableHastePackages && baseName === 'package.json' @@ -97,7 +97,7 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { } this.#getModuleNameByPath = mixedPath => { const metadata = files.getFileMetadata(mixedPath); - if (metadata == null || metadata[H.ID] === '' || metadata[H.ID] == null) { + if (metadata == null || metadata[H.ID] == null) { return null; } return metadata[H.ID]; @@ -235,8 +235,9 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { } onNewOrModifiedFile(relativeFilePath: string, fileMetadata: FileMetadata) { - const id = fileMetadata[H.ID] || null; // Empty string indicates no module + const id = fileMetadata[H.ID]; if (id == null) { + // Not a Haste module or package return; } @@ -312,8 +313,9 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { } onRemovedFile(relativeFilePath: string, fileMetadata: FileMetadata) { - const moduleName = fileMetadata[H.ID] || null; // Empty string indicates no module + const moduleName = fileMetadata[H.ID]; if (moduleName == null) { + // Not a Haste module or package return; } 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 002e985f86..4792b7edbb 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 @@ -191,7 +191,7 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { filePath => ({ [p('/root/Foo.js')]: hasteMetadata('Foo'), - [p('/root/not-haste.js')]: hasteMetadata(''), + [p('/root/not-haste.js')]: hasteMetadata(null), })[filePath] ?? null, ); expect(hasteMap.getModuleNameByPath(p('/root/Foo.js'))).toBe('Foo'); @@ -201,6 +201,6 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { }); }); -function hasteMetadata(hasteName: string): FileMetadata { +function hasteMetadata(hasteName: ?string): FileMetadata { return [0, 0, 0, '', '', 0, hasteName]; } diff --git a/packages/metro-file-map/src/worker.js b/packages/metro-file-map/src/worker.js index 5c135dd397..084cd3ea9d 100644 --- a/packages/metro-file-map/src/worker.js +++ b/packages/metro-file-map/src/worker.js @@ -88,7 +88,8 @@ class Worker { ) { // Process a random file that is returned as a MODULE. if (data.hasteImplModulePath != null) { - id = getHasteImpl(data.hasteImplModulePath).getHasteName(filePath); + id = + getHasteImpl(data.hasteImplModulePath).getHasteName(filePath) || null; } if (computeDependencies) { From 3c2eefd6d564bdb33bdf825e794fa20cd85e3425 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Tue, 30 Sep 2025 07:49:09 -0700 Subject: [PATCH 3/6] Abstract dataIdx Differential Revision: D83364233 --- packages/metro-file-map/src/flow-types.js | 53 +++++----- packages/metro-file-map/src/index.js | 96 +++++++++++++++---- packages/metro-file-map/src/lib/TreeFS.js | 2 +- .../metro-file-map/src/plugins/HastePlugin.js | 62 ++++++------ .../metro-file-map/src/plugins/MockPlugin.js | 4 +- .../haste/__tests__/HastePlugin-test.js | 50 ++++------ .../mocks/__tests__/MockPlugin-test.js | 2 +- 7 files changed, 159 insertions(+), 110 deletions(-) diff --git a/packages/metro-file-map/src/flow-types.js b/packages/metro-file-map/src/flow-types.js index 1085fd7a77..28861f2ee0 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -165,45 +165,46 @@ 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, - }>; - getFileMetadata(mixedPath: string): ?FileMetadata; -} - -export type FileMapPluginInitOptions = $ReadOnly<{ - files: FileSystemState, + getFilePluginData(mixedPath: string): ?PerFileData, + }>, pluginState: ?SerializableState, }>; type V8Serializable = interface {}; -export interface FileMapPlugin { +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; } diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index ddcc92d126..cd0047b602 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'; @@ -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); @@ -299,7 +306,12 @@ export default class FileMap extends EventEmitter { failValidationOnConflicts: throwOnModuleCollision, }); - const plugins: Array> = [this.#hastePlugin]; + const plugins: Array = [ + { + plugin: this.#hastePlugin, + dataIdx: H.ID, + }, + ]; if (options.mocksPattern != null && options.mocksPattern !== '') { this.#mockPlugin = new MockPlugin({ @@ -308,7 +320,10 @@ export default class FileMap extends EventEmitter { rootDir: options.rootDir, throwOnModuleCollision, }); - plugins.push(this.#mockPlugin); + plugins.push({ + plugin: this.#mockPlugin, + dataIdx: null, + }); } this.#plugins = plugins; @@ -428,9 +443,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 +475,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 +599,7 @@ export default class FileMap extends EventEmitter { async _applyFileDelta( fileSystem: MutableFileSystem, - plugins: $ReadOnlyArray>, + plugins: $ReadOnlyArray, delta: $ReadOnly<{ changedFiles: FileData, removedFiles: $ReadOnlySet, @@ -695,13 +729,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 +752,7 @@ export default class FileMap extends EventEmitter { async _takeSnapshotAndPersist( fileSystem: FileSystem, clocks: WatchmanClocks, - plugins: $ReadOnlyArray>, + plugins: $ReadOnlyArray, changed: FileData, removed: Set, ) { @@ -723,7 +762,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 +797,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) { @@ -932,8 +971,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 +1001,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 +1120,13 @@ 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); + } + })(); diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index def4a60cd4..8c6507d715 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -1005,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/plugins/HastePlugin.js b/packages/metro-file-map/src/plugins/HastePlugin.js index a683699ec2..b5ea9b4509 100644 --- a/packages/metro-file-map/src/plugins/HastePlugin.js +++ b/packages/metro-file-map/src/plugins/HastePlugin.js @@ -16,7 +16,6 @@ import type { FileMapDelta, FileMapPlugin, FileMapPluginInitOptions, - FileMetadata, HasteConflict, HasteMap, HasteMapItem, @@ -50,7 +49,9 @@ type HasteMapOptions = $ReadOnly<{ failValidationOnConflicts: boolean, }>; -export default class HastePlugin implements HasteMap, FileMapPlugin { +export default class HastePlugin + implements HasteMap, FileMapPlugin +{ +name = 'haste'; +#rootDir: Path; @@ -75,33 +76,34 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { this.#failValidationOnConflicts = options.failValidationOnConflicts; } - 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] != null) { - 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.#getModuleNameByPath = mixedPath => { - const metadata = files.getFileMetadata(mixedPath); - if (metadata == null || metadata[H.ID] == null) { - return null; + 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); } - return metadata[H.ID]; - }; + } + this.#getModuleNameByPath = files.getFilePluginData; this.#perfLogger?.point('constructHasteMap_end'); this.#perfLogger?.annotate({int: {hasteFiles}}); } @@ -140,7 +142,7 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { 'HastePlugin has not been initialized before getModuleNameByPath', ); } - return this.#getModuleNameByPath(mixedPath); + return this.#getModuleNameByPath(mixedPath) ?? null; } getPackage( @@ -224,18 +226,17 @@ 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]; + onNewOrModifiedFile(relativeFilePath: string, id: ?string) { if (id == null) { // Not a Haste module or package return; @@ -312,8 +313,7 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { hasteMapItem[platform] = module; } - onRemovedFile(relativeFilePath: string, fileMetadata: FileMetadata) { - const moduleName = fileMetadata[H.ID]; + onRemovedFile(relativeFilePath: string, moduleName: ?string) { if (moduleName == null) { // Not a Haste module or package return; diff --git a/packages/metro-file-map/src/plugins/MockPlugin.js b/packages/metro-file-map/src/plugins/MockPlugin.js index 43ad69f589..e7668007fa 100644 --- a/packages/metro-file-map/src/plugins/MockPlugin.js +++ b/packages/metro-file-map/src/plugins/MockPlugin.js @@ -80,7 +80,7 @@ export default class MockPlugin implements FileMapPlugin, IMockMap { includeNodeModules: false, includeSymlinks: false, }), - ].map(({canonicalPath, metadata}) => [canonicalPath, metadata]), + ].map(({canonicalPath}) => [canonicalPath, null]), removed: [], }); } @@ -97,7 +97,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); 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 4792b7edbb..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,10 +72,10 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { { canonicalPath: p('project/Foo.js'), baseName: 'Foo.js', - metadata: hasteMetadata('NameForFoo'), + data: 'NameForFoo', }, ]), - getFileMetadata: jest.fn(), + getFilePluginData: jest.fn(), }, pluginState: null, }; @@ -96,7 +95,7 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { await hasteMap.initialize({ files: { metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES), - getFileMetadata: jest.fn(), + getFilePluginData: jest.fn(), }, pluginState: null, }); @@ -104,7 +103,7 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { 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(); }); @@ -113,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'), ); @@ -131,7 +127,7 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { await hasteMap.initialize({ files: { metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES), - getFileMetadata: jest.fn(), + getFilePluginData: jest.fn(), }, pluginState: null, }); @@ -139,7 +135,7 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { 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(); }); @@ -150,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( @@ -171,27 +167,27 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { describe('getModuleNameByPath', () => { let hasteMap: HasteMapType; - let getFileMetadata; + let getFilePluginData; beforeEach(async () => { hasteMap = new HasteMap(opts); - getFileMetadata = jest.fn().mockReturnValue(null); + getFilePluginData = jest.fn().mockReturnValue(null); await hasteMap.initialize({ files: { metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES), - getFileMetadata, + getFilePluginData, }, pluginState: null, }); }); test('returns the correct module name', () => { - getFileMetadata.mockImplementation( + getFilePluginData.mockImplementation( filePath => ({ - [p('/root/Foo.js')]: hasteMetadata('Foo'), - [p('/root/not-haste.js')]: hasteMetadata(null), + [p('/root/Foo.js')]: 'Foo' as ?string, + [p('/root/not-haste.js')]: null, })[filePath] ?? null, ); expect(hasteMap.getModuleNameByPath(p('/root/Foo.js'))).toBe('Foo'); @@ -200,7 +196,3 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { }); }); }); - -function hasteMetadata(hasteName: ?string): FileMetadata { - return [0, 0, 0, '', '', 0, 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 85d0d8ba04..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,7 +100,7 @@ Duplicate manual mock found for \`foo\`: metadataIterator: () => { throw new Error('should not be used'); }, - getFileMetadata: () => { + getFilePluginData: () => { throw new Error('should not be used'); }, }, From 58b2d1f41ab08fe7ba675afa0f4fe05f041ad8a4 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Tue, 30 Sep 2025 08:51:14 -0700 Subject: [PATCH 4/6] plugins Differential Revision: D76429293 --- packages/metro-file-map/src/constants.js | 2 +- packages/metro-file-map/src/flow-types.js | 5 ++-- packages/metro-file-map/src/index.js | 24 ++++++++++--------- .../metro-file-map/src/lib/FileProcessor.js | 3 ++- 4 files changed, 19 insertions(+), 15 deletions(-) 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/flow-types.js b/packages/metro-file-map/src/flow-types.js index 28861f2ee0..88ec9ef913 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -215,7 +215,7 @@ export type HType = { DEPENDENCIES: 3, SHA1: 4, SYMLINK: 5, - ID: 6, + PLUGINDATA: number, PATH: 0, TYPE: 1, MODULE: 0, @@ -238,7 +238,8 @@ export type FileMetadata = [ /* dependencies */ string, /* sha1 */ ?string, /* symlink */ 0 | 1 | string, // string specifies target, if known - /* id */ ?string, // Haste module/package name, or null for non-Haste + /* plugindata */ + ... ]; export type FileStats = $ReadOnly<{ diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index cd0047b602..90bb372ad5 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -306,12 +306,7 @@ export default class FileMap extends EventEmitter { failValidationOnConflicts: throwOnModuleCollision, }); - const plugins: Array = [ - { - plugin: this.#hastePlugin, - dataIdx: H.ID, - }, - ]; + const plugins: Array = [this.#hastePlugin]; if (options.mocksPattern != null && options.mocksPattern !== '') { this.#mockPlugin = new MockPlugin({ @@ -320,13 +315,14 @@ export default class FileMap extends EventEmitter { rootDir: options.rootDir, throwOnModuleCollision, }); - plugins.push({ - plugin: this.#mockPlugin, - dataIdx: null, - }); + 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: @@ -1130,3 +1126,9 @@ const mapIterator: (Iterator, (T) => S) => Iterable = (it, fn) => 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..687003d6c4 100644 --- a/packages/metro-file-map/src/lib/FileProcessor.js +++ b/packages/metro-file-map/src/lib/FileProcessor.js @@ -239,7 +239,8 @@ function processWorkerReply( const metadataId = metadata.id; if (metadataId != null) { - fileMetadata[H.ID] = metadataId; + // $FlowFixMe[incompatible-type] - treat inexact tuple as array to set tail entries + (fileMetadata as Array)[H.PLUGINDATA] = metadataId; } fileMetadata[H.DEPENDENCIES] = metadata.dependencies From 4aed811b59dc795c5a17fdc417796cbe651a80fd Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Wed, 1 Oct 2025 07:51:38 -0700 Subject: [PATCH 5/6] file map: Split Haste worker logic out as a generic plugin worker Differential Revision: D83572758 --- .../src/__tests__/worker-test.js | 74 ++++++++++---- .../cache/__tests__/DiskCacheManager-test.js | 3 + packages/metro-file-map/src/flow-types.js | 37 ++++++- packages/metro-file-map/src/index.js | 1 + .../metro-file-map/src/lib/FileProcessor.js | 17 ++-- .../src/lib/__tests__/FileProcessor-test.js | 2 + .../metro-file-map/src/plugins/HastePlugin.js | 10 +- .../metro-file-map/src/plugins/MockPlugin.js | 5 + .../src/plugins/haste/worker.js | 70 +++++++++++++ packages/metro-file-map/src/worker.js | 97 +++++++------------ 10 files changed, 224 insertions(+), 92 deletions(-) create mode 100644 packages/metro-file-map/src/plugins/haste/worker.js diff --git a/packages/metro-file-map/src/__tests__/worker-test.js b/packages/metro-file-map/src/__tests__/worker-test.js index 899dc654af..d0ab67cb51 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'; @@ -72,6 +73,27 @@ const defaults: WorkerMessage = { maybeReturnContent: false, }; +const defaultHasteConfig = { + enableHastePackages: true, + failValidationOnConflicts: false, + 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; @@ -98,6 +120,7 @@ describe('worker', () => { }), ).toEqual({ dependencies: ['Banana', 'Strawberry'], + pluginData: [], }); expect( @@ -108,12 +131,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,12 +145,13 @@ 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'), @@ -134,11 +159,11 @@ describe('worker', () => { }), ).toEqual({ dependencies: ['Banana', 'Strawberry'], - id: 'Pear', + pluginData: ['Pear'], }); expect( - await worker({ + await workerWithHaste({ ...defaults, computeDependencies: true, filePath: path.join('/project', 'fruits', 'Strawberry.js'), @@ -146,14 +171,13 @@ describe('worker', () => { }), ).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({ + await workerWithHaste({ ...defaults, computeDependencies: true, enableHastePackages: true, @@ -161,14 +185,13 @@ describe('worker', () => { }), ).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({ + await workerWithHaste({ ...defaults, computeDependencies: true, enableHastePackages: false, @@ -176,7 +199,7 @@ describe('worker', () => { }), ).toEqual({ dependencies: undefined, - id: undefined, + pluginData: [null], }); }); @@ -203,7 +226,10 @@ describe('worker', () => { computeSha1: true, filePath: path.join('/project', 'fruits', 'apple.png'), }), - ).toEqual({sha1: '4caece539b039b16e16206ea2478f8c5ffb2ca05'}); + ).toEqual({ + pluginData: [], + sha1: '4caece539b039b16e16206ea2478f8c5ffb2ca05', + }); expect( await worker({ @@ -211,7 +237,7 @@ describe('worker', () => { computeSha1: false, filePath: path.join('/project', 'fruits', 'Banana.js'), }), - ).toEqual({sha1: undefined}); + ).toEqual({pluginData: [], sha1: undefined}); expect( await worker({ @@ -219,7 +245,10 @@ describe('worker', () => { computeSha1: true, filePath: path.join('/project', 'fruits', 'Banana.js'), }), - ).toEqual({sha1: '7772b628e422e8cf59c526be4bb9f44c0898e3d1'}); + ).toEqual({ + pluginData: [], + sha1: '7772b628e422e8cf59c526be4bb9f44c0898e3d1', + }); expect( await worker({ @@ -227,7 +256,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,7 +268,7 @@ 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'), @@ -244,7 +276,7 @@ describe('worker', () => { }), ).toEqual({ dependencies: undefined, - id: 'Pear', + pluginData: ['Pear'], sha1: undefined, }); @@ -255,21 +287,23 @@ 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'), + hasteImplModulePath: require.resolve('./haste_impl.js'), maybeReturnContent: true, }), ).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'), @@ -279,7 +313,7 @@ describe('worker', () => { ).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/flow-types.js b/packages/metro-file-map/src/flow-types.js index 88ec9ef913..e883612991 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -190,7 +190,20 @@ export type FileMapPluginInitOptions< pluginState: ?SerializableState, }>; -type V8Serializable = interface {}; +export type FileMapPluginWorker = $ReadOnly<{ + workerModulePath: string, + workerSetupArgs: JsonData, +}>; + +export type V8Serializable = + | string + | number + | boolean + | null + | $ReadOnlyArray + | $ReadOnlySet + | $ReadOnlyMap + | {[key: string]: V8Serializable}; export interface FileMapPlugin< SerializableState = V8Serializable, @@ -206,6 +219,14 @@ export interface FileMapPlugin< 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 = { @@ -326,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 @@ -472,9 +501,11 @@ export type WorkerMessage = $ReadOnly<{ 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 90bb372ad5..16cbd4beed 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -370,6 +370,7 @@ export default class FileMap extends EventEmitter { maxFilesPerWorker: options.maxFilesPerWorker, maxWorkers: options.maxWorkers, perfLogger: this._startupPerfLogger, + pluginWorkers: plugins.map(plugin => plugin.getWorker()).filter(Boolean), }); this._buildPromise = null; diff --git a/packages/metro-file-map/src/lib/FileProcessor.js b/packages/metro-file-map/src/lib/FileProcessor.js index 687003d6c4..7b45b6d944 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, @@ -73,6 +74,7 @@ export class FileProcessor { hasteImplModulePath: ?string, maxFilesPerWorker?: ?number, maxWorkers: number, + pluginWorkers: ?$ReadOnlyArray, perfLogger: ?PerfLogger, }>, ) { @@ -82,7 +84,9 @@ export class FileProcessor { 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; } @@ -235,12 +239,13 @@ function processWorkerReply( fileMetadata: FileMetadata, ) { fileMetadata[H.VISITED] = 1; - - const metadataId = metadata.id; - - if (metadataId != null) { + if (metadata.pluginData) { // $FlowFixMe[incompatible-type] - treat inexact tuple as array to set tail entries - (fileMetadata as Array)[H.PLUGINDATA] = metadataId; + (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/__tests__/FileProcessor-test.js b/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js index b37aa8aa80..f14de0a137 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, @@ -30,6 +31,7 @@ const defaultOptions = { hasteImplModulePath: null, maxWorkers: 5, perfLogger: null, + pluginWorkers: [] as $ReadOnlyArray, }; describe('processBatch', () => { diff --git a/packages/metro-file-map/src/plugins/HastePlugin.js b/packages/metro-file-map/src/plugins/HastePlugin.js index b5ea9b4509..186815414a 100644 --- a/packages/metro-file-map/src/plugins/HastePlugin.js +++ b/packages/metro-file-map/src/plugins/HastePlugin.js @@ -16,6 +16,7 @@ import type { FileMapDelta, FileMapPlugin, FileMapPluginInitOptions, + FileMapPluginWorker, HasteConflict, HasteMap, HasteMapItem, @@ -43,7 +44,7 @@ const YIELD_EVERY_NUM_HASTE_FILES = 10000; type HasteMapOptions = $ReadOnly<{ console?: ?Console, enableHastePackages: boolean, - perfLogger: ?PerfLogger, + perfLogger?: ?PerfLogger, platforms: $ReadOnlySet, rootDir: Path, failValidationOnConflicts: boolean, @@ -476,4 +477,11 @@ export default class HastePlugin [...this.#platforms].sort(), ]); } + + getWorker(): FileMapPluginWorker { + return { + workerModulePath: require.resolve('./haste/worker.js'), + workerSetupArgs: {}, + }; + } } diff --git a/packages/metro-file-map/src/plugins/MockPlugin.js b/packages/metro-file-map/src/plugins/MockPlugin.js index e7668007fa..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, @@ -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/worker.js b/packages/metro-file-map/src/plugins/haste/worker.js new file mode 100644 index 0000000000..dc346f92a2 --- /dev/null +++ b/packages/metro-file-map/src/plugins/haste/worker.js @@ -0,0 +1,70 @@ +/** + * 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 {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; + + #getHasteImpl( + requestedModulePath /*: string */, + ) /*: $ReadOnly<{getHasteName: string => ?string}> */ { + if (this.#hasteImpl) { + if (requestedModulePath !== this.#hasteImplModulePath) { + throw new Error('metro-file-map: hasteImplModulePath changed'); + } + return this.#hasteImpl; + } + this.#hasteImplModulePath = requestedModulePath; + // $FlowFixMe[unsupported-syntax] - dynamic require + this.#hasteImpl = require(requestedModulePath); + return this.#hasteImpl; + } + + processFile( + data /*: WorkerMessage */, + utils /*: $ReadOnly<{getContent: () => Buffer }> */, + ) /*: V8Serializable */ { + let hasteName /*: string | null */ = null; + const {filePath, enableHastePackages, hasteImplModulePath} = data; + 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(utils.getContent().toString()); + if (fileData.name) { + hasteName = fileData.name; + } + } catch (err) { + throw new Error(`Cannot parse ${filePath} as JSON: ${err.message}`); + } + } else if ( + hasteImplModulePath != null && + !excludedExtensions.has(filePath.substr(filePath.lastIndexOf('.'))) + ) { + // Process a random file that is returned as a MODULE. + hasteName = + this.#getHasteImpl(hasteImplModulePath).getHasteName(filePath) || null; + } + return hasteName; + } +}; diff --git a/packages/metro-file-map/src/worker.js b/packages/metro-file-map/src/worker.js index 084cd3ea9d..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,44 +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) || null; - } - - 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. @@ -117,8 +90,8 @@ class Worker { } return content && data.maybeReturnContent - ? {content, dependencies, id, sha1} - : {dependencies, id, sha1}; + ? {content, dependencies, pluginData, sha1} + : {dependencies, pluginData, sha1}; } } From 46abc52922a9d55f2d53abb5c8a37d0221031f8a Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Thu, 2 Oct 2025 07:05:53 -0700 Subject: [PATCH 6/6] metro-file-map: Move hasteImplModulePath and enableHastePackages from individual file requests to worker setup Differential Revision: D83656514 --- .../src/__tests__/index-test.js | 15 +++----- .../src/__tests__/worker-test.js | 38 +++++++++---------- packages/metro-file-map/src/flow-types.js | 3 +- packages/metro-file-map/src/index.js | 3 +- .../metro-file-map/src/lib/FileProcessor.js | 12 ++---- .../src/lib/__tests__/FileProcessor-test.js | 2 - .../metro-file-map/src/plugins/HastePlugin.js | 28 ++++++++++++-- .../metro-file-map/src/plugins/haste/types.js | 14 +++++++ .../src/plugins/haste/worker.js | 27 +++++-------- 9 files changed, 78 insertions(+), 64 deletions(-) create mode 100644 packages/metro-file-map/src/plugins/haste/types.js diff --git a/packages/metro-file-map/src/__tests__/index-test.js b/packages/metro-file-map/src/__tests__/index-test.js index 9fae00d317..1df6420416 100644 --- a/packages/metro-file-map/src/__tests__/index-test.js +++ b/packages/metro-file-map/src/__tests__/index-test.js @@ -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, }, ], diff --git a/packages/metro-file-map/src/__tests__/worker-test.js b/packages/metro-file-map/src/__tests__/worker-test.js index d0ab67cb51..45375ee386 100644 --- a/packages/metro-file-map/src/__tests__/worker-test.js +++ b/packages/metro-file-map/src/__tests__/worker-test.js @@ -66,9 +66,9 @@ jest.mock('fs', () => { }); const defaults: WorkerMessage = { + isNodeModules: false, computeDependencies: false, computeSha1: false, - enableHastePackages: false, filePath: path.join('/project', 'notexist.js'), maybeReturnContent: false, }; @@ -76,6 +76,7 @@ const defaults: WorkerMessage = { const defaultHasteConfig = { enableHastePackages: true, failValidationOnConflicts: false, + hasteImplModulePath: require.resolve('./haste_impl.js'), platforms: new Set(['ios', 'android']), rootDir: path.normalize('/project'), }; @@ -106,7 +107,7 @@ describe('worker', () => { const defaults: WorkerMessage = { computeDependencies: false, computeSha1: false, - enableHastePackages: false, + isNodeModules: false, filePath: path.join('/project', 'notexist.js'), maybeReturnContent: false, }; @@ -155,7 +156,6 @@ describe('worker', () => { ...defaults, computeDependencies: true, filePath: path.join('/project', 'fruits', 'Pear.js'), - hasteImplModulePath: require.resolve('./haste_impl.js'), }), ).toEqual({ dependencies: ['Banana', 'Strawberry'], @@ -167,7 +167,6 @@ describe('worker', () => { ...defaults, computeDependencies: true, filePath: path.join('/project', 'fruits', 'Strawberry.js'), - hasteImplModulePath: require.resolve('./haste_impl.js'), }), ).toEqual({ dependencies: [], @@ -177,12 +176,14 @@ describe('worker', () => { test('parses package.json files as haste packages when enableHastePackages=true', async () => { expect( - await workerWithHaste({ - ...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, pluginData: ['haste-package'], @@ -191,12 +192,14 @@ describe('worker', () => { test('does not parse package.json files as haste packages when enableHastePackages=false', async () => { expect( - await workerWithHaste({ - ...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, pluginData: [null], @@ -272,7 +275,6 @@ describe('worker', () => { ...defaults, computeDependencies: false, filePath: path.join('/project', 'fruits', 'Pear.js'), - hasteImplModulePath: path.resolve(__dirname, 'haste_impl.js'), }), ).toEqual({ dependencies: undefined, @@ -291,7 +293,6 @@ describe('worker', () => { ...defaults, computeSha1: true, filePath: path.join('/project', 'fruits', 'Pear.js'), - hasteImplModulePath: require.resolve('./haste_impl.js'), maybeReturnContent: true, }), ).toEqual({ @@ -307,7 +308,6 @@ describe('worker', () => { ...defaults, computeSha1: false, filePath: path.join('/project', 'fruits', 'Pear.js'), - hasteImplModulePath: path.resolve(__dirname, 'haste_impl.js'), maybeReturnContent: true, }), ).toEqual({ diff --git a/packages/metro-file-map/src/flow-types.js b/packages/metro-file-map/src/flow-types.js index e883612991..cbf2253425 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -493,9 +493,8 @@ export type WorkerMessage = $ReadOnly<{ computeDependencies: boolean, computeSha1: boolean, dependencyExtractor?: ?string, - enableHastePackages: boolean, + isNodeModules: boolean, filePath: string, - hasteImplModulePath?: ?string, maybeReturnContent: boolean, }>; diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index 16cbd4beed..556b2ac34f 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -300,6 +300,7 @@ 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, @@ -364,9 +365,7 @@ 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, diff --git a/packages/metro-file-map/src/lib/FileProcessor.js b/packages/metro-file-map/src/lib/FileProcessor.js index 7b45b6d944..9ea252a118 100644 --- a/packages/metro-file-map/src/lib/FileProcessor.js +++ b/packages/metro-file-map/src/lib/FileProcessor.js @@ -69,9 +69,7 @@ export class FileProcessor { constructor( opts: $ReadOnly<{ dependencyExtractor: ?string, - enableHastePackages: boolean, enableWorkerThreads: boolean, - hasteImplModulePath: ?string, maxFilesPerWorker?: ?number, maxWorkers: number, pluginWorkers: ?$ReadOnlyArray, @@ -79,9 +77,7 @@ export class FileProcessor { }>, ) { 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 = { @@ -157,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 @@ -172,9 +168,8 @@ export class FileProcessor { computeDependencies: false, computeSha1: true, dependencyExtractor: null, - enableHastePackages: false, + isNodeModules, filePath: absolutePath, - hasteImplModulePath: null, maybeReturnContent, }; } @@ -185,9 +180,8 @@ export class FileProcessor { computeDependencies, computeSha1, dependencyExtractor: this.#dependencyExtractor, - enableHastePackages: this.#enableHastePackages, + isNodeModules, filePath: absolutePath, - hasteImplModulePath: this.#hasteImplModulePath, maybeReturnContent, }; } 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 f14de0a137..3d632da108 100644 --- a/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js +++ b/packages/metro-file-map/src/lib/__tests__/FileProcessor-test.js @@ -26,9 +26,7 @@ const mockWorkerFn = jest.fn().mockReturnValue({}); const defaultOptions = { dependencyExtractor: null, - enableHastePackages: false, enableWorkerThreads: true, - hasteImplModulePath: null, maxWorkers: 5, perfLogger: null, pluginWorkers: [] as $ReadOnlyArray, diff --git a/packages/metro-file-map/src/plugins/HastePlugin.js b/packages/metro-file-map/src/plugins/HastePlugin.js index 186815414a..6666f3b24e 100644 --- a/packages/metro-file-map/src/plugins/HastePlugin.js +++ b/packages/metro-file-map/src/plugins/HastePlugin.js @@ -44,10 +44,11 @@ const YIELD_EVERY_NUM_HASTE_FILES = 10000; type HasteMapOptions = $ReadOnly<{ console?: ?Console, enableHastePackages: boolean, + hasteImplModulePath?: ?string, perfLogger?: ?PerfLogger, platforms: $ReadOnlySet, rootDir: Path, - failValidationOnConflicts: boolean, + failValidationOnConflicts?: boolean, }>; export default class HastePlugin @@ -61,6 +62,8 @@ export default class HastePlugin +#console: ?Console; +#enableHastePackages: boolean; + +#hasteImplCacheKey: ?string; + +#hasteImplModulePath: ?string; +#perfLogger: ?PerfLogger; +#pathUtils: RootPathUtils; +#platforms: $ReadOnlySet; @@ -70,11 +73,26 @@ export default class HastePlugin 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({ @@ -474,6 +492,7 @@ export default class HastePlugin getCacheKey(): string { return JSON.stringify([ this.#enableHastePackages, + this.#hasteImplCacheKey, [...this.#platforms].sort(), ]); } @@ -481,7 +500,10 @@ export default class HastePlugin getWorker(): FileMapPluginWorker { return { workerModulePath: require.resolve('./haste/worker.js'), - workerSetupArgs: {}, + workerSetupArgs: { + enableHastePackages: this.#enableHastePackages, + hasteImplModulePath: this.#hasteImplModulePath ?? 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 index dc346f92a2..2a4779e13b 100644 --- a/packages/metro-file-map/src/plugins/haste/worker.js +++ b/packages/metro-file-map/src/plugins/haste/worker.js @@ -16,6 +16,7 @@ const excludedExtensions = require('../../workerExclusionList'); const path = require('path'); /*:: +import type {WorkerSetupArgs} from './types'; import type {MetadataWorker, WorkerMessage, V8Serializable} from '../../flow-types'; */ @@ -26,19 +27,12 @@ module.exports = class Worker /*:: implements MetadataWorker */ { #hasteImpl /*: ?$ReadOnly<{getHasteName: string => ?string}> */; #hasteImplModulePath /*: ?string */ = null; - #getHasteImpl( - requestedModulePath /*: string */, - ) /*: $ReadOnly<{getHasteName: string => ?string}> */ { - if (this.#hasteImpl) { - if (requestedModulePath !== this.#hasteImplModulePath) { - throw new Error('metro-file-map: hasteImplModulePath changed'); - } - return this.#hasteImpl; + constructor(setupArgs /*: WorkerSetupArgs */) { + this.#enableHastePackages = setupArgs.enableHastePackages; + if (setupArgs.hasteImplModulePath) { + // $FlowFixMe[unsupported-syntax] - dynamic require + this.#hasteImpl = require(setupArgs.hasteImplModulePath); } - this.#hasteImplModulePath = requestedModulePath; - // $FlowFixMe[unsupported-syntax] - dynamic require - this.#hasteImpl = require(requestedModulePath); - return this.#hasteImpl; } processFile( @@ -46,8 +40,8 @@ module.exports = class Worker /*:: implements MetadataWorker */ { utils /*: $ReadOnly<{getContent: () => Buffer }> */, ) /*: V8Serializable */ { let hasteName /*: string | null */ = null; - const {filePath, enableHastePackages, hasteImplModulePath} = data; - if (enableHastePackages && filePath.endsWith(PACKAGE_JSON)) { + 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()); @@ -58,12 +52,11 @@ module.exports = class Worker /*:: implements MetadataWorker */ { throw new Error(`Cannot parse ${filePath} as JSON: ${err.message}`); } } else if ( - hasteImplModulePath != null && + this.#hasteImpl != null && !excludedExtensions.has(filePath.substr(filePath.lastIndexOf('.'))) ) { // Process a random file that is returned as a MODULE. - hasteName = - this.#getHasteImpl(hasteImplModulePath).getHasteName(filePath) || null; + hasteName = this.#hasteImpl?.getHasteName(filePath) || null; } return hasteName; }