wxt
Version:
⚡ Next-gen Web Extension Framework
318 lines (317 loc) • 11.2 kB
JavaScript
import * as wxtPlugins from "./plugins/index.mjs";
import {
getEntrypointBundlePath,
isHtmlEntrypoint
} from "../../utils/entrypoints.mjs";
import { toArray } from "../../utils/arrays.mjs";
import { safeVarName } from "../../utils/strings.mjs";
import { ViteNodeServer } from "vite-node/server";
import { ViteNodeRunner } from "vite-node/client";
import { installSourcemapsSupport } from "vite-node/source-map";
import { createExtensionEnvironment } from "../../utils/environments/index.mjs";
import { relative } from "node:path";
export async function createViteBuilder(wxtConfig, hooks, getWxtDevServer) {
const vite = await import("vite");
const getBaseConfig = async (baseConfigOptions) => {
const config = await wxtConfig.vite(wxtConfig.env);
config.root = wxtConfig.root;
config.configFile = false;
config.logLevel = "warn";
config.mode = wxtConfig.mode;
config.envPrefix ??= ["VITE_", "WXT_"];
config.build ??= {};
config.publicDir = wxtConfig.publicDir;
config.build.copyPublicDir = false;
config.build.outDir = wxtConfig.outDir;
config.build.emptyOutDir = false;
if (config.build.minify == null && wxtConfig.command === "serve") {
config.build.minify = false;
}
if (config.build.sourcemap == null && wxtConfig.command === "serve") {
config.build.sourcemap = "inline";
}
config.server ??= {};
config.server.watch = {
ignored: [`${wxtConfig.outBaseDir}/**`, `${wxtConfig.wxtDir}/**`]
};
config.legacy ??= {};
config.legacy.skipWebSocketTokenCheck = true;
const server = getWxtDevServer?.();
config.plugins ??= [];
config.plugins.push(
wxtPlugins.download(wxtConfig),
wxtPlugins.devHtmlPrerender(wxtConfig, server),
wxtPlugins.resolveVirtualModules(wxtConfig),
wxtPlugins.devServerGlobals(wxtConfig, server),
wxtPlugins.tsconfigPaths(wxtConfig),
wxtPlugins.noopBackground(),
wxtPlugins.globals(wxtConfig),
wxtPlugins.defineImportMeta(),
wxtPlugins.wxtPluginLoader(wxtConfig),
wxtPlugins.resolveAppConfig(wxtConfig)
);
if (wxtConfig.analysis.enabled && // If included, vite-node entrypoint loader will increment the
// bundleAnalysis's internal build index tracker, which we don't want
!baseConfigOptions?.excludeAnalysisPlugin) {
config.plugins.push(wxtPlugins.bundleAnalysis(wxtConfig));
}
return config;
};
const getLibModeConfig = (entrypoint) => {
const entry = getRollupEntry(entrypoint);
const plugins = [
wxtPlugins.entrypointGroupGlobals(entrypoint)
];
if (entrypoint.type === "content-script-style" || entrypoint.type === "unlisted-style") {
plugins.push(wxtPlugins.cssEntrypoints(entrypoint, wxtConfig));
}
const iifeReturnValueName = safeVarName(entrypoint.name);
const libMode = {
mode: wxtConfig.mode,
plugins,
esbuild: {
// Add a footer with the returned value so it can return values to `scripting.executeScript`
// Footer is added a part of esbuild to make sure it's not minified. It
// get's removed if added to `build.rollupOptions.output.footer`
// See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript#return_value
footer: iifeReturnValueName + ";"
},
build: {
lib: {
entry,
formats: ["iife"],
name: iifeReturnValueName,
fileName: entrypoint.name
},
rollupOptions: {
output: {
// There's only a single output for this build, so we use the desired bundle path for the
// entry output (like "content-scripts/overlay.js")
entryFileNames: getEntrypointBundlePath(
entrypoint,
wxtConfig.outDir,
".js"
),
// Output content script CSS to `content-scripts/`, but all other scripts are written to
// `assets/`.
assetFileNames: ({ name }) => {
if (entrypoint.type === "content-script" && name?.endsWith("css")) {
return `content-scripts/${entrypoint.name}.[ext]`;
} else {
return `assets/${entrypoint.name}.[ext]`;
}
}
}
}
},
define: {
// See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
"process.env.NODE_ENV": JSON.stringify(wxtConfig.mode)
}
};
return libMode;
};
const getMultiPageConfig = (entrypoints) => {
const htmlEntrypoints = new Set(
entrypoints.filter(isHtmlEntrypoint).map((e) => e.name)
);
return {
mode: wxtConfig.mode,
plugins: [
wxtPlugins.multipageMove(entrypoints, wxtConfig),
wxtPlugins.entrypointGroupGlobals(entrypoints)
],
build: {
rollupOptions: {
input: entrypoints.reduce((input, entry) => {
input[entry.name] = getRollupEntry(entry);
return input;
}, {}),
output: {
// Include a hash to prevent conflicts
chunkFileNames: "chunks/[name]-[hash].js",
entryFileNames: ({ name }) => {
if (htmlEntrypoints.has(name)) return "chunks/[name]-[hash].js";
return "[name].js";
},
// We can't control the "name", so we need a hash to prevent conflicts
assetFileNames: "assets/[name]-[hash].[ext]"
}
}
}
};
};
const getCssConfig = (entrypoint) => {
return {
mode: wxtConfig.mode,
plugins: [wxtPlugins.entrypointGroupGlobals(entrypoint)],
build: {
rollupOptions: {
input: {
[entrypoint.name]: entrypoint.inputPath
},
output: {
assetFileNames: () => {
if (entrypoint.type === "content-script-style") {
return `content-scripts/${entrypoint.name}.[ext]`;
} else {
return `assets/${entrypoint.name}.[ext]`;
}
}
}
}
}
};
};
const createViteNodeImporter = async (paths) => {
const baseConfig = await getBaseConfig({
excludeAnalysisPlugin: true
});
baseConfig.optimizeDeps ??= {};
baseConfig.optimizeDeps.noDiscovery = true;
baseConfig.optimizeDeps.include = [];
const envConfig = {
plugins: paths.map(
(path) => wxtPlugins.removeEntrypointMainFunction(wxtConfig, path)
)
};
const config = vite.mergeConfig(baseConfig, envConfig);
const server = await vite.createServer(config);
await server.pluginContainer.buildStart({});
const node = new ViteNodeServer(
// @ts-ignore: Some weird type error...
server
);
installSourcemapsSupport({
getSourceMap: (source) => node.getSourceMap(source)
});
const runner = new ViteNodeRunner({
root: server.config.root,
base: server.config.base,
// when having the server and runner in a different context,
// you will need to handle the communication between them
// and pass to this function
fetchModule(id) {
return node.fetchModule(id);
},
resolveId(id, importer) {
return node.resolveId(id, importer);
}
});
return { runner, server };
};
const requireDefaultExport = (path, mod) => {
const relativePath = relative(wxtConfig.root, path);
if (mod?.default == null) {
const defineFn = relativePath.includes(".content") ? "defineContentScript" : relativePath.includes("background") ? "defineBackground" : "defineUnlistedScript";
throw Error(
`${relativePath}: Default export not found, did you forget to call "export default ${defineFn}(...)"?`
);
}
};
return {
name: "Vite",
version: vite.version,
async importEntrypoint(path) {
const env = createExtensionEnvironment();
const { runner, server } = await createViteNodeImporter([path]);
const res = await env.run(() => runner.executeFile(path));
await server.close();
requireDefaultExport(path, res);
return res.default;
},
async importEntrypoints(paths) {
const env = createExtensionEnvironment();
const { runner, server } = await createViteNodeImporter(paths);
const res = await env.run(
() => Promise.all(
paths.map(async (path) => {
const mod = await runner.executeFile(path);
requireDefaultExport(path, mod);
return mod.default;
})
)
);
await server.close();
return res;
},
async build(group) {
let entryConfig;
if (Array.isArray(group)) entryConfig = getMultiPageConfig(group);
else if (group.inputPath.endsWith(".css"))
entryConfig = getCssConfig(group);
else entryConfig = getLibModeConfig(group);
const buildConfig = vite.mergeConfig(await getBaseConfig(), entryConfig);
await hooks.callHook(
"vite:build:extendConfig",
toArray(group),
buildConfig
);
const result = await vite.build(buildConfig);
return {
entrypoints: group,
chunks: getBuildOutputChunks(result)
};
},
async createServer(info) {
const serverConfig = {
server: {
host: info.host,
port: info.port,
strictPort: true,
origin: info.origin
}
};
const baseConfig = await getBaseConfig();
const finalConfig = vite.mergeConfig(baseConfig, serverConfig);
await hooks.callHook("vite:devServer:extendConfig", finalConfig);
const viteServer = await vite.createServer(finalConfig);
const server = {
async listen() {
await viteServer.listen(info.port);
},
async close() {
await viteServer.close();
},
transformHtml(...args) {
return viteServer.transformIndexHtml(...args);
},
ws: {
send(message, payload) {
return viteServer.ws.send(message, payload);
},
on(message, cb) {
viteServer.ws.on(message, cb);
}
},
watcher: viteServer.watcher,
on(event, cb) {
viteServer.httpServer?.on(event, cb);
}
};
return server;
}
};
}
function getBuildOutputChunks(result) {
if ("on" in result) throw Error("wxt does not support vite watch mode.");
if (Array.isArray(result)) return result.flatMap(({ output }) => output);
return result.output;
}
function getRollupEntry(entrypoint) {
let virtualEntrypointType;
switch (entrypoint.type) {
case "background":
case "unlisted-script":
virtualEntrypointType = entrypoint.type;
break;
case "content-script":
virtualEntrypointType = entrypoint.options.world === "MAIN" ? "content-script-main-world" : "content-script-isolated-world";
break;
}
if (virtualEntrypointType) {
const moduleId = `virtual:wxt-${virtualEntrypointType}-entrypoint`;
return `${moduleId}?${entrypoint.inputPath}`;
}
return entrypoint.inputPath;
}