UNPKG

wxt

Version:

⚡ Next-gen Web Extension Framework

214 lines (210 loc) 6.69 kB
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 }; }