From 32a0ffbc8eedfa2cecb3803f8ec7b6b21c86d5d0 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Thu, 16 Oct 2025 10:20:27 -0700 Subject: [PATCH 01/10] Move getModuleName from FileSystem to HastePlugin Differential Revision: D76735892 --- .../src/__tests__/index-test.js | 41 ++++++++------- packages/metro-file-map/src/flow-types.js | 25 ++++++++-- packages/metro-file-map/src/index.js | 14 +++++- packages/metro-file-map/src/lib/TreeFS.js | 19 +++---- .../src/lib/__tests__/TreeFS-test.js | 2 + .../metro-file-map/src/plugins/HastePlugin.js | 16 ++++++ .../haste/__tests__/HastePlugin-test.js | 50 ++++++++++++++++++- .../mocks/__tests__/MockPlugin-test.js | 3 ++ .../metro/src/node-haste/DependencyGraph.js | 2 +- 9 files changed, 134 insertions(+), 38 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..1e1c6092d0 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -181,6 +181,12 @@ interface FileSystemState { canonicalPath: string, metadata: FileMetadata, }>; + lookup( + mixedPath: string, + ): + | {exists: false} + | {exists: true, type: 'f', metadata: FileMetadata} + | {exists: true, type: 'd'}; } export type FileMapPluginInitOptions = $ReadOnly<{ @@ -253,7 +259,6 @@ export interface FileSystem { changedFiles: FileData, removedFiles: Set, }; - getModuleName(file: Path): ?string; getSerializableSnapshot(): CacheData['fileSystemData']; getSha1(file: Path): ?string; getOrComputeSha1(file: Path): Promise; @@ -340,11 +345,23 @@ export type LookupResult = exists: true, // The real, normal, absolute paths of any symlinks traversed. links: $ReadOnlySet, - // The real, normal, absolute path of the file or directory. + // The real, normal, absolute path of the directory. realPath: string, // Currently lookup always follows symlinks, so can only return // directories or regular files, but this may be extended. - type: 'd' | 'f', + type: 'd', + } + | { + exists: true, + // The real, normal, absolute paths of any symlinks traversed. + links: $ReadOnlySet, + // The real, normal, absolute path of the file. + realPath: string, + // Currently lookup always follows symlinks, so can only return + // directories or regular files, but this may be extended. + type: 'f', + // The file's metadata tuple. Must only be mutated via FileProcessor. + metadata: FileMetadata, }; export interface MockMap { @@ -366,6 +383,8 @@ export interface HasteMap { type?: ?HTypeValue, ): ?Path; + getModuleNameByPath(file: Path): ?string; + getPackage( name: string, platform: ?string, diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index 7430775a89..9b719cfb09 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -430,7 +430,19 @@ export default class FileMap extends EventEmitter { Promise.all( plugins.map(plugin => plugin.initialize({ - files: fileSystem, + files: { + lookup: mixedPath => { + const result = fileSystem.lookup(mixedPath); + if (!result.exists) { + return {exists: false}; + } + if (result.type === 'd') { + return {exists: true, type: 'd'}; + } + return {exists: true, type: 'f', metadata: result.metadata}; + }, + metadataIterator: opts => fileSystem.metadataIterator(opts), + }, pluginState: initialData?.plugins.get(plugin.name), }), ), diff --git a/packages/metro-file-map/src/lib/TreeFS.js b/packages/metro-file-map/src/lib/TreeFS.js index 76a653e6b7..1a7af3e630 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -134,11 +134,6 @@ export default class TreeFS implements MutableFileSystem { return tfs; } - getModuleName(mixedPath: Path): ?string { - const fileMetadata = this._getFileData(mixedPath); - return (fileMetadata && fileMetadata[H.ID]) ?? null; - } - getSize(mixedPath: Path): ?number { const fileMetadata = this._getFileData(mixedPath); return (fileMetadata && fileMetadata[H.SIZE]) ?? null; @@ -267,19 +262,17 @@ export default class TreeFS implements MutableFileSystem { }; } const {canonicalPath, node} = result; - const type = isDirectory(node) ? 'd' : isRegularFile(node) ? 'f' : 'l'; + const realPath = this.#pathUtils.normalToAbsolute(canonicalPath); + if (isDirectory(node)) { + return {exists: true, links, realPath, type: 'd'}; + } invariant( - type !== 'l', + isRegularFile(node), 'lookup follows symlinks, so should never return one (%s -> %s)', mixedPath, canonicalPath, ); - return { - exists: true, - links, - realPath: this.#pathUtils.normalToAbsolute(canonicalPath), - type, - }; + return {exists: true, links, realPath, type: 'f', metadata: node}; } getAllFiles(): Array { 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..dce8a2aab9 100644 --- a/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js +++ b/packages/metro-file-map/src/lib/__tests__/TreeFS-test.js @@ -135,6 +135,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { links: new Set(expectedSymlinks), realPath: expectedRealPath, type: 'f', + metadata: expect.any(Array), }), ); @@ -242,6 +243,7 @@ describe.each([['win32'], ['posix']])('TreeFS on %s', platform => { realPath: expectedRealPath, links: new Set(expectedSymlinks), type: 'f', + metadata: expect.any(Array), }); }, ); diff --git a/packages/metro-file-map/src/plugins/HastePlugin.js b/packages/metro-file-map/src/plugins/HastePlugin.js index e63f4cf27c..ea711c0989 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,12 @@ export default class HastePlugin implements HasteMap, FileMapPlugin { } } } + this.#getModuleNameByPath = mixedPath => { + const result = files.lookup(mixedPath); + return result.exists && result.type === 'f' && result.metadata[H.ID] != '' + ? result.metadata[H.ID] + : null; + }; this.#perfLogger?.point('constructHasteMap_end'); this.#perfLogger?.annotate({int: {hasteFiles}}); } @@ -126,6 +133,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..d32a5ff10a 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'), }, ]), + lookup: 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), + lookup: 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), + lookup: jest.fn(), + }, pluginState: null, }); }); @@ -161,6 +168,45 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { ); }); }); + + describe('getModuleNameByPath', () => { + let hasteMap: HasteMapType; + let lookup; + + beforeEach(async () => { + hasteMap = new HasteMap(opts); + lookup = jest.fn().mockReturnValue(null); + + await hasteMap.initialize({ + files: { + metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES), + lookup, + }, + pluginState: null, + }); + }); + + test('returns the correct module name', () => { + lookup.mockImplementation( + filePath => + ({ + [p('/root/Foo.js')]: { + exists: true, + type: 'f', + metadata: hasteMetadata('Foo'), + }, + [p('/root/not-haste.js')]: { + exists: true, + type: 'f', + metadata: hasteMetadata(''), + }, + })[filePath] ?? {exists: false}, + ); + 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..d2ab239e81 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'); }, + lookup: () => { + 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 1dc470932a..5dfdd61052 100644 --- a/packages/metro/src/node-haste/DependencyGraph.js +++ b/packages/metro/src/node-haste/DependencyGraph.js @@ -383,7 +383,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 98b81cff39e333a82e313c00fb053516ea892f18 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Thu, 16 Oct 2025 10:20:27 -0700 Subject: [PATCH 02/10] 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 1e1c6092d0..fb89184878 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -242,7 +242,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 9b719cfb09..0355301ac8 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; @@ -925,7 +925,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 dce8a2aab9..38e22887d6 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']], ]), @@ -188,8 +188,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'); @@ -216,7 +216,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([ @@ -271,23 +271,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'), @@ -312,24 +312,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( [ @@ -349,7 +349,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: () => { @@ -713,8 +713,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'), @@ -735,10 +743,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]], ]), ); @@ -832,7 +840,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', @@ -870,7 +878,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], }, ]), ); @@ -884,9 +892,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, }); @@ -960,7 +968,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 ea711c0989..7d3b8ea936 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 result = files.lookup(mixedPath); - return result.exists && result.type === 'f' && result.metadata[H.ID] != '' + return result.exists && result.type === 'f' ? result.metadata[H.ID] : null; }; @@ -234,8 +234,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; } @@ -311,8 +312,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 d32a5ff10a..4a8ad207ab 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 @@ -198,7 +198,7 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { [p('/root/not-haste.js')]: { exists: true, type: 'f', - metadata: hasteMetadata(''), + metadata: hasteMetadata(null), }, })[filePath] ?? {exists: false}, ); @@ -209,6 +209,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 bd22e1f7d0d9e48a1af2a5a67e530fccbc8590a8 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Thu, 16 Oct 2025 10:20:27 -0700 Subject: [PATCH 03/10] Abstract dataIdx Differential Revision: D83364233 --- packages/metro-file-map/src/flow-types.js | 63 ++++++------- packages/metro-file-map/src/index.js | 92 ++++++++++++++----- packages/metro-file-map/src/lib/TreeFS.js | 2 +- .../metro-file-map/src/plugins/HastePlugin.js | 60 ++++++------ .../metro-file-map/src/plugins/MockPlugin.js | 6 +- .../haste/__tests__/HastePlugin-test.js | 46 ++++------ .../mocks/__tests__/MockPlugin-test.js | 2 +- 7 files changed, 161 insertions(+), 110 deletions(-) diff --git a/packages/metro-file-map/src/flow-types.js b/packages/metro-file-map/src/flow-types.js index fb89184878..5b52d3a549 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -165,50 +165,51 @@ 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 = void, +> = $ReadOnly<{ + files: $ReadOnly<{ + fileIterator( + opts: $ReadOnly<{ + includeNodeModules: boolean, + includeSymlinks: boolean, + }>, + ): Iterable<{ + baseName: string, + canonicalPath: string, + pluginData: ?PerFileData, }>, - ): Iterable<{ - baseName: string, - canonicalPath: string, - metadata: FileMetadata, - }>; - lookup( - mixedPath: string, - ): - | {exists: false} - | {exists: true, type: 'f', metadata: FileMetadata} - | {exists: true, type: 'd'}; -} - -export type FileMapPluginInitOptions = $ReadOnly<{ - files: FileSystemState, + lookup( + mixedPath: string, + ): + | {exists: false} + | {exists: true, type: 'f', pluginData: PerFileData} + | {exists: true, type: 'd'}, + }>, pluginState: ?SerializableState, }>; type V8Serializable = interface {}; -export interface FileMapPlugin { +export interface FileMapPlugin< + SerializableState = V8Serializable, + PerFileData = 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 0355301ac8..ee0ba197da 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,7 +443,7 @@ export default class FileMap extends EventEmitter { clocks: initialData?.clocks ?? new Map(), }), Promise.all( - plugins.map(plugin => + plugins.map(({plugin, dataIdx}) => plugin.initialize({ files: { lookup: mixedPath => { @@ -439,9 +454,22 @@ export default class FileMap extends EventEmitter { if (result.type === 'd') { return {exists: true, type: 'd'}; } - return {exists: true, type: 'f', metadata: result.metadata}; + return { + exists: true, + type: 'f', + pluginData: + dataIdx != null ? result.metadata[dataIdx] : null, + }; }, - metadataIterator: opts => fileSystem.metadataIterator(opts), + fileIterator: opts => + mapIterator( + fileSystem.metadataIterator(opts), + ({baseName, canonicalPath, metadata}) => ({ + baseName, + canonicalPath, + pluginData: dataIdx != null ? metadata[dataIdx] : null, + }), + ), }, pluginState: initialData?.plugins.get(plugin.name), }), @@ -453,7 +481,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( @@ -577,7 +605,7 @@ export default class FileMap extends EventEmitter { async _applyFileDelta( fileSystem: MutableFileSystem, - plugins: $ReadOnlyArray>, + plugins: $ReadOnlyArray, delta: $ReadOnly<{ changedFiles: FileData, removedFiles: $ReadOnlySet, @@ -707,13 +735,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'); @@ -725,7 +758,7 @@ export default class FileMap extends EventEmitter { async _takeSnapshotAndPersist( fileSystem: FileSystem, clocks: WatchmanClocks, - plugins: $ReadOnlyArray>, + plugins: $ReadOnlyArray, changed: FileData, removed: Set, ) { @@ -735,7 +768,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(), ]), @@ -770,7 +803,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) { @@ -944,8 +977,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) { @@ -969,8 +1007,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({ @@ -1086,3 +1126,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 1a7af3e630..e92e0862c9 100644 --- a/packages/metro-file-map/src/lib/TreeFS.js +++ b/packages/metro-file-map/src/lib/TreeFS.js @@ -999,7 +999,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 7d3b8ea936..2da0d33cc0 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,30 +76,39 @@ 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, + pluginData: hasteId, + } of files.fileIterator({ // 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.setModule(hasteId, [ + canonicalPath, + this.#enableHastePackages && baseName === 'package.json' + ? H.PACKAGE + : H.MODULE, + ]); + if (++hasteFiles % YIELD_EVERY_NUM_HASTE_FILES === 0) { + await new Promise(setImmediate); } } this.#getModuleNameByPath = mixedPath => { const result = files.lookup(mixedPath); - return result.exists && result.type === 'f' - ? result.metadata[H.ID] + return result.exists && + result.type === 'f' && + typeof result.pluginData === 'string' + ? result.pluginData : null; }; this.#perfLogger?.point('constructHasteMap_end'); @@ -139,7 +149,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( @@ -223,18 +233,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; @@ -311,8 +320,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..f3982a2907 100644 --- a/packages/metro-file-map/src/plugins/MockPlugin.js +++ b/packages/metro-file-map/src/plugins/MockPlugin.js @@ -76,11 +76,11 @@ export default class MockPlugin implements FileMapPlugin, IMockMap { // Otherwise, traverse all files to rebuild await this.bulkUpdate({ addedOrModified: [ - ...files.metadataIterator({ + ...files.fileIterator({ 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 4a8ad207ab..178cb77cc1 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'), + pluginData: 'NameForFoo', }, { canonicalPath: p('project/Bar.js'), baseName: 'Bar.js', - metadata: hasteMetadata('Bar'), + pluginData: 'Bar', }, { canonicalPath: p('project/Duplicate.js'), baseName: 'Duplicate.js', - metadata: hasteMetadata('Duplicate'), + pluginData: 'Duplicate', }, { canonicalPath: p('project/other/Duplicate.js'), baseName: 'Duplicate.js', - metadata: hasteMetadata('Duplicate'), + pluginData: 'Duplicate', }, ]; @@ -69,11 +68,11 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { const hasteMap = new HasteMap(opts); const initialState = { files: { - metadataIterator: jest.fn().mockReturnValue([ + fileIterator: jest.fn().mockReturnValue([ { canonicalPath: p('project/Foo.js'), baseName: 'Foo.js', - metadata: hasteMetadata('NameForFoo'), + pluginData: 'NameForFoo', }, ]), lookup: jest.fn(), @@ -81,7 +80,7 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { pluginState: null, }; await hasteMap.initialize(initialState); - expect(initialState.files.metadataIterator).toHaveBeenCalledWith({ + expect(initialState.files.fileIterator).toHaveBeenCalledWith({ includeNodeModules: false, includeSymlinks: false, }); @@ -95,7 +94,7 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { hasteMap = new HasteMap(opts); await hasteMap.initialize({ files: { - metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES), + fileIterator: jest.fn().mockReturnValue(INITIAL_FILES), lookup: 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'), ); @@ -130,7 +126,7 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { hasteMap = new HasteMap(opts); await hasteMap.initialize({ files: { - metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES), + fileIterator: jest.fn().mockReturnValue(INITIAL_FILES), lookup: 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( @@ -179,7 +175,7 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { await hasteMap.initialize({ files: { - metadataIterator: jest.fn().mockReturnValue(INITIAL_FILES), + fileIterator: jest.fn().mockReturnValue(INITIAL_FILES), lookup, }, pluginState: null, @@ -193,12 +189,12 @@ describe.each([['win32'], ['posix']])('HastePlugin on %s', platform => { [p('/root/Foo.js')]: { exists: true, type: 'f', - metadata: hasteMetadata('Foo'), + pluginData: 'Foo' as ?string, }, [p('/root/not-haste.js')]: { exists: true, type: 'f', - metadata: hasteMetadata(null), + pluginData: null as ?string, }, })[filePath] ?? {exists: false}, ); @@ -208,7 +204,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 d2ab239e81..6796c29430 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 @@ -97,7 +97,7 @@ Duplicate manual mock found for \`foo\`: const mockMap = new MockMap(opts); await mockMap.initialize({ files: { - metadataIterator: () => { + fileIterator: () => { throw new Error('should not be used'); }, lookup: () => { From bf95cf368cdbaf09f96d7f8d908bfec89bf32465 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Thu, 16 Oct 2025 10:20:27 -0700 Subject: [PATCH 04/10] 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 5b52d3a549..68ebd1d98b 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -220,7 +220,7 @@ export type HType = { DEPENDENCIES: 3, SHA1: 4, SYMLINK: 5, - ID: 6, + PLUGINDATA: number, PATH: 0, TYPE: 1, MODULE: 0, @@ -243,7 +243,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 ee0ba197da..210cfdef1f 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: @@ -1136,3 +1132,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 fecebd400d2acf965e86e18318cacc20c29d8776 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Thu, 16 Oct 2025 10:20:27 -0700 Subject: [PATCH 05/10] metro-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 | 26 +++-- .../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, 239 insertions(+), 102 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 68ebd1d98b..87fc267ad8 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -195,7 +195,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, @@ -211,6 +224,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 = { @@ -331,6 +352,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 @@ -489,9 +518,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 210cfdef1f..46040b516e 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -25,6 +25,7 @@ import type { EventsQueue, FileData, FileMapPlugin, + FileMapPluginWorker, FileMetadata, FileSystem, HasteMapData, @@ -319,10 +320,20 @@ export default class FileMap extends EventEmitter { } let dataSlot: number = H.PLUGINDATA; - this.#plugins = plugins.map(plugin => ({ - plugin, - dataIdx: isDataPlugin(plugin) ? dataSlot++ : null, - })); + + const indexedPlugins: Array = []; + const pluginWorkers: Array = []; + for (const plugin of plugins) { + const maybeWorker = plugin.getWorker(); + indexedPlugins.push({ + plugin, + dataIdx: maybeWorker != null ? dataSlot++ : null, + }); + if (maybeWorker != null) { + pluginWorkers.push(maybeWorker); + } + } + this.#plugins = indexedPlugins; const buildParameters: BuildParameters = { computeDependencies: @@ -370,6 +381,7 @@ export default class FileMap extends EventEmitter { maxFilesPerWorker: options.maxFilesPerWorker, maxWorkers: options.maxWorkers, perfLogger: this._startupPerfLogger, + pluginWorkers, }); this._buildPromise = null; @@ -1132,9 +1144,3 @@ 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 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 2da0d33cc0..f696b4e7c4 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, @@ -483,4 +484,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 f3982a2907..b57798a2fc 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 fd5027d9e6e8e399191b084dfb96b03b503c3926 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Thu, 16 Oct 2025 10:20:27 -0700 Subject: [PATCH 06/10] 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 87fc267ad8..87d0391cb4 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -510,9 +510,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 46040b516e..03ab29ac26 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -301,6 +301,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, @@ -375,9 +376,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 f696b4e7c4..1f818e0376 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({ @@ -481,6 +499,7 @@ export default class HastePlugin getCacheKey(): string { return JSON.stringify([ this.#enableHastePackages, + this.#hasteImplCacheKey, [...this.#platforms].sort(), ]); } @@ -488,7 +507,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; } From 0b9bc0ae106d8569c57601ae94d5856d38494a1d Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Thu, 16 Oct 2025 10:20:27 -0700 Subject: [PATCH 07/10] Move `dependencyExtractor` from individual file requests to worker setup Differential Revision: D84631893 --- .../src/__tests__/index-test.js | 21 ++++++++++++++----- .../src/__tests__/worker-test.js | 5 +++-- packages/metro-file-map/src/flow-types.js | 2 +- .../metro-file-map/src/lib/FileProcessor.js | 3 +-- packages/metro-file-map/src/worker.js | 8 ++++--- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/metro-file-map/src/__tests__/index-test.js b/packages/metro-file-map/src/__tests__/index-test.js index 1df6420416..d7127c8e2b 100644 --- a/packages/metro-file-map/src/__tests__/index-test.js +++ b/packages/metro-file-map/src/__tests__/index-test.js @@ -1455,6 +1455,22 @@ describe('FileMap', () => { expect.objectContaining({ // With maxFilesPerWorker = 2 and 5 files, we should have 3 workers. numWorkers: 3, + setupArgs: [ + { + dependencyExtractor, + plugins: [ + { + workerModulePath: expect.stringMatching( + /src[\\\/]plugins[\\\/]haste[\\\/]worker\.js/, + ), + workerSetupArgs: { + enableHastePackages: true, + hasteImplModulePath: null, + }, + }, + ], + }, + ], }), ); @@ -1465,7 +1481,6 @@ describe('FileMap', () => { { computeDependencies: true, computeSha1: false, - dependencyExtractor, filePath: path.join('/', 'project', 'fruits', 'Banana.js'), isNodeModules: false, maybeReturnContent: false, @@ -1475,7 +1490,6 @@ describe('FileMap', () => { { computeDependencies: true, computeSha1: false, - dependencyExtractor, filePath: path.join('/', 'project', 'fruits', 'Pear.js'), isNodeModules: false, maybeReturnContent: false, @@ -1485,7 +1499,6 @@ describe('FileMap', () => { { computeDependencies: true, computeSha1: false, - dependencyExtractor, filePath: path.join('/', 'project', 'fruits', 'Strawberry.js'), isNodeModules: false, maybeReturnContent: false, @@ -1495,7 +1508,6 @@ describe('FileMap', () => { { computeDependencies: true, computeSha1: false, - dependencyExtractor, filePath: path.join('/', 'project', 'fruits', '__mocks__', 'Pear.js'), isNodeModules: false, maybeReturnContent: false, @@ -1505,7 +1517,6 @@ describe('FileMap', () => { { computeDependencies: true, computeSha1: false, - dependencyExtractor, filePath: path.join('/', 'project', 'vegetables', 'Melon.js'), 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 45375ee386..9eb00c3843 100644 --- a/packages/metro-file-map/src/__tests__/worker-test.js +++ b/packages/metro-file-map/src/__tests__/worker-test.js @@ -138,10 +138,11 @@ describe('worker', () => { test('accepts a custom dependency extractor', async () => { expect( - await worker({ + await new Worker({ + dependencyExtractor: path.join(__dirname, 'dependencyExtractor.js'), + }).processFile({ ...defaults, computeDependencies: true, - dependencyExtractor: path.join(__dirname, 'dependencyExtractor.js'), filePath: path.join('/project', 'fruits', 'Pear.js'), }), ).toEqual({ diff --git a/packages/metro-file-map/src/flow-types.js b/packages/metro-file-map/src/flow-types.js index 87d0391cb4..8bcecf2161 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -509,7 +509,6 @@ export type WatchmanClocks = Map; export type WorkerMessage = $ReadOnly<{ computeDependencies: boolean, computeSha1: boolean, - dependencyExtractor?: ?string, isNodeModules: boolean, filePath: string, maybeReturnContent: boolean, @@ -523,5 +522,6 @@ export type WorkerMetadata = $ReadOnly<{ }>; export type WorkerSetupArgs = $ReadOnly<{ + dependencyExtractor?: ?string, plugins?: $ReadOnlyArray, }>; diff --git a/packages/metro-file-map/src/lib/FileProcessor.js b/packages/metro-file-map/src/lib/FileProcessor.js index 9ea252a118..08d880f6cc 100644 --- a/packages/metro-file-map/src/lib/FileProcessor.js +++ b/packages/metro-file-map/src/lib/FileProcessor.js @@ -81,6 +81,7 @@ export class FileProcessor { this.#maxFilesPerWorker = opts.maxFilesPerWorker ?? MAX_FILES_PER_WORKER; this.#maxWorkers = opts.maxWorkers; this.#workerArgs = { + dependencyExtractor: this.#dependencyExtractor ?? null, plugins: [...(opts.pluginWorkers ?? [])], }; this.#inBandWorker = new Worker(this.#workerArgs); @@ -167,7 +168,6 @@ export class FileProcessor { return { computeDependencies: false, computeSha1: true, - dependencyExtractor: null, isNodeModules, filePath: absolutePath, maybeReturnContent, @@ -179,7 +179,6 @@ export class FileProcessor { return { computeDependencies, computeSha1, - dependencyExtractor: this.#dependencyExtractor, isNodeModules, filePath: absolutePath, maybeReturnContent, diff --git a/packages/metro-file-map/src/worker.js b/packages/metro-file-map/src/worker.js index 34d3a479cb..d111a36a43 100644 --- a/packages/metro-file-map/src/worker.js +++ b/packages/metro-file-map/src/worker.js @@ -33,9 +33,11 @@ function sha1hex(content /*: string | Buffer */) /*: string */ { } class Worker { + #dependencyExtractorPath /*: ?string */; #plugins /*: $ReadOnlyArray */; - constructor({plugins = []} /*: WorkerSetupArgs */) { + constructor({plugins = [], dependencyExtractor} /*: WorkerSetupArgs */) { + this.#dependencyExtractorPath = dependencyExtractor; this.#plugins = plugins.map(({workerModulePath, workerSetupArgs}) => { // $FlowFixMe[unsupported-syntax] - dynamic require const PluginWorker = require(workerModulePath); @@ -68,9 +70,9 @@ class Worker { !excludedExtensions.has(filePath.substr(filePath.lastIndexOf('.'))) ) { const dependencyExtractor /*: ?DependencyExtractor */ = - data.dependencyExtractor != null + this.#dependencyExtractorPath != null ? // $FlowFixMe[unsupported-syntax] - dynamic require - require(data.dependencyExtractor) + require(this.#dependencyExtractorPath) : null; dependencies = Array.from( From 442ad2551a64df2d50d2020cf8163950341d6f0a Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Thu, 16 Oct 2025 10:31:49 -0700 Subject: [PATCH 08/10] Filter no-op jobs for metro-file-map workers before calculating number of workers Differential Revision: D84835964 --- .../metro-file-map/src/lib/FileProcessor.js | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/metro-file-map/src/lib/FileProcessor.js b/packages/metro-file-map/src/lib/FileProcessor.js index 08d880f6cc..af4862f866 100644 --- a/packages/metro-file-map/src/lib/FileProcessor.js +++ b/packages/metro-file-map/src/lib/FileProcessor.js @@ -98,9 +98,24 @@ export class FileProcessor { }>, }> { const errors = []; + + const workerJobs = files + .map(([absolutePath, fileMetadata]) => { + const maybeWorkerInput = this.#getWorkerInput( + absolutePath, + fileMetadata, + req, + ); + if (!maybeWorkerInput) { + return null; + } + return [maybeWorkerInput, fileMetadata]; + }) + .filter(Boolean); + const numWorkers = Math.min( this.#maxWorkers, - Math.ceil(files.length / this.#maxFilesPerWorker), + Math.ceil(workerJobs.length / this.#maxFilesPerWorker), ); const batchWorker = this.#getBatchWorker(numWorkers); @@ -111,20 +126,15 @@ export class FileProcessor { } await Promise.all( - files.map(([absolutePath, fileMetadata]) => { - const maybeWorkerInput = this.#getWorkerInput( - absolutePath, - fileMetadata, - req, - ); - if (!maybeWorkerInput) { - return null; - } + workerJobs.map(([workerInput, fileMetadata]) => { return batchWorker - .processFile(maybeWorkerInput) + .processFile(workerInput) .then(reply => processWorkerReply(reply, fileMetadata)) .catch(error => - errors.push({absolutePath, error: normalizeWorkerError(error)}), + errors.push({ + absolutePath: workerInput.filePath, + error: normalizeWorkerError(error), + }), ); }), ); From 1d452c9e77cb0db547ce85180c708217700d03f2 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Thu, 16 Oct 2025 12:59:17 -0700 Subject: [PATCH 09/10] lazy/match --- packages/metro-file-map/src/flow-types.js | 2 ++ packages/metro-file-map/src/plugins/HastePlugin.js | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/packages/metro-file-map/src/flow-types.js b/packages/metro-file-map/src/flow-types.js index 8bcecf2161..0abfe35cf3 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -196,6 +196,8 @@ export type FileMapPluginInitOptions< }>; export type FileMapPluginWorker = $ReadOnly<{ + lazy: boolean, + match: boolean | RegExp, workerModulePath: string, workerSetupArgs: JsonData, }>; diff --git a/packages/metro-file-map/src/plugins/HastePlugin.js b/packages/metro-file-map/src/plugins/HastePlugin.js index 1f818e0376..9a875eb23d 100644 --- a/packages/metro-file-map/src/plugins/HastePlugin.js +++ b/packages/metro-file-map/src/plugins/HastePlugin.js @@ -506,6 +506,14 @@ export default class HastePlugin getWorker(): FileMapPluginWorker { return { + // All files must be visited eagerly. + lazy: false, + match: + this.#hasteImplModulePath != null + ? true + : this.#enableHastePackages + ? /package\.json$/ + : false, workerModulePath: require.resolve('./haste/worker.js'), workerSetupArgs: { enableHastePackages: this.#enableHastePackages, From 446bd39c7e98033d4d0f96c2dc95395e35cc1e58 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Fri, 17 Oct 2025 15:45:53 -0700 Subject: [PATCH 10/10] WIP - file map plugins Differential Revision: D84959611 --- .../src/__tests__/index-test.js | 2 ++ packages/metro-file-map/src/index.js | 23 ------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/packages/metro-file-map/src/__tests__/index-test.js b/packages/metro-file-map/src/__tests__/index-test.js index d7127c8e2b..780ad6acf1 100644 --- a/packages/metro-file-map/src/__tests__/index-test.js +++ b/packages/metro-file-map/src/__tests__/index-test.js @@ -1460,6 +1460,8 @@ describe('FileMap', () => { dependencyExtractor, plugins: [ { + lazy: false, + match: /package\.json$/, workerModulePath: expect.stringMatching( /src[\\\/]plugins[\\\/]haste[\\\/]worker\.js/, ), diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index 03ab29ac26..902b3f35bc 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -88,7 +88,6 @@ export type InputOptions = $ReadOnly<{ retainAllFiles: boolean, rootDir: string, roots: $ReadOnlyArray, - skipPackageJson?: ?boolean, // Module paths that should export a 'getCacheKey' method dependencyExtractor?: ?string, @@ -156,7 +155,6 @@ const CACHE_BREAKER = '11'; const CHANGE_INTERVAL = 30; const NODE_MODULES = path.sep + 'node_modules' + path.sep; -const PACKAGE_JSON = path.sep + 'package.json'; const VCS_DIRECTORIES = /[/\\]\.(git|hg)[/\\]/.source; const WATCHMAN_REQUIRED_CAPABILITIES = [ 'field-content.sha1hex', @@ -650,27 +648,6 @@ export default class FileMap extends EventEmitter { continue; } - if ( - this._options.skipPackageJson && - relativeFilePath.endsWith(PACKAGE_JSON) - ) { - continue; - } - - if ( - fileData[H.SYMLINK] === 0 && - !this._options.computeDependencies && - !this._options.computeSha1 && - this._options.hasteImplModulePath == null && - !( - this._options.enableHastePackages && - relativeFilePath.endsWith(PACKAGE_JSON) - ) - ) { - // Nothing to process - continue; - } - // SHA-1, if requested, should already be present thanks to the crawler. const absolutePath = this._pathUtils.normalToAbsolute(relativeFilePath);