UNPKG

wxt

Version:

⚡ Next-gen Web Extension Framework

159 lines (158 loc) 7.43 kB
import { normalizePath } from "./paths.mjs"; import { getEntrypointBundlePath, isHtmlEntrypoint } from "./entrypoints.mjs"; import { filterTruthy, toArray } from "./arrays.mjs"; import { wxt } from "../wxt.mjs"; import { detectDevChanges, getRelevantDevChangedFiles } from "./building/detect-dev-changes.mjs"; import { findEntrypoints } from "./building/find-entrypoints.mjs"; import { groupEntrypoints } from "./building/group-entrypoints.mjs"; import { getContentScriptJs, mapWxtOptionsToRegisteredContentScript } from "./content-scripts.mjs"; import { getContentScriptCssFiles, getContentScriptsCssMap } from "./manifest.mjs"; import { rebuild } from "./building/rebuild.mjs"; import "./building/index.mjs"; import { isBabelSyntaxError, logBabelSyntaxError } from "./syntax-errors.mjs"; import { relative } from "node:path"; import { styleText } from "node:util"; import { Mutex } from "async-mutex"; //#region src/core/utils/create-file-reloader.ts /** * Returns a function responsible for reloading different parts of the extension * when a file changes. */ function createFileReloader(server) { const fileChangedMutex = new Mutex(); const changeQueue = []; let processLoop; const processQueue = async () => { await fileChangedMutex.runExclusive(async () => { 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(relevantFileChanges, server.currentOutput); 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; } 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)])); wxt.logger.info(`Changed: ${changedFilesToLog.map((file) => styleText("dim", relative(wxt.config.root, file))).join(", ")}`); const rebuildGroups = changes.type === "no-change" ? newEntrypointGroups : mergeEntrypointGroups(getLatestRebuildGroups(changes.rebuildGroups, allEntrypointGroups), newEntrypointGroups); try { const { output: newOutput } = await rebuild(allEntrypoints, rebuildGroups, changes.type === "no-change" ? server.currentOutput : changes.cachedOutput); server.currentOutput = newOutput; 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 = rebuildGroups.flat().map((entry) => entry.name); wxt.logger.success(`Reloaded: ${getFilenameList(rebuiltNames)}`); } } catch {} }).catch((error) => { if (!isBabelSyntaxError(error)) throw error; logBabelSyntaxError(error); }); }; 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, path) => { changeQueue.push([event, path]); processLoop ??= queueWorker().finally(() => { processLoop = void 0; }); await processLoop; }; } function getNewEntrypointGroups(normalizedFileChanges, allEntrypointGroups, currentOutput) { 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, allEntrypointGroups) { const groupByEntrypointPath = /* @__PURE__ */ new Map(); 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) { const deduped = /* @__PURE__ */ new Map(); groups.flat().forEach((group) => { deduped.set(getEntrypointGroupKey(group), group); }); return [...deduped.values()]; } function getEntrypointGroupKey(group) { 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. */ 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) => styleText("cyan", name)).join(styleText("dim", ", ")); } //#endregion export { createFileReloader, reloadContentScripts };