wxt
Version:
⚡ Next-gen Web Extension Framework
214 lines (210 loc) • 6.69 kB
JavaScript
import fs from "fs-extra";
import { dirname, relative, resolve } from "node:path";
import { getEntrypointBundlePath, isHtmlEntrypoint } from "./utils/entrypoints.mjs";
import { getEntrypointGlobals, getGlobals } from "./utils/globals.mjs";
import { normalizePath } from "./utils/paths.mjs";
import path from "node:path";
import { parseI18nMessages } from "./utils/i18n.mjs";
import { writeFileIfDifferent, getPublicFiles } from "./utils/fs.mjs";
import { wxt } from "./wxt.mjs";
export async function generateWxtDir(entrypoints) {
await fs.ensureDir(wxt.config.typesDir);
const entries = [
// Hard-coded 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 fs.ensureDir(dirname(file.path));
await writeFileIfDifferent(file.path, file.text);
})
);
}
async function getPathsDeclarationEntry(entrypoints) {
const paths = entrypoints.map(
(entry) => getEntrypointBundlePath(
entry,
wxt.config.outDir,
isHtmlEntrypoint(entry) ? ".html" : ".js"
)
).concat(await getPublicFiles());
await wxt.hooks.callHook("prepare:publicPaths", wxt, paths);
const unions = [
` | ""`,
` | "/"`,
...paths.map(normalizePath).sort().map((path2) => ` | "/${path2}"`)
].join("\n");
const template = `// 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;
}
}
`;
return {
path: "types/paths.d.ts",
text: template.replace("{{ union }}", unions || " | never"),
tsReference: true
};
}
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 fs.exists(defaultLocalePath)) {
const content = JSON.parse(await fs.readFile(defaultLocalePath, "utf-8"));
messages = parseI18nMessages(content);
} 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}"`);
}
const comment = commentLines.length > 0 ? `/**
${commentLines.map((line) => ` * ${line}`.trimEnd()).join("\n")}
*/
` : "";
return ` ${comment}getMessage(
messageName: ${keyType},
substitutions?: string | string[],
options?: GetMessageOptions,
): string;`;
};
const overrides = [
// Generate individual overloads for each message so JSDoc contains description and base translation.
...messages.map(
(message) => renderGetMessageOverload(
`"${message.name}"`,
message.description,
message.message
)
),
// Include a final union-based override so TS accepts valid string templates or concatenations
// ie: browser.i18n.getMessage(`some_enum_${enumValue}`)
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() {
const globals = [...getGlobals(wxt.config), ...getEntrypointGlobals("")];
return {
path: "types/globals.d.ts",
text: [
"// Generated by wxt",
"interface ImportMetaEnv {",
...globals.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 = (path2) => {
const res = normalizePath(relative(dir, path2));
if (res.startsWith(".") || res.startsWith("/")) return res;
return "./" + res;
};
const paths = Object.entries(wxt.config.alias).flatMap(([alias, absolutePath]) => {
const aliasPath = getTsconfigPath(absolutePath);
return [
`"${alias}": ["${aliasPath}"]`,
`"${alias}/*": ["${aliasPath}/*"]`
];
}).map((line) => ` ${line}`).join(",\n");
const text = `{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"paths": {
${paths}
}
},
"include": [
"${getTsconfigPath(wxt.config.root)}/**/*",
"./wxt.d.ts"
],
"exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
}`;
return {
path: "tsconfig.json",
text
};
}