UNPKG

wxt

Version:

⚡ Next-gen Web Extension Framework

493 lines (492 loc) 16.2 kB
import { loadConfig } from "c12"; import { resolve as esmResolve } from "import-meta-resolve"; import path from "node:path"; import { createFsCache } from "./utils/cache.mjs"; import consola, { LogLevels } from "consola"; import defu from "defu"; import fs from "fs-extra"; import { normalizePath } from "./utils/paths.mjs"; import glob from "fast-glob"; import { builtinModules } from "../builtin-modules/index.mjs"; import { getEslintVersion } from "./utils/eslint.mjs"; import { safeStringToNumber } from "./utils/number.mjs"; import { loadEnv } from "./utils/env.mjs"; import { getPort } from "get-port-please"; import { fileURLToPath, pathToFileURL } from "node:url"; export 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 modeSuffixes = { production: "", development: "-dev" }; const modeSuffix = modeSuffixes[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"; if (inlineConfig.runner != null || userConfig.runner != null) { logger.warn( "`InlineConfig#runner` is deprecated, use `InlineConfig#webExt` instead. See https://wxt.dev/guide/resources/upgrading.html#v0-19-0-rarr-v0-20-0" ); } const runnerConfig = await loadConfig({ name: "web-ext", cwd: root, globalRc: true, rcFile: ".webextrc", overrides: inlineConfig.webExt ?? inlineConfig.runner, defaults: userConfig.webExt ?? userConfig.runner }); 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") { if (mergedConfig.dev?.server?.hostname) logger.warn( `The 'hostname' option is deprecated, please use 'host' or 'origin' depending on your circumstances.` ); const host = mergedConfig.dev?.server?.host ?? mergedConfig.dev?.server?.hostname ?? "localhost"; let port = mergedConfig.dev?.server?.port; const origin = mergedConfig.dev?.server?.origin ?? mergedConfig.dev?.server?.hostname ?? "localhost"; if (port == null || !isFinite(port)) { port = await getPort({ // Passing host required for Mac, unsure of Windows/Linux host, port: 3e3, portRange: [3001, 3010] }); } const originWithProtocolAndPort = [ origin.match(/^https?:\/\//) ? "" : "http://", origin, origin.match(/:[0-9]+$/) ? "" : `:${port}` ].join(""); devServerConfig = { host, port, origin: originWithProtocolAndPort, 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] = // @ts-expect-error 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, runnerConfig, srcDir, typesDir, wxtDir, zip: resolveZipConfig(root, browser, outBaseDir, mergedConfig), analysis: resolveAnalysisConfig(root, mergedConfig), userConfigMetadata: userConfigMetadata ?? {}, alias, experimental: defu(mergedConfig.experimental, {}), 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 ?? {}); } 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); const inline = await resolveManifestConfig(env, inlineConfig.manifest); return defu(inline, user); }; const merged = defu(inlineConfig, userConfig); const builderConfig = await mergeBuilderConfig( merged.logger ?? consola, inlineConfig, userConfig ); return { ...merged, // Custom merge values imports, manifest, ...builderConfig }; } function resolveZipConfig(root, browser, outBaseDir, mergedConfig) { const downloadedPackagesDir = path.resolve(root, ".wxt/local_modules"); return { name: void 0, sourcesTemplate: "{{name}}-{{version}}-sources.zip", artifactTemplate: "{{name}}-{{version}}-{{browser}}.zip", sourcesRoot: root, includeSources: [], compressionLevel: 9, ...mergedConfig.zip, zipSources: mergedConfig.zip?.zipSources ?? ["firefox", "opera"].includes(browser), exclude: mergedConfig.zip?.exclude ?? [], excludeSources: [ "**/node_modules", // WXT files "**/web-ext.config.ts", // Hidden files "**/.*", // Tests "**/__tests__/**", "**/*.+(test|spec).?(c|m)+(j|t)s?(x)", // Output directory `${path.relative(root, outBaseDir)}/**`, // From user ...mergedConfig.zip?.excludeSources ?? [] ], 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(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" }], 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(["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(wxtDir, options) { const inlineEnabled = options === false ? false : options?.eslintrc?.enabled ?? "auto"; let enabled; switch (inlineEnabled) { case "auto": const version = await getEslintVersion(); let major = parseInt(version[0]); if (isNaN(major)) enabled = false; if (major <= 8) enabled = 8; else if (major >= 9) enabled = 9; else enabled = false; break; case true: enabled = 8; break; default: enabled = inlineEnabled; } return { enabled, filePath: path.resolve( wxtDir, enabled === 9 ? "eslint-auto-imports.mjs" : "eslintrc-auto-import.json" ), globalsPropValue: true }; } function resolveWxtModuleDir() { const importer = typeof __filename === "string" ? pathToFileURL(__filename).href : import.meta.url; const url = esmResolve("wxt", importer); return path.resolve(fileURLToPath(url), "../.."); } async function isDirMissing(dir) { return !await fs.exists(dir); } function logMissingDir(logger, name, expected) { logger.warn( `${name} directory not found: ./${normalizePath( path.relative(process.cwd(), expected) )}` ); } const COMMAND_MODES = { build: "production", serve: "development" }; export 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 = await inlineConfig.vite?.(env) ?? {}; const resolvedUserConfig = await userConfig.vite?.(env) ?? {}; return vite.mergeConfig(resolvedUserConfig, resolvedInlineConfig); } }; } throw Error("Builder not found. Make sure vite is installed."); } export 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 resolvedModulePath = esmResolve(moduleId, importer); const mod = await import( /* @vite-ignore */ resolvedModulePath ); 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 }).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]; }