wxt
Version:
⚡ Next-gen Web Extension Framework
202 lines (198 loc) • 7.09 kB
JavaScript
import { normalizePath } from "./utils/paths.mjs";
import { getEntrypointBundlePath, isHtmlEntrypoint } from "./utils/entrypoints.mjs";
import "./utils/index.mjs";
import { getEntrypointGlobals, getGlobals } from "./utils/globals.mjs";
import { wxt } from "./wxt.mjs";
import { getPublicFiles, pathExists, writeFileIfDifferent } from "./utils/fs.mjs";
import { parseI18nMessages } from "./utils/i18n.mjs";
import { mkdir, readFile } from "node:fs/promises";
import path, { dirname, relative, resolve } from "node:path";
//#region src/core/generate-wxt-dir.ts
/**
* Generate and write all the files inside the `InternalConfig.typesDir`
* directory.
*/
async function generateWxtDir(entrypoints) {
await mkdir(wxt.config.typesDir, { recursive: true });
const entries = [{ module: "wxt/vite-builder-env" }];
wxt.config.userModules.forEach((module) => {
if (module.type === "node_module") entries.push({ module: module.id });
});
entries.push(await getPathsDeclarationEntry(entrypoints));
entries.push(await getI18nDeclarationEntry());
entries.push(await getGlobalsDeclarationEntry());
entries.push(await getTsConfigEntry());
await wxt.hooks.callHook("prepare:types", wxt, entries);
entries.push(getMainDeclarationEntry(entries));
const absoluteFileEntries = entries.filter((entry) => "path" in entry).map((entry) => ({
...entry,
path: resolve(wxt.config.wxtDir, entry.path)
}));
await Promise.all(absoluteFileEntries.map(async (file) => {
await mkdir(dirname(file.path), { recursive: true });
await writeFileIfDifferent(file.path, file.text);
}));
}
async function getPathsDeclarationEntry(entrypoints) {
const paths = entrypoints.map((entry) => getEntrypointBundlePath(entry, wxt.config.outDir, getEntrypointPublicExt(entry))).concat(await getPublicFiles());
await wxt.hooks.callHook("prepare:publicPaths", wxt, paths);
const unions = [
` | ""`,
` | "/"`,
...paths.map(normalizePublicPathEntry).sort((a, b) => a.path.localeCompare(b.path) || a.type.localeCompare(b.type)).map(renderPublicPathUnionMember)
].join("\n");
return {
path: "types/paths.d.ts",
text: `// Generated by wxt
import "wxt/browser";
declare module "wxt/browser" {
export type PublicPath =
{{ union }}
type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
export interface WxtRuntime {
getURL(path: PublicPath): string;
getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
}
}
`.replace("{{ union }}", unions || " | never"),
tsReference: true
};
}
function normalizePublicPathEntry(entry) {
if (typeof entry === "string") return {
type: "string",
path: normalizePath(entry)
};
return {
type: entry.type,
path: normalizePath(entry.path)
};
}
function renderPublicPathUnionMember(entry) {
const path = entry.path.startsWith("/") ? entry.path : `/${entry.path}`;
switch (entry.type) {
case "templateLiteral": return ` | \`${path}\``;
case "string": return ` | "${path}"`;
}
}
function getEntrypointPublicExt(entry) {
if (isHtmlEntrypoint(entry)) return ".html";
switch (entry.type) {
case "content-script-style":
case "unlisted-style": return ".css";
default: return ".js";
}
}
async function getI18nDeclarationEntry() {
const defaultLocale = wxt.config.manifest.default_locale;
const template = `// Generated by wxt
import "wxt/browser";
declare module "wxt/browser" {
/**
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
*/
interface GetMessageOptions {
/**
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
*/
escapeLt?: boolean
}
export interface WxtI18n extends I18n.Static {
{{ overrides }}
}
}
`;
const defaultLocalePath = path.resolve(wxt.config.publicDir, "_locales", defaultLocale ?? "", "messages.json");
let messages;
if (await pathExists(defaultLocalePath)) messages = parseI18nMessages(JSON.parse(await readFile(defaultLocalePath, "utf-8")));
else messages = parseI18nMessages({});
const renderGetMessageOverload = (keyType, description, translation) => {
const commentLines = [];
if (description) commentLines.push(...description.split("\n"));
if (translation) {
if (commentLines.length > 0) commentLines.push("");
commentLines.push(`"${translation}"`);
}
return ` ${commentLines.length > 0 ? `/**\n${commentLines.map((line) => ` * ${line}`.trimEnd()).join("\n")}\n */\n ` : ""}getMessage(
messageName: ${keyType},
substitutions?: string | string[],
options?: GetMessageOptions,
): string;`;
};
const overrides = [...messages.map((message) => renderGetMessageOverload(`"${message.name}"`, message.description, message.message)), renderGetMessageOverload(messages.map((message) => `"${message.name}"`).join(" | "))];
return {
path: "types/i18n.d.ts",
text: template.replace("{{ overrides }}", overrides.join("\n")),
tsReference: true
};
}
async function getGlobalsDeclarationEntry() {
return {
path: "types/globals.d.ts",
text: [
"// Generated by wxt",
"interface ImportMetaEnv {",
...[...getGlobals(wxt.config), ...getEntrypointGlobals("")].map((global) => ` readonly ${global.name}: ${global.type};`),
"}",
"interface ImportMeta {",
" readonly env: ImportMetaEnv",
"}",
""
].join("\n"),
tsReference: true
};
}
function getMainDeclarationEntry(references) {
const lines = ["// Generated by wxt"];
references.forEach((ref) => {
if ("module" in ref) return lines.push(`/// <reference types="${ref.module}" />`);
else if (ref.tsReference) {
const absolutePath = resolve(wxt.config.wxtDir, ref.path);
const relativePath = relative(wxt.config.wxtDir, absolutePath);
lines.push(`/// <reference path="./${normalizePath(relativePath)}" />`);
}
});
return {
path: "wxt.d.ts",
text: lines.join("\n") + "\n"
};
}
async function getTsConfigEntry() {
const dir = wxt.config.wxtDir;
const getTsconfigPath = (path) => {
const res = normalizePath(relative(dir, path));
if (res.startsWith(".") || res.startsWith("/")) return res;
return "./" + res;
};
const tsconfig = {
compilerOptions: {
lib: [
"ESNext",
"DOM",
"DOM.Iterable"
],
target: "ESNext",
module: "Preserve",
moduleDetection: "force",
moduleResolution: "Bundler",
allowImportingTsExtensions: true,
verbatimModuleSyntax: true,
noEmit: true,
strict: true,
skipLibCheck: true,
noFallthroughCasesInSwitch: true,
noUncheckedIndexedAccess: true,
noImplicitOverride: true,
paths: Object.fromEntries(Object.entries(wxt.config.alias).map(([alias, absolutePath]) => [alias, getTsconfigPath(absolutePath)]).flatMap(([alias, path]) => [[alias, [path]], [alias + "/*", [path + "/*"]]]))
},
include: [`${getTsconfigPath(wxt.config.root)}/**/*`, "./wxt.d.ts"],
exclude: [getTsconfigPath(wxt.config.root) + "/**/node_modules", getTsconfigPath(wxt.config.outBaseDir)]
};
await wxt.hooks.callHook("prepare:tsconfig", wxt, { tsconfig });
return {
path: "tsconfig.json",
text: JSON.stringify(tsconfig, null, 2)
};
}
//#endregion
export { generateWxtDir };