UNPKG

wxt

Version:

⚡ Next-gen Web Extension Framework

265 lines (264 loc) 9.02 kB
import { debounce } from "perfect-debounce"; import chokidar from "chokidar"; import { getEntrypointBundlePath, isHtmlEntrypoint } from "./utils/entrypoints.mjs"; import { getContentScriptCssFiles, getContentScriptsCssMap } from "./utils/manifest.mjs"; import { internalBuild, detectDevChanges, rebuild, findEntrypoints } from "./utils/building/index.mjs"; import { createExtensionRunner } from "./runners/index.mjs"; import { Mutex } from "async-mutex"; import pc from "picocolors"; import { relative } from "node:path"; import { deinitWxtModules, initWxtModules, registerWxt, wxt } from "./wxt.mjs"; import { unnormalizePath } from "./utils/paths.mjs"; import { getContentScriptJs, mapWxtOptionsToRegisteredContentScript } from "./utils/content-scripts.mjs"; import { createKeyboardShortcuts } from "./keyboard-shortcuts.mjs"; import { isBabelSyntaxError, logBabelSyntaxError } from "./utils/syntax-errors.mjs"; export async function createServer(inlineConfig) { await registerWxt("serve", inlineConfig); wxt.server = await createServerInternal(); await wxt.hooks.callHook("server:created", wxt, wxt.server); return wxt.server; } async function createServerInternal() { const getServerInfo = () => { const { host, port, origin } = wxt.config.dev.server; return { host, port, origin }; }; let [runner, builderServer] = await Promise.all([ createExtensionRunner(), wxt.builder.createServer(getServerInfo()) ]); let wasStopped = false; const server = { get host() { return getServerInfo().host; }, get port() { return getServerInfo().port; }, get origin() { return getServerInfo().origin; }, get watcher() { return builderServer.watcher; }, get ws() { return builderServer.ws; }, currentOutput: void 0, async start() { if (wasStopped) { await wxt.reloadConfig(); runner = await createExtensionRunner(); builderServer = await wxt.builder.createServer(getServerInfo()); await initWxtModules(); } await builderServer.listen(); const hostInfo = server.host === "localhost" ? "" : ` (listening on ${server.host})`; wxt.logger.success(`Started dev server @ ${server.origin}${hostInfo}`); await wxt.hooks.callHook("server:started", wxt, server); server.ws.on("wxt:background-initialized", () => { if (server.currentOutput == null) return; reloadContentScripts(server.currentOutput.steps, server); }); await buildAndOpenBrowser(); const reloadOnChange = createFileReloader(server); server.watcher.on("all", async (...args) => { await reloadOnChange(args[0], args[1]); keyboardShortcuts.start(); }); keyboardShortcuts.printHelp({ canReopenBrowser: !wxt.config.runnerConfig.config.disabled && !!runner.canOpen?.() }); }, async stop() { wasStopped = true; keyboardShortcuts.stop(); await runner.closeBrowser(); await builderServer.close(); await wxt.hooks.callHook("server:closed", wxt, server); deinitWxtModules(); server.currentOutput = void 0; }, async restart() { await server.stop(); await server.start(); }, transformHtml(url, html, originalUrl) { return builderServer.transformHtml(url, html, originalUrl); }, reloadContentScript(payload) { server.ws.send("wxt:reload-content-script", payload); }, reloadPage(path) { server.ws.send("wxt:reload-page", path); }, reloadExtension() { server.ws.send("wxt:reload-extension"); }, async restartBrowser() { await runner.closeBrowser(); keyboardShortcuts.stop(); await wxt.reloadConfig(); runner = await createExtensionRunner(); await runner.openBrowser(); keyboardShortcuts.start(); } }; const keyboardShortcuts = createKeyboardShortcuts(server); const buildAndOpenBrowser = async () => { try { server.currentOutput = await internalBuild(); } catch (err) { if (!isBabelSyntaxError(err)) { throw err; } logBabelSyntaxError(err); wxt.logger.info("Waiting for syntax error to be fixed..."); await new Promise((resolve) => { const watcher = chokidar.watch(err.id, { ignoreInitial: true }); watcher.on("all", () => { watcher.close(); wxt.logger.info("Syntax error resolved, rebuilding..."); resolve(); }); }); return buildAndOpenBrowser(); } try { server.watcher.add(getExternalOutputDependencies(server)); } catch (err) { wxt.config.logger.warn("Failed to register additional file paths:", err); } await runner.openBrowser(); }; builderServer.on?.("close", () => keyboardShortcuts.stop()); return server; } function createFileReloader(server) { const fileChangedMutex = new Mutex(); const changeQueue = []; const cb = async (event, path) => { 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; } wxt.logger.info( `Changed: ${Array.from(new Set(fileChanges)).map((file) => pc.dim(relative(wxt.config.root, file))).join(", ")}` ); 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; 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 { } }); await reloading.catch((error) => { if (!isBabelSyntaxError(error)) { throw error; } logBabelSyntaxError(error); }); }; return debounce(cb, wxt.config.dev.server.watchDebounce, { leading: true, trailing: false }); } function reloadContentScripts(steps, server) { 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, server) { 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) { return names.map((name) => { return pc.cyan(name); }).join(pc.dim(", ")); } function getExternalOutputDependencies(server) { return server.currentOutput?.steps.flatMap((step, i) => { if (Array.isArray(step.entrypoints) && i === 0) { return []; } return step.chunks.flatMap((chunk) => { if (chunk.type === "asset") return []; return chunk.moduleIds; }); }).filter( (file) => !file.includes("node_modules") && !file.startsWith("\0") ).map(unnormalizePath).filter((file) => !file.startsWith(wxt.config.root)) ?? []; }