UNPKG

wxt

Version:

⚡ Next-gen Web Extension Framework

361 lines (360 loc) 14 kB
import { normalizePath } from "../../utils/paths.mjs"; import { getEntrypointBundlePath, isHtmlEntrypoint } from "../../utils/entrypoints.mjs"; import "../../utils/index.mjs"; import { toArray } from "../../utils/arrays.mjs"; import { createExtensionEnvironment } from "../../utils/environments/extension-environment.mjs"; import "../../utils/environments/index.mjs"; import { safeVarName } from "../../utils/strings.mjs"; import { devHtmlPrerender } from "./plugins/devHtmlPrerender.mjs"; import { devServerGlobals } from "./plugins/devServerGlobals.mjs"; import { resolveVirtualModules } from "./plugins/resolveVirtualModules.mjs"; import { tsconfigPaths } from "./plugins/tsconfigPaths.mjs"; import { noopBackground } from "./plugins/noopBackground.mjs"; import { cssEntrypoints } from "./plugins/cssEntrypoints.mjs"; import { bundleAnalysis } from "./plugins/bundleAnalysis.mjs"; import { globals } from "./plugins/globals.mjs"; import { entrypointGroupGlobals } from "./plugins/entrypointGroupGlobals.mjs"; import { defineImportMeta } from "./plugins/defineImportMeta.mjs"; import { removeEntrypointMainFunction } from "./plugins/removeEntrypointMainFunction.mjs"; import { wxtPluginLoader } from "./plugins/wxtPluginLoader.mjs"; import { resolveAppConfig } from "./plugins/resolveAppConfig.mjs"; import { iifeFooter } from "./plugins/iifeFooter.mjs"; import { iifeAnonymous } from "./plugins/iifeAnonymous.mjs"; import "./plugins/index.mjs"; import { mkdir, readdir, rename, rmdir, stat } from "node:fs/promises"; import { dirname, extname, join, relative, resolve } from "node:path"; //#region src/core/builders/vite/index.ts async function createViteBuilder(wxtConfig, hooks, getWxtDevServer) { const vite = await import("vite"); /** * Returns the base vite config shared by all builds based on the inline and * user config. */ 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 = { ...wxtConfig.watchOptions, ignored: [ `${wxtConfig.outBaseDir}/**`, `${wxtConfig.wxtDir}/**`, ...getRunnerProfileWatchIgnores(wxtConfig), ...toArray(wxtConfig.watchOptions.ignored ?? []) ] }; config.legacy ??= {}; config.legacy.skipWebSocketTokenCheck = true; if (isRolldownVersion(vite.version)) {} else { config.esbuild ??= {}; if (config.esbuild) config.esbuild.charset = "ascii"; } const server = getWxtDevServer?.(); config.plugins ??= []; config.plugins.push(devHtmlPrerender(wxtConfig, server), resolveVirtualModules(wxtConfig), devServerGlobals(wxtConfig, server), tsconfigPaths(wxtConfig), noopBackground(), globals(wxtConfig), defineImportMeta(), wxtPluginLoader(wxtConfig), resolveAppConfig(wxtConfig)); if (wxtConfig.analysis.enabled && !baseConfigOptions?.excludeAnalysisPlugin) config.plugins.push(bundleAnalysis(wxtConfig)); return config; }; /** * Return the basic config for building an entrypoint in [lib * mode](https://vitejs.dev/guide/build.html#library-mode). */ const getLibModeConfig = (entrypoint) => { const entry = getRollupEntry(entrypoint); const plugins = [entrypointGroupGlobals(entrypoint)]; let iifeReturnValueName = safeVarName(entrypoint.name); if (entrypoint.type === "content-script-style" || entrypoint.type === "unlisted-style") plugins.push(cssEntrypoints(entrypoint, wxtConfig)); if (entrypoint.type === "content-script" || entrypoint.type === "unlisted-script") { if (typeof entrypoint.options.globalName === "string") iifeReturnValueName = entrypoint.options.globalName; else if (typeof entrypoint.options.globalName === "function") iifeReturnValueName = entrypoint.options.globalName(entrypoint); if (!entrypoint.options.globalName) plugins.push(iifeAnonymous(iifeReturnValueName)); else plugins.push(iifeFooter(iifeReturnValueName)); } return { mode: wxtConfig.mode, plugins, build: { lib: { entry, formats: ["iife"], name: iifeReturnValueName, fileName: entrypoint.name }, rollupOptions: { output: { entryFileNames: getEntrypointBundlePath(entrypoint, wxtConfig.outDir, ".js"), assetFileNames: (assetInfo) => { if (entrypoint.type === "content-script" && getRollupAssetNames(assetInfo).some((name) => name.endsWith("css"))) return `content-scripts/${entrypoint.name}.[ext]`; else return `assets/${entrypoint.name}.[ext]`; } } } }, define: { "process.env.NODE_ENV": JSON.stringify(wxtConfig.mode) } }; }; /** * Return the basic config for building multiple entrypoints in [multi-page * mode](https://vitejs.dev/guide/build.html#multi-page-app). */ const getMultiPageConfig = (entrypoints) => { const htmlEntrypoints = new Set(entrypoints.filter(isHtmlEntrypoint).map((e) => e.name)); return { mode: wxtConfig.mode, plugins: [entrypointGroupGlobals(entrypoints)], build: { rollupOptions: { input: entrypoints.reduce((input, entry) => { input[entry.name] = getRollupEntry(entry); return input; }, {}), output: { chunkFileNames: "chunks/[name]-[hash].js", entryFileNames: ({ name }) => { if (htmlEntrypoints.has(name)) return "chunks/[name]-[hash].js"; return "[name].js"; }, assetFileNames: "assets/[name]-[hash].[ext]" } } } }; }; /** * Return the basic config for building a single CSS entrypoint in [multi-page * mode](https://vitejs.dev/guide/build.html#multi-page-app). */ const getCssConfig = (entrypoint) => { return { mode: wxtConfig.mode, plugins: [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 createImporterEnvironment = async (paths) => { const baseConfig = await getBaseConfig({ excludeAnalysisPlugin: true }); baseConfig.optimizeDeps ??= {}; baseConfig.optimizeDeps.noDiscovery = true; baseConfig.optimizeDeps.include = []; const envConfig = { plugins: paths.map((path) => removeEntrypointMainFunction(wxtConfig, path)) }; const importerConfig = vite.mergeConfig(baseConfig, envConfig); const config = await vite.resolveConfig(vite.mergeConfig(importerConfig || {}, { configFile: false, envDir: false, cacheDir: process.cwd(), environments: { inline: { consumer: "server", dev: { moduleRunnerTransform: true }, resolve: { external: true, mainFields: [], conditions: ["node"] } } } }), "serve"); const environment = vite.createRunnableDevEnvironment("inline", config, { runnerOptions: { hmr: { logger: false } }, hot: false }); await environment.init(); return environment; }; function 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 [module] = await this.importEntrypoints([path]); return module; }, async importEntrypoints(paths) { const context = createExtensionEnvironment(); const environment = await createImporterEnvironment(paths); try { return await context.run(async () => await Promise.all(paths.map(async (path) => { const module = await environment.runner.import(path); requireDefaultExport(path, module); return module.default; }))); } finally { await environment.close(); } }, async build(group) { let entryConfig; if (Array.isArray(group)) entryConfig = getMultiPageConfig(group); else if (group.type === "content-script-style" || group.type === "unlisted-style") entryConfig = getCssConfig(group); else entryConfig = getLibModeConfig(group); const buildConfig = vite.mergeConfig(await getBaseConfig(), entryConfig); await hooks.callHook("vite:build:extendConfig", toArray(group), buildConfig); return { entrypoints: group, chunks: await moveHtmlFiles(wxtConfig, group, getBuildOutputChunks(await vite.build(buildConfig))) }; }, 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); return { 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); } }; } }; } function getRunnerProfileWatchIgnores(wxtConfig) { const root = normalizePath(wxtConfig.root); const chromiumArgProfiles = extractPathArgs(wxtConfig.webExt.config?.chromiumArgs, "--user-data-dir"); const firefoxArgProfiles = extractPathArgs(wxtConfig.webExt.config?.firefoxArgs, "-profile"); const profiles = [ wxtConfig.webExt.config?.chromiumProfile, wxtConfig.webExt.config?.firefoxProfile, ...chromiumArgProfiles, ...firefoxArgProfiles ].filter((profile) => typeof profile === "string"); return Array.from(new Set(profiles.map((profile) => normalizePath(resolve(wxtConfig.root, profile))).filter((profilePath) => profilePath !== root).map((profilePath) => `${profilePath}/**`))); } function extractPathArgs(args, flag) { if (!args?.length) return []; const paths = []; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg.startsWith(`${flag}=`)) { const value = arg.slice(flag.length + 1).trim(); if (value) paths.push(value); continue; } if (arg === flag) { const nextValue = args[i + 1]?.trim(); if (nextValue) paths.push(nextValue); i += 1; } } return paths; } function getRollupAssetNames(assetInfo) { if (Array.isArray(assetInfo.names)) return assetInfo.names; return assetInfo.name ? [assetInfo.name] : []; } 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; } /** * Returns the input module ID (virtual or real file) for an entrypoint. The * returned string should be passed as an input to rollup. */ 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) return `${`virtual:wxt-${virtualEntrypointType}-entrypoint`}?${entrypoint.inputPath}`; return entrypoint.inputPath; } /** * Ensures the HTML files output by a multi-page build are in the correct * location. This does two things: * * 1. Moves the HTML files to their final location at * `<outDir>/<entrypoint.name>.html`. * 2. Updates the bundle so it summarizes the files correctly in the returned build * output. * * Assets (JS and CSS) are output to the `<outDir>/assets` directory, and don't * need to be modified. HTML files access them via absolute URLs, so we don't * need to update any import paths in the HTML files either. */ async function moveHtmlFiles(config, group, chunks) { if (!Array.isArray(group)) return chunks; const entryMap = group.reduce((map, entry) => { const a = normalizePath(relative(config.root, entry.inputPath)); map[a] = entry; return map; }, {}); const movedChunks = await Promise.all(chunks.map(async (chunk) => { if (!chunk.fileName.endsWith(".html")) return chunk; const entry = entryMap[chunk.fileName]; const oldBundlePath = chunk.fileName; const newBundlePath = getEntrypointBundlePath(entry, config.outDir, extname(chunk.fileName)); const oldAbsPath = join(config.outDir, oldBundlePath); const newAbsPath = join(config.outDir, newBundlePath); await mkdir(dirname(newAbsPath), { recursive: true }); await rename(oldAbsPath, newAbsPath); return { ...chunk, fileName: newBundlePath }; })); await removeEmptyDirs(config.outDir); return movedChunks; } /** Recursively remove all directories that are empty/ */ async function removeEmptyDirs(dir) { const files = await readdir(dir); for (const file of files) { const filePath = join(dir, file); if ((await stat(filePath)).isDirectory()) await removeEmptyDirs(filePath); } try { await rmdir(dir); } catch {} } function isRolldownVersion(version) { return Number(version.split(".")[0]) >= 8; } //#endregion export { createViteBuilder };