Skip to content
147 changes: 147 additions & 0 deletions packages/wxt/src/core/__tests__/create-server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createFileReloader } from '../utils/create-file-reloader';
import { findEntrypoints, rebuild } from '../utils/building';
import {
fakeBackgroundEntrypoint,
fakeBuildOutput,
fakeDevServer,
fakeOutputChunk,
fakePopupEntrypoint,
setFakeWxt,
} from '../utils/testing/fake-objects';

vi.mock('../utils/building', async () => {
const actual =
await vi.importActual<typeof import('../utils/building')>(
'../utils/building',
);
return {
...actual,
findEntrypoints: vi.fn(),
rebuild: vi.fn(),
};
});

describe('createFileReloader', () => {
beforeEach(() => {
vi.useFakeTimers();
setFakeWxt({
config: {
root: '/root',
entrypointsDir: '/root/src/entrypoints',
dev: {
server: {
watchDebounce: 100,
},
},
},
});
vi.mocked(findEntrypoints).mockResolvedValue([]);
});

afterEach(() => {
vi.clearAllMocks();
vi.useRealTimers();
});

it('should detect relevant file changes even when noisy file events happen first', async () => {
const relevantFile = '/root/src/entrypoints/background.ts';
const noisyProfileFile =
'/root/private/.dev-profile/Default/Cache/Cache_Data/d573fa6484e43cf9_0';
const backgroundEntrypoint = fakeBackgroundEntrypoint({
inputPath: relevantFile,
skipped: false,
});
const currentOutput = fakeBuildOutput({
steps: [
{
entrypoints: backgroundEntrypoint,
chunks: [fakeOutputChunk({ moduleIds: [relevantFile] })],
},
],
publicAssets: [],
});
const server = fakeDevServer({
currentOutput,
reloadExtension: vi.fn(),
});

vi.mocked(rebuild).mockResolvedValue({
output: currentOutput,
manifest: currentOutput.manifest,
warnings: [],
});
vi.mocked(findEntrypoints).mockResolvedValue([backgroundEntrypoint]);

const reloadOnChange = createFileReloader(server);

const fixedFirst = reloadOnChange('change', noisyProfileFile);
await vi.advanceTimersByTimeAsync(50);
const fixedSecond = reloadOnChange('change', relevantFile);
await vi.advanceTimersByTimeAsync(500);
await Promise.all([fixedFirst, fixedSecond]);

expect(rebuild).toBeCalledTimes(1);
const [allEntrypoints, rebuiltGroups] = vi.mocked(rebuild).mock.calls[0];
expect(
allEntrypoints.some((entry) => entry.inputPath === relevantFile),
).toBe(true);
expect(
rebuiltGroups.flat().some((entry) => entry.inputPath === relevantFile),
).toBe(true);
expect(server.reloadExtension).toBeCalledTimes(1);
});

it('should rebuild and reload extension when a new entrypoint is added', async () => {
const backgroundFile = '/root/src/entrypoints/background.ts';
const newEntrypointFile = '/root/src/entrypoints/popup.html';
const backgroundEntrypoint = fakeBackgroundEntrypoint({
inputPath: backgroundFile,
skipped: false,
});
const popupEntrypoint = fakePopupEntrypoint({
inputPath: newEntrypointFile,
skipped: false,
});
const currentOutput = fakeBuildOutput({
steps: [
{
entrypoints: backgroundEntrypoint,
chunks: [fakeOutputChunk({ moduleIds: [backgroundFile] })],
},
],
publicAssets: [],
});
const server = fakeDevServer({
currentOutput,
reloadExtension: vi.fn(),
});

vi.mocked(findEntrypoints).mockResolvedValue([
backgroundEntrypoint,
popupEntrypoint,
]);
vi.mocked(rebuild).mockResolvedValue({
output: currentOutput,
manifest: currentOutput.manifest,
warnings: [],
});

const reloadOnChange = createFileReloader(server);
const trigger = reloadOnChange('add', newEntrypointFile);
await vi.advanceTimersByTimeAsync(500);
await trigger;

expect(rebuild).toBeCalledTimes(1);
const [allEntrypoints, rebuiltGroups, cachedOutput] =
vi.mocked(rebuild).mock.calls[0];
expect(allEntrypoints).toEqual([backgroundEntrypoint, popupEntrypoint]);
expect(
rebuiltGroups
.flat()
.some((entry) => entry.inputPath === newEntrypointFile),
).toBe(true);
expect(cachedOutput).toEqual(currentOutput);
expect(server.reloadExtension).toBeCalledTimes(1);
});
});
61 changes: 59 additions & 2 deletions packages/wxt/src/core/builders/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { ViteNodeServer } from 'vite-node/server';
import { ViteNodeRunner } from 'vite-node/client';
import { installSourcemapsSupport } from 'vite-node/source-map';
import { createExtensionEnvironment } from '../../utils/environments';
import { dirname, extname, join, relative } from 'node:path';
import { dirname, extname, join, relative, resolve } from 'node:path';
import fs from 'fs-extra';
import { normalizePath } from '../../utils';

Expand Down Expand Up @@ -66,7 +66,11 @@ export async function createViteBuilder(

config.server ??= {};
config.server.watch = {
ignored: [`${wxtConfig.outBaseDir}/**`, `${wxtConfig.wxtDir}/**`],
ignored: [
`${wxtConfig.outBaseDir}/**`,
`${wxtConfig.wxtDir}/**`,
...getRunnerProfileWatchIgnores(wxtConfig),
],
};

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

export function getRunnerProfileWatchIgnores(
wxtConfig: ResolvedConfig,
): string[] {
const root = normalizePath(wxtConfig.root);
const chromiumArgProfiles = extractPathArgs(
wxtConfig.runnerConfig.config?.chromiumArgs,
'--user-data-dir',
);
const firefoxArgProfiles = extractPathArgs(
wxtConfig.runnerConfig.config?.firefoxArgs,
'-profile',
);
const profiles = [
wxtConfig.runnerConfig.config?.chromiumProfile,
wxtConfig.runnerConfig.config?.firefoxProfile,
...chromiumArgProfiles,
...firefoxArgProfiles,
].filter((profile): profile is string => typeof profile === 'string');

return Array.from(
new Set(
profiles
.map((profile) => normalizePath(resolve(wxtConfig.root, profile)))
// Avoid accidentally disabling all file watching.
.filter((profilePath) => profilePath !== root)
.map((profilePath) => `${profilePath}/**`),
),
);
}

function extractPathArgs(args: string[] | undefined, flag: string): string[] {
if (!args?.length) return [];

const paths: string[] = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];

if (arg.startsWith(`${flag}=`)) {
const value = arg.slice(flag.length + 1).trim();
if (value) paths.push(value);
continue;
}

if (arg === flag) {
const nextValue = args[i + 1]?.trim();
if (nextValue) paths.push(nextValue);
i += 1;
}
}

return paths;
}

function getBuildOutputChunks(
result: Awaited<ReturnType<typeof vite.build>>,
): BuildStepOutput['chunks'] {
Expand Down
Loading