UNPKG

wxt

Version:

⚡ Next-gen Web Extension Framework

318 lines (317 loc) 11.2 kB
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; }