Skip to content

Commit 8767b01

Browse files
ZerGo0PatrykKuniczak
authored andcommitted
fix: file reloading mechanism and add profile watch ignores
- Exported `createFileReloader` function to allow external usage. - Improved the file reloading process by implementing a queue worker to handle multiple file changes without dropping events. - Added `getRunnerProfileWatchIgnores` function to dynamically generate ignored paths based on user profiles for Chromium and Firefox. - Updated Vite builder configuration to include these new ignore patterns.
1 parent cf31fbe commit 8767b01

3 files changed

Lines changed: 169 additions & 13 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { createFileReloader } from '../create-server';
3+
import { detectDevChanges, findEntrypoints, rebuild } from '../utils/building';
4+
import {
5+
fakeBuildOutput,
6+
fakeDevServer,
7+
setFakeWxt,
8+
} from '../utils/testing/fake-objects';
9+
10+
vi.mock('../utils/building', () => ({
11+
detectDevChanges: vi.fn(),
12+
findEntrypoints: vi.fn(),
13+
internalBuild: vi.fn(),
14+
rebuild: vi.fn(),
15+
}));
16+
17+
describe('createFileReloader', () => {
18+
beforeEach(() => {
19+
vi.useFakeTimers();
20+
setFakeWxt({
21+
config: {
22+
root: '/root',
23+
dev: {
24+
server: {
25+
watchDebounce: 100,
26+
},
27+
},
28+
},
29+
});
30+
vi.mocked(findEntrypoints).mockResolvedValue([]);
31+
});
32+
33+
afterEach(() => {
34+
vi.clearAllMocks();
35+
vi.useRealTimers();
36+
});
37+
38+
it('should detect relevant file changes even when noisy file events happen first', async () => {
39+
const relevantFile = '/root/src/entrypoints/background.ts';
40+
const noisyProfileFile =
41+
'/root/private/.dev-profile/Default/Cache/Cache_Data/d573fa6484e43cf9_0';
42+
const currentOutput = fakeBuildOutput({
43+
steps: [],
44+
publicAssets: [],
45+
});
46+
const server = fakeDevServer({
47+
currentOutput,
48+
reloadExtension: vi.fn(),
49+
});
50+
51+
vi.mocked(detectDevChanges).mockImplementation((fileChanges, output) => {
52+
if (fileChanges.includes(relevantFile)) {
53+
return {
54+
type: 'extension-reload',
55+
rebuildGroups: [],
56+
cachedOutput: output,
57+
};
58+
}
59+
return { type: 'no-change' };
60+
});
61+
vi.mocked(rebuild).mockResolvedValue({
62+
output: currentOutput,
63+
manifest: currentOutput.manifest,
64+
warnings: [],
65+
});
66+
67+
const reloadOnChange = createFileReloader(server);
68+
69+
const fixedFirst = reloadOnChange('change', noisyProfileFile);
70+
await vi.advanceTimersByTimeAsync(50);
71+
const fixedSecond = reloadOnChange('change', relevantFile);
72+
await vi.advanceTimersByTimeAsync(500);
73+
await Promise.all([fixedFirst, fixedSecond]);
74+
75+
const seenFiles = vi
76+
.mocked(detectDevChanges)
77+
.mock.calls.flatMap(([fileChanges]) => fileChanges);
78+
79+
expect(seenFiles).toContain(relevantFile);
80+
expect(server.reloadExtension).toBeCalledTimes(1);
81+
});
82+
});

packages/wxt/src/core/builders/vite/index.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { ViteNodeServer } from 'vite-node/server';
2525
import { ViteNodeRunner } from 'vite-node/client';
2626
import { installSourcemapsSupport } from 'vite-node/source-map';
2727
import { createExtensionEnvironment } from '../../utils/environments';
28-
import { dirname, extname, join, relative } from 'node:path';
28+
import { dirname, extname, join, relative, resolve } from 'node:path';
2929
import fs from 'fs-extra';
3030
import { normalizePath } from '../../utils';
3131

@@ -66,7 +66,11 @@ export async function createViteBuilder(
6666

6767
config.server ??= {};
6868
config.server.watch = {
69-
ignored: [`${wxtConfig.outBaseDir}/**`, `${wxtConfig.wxtDir}/**`],
69+
ignored: [
70+
`${wxtConfig.outBaseDir}/**`,
71+
`${wxtConfig.wxtDir}/**`,
72+
...getRunnerProfileWatchIgnores(wxtConfig),
73+
],
7074
};
7175

7276
// TODO: Remove once https://github.com/wxt-dev/wxt/pull/1411 is merged
@@ -371,6 +375,59 @@ export async function createViteBuilder(
371375
};
372376
}
373377

