From ecd43f09a0fa8945f2bbfe6e61c9655f6c78d931 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 26 Apr 2026 21:24:31 -0500 Subject: [PATCH 1/2] chore: Move `createFileReloader` into it's own file --- packages/wxt/src/core/create-server.ts | 177 +----------------- .../src/core/utils/create-file-reloader.ts | 163 ++++++++++++++++ 2 files changed, 165 insertions(+), 175 deletions(-) create mode 100644 packages/wxt/src/core/utils/create-file-reloader.ts diff --git a/packages/wxt/src/core/create-server.ts b/packages/wxt/src/core/create-server.ts index 8cebff66d..6bb7b5bf8 100644 --- a/packages/wxt/src/core/create-server.ts +++ b/packages/wxt/src/core/create-server.ts @@ -1,35 +1,11 @@ -import { debounce } from 'perfect-debounce'; import chokidar from 'chokidar'; -import { - BuildStepOutput, - EntrypointGroup, - InlineConfig, - ServerInfo, - WxtDevServer, -} from '../types'; -import { getEntrypointBundlePath, isHtmlEntrypoint } from './utils/entrypoints'; -import { - getContentScriptCssFiles, - getContentScriptsCssMap, -} from './utils/manifest'; -import { - internalBuild, - detectDevChanges, - rebuild, - findEntrypoints, -} from './utils/building'; +import { InlineConfig, ServerInfo, WxtDevServer } from '../types'; +import { internalBuild } from './utils/building'; import { createExtensionRunner } from './runners'; -import { Mutex } from 'async-mutex'; -import { relative } from 'node:path'; import { deinitWxtModules, initWxtModules, registerWxt, wxt } from './wxt'; import { unnormalizePath } from './utils/paths'; -import { - getContentScriptJs, - mapWxtOptionsToRegisteredContentScript, -} from './utils/content-scripts'; import { createKeyboardShortcuts } from './keyboard-shortcuts'; import { isBabelSyntaxError, logBabelSyntaxError } from './utils/syntax-errors'; -import { styleText } from 'node:util'; /** * Creates a dev server and pre-builds all the files that need to exist before @@ -199,155 +175,6 @@ async function createServerInternal(): Promise { return server; } -/** - * Returns a function responsible for reloading different parts of the extension - * when a file changes. - */ -function createFileReloader(server: WxtDevServer) { - const fileChangedMutex = new Mutex(); - const changeQueue: Array<[string, string]> = []; - - const cb = async (event: string, path: string) => { - changeQueue.push([event, path]); - - 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; - - await wxt.reloadConfig(); - - const changes = detectDevChanges(fileChanges, server.currentOutput); - if (changes.type === 'no-change') return; - - if (changes.type === 'full-restart') { - wxt.logger.info('Config changed, restarting server...'); - server.restart(); - return; - } - - if (changes.type === 'browser-restart') { - wxt.logger.info('Runner config changed, restarting browser...'); - server.restartBrowser(); - return; - } - - // Log the entrypoints that were effected - wxt.logger.info( - `Changed: ${Array.from(new Set(fileChanges)) - .map((file) => styleText('dim', relative(wxt.config.root, file))) - .join(', ')}`, - ); - - // Rebuild entrypoints on change - const allEntrypoints = await findEntrypoints(); - 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, - ); - 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); - - const rebuiltNames = changes.rebuildGroups - .flat() - .map((entry) => entry.name); - wxt.logger.success(`Reloaded: ${getFilenameList(rebuiltNames)}`); - break; - } - } catch { - // Catch build errors instead of crashing. Don't log error either, builder should have already logged it - } - }); - - await reloading.catch((error) => { - if (!isBabelSyntaxError(error)) { - throw error; - } - // Log syntax errors without crashing the server. - logBabelSyntaxError(error); - }); - }; - - return debounce(cb, wxt.config.dev.server!.watchDebounce, { - leading: true, - trailing: false, - }); -} - -/** - * From the server, tell the client to reload content scripts from the provided - * build step outputs. - */ -function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) { - if (wxt.config.manifestVersion === 3) { - steps.forEach((step) => { - if (server.currentOutput == null) return; - - const entry = step.entrypoints; - if (Array.isArray(entry) || entry.type !== 'content-script') return; - - const js = getContentScriptJs(wxt.config, entry); - const cssMap = getContentScriptsCssMap(server.currentOutput, [entry]); - const css = getContentScriptCssFiles([entry], cssMap); - - server.reloadContentScript({ - registration: entry.options.registration, - contentScript: mapWxtOptionsToRegisteredContentScript( - entry.options, - js, - css, - ), - }); - }); - } else { - server.reloadExtension(); - } -} - -function reloadHtmlPages( - groups: EntrypointGroup[], - server: WxtDevServer, -): { reloadedNames: string[] } { - // groups might contain other files like background/content scripts, and we only care about the HTMl pages - const htmlEntries = groups.flat().filter(isHtmlEntrypoint); - - htmlEntries.forEach((entry) => { - const path = getEntrypointBundlePath(entry, wxt.config.outDir, '.html'); - server.reloadPage(path); - }); - - return { - reloadedNames: htmlEntries.map((entry) => entry.name), - }; -} - -function getFilenameList(names: string[]): string { - return names - .map((name) => styleText('cyan', name)) - .join(styleText('dim', ', ')); -} - /** * Based on the current build output, return a list of files that are: * diff --git a/packages/wxt/src/core/utils/create-file-reloader.ts b/packages/wxt/src/core/utils/create-file-reloader.ts new file mode 100644 index 000000000..ce4322048 --- /dev/null +++ b/packages/wxt/src/core/utils/create-file-reloader.ts @@ -0,0 +1,163 @@ +import { debounce } from 'perfect-debounce'; +import { Mutex } from 'async-mutex'; +import { relative } from 'node:path'; +import { BuildStepOutput, EntrypointGroup, WxtDevServer } from '../../types'; +import { wxt } from '../wxt'; +import { detectDevChanges, findEntrypoints, rebuild } from './building'; +import { getEntrypointBundlePath, isHtmlEntrypoint } from './entrypoints'; +import { getContentScriptCssFiles, getContentScriptsCssMap } from './manifest'; +import { + getContentScriptJs, + mapWxtOptionsToRegisteredContentScript, +} from './content-scripts'; +import { isBabelSyntaxError, logBabelSyntaxError } from './syntax-errors'; +import { styleText } from 'node:util'; + +/** + * Returns a function responsible for reloading different parts of the extension + * when a file changes. + */ +export function createFileReloader(server: WxtDevServer) { + const fileChangedMutex = new Mutex(); + const changeQueue: Array<[string, string]> = []; + + const cb = async (event: string, path: string) => { + changeQueue.push([event, path]); + + 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; + + await wxt.reloadConfig(); + + const changes = detectDevChanges(fileChanges, server.currentOutput); + if (changes.type === 'no-change') return; + + if (changes.type === 'full-restart') { + wxt.logger.info('Config changed, restarting server...'); + server.restart(); + return; + } + + if (changes.type === 'browser-restart') { + wxt.logger.info('Runner config changed, restarting browser...'); + server.restartBrowser(); + return; + } + + // Log the entrypoints that were effected + wxt.logger.info( + `Changed: ${Array.from(new Set(fileChanges)) + .map((file) => styleText('dim', relative(wxt.config.root, file))) + .join(', ')}`, + ); + + // Rebuild entrypoints on change + const allEntrypoints = await findEntrypoints(); + 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, + ); + 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); + + const rebuiltNames = changes.rebuildGroups + .flat() + .map((entry) => entry.name); + wxt.logger.success(`Reloaded: ${getFilenameList(rebuiltNames)}`); + break; + } + } catch { + // Catch build errors instead of crashing. Don't log error either, builder should have already logged it + } + }); + + await reloading.catch((error) => { + if (!isBabelSyntaxError(error)) { + throw error; + } + // Log syntax errors without crashing the server. + logBabelSyntaxError(error); + }); + }; + + return debounce(cb, wxt.config.dev.server!.watchDebounce, { + leading: true, + trailing: false, + }); +} + +/** + * From the server, tell the client to reload content scripts from the provided + * build step outputs. + */ +function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) { + if (wxt.config.manifestVersion === 3) { + steps.forEach((step) => { + if (server.currentOutput == null) return; + + const entry = step.entrypoints; + if (Array.isArray(entry) || entry.type !== 'content-script') return; + + const js = getContentScriptJs(wxt.config, entry); + const cssMap = getContentScriptsCssMap(server.currentOutput, [entry]); + const css = getContentScriptCssFiles([entry], cssMap); + + server.reloadContentScript({ + registration: entry.options.registration, + contentScript: mapWxtOptionsToRegisteredContentScript( + entry.options, + js, + css, + ), + }); + }); + } else { + server.reloadExtension(); + } +} + +function reloadHtmlPages( + groups: EntrypointGroup[], + server: WxtDevServer, +): { reloadedNames: string[] } { + // groups might contain other files like background/content scripts, and we only care about the HTMl pages + const htmlEntries = groups.flat().filter(isHtmlEntrypoint); + + htmlEntries.forEach((entry) => { + const path = getEntrypointBundlePath(entry, wxt.config.outDir, '.html'); + server.reloadPage(path); + }); + + return { + reloadedNames: htmlEntries.map((entry) => entry.name), + }; +} + +function getFilenameList(names: string[]): string { + return names + .map((name) => styleText('cyan', name)) + .join(styleText('dim', ', ')); +} From c46d85b0d9ff469d11cff9200fd6b39cfa97ae78 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 26 Apr 2026 21:29:25 -0500 Subject: [PATCH 2/2] Fix checks --- packages/wxt/src/core/create-server.ts | 4 ++++ packages/wxt/src/core/utils/create-file-reloader.ts | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/wxt/src/core/create-server.ts b/packages/wxt/src/core/create-server.ts index 6bb7b5bf8..da748345f 100644 --- a/packages/wxt/src/core/create-server.ts +++ b/packages/wxt/src/core/create-server.ts @@ -6,6 +6,10 @@ import { deinitWxtModules, initWxtModules, registerWxt, wxt } from './wxt'; import { unnormalizePath } from './utils/paths'; import { createKeyboardShortcuts } from './keyboard-shortcuts'; import { isBabelSyntaxError, logBabelSyntaxError } from './utils/syntax-errors'; +import { + createFileReloader, + reloadContentScripts, +} from './utils/create-file-reloader'; /** * Creates a dev server and pre-builds all the files that need to exist before diff --git a/packages/wxt/src/core/utils/create-file-reloader.ts b/packages/wxt/src/core/utils/create-file-reloader.ts index ce4322048..e12ae09e3 100644 --- a/packages/wxt/src/core/utils/create-file-reloader.ts +++ b/packages/wxt/src/core/utils/create-file-reloader.ts @@ -113,7 +113,10 @@ export function createFileReloader(server: WxtDevServer) { * From the server, tell the client to reload content scripts from the provided * build step outputs. */ -function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) { +export function reloadContentScripts( + steps: BuildStepOutput[], + server: WxtDevServer, +) { if (wxt.config.manifestVersion === 3) { steps.forEach((step) => { if (server.currentOutput == null) return;