Skip to content
Merged
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.5",
Expand Down
56 changes: 55 additions & 1 deletion 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 @@ -76,6 +76,7 @@ export async function createViteBuilder(
ignored: [
`${wxtConfig.outBaseDir}/**`,
`${wxtConfig.wxtDir}/**`,
...getRunnerProfileWatchIgnores(wxtConfig),
...toArray(wxtConfig.watchOptions.ignored ?? []),
],
};
Expand Down Expand Up @@ -404,6 +405,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 getRollupAssetNames(assetInfo: RollupAssetNameInfo): string[] {
if (Array.isArray(assetInfo.names)) return assetInfo.names;
return assetInfo.name ? [assetInfo.name] : [];
Expand Down
180 changes: 180 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,180 @@
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);
expect(server.reloadContentScript).not.toBeCalled();
});

it('should ignore entrypoint directory changes when no new entrypoints are found', async () => {
const backgroundFile = '/root/src/entrypoints/background.ts';
const ignoredEntrypointDirFile = '/root/src/entrypoints/.DS_Store';
const backgroundEntrypoint = fakeBackgroundEntrypoint({
inputPath: backgroundFile,
skipped: false,
});
const currentOutput = fakeBuildOutput({
steps: [
{
entrypoints: backgroundEntrypoint,
chunks: [fakeOutputChunk({ moduleIds: [backgroundFile] })],
},
],
publicAssets: [],
});
const server = fakeDevServer({
currentOutput,
reloadContentScript: vi.fn(),
reloadExtension: vi.fn(),
});

vi.mocked(findEntrypoints).mockResolvedValue([backgroundEntrypoint]);

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

expect(rebuild).not.toBeCalled();
expect(server.reloadContentScript).not.toBeCalled();
expect(server.reloadExtension).not.toBeCalled();
});
});
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
Loading
Loading