378+
export function getRunnerProfileWatchIgnores(
379+
wxtConfig: ResolvedConfig,
380+
): string[] {
381+
const root = normalizePath(wxtConfig.root);
382+
const chromiumArgProfiles = extractPathArgs(
383+
wxtConfig.runnerConfig.config?.chromiumArgs,
384+
'--user-data-dir',
385+
);
386+
const firefoxArgProfiles = extractPathArgs(
387+
wxtConfig.runnerConfig.config?.firefoxArgs,
388+
'-profile',
389+
);
390+
const profiles = [
391+
wxtConfig.runnerConfig.config?.chromiumProfile,
392+
wxtConfig.runnerConfig.config?.firefoxProfile,
393+
...chromiumArgProfiles,
394+
...firefoxArgProfiles,
395+
].filter((profile): profile is string => typeof profile === 'string');
396+
397+
return Array.from(
398+
new Set(
399+
profiles
400+
.map((profile) => normalizePath(resolve(wxtConfig.root, profile)))
401+
// Avoid accidentally disabling all file watching.
402+
.filter((profilePath) => profilePath !== root)
403+
.map((profilePath) => `${profilePath}/**`),
404+
),
405+
);
406+
}
407+
408+
function extractPathArgs(args: string[] | undefined, flag: string): string[] {
409+
if (!args?.length) return [];
410+
411+
const paths: string[] = [];
412+
for (let i = 0; i < args.length; i++) {
413+
const arg = args[i];
414+
415+
if (arg.startsWith(`${flag}=`)) {
416+
const value = arg.slice(flag.length + 1).trim();
417+
if (value) paths.push(value);
418+
continue;
419+
}
420+
421+
if (arg === flag) {
422+
const nextValue = args[i + 1]?.trim();
423+
if (nextValue) paths.push(nextValue);
424+
i += 1;
425+
}
426+
}
427+
428+
return paths;
429+
}
430+
374431
function getBuildOutputChunks(
375432
result: Awaited<ReturnType<typeof vite.build>>,
376433
): BuildStepOutput['chunks'] {

packages/wxt/src/core/create-server.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { debounce } from 'perfect-debounce';
21
import chokidar from 'chokidar';
32
import {
43
BuildStepOutput,
@@ -202,20 +201,18 @@ async function createServerInternal(): Promise<WxtDevServer> {
202201
* Returns a function responsible for reloading different parts of the extension when a file
203202
* changes.
204203
*/
205-
function createFileReloader(server: WxtDevServer) {
204+
export function createFileReloader(server: WxtDevServer) {
206205
const fileChangedMutex = new Mutex();
207206
const changeQueue: Array<[string, string]> = [];
207+
let processLoop: Promise<void> | undefined;
208208

209-
const cb = async (event: string, path: string) => {
210-
changeQueue.push([event, path]);
211-
209+
const processQueue = async () => {
212210
const reloading = fileChangedMutex.runExclusive(async () => {
213-
if (server.currentOutput == null) return;
214-
215211
const fileChanges = changeQueue
216212
.splice(0, changeQueue.length)
217213
.map(([_, file]) => file);
218214
if (fileChanges.length === 0) return;
215+
if (server.currentOutput == null) return;
219216

220217
await wxt.reloadConfig();
221218

@@ -288,10 +285,30 @@ function createFileReloader(server: WxtDevServer) {
288285
});
289286
};
290287

291-
return debounce(cb, wxt.config.dev.server!.watchDebounce, {
292-
leading: true,
293-
trailing: false,
294-
});
288+
const waitForDebounceWindow = async () => {
289+
await new Promise((resolve) => {
290+
setTimeout(resolve, wxt.config.dev.server!.watchDebounce);
291+
});
292+
};
293+
294+
const queueWorker = async () => {
295+
while (true) {
296+
await processQueue();
297+
298+
await waitForDebounceWindow();
299+
if (changeQueue.length === 0) break;
300+
}
301+
};
302+
303+
return async (event: string, path: string) => {
304+
// Queue every event before debouncing so we never drop changes.
305+
changeQueue.push([event, path]);
306+
307+
processLoop ??= queueWorker().finally(() => {
308+
processLoop = undefined;
309+
});
310+
await processLoop;
311+
};
295312
}
296313

297314
/**

0 commit comments

Comments
 (0)