wxt
Version:
⚡ Next-gen Web Extension Framework
159 lines (158 loc) • 7.43 kB
JavaScript
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 };