Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/wxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"nypm": "^0.6.5",
"ohash": "^2.0.11",
"open": "^11.0.0",
"perfect-debounce": "^2.1.0",
"picomatch": "^4.0.3",
"prompts": "^2.4.2",
"publish-browser-extension": "^2.3.0 || ^3.0.2 || ^4.0.4",
Expand Down
61 changes: 59 additions & 2 deletions packages/wxt/src/core/builders/vite/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Hookable } from 'hookable';
import { mkdir, readdir, rename, rmdir, stat } from 'node:fs/promises';
import { dirname, extname, join, relative } from 'node:path';
import { dirname, extname, join, relative, resolve } from 'node:path';
import type * as vite from 'vite';
import { ViteNodeRunner } from 'vite-node/client';
import { ViteNodeServer } from 'vite-node/server';
Expand Down Expand Up @@ -67,7 +67,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 @@ -392,6 +396,59 @@ export async function createViteBuilder(
};
}

export function getRunnerProfileWatchIgnores(
wxtConfig: ResolvedConfig,
): string[] {
const root = normalizePath(wxtConfig.root);
const chromiumArgProfiles = extractPathArgs(
wxtConfig.runnerConfig.config?.chromiumArgs,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm working on adding an alternative runner in the next major version and hardcoding access to one of the runner's config will prevent me from abstracting it.

It works for now like this, but we'll likely have to add a hook for this in the future...

Just thinking out loud, no changes here.

'--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
145 changes: 145 additions & 0 deletions packages/wxt/src/core/utils/__tests__/create-file-reloader.test.ts
Comment thread
aklinker1 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createFileReloader } from '../create-file-reloader';
import { findEntrypoints, rebuild } from '../building';
import {
fakeBackgroundEntrypoint,
fakeBuildOutput,
fakeDevServer,
fakeOutputChunk,
fakePopupEntrypoint,
setFakeWxt,
} from '../testing/fake-objects';

vi.mock('../building', async () => {
const actual =
await vi.importActual<typeof import('../building')>('../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 () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed you resolved that TODO, thanks!

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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,60 @@ describe('Detect Dev Changes', () => {
expect(actual).toEqual(expected);
});

it('should ignore unrelated changed files when checking html-only reloads', async () => {
const changedPath = '/root/page1.html';
const unrelatedPath =
'/root/private/.dev-profile/Default/Cache/Cache_Data/1004_0';
const htmlPage1 = fakePopupEntrypoint({
inputPath: changedPath,
});
const htmlPage2 = fakeOptionsEntrypoint({
inputPath: '/root/page2.html',
});
const htmlPage3 = fakeGenericEntrypoint({
type: 'sandbox',
inputPath: '/root/page3.html',
});

const step1: BuildStepOutput = {
entrypoints: [htmlPage1, htmlPage2],
chunks: [
fakeOutputAsset({
fileName: 'page1.html',
}),
],
};
const step2: BuildStepOutput = {
entrypoints: [htmlPage3],
chunks: [
fakeOutputAsset({
fileName: 'page2.html',
}),
],
};

const currentOutput: BuildOutput = {
manifest: fakeManifest(),
publicAssets: [],
steps: [step1, step2],
};
const expected: DevModeChange = {
type: 'html-reload',
cachedOutput: {
...currentOutput,
steps: [step2],
},
rebuildGroups: [[htmlPage1, htmlPage2]],
};

const actual = detectDevChanges(
[unrelatedPath, changedPath],
currentOutput,
);

expect(actual).toEqual(expected);
});

it('should detect changes to entrypoints/<name>/index.html files', async () => {
const changedPath = '/root/page1/index.html';
const htmlPage1 = fakePopupEntrypoint({
Expand Down
32 changes: 25 additions & 7 deletions packages/wxt/src/core/utils/building/detect-dev-changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,35 @@ export function detectDevChanges(
changedFiles: string[],
currentOutput: BuildOutput,
): DevModeChange {
const isConfigChange = some(
const relevantChangedFiles = getRelevantDevChangedFiles(
changedFiles,
currentOutput,
);

const isConfigChange = some(
relevantChangedFiles,
(file) => file === wxt.config.userConfigMetadata.configFile,
);
if (isConfigChange) return { type: 'full-restart' };

const isWxtModuleChange = some(changedFiles, (file) =>
const isWxtModuleChange = some(relevantChangedFiles, (file) =>
file.startsWith(wxt.config.modulesDir),
);
if (isWxtModuleChange) return { type: 'full-restart' };

const isRunnerChange = some(
changedFiles,
relevantChangedFiles,
(file) => file === wxt.config.runnerConfig.configFile,
);
if (isRunnerChange) return { type: 'browser-restart' };

const changedSteps = new Set(
changedFiles.flatMap((changedFile) =>
relevantChangedFiles.flatMap((changedFile) =>
findEffectedSteps(changedFile, currentOutput),
),
);
if (changedSteps.size === 0) {
const hasPublicChange = some(changedFiles, (file) =>
const hasPublicChange = some(relevantChangedFiles, (file) =>
file.startsWith(wxt.config.publicDir),
);
if (hasPublicChange) {
Expand Down Expand Up @@ -102,8 +107,8 @@ export function detectDevChanges(
}

const isOnlyHtmlChanges =
changedFiles.length > 0 &&
every(changedFiles, (file) => file.endsWith('.html'));
relevantChangedFiles.length > 0 &&
every(relevantChangedFiles, (file) => file.endsWith('.html'));
if (isOnlyHtmlChanges) {
return {
type: 'html-reload',
Expand Down Expand Up @@ -134,6 +139,19 @@ export function detectDevChanges(
};
}

export function getRelevantDevChangedFiles(
changedFiles: string[],
currentOutput: BuildOutput,
): string[] {
return Array.from(new Set(changedFiles)).filter((changedFile) => {
if (changedFile === wxt.config.userConfigMetadata.configFile) return true;
if (changedFile.startsWith(wxt.config.modulesDir)) return true;
if (changedFile === wxt.config.runnerConfig.configFile) return true;
if (changedFile.startsWith(wxt.config.publicDir)) return true;
return findEffectedSteps(changedFile, currentOutput).length > 0;
});
}

/**
* For a single change, return all the step of the build output that were
* effected by it.
Expand Down
Loading