UNPKG

wxt

Version:

⚡ Next-gen Web Extension Framework

391 lines (389 loc) 12 kB
import { relative, resolve } from "path"; import fs from "fs-extra"; import { minimatch } from "minimatch"; import { parseHTML } from "linkedom"; import JSON5 from "json5"; import glob from "fast-glob"; import { getEntrypointName, isHtmlEntrypoint, isJsEntrypoint, resolvePerBrowserOptions } from "../../utils/entrypoints.mjs"; import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from "../../utils/constants.mjs"; import { CSS_EXTENSIONS_PATTERN } from "../../utils/paths.mjs"; import pc from "picocolors"; import { wxt } from "../../wxt.mjs"; import { camelCase } from "scule"; export async function findEntrypoints() { await fs.mkdir(wxt.config.wxtDir, { recursive: true }); try { await fs.writeJson( resolve(wxt.config.wxtDir, "tsconfig.json"), {}, { flag: "wx" } ); } catch (err) { if (!(err instanceof Error) || !("code" in err) || err.code !== "EEXIST") { throw err; } } const relativePaths = await glob(Object.keys(PATH_GLOB_TO_TYPE_MAP), { cwd: wxt.config.entrypointsDir }); relativePaths.sort(); const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP); const entrypointInfos = relativePaths.reduce((results, relativePath) => { const inputPath = resolve(wxt.config.entrypointsDir, relativePath); const name = getEntrypointName(wxt.config.entrypointsDir, inputPath); const matchingGlob = pathGlobs.find( (glob2) => minimatch(relativePath, glob2) ); if (matchingGlob) { const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob]; results.push({ name, inputPath, type }); } return results; }, []).filter(({ name, inputPath }, _, entrypointInfos2) => { if (inputPath.endsWith(".html")) return true; const isIndexFile = /index\..+$/.test(inputPath); if (!isIndexFile) return true; const hasIndexHtml = entrypointInfos2.some( (entry) => entry.name === name && entry.inputPath.endsWith("index.html") ); if (hasIndexHtml) return false; return true; }); await wxt.hooks.callHook("entrypoints:found", wxt, entrypointInfos); preventNoEntrypoints(entrypointInfos); preventDuplicateEntrypointNames(entrypointInfos); let hasBackground = false; const entrypointOptions = await importEntrypoints(entrypointInfos); const entrypointsWithoutSkipped = await Promise.all( entrypointInfos.map(async (info) => { const { type } = info; const options = entrypointOptions[info.inputPath] ?? {}; switch (type) { case "popup": return await getPopupEntrypoint(info, options); case "sidepanel": return await getSidepanelEntrypoint(info, options); case "options": return await getOptionsEntrypoint(info, options); case "background": hasBackground = true; return await getBackgroundEntrypoint(info, options); case "content-script": return await getContentScriptEntrypoint(info, options); case "unlisted-page": return await getUnlistedPageEntrypoint(info, options); case "unlisted-script": return await getUnlistedScriptEntrypoint(info, options); case "content-script-style": return { ...info, type, outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR), options: { include: options.include, exclude: options.exclude } }; default: return { ...info, type, outputDir: wxt.config.outDir, options: { include: options.include, exclude: options.exclude } }; } }) ); if (wxt.config.command === "serve" && !hasBackground) { entrypointsWithoutSkipped.push( await getBackgroundEntrypoint( { inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID, name: "background", type: "background" }, {} ) ); } const entrypoints = entrypointsWithoutSkipped.map((entry) => ({ ...entry, skipped: isEntrypointSkipped(entry) })); await wxt.hooks.callHook("entrypoints:resolved", wxt, entrypoints); wxt.logger.debug("All entrypoints:", entrypoints); const skippedEntrypointNames = entrypoints.filter((item) => item.skipped).map((item) => item.name); if (skippedEntrypointNames.length) { wxt.logger.warn( [ "The following entrypoints have been skipped:", ...skippedEntrypointNames.map( (item) => `${pc.dim("-")} ${pc.cyan(item)}` ) ].join("\n") ); } return entrypoints; } async function importEntrypoints(infos) { const resMap = {}; const htmlInfos = infos.filter((info) => isHtmlEntrypoint(info)); const jsInfos = infos.filter((info) => isJsEntrypoint(info)); await Promise.all([ // HTML ...htmlInfos.map(async (info) => { const res = await importHtmlEntrypoint(info); resMap[info.inputPath] = res; }), // JS (async () => { const res = await wxt.builder.importEntrypoints( jsInfos.map((info) => info.inputPath) ); res.forEach((res2, i) => { resMap[jsInfos[i].inputPath] = res2; }); })() // CSS - never has options ]); return resMap; } async function importHtmlEntrypoint(info) { const content = await fs.readFile(info.inputPath, "utf-8"); const { document } = parseHTML(content); const metaTags = document.querySelectorAll("meta"); const res = { title: document.querySelector("title")?.textContent || void 0 }; metaTags.forEach((tag) => { const name = tag.name; if (!name.startsWith("manifest.")) return; const key = camelCase(name.slice(9)); try { res[key] = JSON5.parse(tag.content); } catch { res[key] = tag.content; } }); return res; } function preventDuplicateEntrypointNames(files) { const namesToPaths = files.reduce( (map, { name, inputPath }) => { map[name] ??= []; map[name].push(inputPath); return map; }, {} ); const errorLines = Object.entries(namesToPaths).reduce( (lines, [name, absolutePaths]) => { if (absolutePaths.length > 1) { lines.push(`- ${name}`); absolutePaths.forEach((absolutePath) => { lines.push(` - ${relative(wxt.config.root, absolutePath)}`); }); } return lines; }, [] ); if (errorLines.length > 0) { const errorContent = errorLines.join("\n"); throw Error( `Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed. ${errorContent}` ); } } function preventNoEntrypoints(files) { if (files.length === 0) { throw Error(`No entrypoints found in ${wxt.config.entrypointsDir}`); } } async function getPopupEntrypoint(info, options) { const stictOptions = resolvePerBrowserOptions( { browserStyle: options.browserStyle, exclude: options.exclude, include: options.include, defaultIcon: options.defaultIcon, defaultTitle: options.title, mv2Key: options.type }, wxt.config.browser ); if (stictOptions.mv2Key && stictOptions.mv2Key !== "page_action") stictOptions.mv2Key = "browser_action"; return { type: "popup", name: "popup", options: stictOptions, inputPath: info.inputPath, outputDir: wxt.config.outDir }; } async function getOptionsEntrypoint(info, options) { return { type: "options", name: "options", options: resolvePerBrowserOptions( { browserStyle: options.browserStyle, chromeStyle: options.chromeStyle, exclude: options.exclude, include: options.include, openInTab: options.openInTab }, wxt.config.browser ), inputPath: info.inputPath, outputDir: wxt.config.outDir }; } async function getUnlistedPageEntrypoint(info, options) { return { type: "unlisted-page", name: info.name, inputPath: info.inputPath, outputDir: wxt.config.outDir, options: { include: options.include, exclude: options.exclude } }; } async function getUnlistedScriptEntrypoint({ inputPath, name }, options) { return { type: "unlisted-script", name, inputPath, outputDir: wxt.config.outDir, options: resolvePerBrowserOptions( { include: options.include, exclude: options.exclude }, wxt.config.browser ) }; } async function getBackgroundEntrypoint({ inputPath, name }, options) { const strictOptions = resolvePerBrowserOptions( { include: options.include, exclude: options.exclude, persistent: options.persistent, type: options.type }, wxt.config.browser ); if (wxt.config.manifestVersion !== 3) { delete strictOptions.type; } return { type: "background", name, inputPath, outputDir: wxt.config.outDir, options: strictOptions }; } async function getContentScriptEntrypoint({ inputPath, name }, options) { return { type: "content-script", name, inputPath, outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR), options: resolvePerBrowserOptions( options, wxt.config.browser ) }; } async function getSidepanelEntrypoint(info, options) { return { type: "sidepanel", name: info.name, options: resolvePerBrowserOptions( { browserStyle: options.browserStyle, exclude: options.exclude, include: options.include, defaultIcon: options.defaultIcon, defaultTitle: options.title, openAtInstall: options.openAtInstall }, wxt.config.browser ), inputPath: info.inputPath, outputDir: wxt.config.outDir }; } function isEntrypointSkipped(entry) { if (wxt.config.filterEntrypoints != null) { return !wxt.config.filterEntrypoints.has(entry.name); } const { include, exclude } = entry.options; if (include?.length && exclude?.length) { wxt.logger.warn( `The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint skipped.` ); return true; } if (exclude?.length && !include?.length) { return exclude.includes(wxt.config.browser); } if (include?.length && !exclude?.length) { return !include.includes(wxt.config.browser); } return false; } const PATH_GLOB_TO_TYPE_MAP = { "sandbox.html": "sandbox", "sandbox/index.html": "sandbox", "*.sandbox.html": "sandbox", "*.sandbox/index.html": "sandbox", "bookmarks.html": "bookmarks", "bookmarks/index.html": "bookmarks", "history.html": "history", "history/index.html": "history", "newtab.html": "newtab", "newtab/index.html": "newtab", "sidepanel.html": "sidepanel", "sidepanel/index.html": "sidepanel", "*.sidepanel.html": "sidepanel", "*.sidepanel/index.html": "sidepanel", "devtools.html": "devtools", "devtools/index.html": "devtools", "background.[jt]s": "background", "background/index.[jt]s": "background", [VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: "background", "content.[jt]s?(x)": "content-script", "content/index.[jt]s?(x)": "content-script", "*.content.[jt]s?(x)": "content-script", "*.content/index.[jt]s?(x)": "content-script", [`content.${CSS_EXTENSIONS_PATTERN}`]: "content-script-style", [`*.content.${CSS_EXTENSIONS_PATTERN}`]: "content-script-style", [`content/index.${CSS_EXTENSIONS_PATTERN}`]: "content-script-style", [`*.content/index.${CSS_EXTENSIONS_PATTERN}`]: "content-script-style", "popup.html": "popup", "popup/index.html": "popup", "options.html": "options", "options/index.html": "options", "*.html": "unlisted-page", "*/index.html": "unlisted-page", "*.[jt]s?(x)": "unlisted-script", "*/index.[jt]s?(x)": "unlisted-script", [`*.${CSS_EXTENSIONS_PATTERN}`]: "unlisted-style", [`*/index.${CSS_EXTENSIONS_PATTERN}`]: "unlisted-style" }; const CONTENT_SCRIPT_OUT_DIR = "content-scripts";