From 4748b4ffdd1192cfcb3ae6e5916f0e3215efa902 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Mon, 13 Feb 2023 08:23:00 -0800 Subject: [PATCH] Propagate symlink events even if they match ignore patterns Differential Revision: D43214089 fbshipit-source-id: 5024a1afbb34fe11c63a5a17e8b97b90c657ca9b --- .../src/__tests__/index-test.js | 114 +++++++++++++++++- packages/metro-file-map/src/index.js | 37 +++--- 2 files changed, 133 insertions(+), 18 deletions(-) diff --git a/packages/metro-file-map/src/__tests__/index-test.js b/packages/metro-file-map/src/__tests__/index-test.js index a56567e834..ceab380c7e 100644 --- a/packages/metro-file-map/src/__tests__/index-test.js +++ b/packages/metro-file-map/src/__tests__/index-test.js @@ -309,7 +309,7 @@ describe('HasteMap', () => { expect(fileSystem.matchFiles('.git')).toEqual([]); }); - it('warn on ignore pattern except for regex', async () => { + it('throw on ignore pattern except for regex', async () => { const config = {ignorePattern: 'Kiwi', ...defaultConfig}; mockFs['/project/fruits/Kiwi.js'] = ` // Kiwi! @@ -1410,8 +1410,12 @@ describe('HasteMap', () => { if (options.mockFs) { mockFs = options.mockFs; } - const watchConfig = {...defaultConfig, watch: true}; - const hm = new HasteMap(watchConfig); + const config = { + ...defaultConfig, + watch: true, + ...options.config, + }; + const hm = new HasteMap(config); await hm.build(); try { await fn(hm); @@ -1457,6 +1461,12 @@ describe('HasteMap', () => { size: null, }; + const MOCK_CHANGE_LINK = { + type: 'l', + modifiedTime: 46, + size: 5, + }; + const MOCK_CHANGE_FOLDER = { type: 'd', modifiedTime: 45, @@ -1561,6 +1571,104 @@ describe('HasteMap', () => { }, ); + hm_it( + 'does not emit changes for regular files with unwatched extensions', + async hm => { + const {fileSystem} = await hm.build(); + mockFs[path.join('/', 'project', 'fruits', 'Banana.unwatched')] = ''; + + const e = mockEmitters[path.join('/', 'project', 'fruits')]; + e.emit( + 'all', + 'add', + path.join('Banana.js'), + path.join('/', 'project', 'fruits', ''), + MOCK_CHANGE_FILE, + ); + e.emit( + 'all', + 'add', + path.join('Banana.unwatched'), + path.join('/', 'project', 'fruits', ''), + MOCK_CHANGE_FILE, + ); + const {eventsQueue} = await waitForItToChange(hm); + const filePath = path.join('/', 'project', 'fruits', 'Banana.js'); + expect(eventsQueue).toHaveLength(1); + expect(eventsQueue).toEqual([ + {filePath, metadata: MOCK_CHANGE_FILE, type: 'add'}, + ]); + expect(fileSystem.getModuleName(filePath)).toBeDefined(); + }, + ); + + hm_it('does not emit delete events for unknown files', async hm => { + const {fileSystem} = await hm.build(); + mockFs[path.join('/', 'project', 'fruits', 'Banana.unwatched')] = ''; + + const e = mockEmitters[path.join('/', 'project', 'fruits')]; + e.emit( + 'all', + 'delete', + path.join('Banana.js'), + path.join('/', 'project', 'fruits', ''), + null, + ); + e.emit( + 'all', + 'delete', + path.join('Unknown.ext'), + path.join('/', 'project', 'fruits', ''), + null, + ); + const {eventsQueue} = await waitForItToChange(hm); + const filePath = path.join('/', 'project', 'fruits', 'Banana.js'); + expect(eventsQueue).toHaveLength(1); + expect(eventsQueue).toEqual([ + {filePath, metadata: MOCK_DELETE_FILE, type: 'delete'}, + ]); + expect(fileSystem.getModuleName(filePath)).toBeDefined(); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.error).not.toHaveBeenCalled(); + }); + + hm_it( + 'does emit changes for symlinks with unlisted extensions', + async hm => { + const {fileSystem} = await hm.build(); + const e = mockEmitters[path.join('/', 'project', 'fruits')]; + mockFs[path.join('/', 'project', 'fruits', 'LinkToStrawberry.ext')] = { + link: 'Strawberry.js', + }; + e.emit( + 'all', + 'add', + path.join('LinkToStrawberry.ext'), + path.join('/', 'project', 'fruits', ''), + MOCK_CHANGE_LINK, + ); + const {eventsQueue} = await waitForItToChange(hm); + const filePath = path.join( + '/', + 'project', + 'fruits', + 'LinkToStrawberry.ext', + ); + expect(eventsQueue).toHaveLength(1); + expect(eventsQueue).toEqual([ + {filePath, metadata: MOCK_CHANGE_LINK, type: 'add'}, + ]); + const linkStats = fileSystem.linkStats(filePath); + expect(linkStats).toEqual({ + fileType: 'l', + modifiedTime: 46, + }); + // getModuleName traverses the symlink, verifying the link is read. + expect(fileSystem.getModuleName(filePath)).toEqual('Strawberry'); + }, + {config: {enableSymlinks: true}}, + ); + hm_it( 'correctly tracks changes to both platform-specific versions of a single module name', async hm => { diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index a6ea11b6ea..e5737d91c3 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -891,7 +891,9 @@ export default class HasteMap extends EventEmitter { this._options.throwOnModuleCollision = false; this._options.retainAllFiles = true; - const extensions = this._options.extensions; + const hasWatchedExtension = (filePath: string) => + this._options.extensions.some(ext => filePath.endsWith(ext)); + const rootDir = this._options.rootDir; let changeQueue: Promise = Promise.resolve(); @@ -930,15 +932,24 @@ export default class HasteMap extends EventEmitter { root: Path, metadata: ?ChangeEventMetadata, ) => { - const absoluteFilePath = path.join(root, normalizePathSep(filePath)); if ( - (metadata && metadata.type === 'd') || - this._ignore(absoluteFilePath) || - !extensions.some(extension => absoluteFilePath.endsWith(extension)) + metadata && + // Ignore all directory events + (metadata.type === 'd' || + // Ignore regular files with unwatched extensions + (metadata.type === 'f' && !hasWatchedExtension(filePath))) ) { return; } + const absoluteFilePath = path.join(root, normalizePathSep(filePath)); + + // Ignore files (including symlinks) whose path matches ignorePattern + // (we don't ignore node_modules in watch mode) + if (this._options.ignorePattern.test(absoluteFilePath)) { + return; + } + const relativeFilePath = fastPath.relative(rootDir, absoluteFilePath); const linkStats = fileSystem.linkStats(relativeFilePath); @@ -1034,10 +1045,11 @@ export default class HasteMap extends EventEmitter { // point. } } else if (type === 'delete') { - invariant( - linkStats?.fileType != null, - 'delete event received for file of unknown type', - ); + if (linkStats == null) { + // Don't emit deletion events for files we weren't retaining. + // This is expected for deletion of an ignored file. + return null; + } enqueueEvent({ modifiedTime: null, size: null, @@ -1161,12 +1173,7 @@ export default class HasteMap extends EventEmitter { * Helpers */ _ignore(filePath: Path): boolean { - const ignorePattern = this._options.ignorePattern; - const ignoreMatched = - ignorePattern instanceof RegExp - ? ignorePattern.test(filePath) - : ignorePattern && ignorePattern(filePath); - + const ignoreMatched = this._options.ignorePattern.test(filePath); return ( ignoreMatched || (!this._options.retainAllFiles && filePath.includes(NODE_MODULES))