UNPKG

wxt

Version:

⚡ Next-gen Web Extension Framework

444 lines (443 loc) 15.8 kB
import { builtinModules } from "../builtin-modules/index.mjs"; import { normalizePath } from "./utils/paths.mjs"; import "./utils/index.mjs"; import { createFsCache } from "./utils/cache.mjs"; import { getEslintVersion } from "./utils/eslint.mjs"; import { safeStringToNumber } from "./utils/number.mjs"; import { loadEnv } from "./utils/env.mjs"; import { createSafariRunner } from "./runners/safari.mjs"; import { createWslRunner } from "./runners/wsl.mjs"; import { createManualRunner } from "./runners/manual.mjs"; import { pathExists } from "./utils/fs.mjs"; import { glob } from "tinyglobby"; import path from "node:path"; import { loadConfig } from "c12"; import { resolve as resolve$1 } from "import-meta-resolve"; import consola, { LogLevels } from "consola"; import defu from "defu"; import { getPort } from "get-port-please"; import { fileURLToPath, pathToFileURL } from "node:url"; import isWsl from "is-wsl"; //#region src/core/resolve-config.ts /** * Given an inline config, discover the config file if necessary, merge the * results, resolve any relative paths, and apply any defaults. * * Inline config always has priority over user config. Cli flags are passed as * inline config if set. If unset, undefined is passed in, letting this function * decide default values. */ async function resolveConfig(inlineConfig, command) { let userConfig = {}; let userConfigMetadata; if (inlineConfig.configFile !== false) { const { config: loadedConfig, ...metadata } = await loadConfig({ configFile: inlineConfig.configFile, name: "wxt", cwd: inlineConfig.root ?? process.cwd() }); userConfig = loadedConfig ?? {}; userConfigMetadata = metadata; } const mergedConfig = await mergeInlineConfig(inlineConfig, userConfig); const debug = mergedConfig.debug ?? false; const logger = mergedConfig.logger ?? consola; if (debug) logger.level = LogLevels.debug; const browser = mergedConfig.browser ?? "chrome"; const targetBrowsers = mergedConfig.targetBrowsers ?? []; if (targetBrowsers.length > 0 && !targetBrowsers.includes(browser)) throw new Error(`Current target browser \`${browser}\` is not in your \`targetBrowsers\` list!`); const manifestVersion = mergedConfig.manifestVersion ?? (browser === "firefox" || browser === "safari" ? 2 : 3); const mode = mergedConfig.mode ?? COMMAND_MODES[command]; const env = { browser, command, manifestVersion, mode }; loadEnv(mode, browser); const root = path.resolve(inlineConfig.root ?? userConfig.root ?? process.cwd()); const wxtDir = path.resolve(root, ".wxt"); const wxtModuleDir = resolveWxtModuleDir(); const srcDir = path.resolve(root, mergedConfig.srcDir ?? root); const entrypointsDir = path.resolve(srcDir, mergedConfig.entrypointsDir ?? "entrypoints"); if (await isDirMissing(entrypointsDir)) logMissingDir(logger, "Entrypoints", entrypointsDir); const modulesDir = path.resolve(root, mergedConfig.modulesDir ?? "modules"); const filterEntrypoints = mergedConfig.filterEntrypoints?.length ? new Set(mergedConfig.filterEntrypoints) : void 0; const publicDir = path.resolve(root, mergedConfig.publicDir ?? "public"); const typesDir = path.resolve(wxtDir, "types"); const outBaseDir = path.resolve(root, mergedConfig.outDir ?? ".output"); const modeSuffix = { production: "", development: "-dev" }[mode] ?? `-${mode}`; const outDirTemplate = (mergedConfig.outDirTemplate ?? `${browser}-mv${manifestVersion}${modeSuffix}`).replaceAll("{{browser}}", browser).replaceAll("{{manifestVersion}}", manifestVersion.toString()).replaceAll("{{modeSuffix}}", modeSuffix).replaceAll("{{mode}}", mode).replaceAll("{{command}}", command); const outDir = path.resolve(outBaseDir, outDirTemplate); const reloadCommand = mergedConfig.dev?.reloadCommand ?? "Alt+R"; const webExt = await loadConfig({ name: "web-ext", cwd: root, globalRc: true, rcFile: ".webextrc", overrides: inlineConfig.webExt, defaults: userConfig.webExt }); const alias = Object.fromEntries(Object.entries({ ...mergedConfig.alias, "@": srcDir, "~": srcDir, "@@": root, "~~": root }).map(([key, value]) => [key, path.resolve(root, value)])); let devServerConfig; if (command === "serve") { const host = mergedConfig.dev?.server?.host ?? "localhost"; let port = mergedConfig.dev?.server?.port; const origin = mergedConfig.dev?.server?.origin ?? "localhost"; const strictPort = mergedConfig.dev?.server?.strictPort ?? false; if (port == null || !isFinite(port)) port = await getPort({ host, port: 3e3, portRange: [3001, 3010] }); else if (!strictPort) port = await getPort({ host, port }); const originWithProtocolAndPort = [ origin.match(/^https?:\/\//) ? "" : "http://", origin, origin.match(/:[0-9]+$/) ? "" : `:${port}` ].join(""); devServerConfig = { host, port, origin: originWithProtocolAndPort, strictPort, watchDebounce: safeStringToNumber(process.env.WXT_WATCH_DEBOUNCE) ?? 800 }; } const userModules = await resolveWxtUserModules(root, modulesDir, mergedConfig.modules); const moduleOptions = userModules.reduce((map, module) => { if (module.configKey) map[module.configKey] = mergedConfig[module.configKey]; return map; }, {}); return { browser, targetBrowsers, command, debug, entrypointsDir, modulesDir, filterEntrypoints, env, fsCache: createFsCache(wxtDir), imports: await getUnimportOptions(wxtDir, srcDir, logger, mergedConfig), logger, manifest: await resolveManifestConfig(env, mergedConfig.manifest), manifestVersion, mode, outBaseDir, outDir, publicDir, wxtModuleDir, root, webExt, runner: command === "serve" ? await resolveRunner(browser, logger, mergedConfig) : createManualRunner(), srcDir, typesDir, wxtDir, zip: resolveZipConfig(root, browser, outBaseDir, mergedConfig), analysis: resolveAnalysisConfig(root, mergedConfig), userConfigMetadata: userConfigMetadata ?? {}, alias, experimental: defu(mergedConfig.experimental, {}), suppressWarnings: mergedConfig.suppressWarnings ?? {}, watchOptions: mergedConfig.watchOptions ?? {}, dev: { server: devServerConfig, reloadCommand }, hooks: mergedConfig.hooks ?? {}, vite: mergedConfig.vite ?? (() => ({})), builtinModules, userModules, plugins: [], ...moduleOptions }; } async function resolveManifestConfig(env, manifest) { return typeof manifest === "function" ? await manifest(env) : await (manifest ?? {}); } /** * Merge the inline config and user config. Inline config is given priority. * Defaults are not applied here. */ async function mergeInlineConfig(inlineConfig, userConfig) { const imports = inlineConfig.imports === false || userConfig.imports === false ? false : userConfig.imports == null && inlineConfig.imports == null ? void 0 : defu(inlineConfig.imports ?? {}, userConfig.imports ?? {}); const manifest = async (env) => { const user = await resolveManifestConfig(env, userConfig.manifest); return defu(await resolveManifestConfig(env, inlineConfig.manifest), user); }; const merged = defu(inlineConfig, userConfig); const builderConfig = await mergeBuilderConfig(merged.logger ?? consola, inlineConfig, userConfig); return { ...merged, imports, manifest, ...builderConfig }; } function resolveZipConfig(root, browser, outBaseDir, mergedConfig) { const downloadedPackagesDir = path.resolve(root, ".wxt/local_modules"); return { name: void 0, sourcesTemplate: "{{name}}-{{packageVersion}}-sources{{modeSuffix}}.zip", artifactTemplate: "{{name}}-{{packageVersion}}-{{browser}}{{modeSuffix}}.zip", sourcesRoot: root, includeSources: mergedConfig.zip?.includeSources ?? ["**/*"], compressionLevel: 9, ...mergedConfig.zip, zipSources: mergedConfig.zip?.zipSources ?? ["firefox", "opera"].includes(browser), exclude: mergedConfig.zip?.exclude ?? [], excludeSources: [ "**/node_modules", "**/web-ext.config.ts", "**/__tests__/**", "**/*.+(test|spec).?(c|m)+(j|t)s?(x)", `${path.relative(root, outBaseDir)}/**`, ...mergedConfig.zip?.excludeSources ?? [] ], dotSources: mergedConfig.zip?.dotSources ?? false, downloadPackages: mergedConfig.zip?.downloadPackages ?? [], downloadedPackagesDir }; } function resolveAnalysisConfig(root, mergedConfig) { const analysisOutputFile = path.resolve(root, mergedConfig.analysis?.outputFile ?? "stats.html"); const analysisOutputDir = path.dirname(analysisOutputFile); const analysisOutputName = path.parse(analysisOutputFile).name; return { enabled: mergedConfig.analysis?.enabled ?? false, open: mergedConfig.analysis?.open ?? false, template: mergedConfig.analysis?.template ?? "treemap", outputFile: analysisOutputFile, outputDir: analysisOutputDir, outputName: analysisOutputName, keepArtifacts: mergedConfig.analysis?.keepArtifacts ?? false }; } async function getUnimportOptions(wxtDir, srcDir, logger, config) { const disabled = config.imports === false; const eslintrc = await getUnimportEslintOptions(logger, wxtDir, config.imports); const invalidExports = ["options"]; const defineImportsAndTypes = (imports, typeImports) => [...imports, ...typeImports.map((name) => ({ name, type: true }))]; const defaultOptions = { imports: [{ name: "fakeBrowser", from: "wxt/testing/fake-browser" }], presets: [ { from: "wxt/browser", imports: defineImportsAndTypes(["browser"], ["Browser"]) }, { from: "wxt/utils/storage", imports: defineImportsAndTypes(["storage"], [ "StorageArea", "WxtStorage", "WxtStorageItem", "StorageArea", "StorageItemKey", "StorageAreaChanges", "MigrationError" ]) }, { from: "wxt/utils/app-config", imports: defineImportsAndTypes(["getAppConfig", "useAppConfig"], []) }, { from: "wxt/utils/content-script-context", imports: defineImportsAndTypes(["ContentScriptContext"], ["WxtWindowEventMap"]) }, { from: "wxt/utils/content-script-ui/iframe", imports: defineImportsAndTypes(["createIframeUi"], ["IframeContentScriptUi", "IframeContentScriptUiOptions"]), ignore: invalidExports }, { from: "wxt/utils/content-script-ui/integrated", imports: defineImportsAndTypes(["createIntegratedUi"], ["IntegratedContentScriptUi", "IntegratedContentScriptUiOptions"]), ignore: invalidExports }, { from: "wxt/utils/content-script-ui/shadow-root", imports: defineImportsAndTypes(["createShadowRootUi"], ["ShadowRootContentScriptUi", "ShadowRootContentScriptUiOptions"]), ignore: invalidExports }, { from: "wxt/utils/content-script-ui/types", imports: defineImportsAndTypes([], [ "ContentScriptUi", "ContentScriptUiOptions", "ContentScriptOverlayAlignment", "ContentScriptAppendMode", "ContentScriptInlinePositioningOptions", "ContentScriptOverlayPositioningOptions", "ContentScriptModalPositioningOptions", "ContentScriptPositioningOptions", "ContentScriptAnchoredOptions", "AutoMountOptions", "StopAutoMount", "AutoMount" ]) }, { from: "wxt/utils/define-app-config", imports: defineImportsAndTypes(["defineAppConfig"], ["WxtAppConfig"]) }, { from: "wxt/utils/define-background", imports: defineImportsAndTypes(["defineBackground"], []) }, { from: "wxt/utils/define-content-script", imports: defineImportsAndTypes(["defineContentScript"], []) }, { from: "wxt/utils/define-unlisted-script", imports: defineImportsAndTypes(["defineUnlistedScript"], []) }, { from: "wxt/utils/define-wxt-plugin", imports: defineImportsAndTypes(["defineWxtPlugin"], []) }, { from: "wxt/utils/inject-script", imports: defineImportsAndTypes(["injectScript"], ["ScriptPublicPath", "InjectScriptOptions"]), ignore: invalidExports }, { from: "wxt/utils/match-patterns", imports: defineImportsAndTypes(["InvalidMatchPattern", "MatchPattern"], []) } ], virtualImports: ["#imports"], debugLog: logger.debug, warn: logger.warn, dirsScanOptions: { cwd: srcDir }, eslintrc, dirs: disabled ? [] : [ "components", "composables", "hooks", "utils" ], disabled }; return defu(config.imports ?? {}, defaultOptions); } async function getUnimportEslintOptions(logger, wxtDir, options) { const inlineEnabled = options === false ? false : options?.eslintrc?.enabled ?? "auto"; const version = await getEslintVersion(); const major = parseInt(version[0]); let enabled; switch (inlineEnabled) { case "auto": case true: if (isNaN(major)) { if (inlineEnabled === true) logger.warn("Could not determine installed ESLint version, `eslint-auto-imports.mjs` not generated"); enabled = false; } else if (major <= 8) enabled = 8; else if (major >= 9) enabled = 9; else enabled = false; break; default: enabled = inlineEnabled; } return { enabled, filePath: path.resolve(wxtDir, enabled === 8 ? "eslintrc-auto-import.json" : "eslint-auto-imports.mjs"), globalsPropValue: true }; } /** Returns the path to `node_modules/wxt`. */ function resolveWxtModuleDir() { const url = resolve$1("wxt", import.meta.url); return path.resolve(fileURLToPath(url), "../.."); } async function isDirMissing(dir) { return !await pathExists(dir); } function logMissingDir(logger, name, expected) { logger.warn(`${name} directory not found: ./${normalizePath(path.relative(process.cwd(), expected))}`); } /** Map of `ConfigEnv` commands to their default modes. */ const COMMAND_MODES = { build: "production", serve: "development" }; async function mergeBuilderConfig(logger, inlineConfig, userConfig) { const vite = await import("vite").catch((err) => { logger.debug("Failed to import vite:", err); }); if (vite) return { vite: async (env) => { const [resolvedInlineConfig = {}, resolvedUserConfig = {}] = await Promise.all([inlineConfig.vite?.(env), userConfig.vite?.(env)]); return vite.mergeConfig(resolvedUserConfig, resolvedInlineConfig); } }; throw Error("Builder not found. Make sure vite is installed."); } async function resolveWxtUserModules(root, modulesDir, modules = []) { const importer = pathToFileURL(path.join(root, "index.js")).href; const npmModules = await Promise.all(modules.map(async (moduleId) => { const mod = await import(resolve$1(moduleId, importer)); if (mod.default == null) throw Error("Module missing default export: " + moduleId); return { ...mod.default, type: "node_module", id: moduleId }; })); const localModulePaths = await glob(["*.[tj]s", "*/index.[tj]s"], { cwd: modulesDir, onlyFiles: true, expandDirectories: false }).catch(() => []); localModulePaths.sort(); const localModules = await Promise.all(localModulePaths.map(async (file) => { const absolutePath = normalizePath(path.resolve(modulesDir, file)); const { config } = await loadConfig({ configFile: absolutePath, globalRc: false, rcFile: false, packageJson: false, envName: false, dotenv: false }); if (config == null) throw Error(`No config found for ${file}. Did you forget to add a default export?`); config.name ??= file; return { ...config, type: "local", id: absolutePath }; })); return [...npmModules, ...localModules]; } async function resolveRunner(browser, logger, mergedConfig) { if (browser === "safari") return createSafariRunner(); if (isWsl) return createWslRunner(); try { const { createWebExtRunner } = await import("./runners/web-ext.mjs"); return mergedConfig.webExt?.disabled ? createManualRunner() : createWebExtRunner(); } catch (err) { if (err?.code !== "ERR_MODULE_NOT_FOUND") throw err; logger.debug("Error loading the web-ext runner", err); } return createManualRunner(); } //#endregion export { resolveConfig };