wxt
Version:
⚡ Next-gen Web Extension Framework
444 lines (443 loc) • 15.8 kB
JavaScript
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 };