wxt
Version:
⚡ Next-gen Web Extension Framework
493 lines (492 loc) • 16.2 kB
JavaScript
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];
}