diff --git a/src/watcher.js b/src/watcher.js index d4a784b474..3fb9392d23 100644 --- a/src/watcher.js +++ b/src/watcher.js @@ -25,10 +25,18 @@ export default function onSourceChange({ const ignored = watchIgnored && process.platform === 'win32' ? watchIgnored.map((it) => it.replace(/\\/g, '/')) - : watchIgnored; + : (watchIgnored || []).slice(0); + + // Chrome may write to the loaded extension directory: + // https://github.com/mozilla/web-ext/issues/3468 + // Ignore these to avoid permanent auto-reload. + // Although these files are only expected at the top level, we ignore them + // globally in case a subdirectory is also loaded as a Chrome extension. + ignored.push('**/_metadata'); + ignored.push('**/Cached Theme.pak'); // TODO: For network disks, we would need to add {poll: true}. - const watcher = ignored ? new Watchpack({ ignored }) : new Watchpack(); + const watcher = new Watchpack({ ignored }); // Allow multiple files to be changed before reloading the extension const executeImmediately = false; diff --git a/tests/unit/test.watcher.js b/tests/unit/test.watcher.js index 47b3f7266b..c7ef588e52 100644 --- a/tests/unit/test.watcher.js +++ b/tests/unit/test.watcher.js @@ -14,10 +14,13 @@ import { withTempDir } from '../../src/util/temp-dir.js'; import { makeSureItFails } from './helpers.js'; describe('watcher', () => { - const watchChange = ({ watchFile, touchedFile } = {}) => + const watchChange = ({ prepTempDir, watchFile, touchedFile } = {}) => withTempDir(async (tmpDir) => { const artifactsDir = path.join(tmpDir.path(), 'web-ext-artifacts'); const someFile = path.join(tmpDir.path(), touchedFile); + if (prepTempDir) { + await prepTempDir(tmpDir); + } if (watchFile) { watchFile = watchFile.map((f) => path.join(tmpDir.path(), f)); @@ -237,4 +240,107 @@ describe('watcher', () => { await waitDebounce(); })); }); + + describe('watcher ignores _metadata', () => { + // _metadata should be ignored to avoid reload loop, see: + // https://github.com/mozilla/web-ext/issues/3468 + + // _metadata/generated_indexed_rulesets/_ruleset1 (original test case) + let promiseTouchedMetadataDirContent; + // Immediate child of _metadata without subdirectory. + let promiseTouchedMetadataDirWithoutSub; + // _metadata is also ignored when nested, not just at the top. + let promiseTouchedMetadataDirNested; + // _metadata is a directory, but the implementation also ignores files... + let promiseTouchedMetadataFile; + // Check behavior of --watch-file=_metadata + let promiseTouchedMetadataFileWithWatchFile; + // Chrome may also write "Cached Theme.pak" (outside _metadata directory). + let promiseTouchedTheme; + + it('ignores change to _metadata directory content (setup)', () => { + // Simulates scenario from https://github.com/mozilla/web-ext/issues/3468 + promiseTouchedMetadataDirContent = watchChange({ + prepTempDir: async (tmpDir) => { + const metadataDir = path.join(tmpDir.path(), '_metadata'); + await fs.mkdir(metadataDir); + await fs.mkdir(path.join(metadataDir, 'generated_indexed_rulesets')); + }, + touchedFile: path.join( + '_metadata', + 'generated_indexed_rulesets', + '_ruleset1', + ), + }); + }); + + it('ignores change to _metadata directory without subdirectory (setup)', () => { + // Simulates scenario from https://github.com/mozilla/web-ext/issues/3468 + promiseTouchedMetadataDirWithoutSub = watchChange({ + prepTempDir: async (tmpDir) => { + await fs.mkdir(path.join(tmpDir.path(), '_metadata')); + }, + touchedFile: path.join('_metadata', 'somefile'), + }); + }); + + it('ignores change to non-toplevel _metadata directory (setup)', () => { + promiseTouchedMetadataDirNested = watchChange({ + prepTempDir: async (tmpDir) => { + const parentDir = path.join(tmpDir.path(), 'parent'); + await fs.mkdir(parentDir); + await fs.mkdir(path.join(parentDir, '_metadata')); + }, + touchedFile: path.join('parent', '_metadata', 'somefile'), + }); + }); + + it('ignores change to _metadata file (setup)', () => { + promiseTouchedMetadataFile = watchChange({ touchedFile: '_metadata' }); + }); + + it('ignores change to _metadata file despite --watch-file (setup)', () => { + promiseTouchedMetadataFileWithWatchFile = watchChange({ + watchFile: ['_metadata'], + touchedFile: '_metadata', + }); + }); + + it('igmores change to Cached Theme.pak (setup)', () => { + // When a theme is loaded in Chrome, it writes to "Cached Theme.pak" in + // the source directory, which would result in permanent auto-reload + // unless we disabled auto reload. + promiseTouchedTheme = watchChange({ touchedFile: 'Cached Theme.pak' }); + }); + + it('ignores change to _metadata directory content (await)', async () => { + const { onChange } = await promiseTouchedMetadataDirContent; + sinon.assert.notCalled(onChange); + }); + + it('ignores change to _metadata directory without subdirectory (await)', async () => { + const { onChange } = await promiseTouchedMetadataDirWithoutSub; + sinon.assert.notCalled(onChange); + }); + + it('ignores change to non-toplevel _metadata directory (await)', async () => { + const { onChange } = await promiseTouchedMetadataDirNested; + sinon.assert.notCalled(onChange); + }); + + it('ignores change to _metadata file (await)', async () => { + const { onChange } = await promiseTouchedMetadataFile; + sinon.assert.notCalled(onChange); + }); + + it('ignores change to _metadata file despite --watch-file (await)', async () => { + const { onChange } = await promiseTouchedMetadataFileWithWatchFile; + sinon.assert.notCalled(onChange); + }); + + it('igmores change to Cached Theme.pak (await)', async () => { + const { onChange } = await promiseTouchedTheme; + sinon.assert.notCalled(onChange); + }); + }); });