fix: file reloading mechanism and add profile watch ignores (#2112)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
This commit is contained in:
@@ -325,7 +325,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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
@@ -76,6 +76,7 @@ export async function createViteBuilder(
|
||||
ignored: [
|
||||
`${wxtConfig.outBaseDir}/**`,
|
||||
`${wxtConfig.wxtDir}/**`,
|
||||
...getRunnerProfileWatchIgnores(wxtConfig),
|
||||
...toArray(wxtConfig.watchOptions.ignored ?? []),
|
||||
],
|
||||
};
|
||||
@@ -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,
|
||||
'--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] : [];
|
||||
|
||||
@@ -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 () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
@@ -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',
|
||||
@@ -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.
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import { debounce } from 'perfect-debounce';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { relative } from 'node:path';
|
||||
import { BuildStepOutput, EntrypointGroup, WxtDevServer } from '../../types';
|
||||
import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
EntrypointGroup,
|
||||
WxtDevServer,
|
||||
} from '../../types';
|
||||
import { wxt } from '../wxt';
|
||||
import { detectDevChanges, findEntrypoints, rebuild } from './building';
|
||||
import {
|
||||
detectDevChanges,
|
||||
findEntrypoints,
|
||||
getRelevantDevChangedFiles,
|
||||
groupEntrypoints,
|
||||
rebuild,
|
||||
} from './building';
|
||||
import { getEntrypointBundlePath, isHtmlEntrypoint } from './entrypoints';
|
||||
import { getContentScriptCssFiles, getContentScriptsCssMap } from './manifest';
|
||||
import {
|
||||
@@ -12,6 +22,8 @@ import {
|
||||
} from './content-scripts';
|
||||
import { isBabelSyntaxError, logBabelSyntaxError } from './syntax-errors';
|
||||
import { styleText } from 'node:util';
|
||||
import { filterTruthy, toArray } from './arrays';
|
||||
import { normalizePath } from './paths';
|
||||
|
||||
/**
|
||||
* Returns a function responsible for reloading different parts of the extension
|
||||
@@ -20,22 +32,44 @@ import { styleText } from 'node:util';
|
||||
export function createFileReloader(server: WxtDevServer) {
|
||||
const fileChangedMutex = new Mutex();
|
||||
const changeQueue: Array<[string, string]> = [];
|
||||
let processLoop: Promise<void> | undefined;
|
||||
|
||||
const cb = async (event: string, path: string) => {
|
||||
changeQueue.push([event, path]);
|
||||
|
||||
const processQueue = async () => {
|
||||
const reloading = fileChangedMutex.runExclusive(async () => {
|
||||
if (server.currentOutput == null) return;
|
||||
|
||||
const fileChanges = changeQueue
|
||||
.splice(0, changeQueue.length)
|
||||
.map(([_, file]) => file);
|
||||
if (fileChanges.length === 0) return;
|
||||
if (server.currentOutput == null) return;
|
||||
|
||||
const normalizedFileChanges = fileChanges.map(normalizePath);
|
||||
const relevantFileChanges = getRelevantDevChangedFiles(
|
||||
fileChanges,
|
||||
server.currentOutput,
|
||||
);
|
||||
const normalizedEntrypointsDir = normalizePath(wxt.config.entrypointsDir);
|
||||
const hasEntrypointDirChange = normalizedFileChanges.some(
|
||||
(file) =>
|
||||
file === normalizedEntrypointsDir ||
|
||||
file.startsWith(`${normalizedEntrypointsDir}/`),
|
||||
);
|
||||
if (relevantFileChanges.length === 0 && !hasEntrypointDirChange) return;
|
||||
|
||||
await wxt.reloadConfig();
|
||||
const allEntrypoints = await findEntrypoints();
|
||||
const allEntrypointGroups = groupEntrypoints(allEntrypoints);
|
||||
const newEntrypointGroups = getNewEntrypointGroups(
|
||||
normalizedFileChanges,
|
||||
allEntrypointGroups,
|
||||
server.currentOutput,
|
||||
);
|
||||
if (relevantFileChanges.length === 0 && newEntrypointGroups.length === 0)
|
||||
return;
|
||||
|
||||
const changes = detectDevChanges(fileChanges, server.currentOutput);
|
||||
if (changes.type === 'no-change') return;
|
||||
const changes = detectDevChanges(
|
||||
relevantFileChanges,
|
||||
server.currentOutput,
|
||||
);
|
||||
|
||||
if (changes.type === 'full-restart') {
|
||||
wxt.logger.info('Config changed, restarting server...');
|
||||
@@ -48,46 +82,58 @@ export function createFileReloader(server: WxtDevServer) {
|
||||
server.restartBrowser();
|
||||
return;
|
||||
}
|
||||
const hasNewEntrypoints = newEntrypointGroups.length > 0;
|
||||
if (changes.type === 'no-change' && !hasNewEntrypoints) return;
|
||||
|
||||
const changedFilesToLog = Array.from(
|
||||
new Set([
|
||||
...relevantFileChanges,
|
||||
...newEntrypointGroups
|
||||
.flatMap((group) => toArray(group))
|
||||
.map((entry) => entry.inputPath),
|
||||
]),
|
||||
);
|
||||
|
||||
// Log the entrypoints that were effected
|
||||
wxt.logger.info(
|
||||
`Changed: ${Array.from(new Set(fileChanges))
|
||||
`Changed: ${changedFilesToLog
|
||||
.map((file) => styleText('dim', relative(wxt.config.root, file)))
|
||||
.join(', ')}`,
|
||||
);
|
||||
|
||||
// Rebuild entrypoints on change
|
||||
const allEntrypoints = await findEntrypoints();
|
||||
const rebuildGroups =
|
||||
changes.type === 'no-change'
|
||||
? newEntrypointGroups
|
||||
: mergeEntrypointGroups(
|
||||
getLatestRebuildGroups(
|
||||
changes.rebuildGroups,
|
||||
allEntrypointGroups,
|
||||
),
|
||||
newEntrypointGroups,
|
||||
);
|
||||
try {
|
||||
const { output: newOutput } = await rebuild(
|
||||
allEntrypoints,
|
||||
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
|
||||
changes.rebuildGroups,
|
||||
changes.cachedOutput,
|
||||
rebuildGroups,
|
||||
changes.type === 'no-change'
|
||||
? server.currentOutput
|
||||
: changes.cachedOutput,
|
||||
);
|
||||
server.currentOutput = newOutput;
|
||||
|
||||
// Perform reloads
|
||||
switch (changes.type) {
|
||||
case 'extension-reload':
|
||||
server.reloadExtension();
|
||||
wxt.logger.success(`Reloaded extension`);
|
||||
break;
|
||||
case 'html-reload':
|
||||
const { reloadedNames } = reloadHtmlPages(
|
||||
changes.rebuildGroups,
|
||||
server,
|
||||
);
|
||||
wxt.logger.success(`Reloaded: ${getFilenameList(reloadedNames)}`);
|
||||
break;
|
||||
case 'content-script-reload':
|
||||
reloadContentScripts(changes.changedSteps, server);
|
||||
if (hasNewEntrypoints || changes.type === 'extension-reload') {
|
||||
server.reloadExtension();
|
||||
wxt.logger.success(`Reloaded extension`);
|
||||
} else if (changes.type === 'html-reload') {
|
||||
const { reloadedNames } = reloadHtmlPages(rebuildGroups, server);
|
||||
wxt.logger.success(`Reloaded: ${getFilenameList(reloadedNames)}`);
|
||||
} else if (changes.type === 'content-script-reload') {
|
||||
reloadContentScripts(changes.changedSteps, server);
|
||||
|
||||
const rebuiltNames = changes.rebuildGroups
|
||||
.flat()
|
||||
.map((entry) => entry.name);
|
||||
wxt.logger.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
|
||||
break;
|
||||
const rebuiltNames = rebuildGroups.flat().map((entry) => entry.name);
|
||||
wxt.logger.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
|
||||
}
|
||||
} catch {
|
||||
// Catch build errors instead of crashing. Don't log error either, builder should have already logged it
|
||||
@@ -103,12 +149,98 @@ export function createFileReloader(server: WxtDevServer) {
|
||||
});
|
||||
};
|
||||
|
||||
return debounce(cb, wxt.config.dev.server!.watchDebounce, {
|
||||
leading: true,
|
||||
trailing: false,
|
||||
const waitForDebounceWindow = async () => {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, wxt.config.dev.server!.watchDebounce);
|
||||
});
|
||||
};
|
||||
|
||||
const queueWorker = async () => {
|
||||
while (true) {
|
||||
await processQueue();
|
||||
|
||||
await waitForDebounceWindow();
|
||||
if (changeQueue.length === 0) break;
|
||||
}
|
||||
};
|
||||
|
||||
return async (event: string, path: string) => {
|
||||
// Queue every event before debouncing so we never drop changes.
|
||||
changeQueue.push([event, path]);
|
||||
|
||||
processLoop ??= queueWorker().finally(() => {
|
||||
processLoop = undefined;
|
||||
});
|
||||
await processLoop;
|
||||
};
|
||||
}
|
||||
|
||||
function getNewEntrypointGroups(
|
||||
normalizedFileChanges: string[],
|
||||
allEntrypointGroups: EntrypointGroup[],
|
||||
currentOutput: BuildOutput,
|
||||
): EntrypointGroup[] {
|
||||
const changedFiles = new Set(normalizedFileChanges);
|
||||
const builtEntrypointPaths = new Set(
|
||||
currentOutput.steps.flatMap((step) =>
|
||||
toArray(step.entrypoints).map((entry) => normalizePath(entry.inputPath)),
|
||||
),
|
||||
);
|
||||
|
||||
return allEntrypointGroups.filter((group) => {
|
||||
const groupEntrypoints = toArray(group);
|
||||
const hasNewEntrypoint = groupEntrypoints.some(
|
||||
(entry) => !builtEntrypointPaths.has(normalizePath(entry.inputPath)),
|
||||
);
|
||||
const changedEntrypoint = groupEntrypoints.some((entry) =>
|
||||
changedFiles.has(normalizePath(entry.inputPath)),
|
||||
);
|
||||
return hasNewEntrypoint && changedEntrypoint;
|
||||
});
|
||||
}
|
||||
|
||||
function getLatestRebuildGroups(
|
||||
rebuildGroups: EntrypointGroup[],
|
||||
allEntrypointGroups: EntrypointGroup[],
|
||||
): EntrypointGroup[] {
|
||||
const groupByEntrypointPath = new Map<string, EntrypointGroup>();
|
||||
|
||||
allEntrypointGroups.forEach((group) => {
|
||||
toArray(group).forEach((entry) => {
|
||||
groupByEntrypointPath.set(normalizePath(entry.inputPath), group);
|
||||
});
|
||||
});
|
||||
|
||||
return mergeEntrypointGroups(
|
||||
rebuildGroups.flatMap((group) => {
|
||||
return filterTruthy(
|
||||
toArray(group).map((entry) =>
|
||||
groupByEntrypointPath.get(normalizePath(entry.inputPath)),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function mergeEntrypointGroups(
|
||||
...groups: EntrypointGroup[][]
|
||||
): EntrypointGroup[] {
|
||||
const deduped = new Map<string, EntrypointGroup>();
|
||||
|
||||
groups.flat().forEach((group) => {
|
||||
deduped.set(getEntrypointGroupKey(group), group);
|
||||
});
|
||||
|
||||
return [...deduped.values()];
|
||||
}
|
||||
|
||||
function getEntrypointGroupKey(group: EntrypointGroup): string {
|
||||
return toArray(group)
|
||||
.map((entry) => normalizePath(entry.inputPath))
|
||||
.sort()
|
||||
.join('|');
|
||||
}
|
||||
|
||||
/**
|
||||
* From the server, tell the client to reload content scripts from the provided
|
||||
* build step outputs.
|
||||
|
||||
Reference in New Issue
Block a user