UNPKG

@tomjs/vite-plugin-electron

Version:

A simple vite plugin for electron, supports esm/cjs, support esm in electron v28+

457 lines (450 loc) 14 kB
import { t as ELECTRON_EXIT } from "./electron-BUQU2XNS.mjs"; import fs from "node:fs"; import path from "node:path"; import cloneDeep from "lodash.clonedeep"; import merge from "lodash.merge"; import os from "node:os"; import { cwd } from "node:process"; import shell from "shelljs"; import dayjs from "dayjs"; import { blue, gray, green, red, yellow } from "kolorist"; import cp, { spawn } from "node:child_process"; import electron from "electron"; import { execa } from "execa"; import { build } from "tsdown"; //#region src/constants.ts const PLUGIN_NAME = "tomjs:electron"; //#endregion //#region src/logger.ts /** * 日志 */ var Logger = class { constructor(tag, withTime) { this.tag = PLUGIN_NAME; this.withTime = true; this.tag = `[${tag}]`; this.withTime = withTime ?? true; } getTime() { return `${this.withTime ? dayjs().format("HH:mm:ss") : ""} `; } /** * 调试 */ debug(msg, ...rest) { console.log(`${this.getTime()}${gray(this.tag)}`, msg, ...rest); } /** * 调试日志 等同 debug */ log(msg, ...rest) { this.debug(msg, ...rest); } info(msg, ...rest) { console.log(`${this.getTime()}${blue(this.tag)}`, msg, ...rest); } warn(msg, ...rest) { console.log(`${this.getTime()}${yellow(this.tag)}`, msg, ...rest); } error(msg, ...rest) { console.log(`${this.getTime()}${red(this.tag)}`, msg, ...rest); } success(msg, ...rest) { console.log(`${this.getTime()}${green(this.tag)}`, msg, ...rest); } }; function createLogger(tag) { return new Logger(tag || PLUGIN_NAME, true); } //#endregion //#region src/utils.ts function readJson(path$1) { if (fs.existsSync(path$1)) return JSON.parse(fs.readFileSync(path$1, "utf8")); } function writeJson(path$1, data) { fs.writeFileSync(path$1, JSON.stringify(data, null, 2), "utf8"); } /** * @see https://github.com/vitejs/vite/blob/v4.0.1/packages/vite/src/node/constants.ts#L137-L147 */ function resolveHostname(hostname) { const loopbackHosts = new Set([ "localhost", "127.0.0.1", "::1", "0000:0000:0000:0000:0000:0000:0000:0001" ]); const wildcardHosts = new Set([ "0.0.0.0", "::", "0000:0000:0000:0000:0000:0000:0000:0000" ]); return loopbackHosts.has(hostname) || wildcardHosts.has(hostname) ? "localhost" : hostname; } function resolveServerUrl(server) { const addressInfo = server.httpServer.address(); const isAddressInfo = (x) => x?.address; if (isAddressInfo(addressInfo)) { const { address, port } = addressInfo; const hostname = resolveHostname(address); const options = server.config.server; const protocol = options.https ? "https" : "http"; const devBase = server.config.base; const path$1 = typeof options.open === "string" ? options.open : devBase; return path$1.startsWith("http") ? path$1 : `${protocol}://${hostname}:${port}${path$1}`; } } /** * Inspired `tree-kill`, implemented based on sync-api. #168 * @see https://github.com/pkrumins/node-tree-kill/blob/v1.2.2/index.js */ function treeKillSync(pid) { if (process.platform === "win32") cp.execSync(`taskkill /pid ${pid} /T /F`); else killTree(pidTree({ pid, ppid: process.pid })); } function pidTree(tree) { const command = process.platform === "darwin" ? `pgrep -P ${tree.pid}` : `ps -o pid --no-headers --ppid ${tree.ppid}`; try { const childs = cp.execSync(command, { encoding: "utf8" }).match(/\d+/g)?.map((id) => +id); if (childs) tree.children = childs.map((cid) => pidTree({ pid: cid, ppid: tree.pid })); } catch {} return tree; } function killTree(tree) { if (tree.children) for (const child of tree.children) killTree(child); try { process.kill(tree.pid); } catch {} } //#endregion //#region src/builder.ts const logger$1 = createLogger(); function getMirror() { let mirror = process.env.ELECTRON_MIRROR; if (mirror) return mirror; const res = shell.exec("npm config get registry", { silent: true }); if (res.code === 0) { let registry = res.stdout; if (!registry) return; registry = registry.trim(); if (registry && ["registry.npmmirror.com", "registry.npm.taobao.org"].find((s) => registry.includes(s))) mirror = "https://npmmirror.com/mirrors/electron"; } return mirror; } function getBuilderConfig(options, resolvedConfig) { const config = { directories: { buildResources: "electron/build", app: path.dirname(resolvedConfig.build.outDir), output: "release/${version}" }, files: [ "main", "preload", "renderer" ], artifactName: "${productName}-${version}-${os}-${arch}.${ext}", electronDownload: { mirror: getMirror() }, electronLanguages: ["zh-CN", "en-US"], win: { target: [{ target: "nsis", arch: ["x64"] }] }, mac: { target: ["zip"], sign: async () => {} }, linux: { target: ["zip"] }, nsis: { oneClick: false, perMachine: false, allowToChangeInstallationDirectory: true, deleteAppDataOnUninstall: false } }; if (typeof options.builder == "boolean") return config; const { appId, productName } = options.builder || {}; return merge(config, { appId, productName }, options.builder?.builderConfig); } function createPkg(options, resolvedConfig) { const externals = options.external || []; const viteExternals = resolvedConfig.build.rollupOptions?.external; if (Array.isArray(viteExternals)) externals.push(...viteExternals); else if (typeof viteExternals === "string") externals.push(viteExternals); const pkg = readJson(path.join(process.cwd(), "package.json")); if (!pkg) throw new Error(`package.json not found in ${process.cwd()}`); const outDir = path.dirname(resolvedConfig.build.outDir); let main = pkg.main; if (main) { main = main.replace("./", ""); main = main.substring(main.indexOf(outDir) + outDir.length); if (main.startsWith("/")) main = main.substring(1); } else main = `main/index.${options?.main?.format === "esm" ? "" : "m"}js`; const newPkg = { name: pkg.name, version: pkg.version, description: pkg.description, type: pkg.type || "commonjs", author: getAuthor(pkg.author), main, dependencies: getDeps() }; writeJson(path.join(outDir, "package.json"), newPkg); function getAuthor(author) { const uname = os.userInfo().username; if (!author) return uname; else if (typeof author === "string") return author; else if (typeof author === "object") { if (!author.name) return uname; const email = author.email ? ` <${author.email}>` : ""; return `${author.name}${email}`; } return uname; } function checkDepName(rules, name) { return !!rules.find((s) => { if (typeof s === "string") return s.includes(name); else return s.test(name); }); } function getDeps() { const deps = pkg.dependencies || {}; const newDeps = {}; Object.keys(deps).forEach((name) => { if (checkDepName(externals, name)) newDeps[name] = deps[name]; }); return newDeps; } return newPkg; } async function runElectronBuilder(options, resolvedConfig) { if (typeof options.builder == "boolean" && options.builder === false) return; logger$1.info("building electron app..."); const DIST_PATH = path.join(cwd(), path.dirname(resolvedConfig.build.outDir)); createPkg(options, resolvedConfig); logger$1.info(`create package.json and exec "npm install"`); shell.exec(`cd ${DIST_PATH} && npm install --emit=dev`); logger$1.info(`run electron-builder to package app`); const config = getBuilderConfig(options, resolvedConfig); const { build: build$1 } = await import("electron-builder"); await build$1({ config }); } //#endregion //#region src/main.ts const logger = createLogger(); function getBuildOptions(options) { return ["main", "preload"].filter((s) => options[s] && options[s].entry).map((s) => { options[s].__NAME__ = s; return options[s]; }).map((cfg) => { return { ...cfg, logLevel: "silent" }; }); } /** * startup electron app */ async function startup(options) { console.log("startup electron debug mode:", options.debug); if (options.debug) return; await startup.exit(); const args = [".", "--no-sandbox"]; if (options.inspect) if (typeof options.inspect === "number") args.push(`--inspect=${options.inspect}`); else args.push(`--inspect`); process.electronApp = spawn(electron, args, { stdio: [ "inherit", "inherit", "inherit", "ipc" ] }); process.electronApp.once("exit", process.exit); if (!startup.hookedProcessExit) { startup.hookedProcessExit = true; process.once("exit", startup.exit); } } startup.send = (message) => { if (process.electronApp) process.electronApp.send?.(message); }; startup.hookedProcessExit = false; startup.exit = async () => { if (!process.electronApp) return; await new Promise((resolve) => { startup.send(ELECTRON_EXIT); process.electronApp.removeAllListeners(); process.electronApp.once("exit", resolve); treeKillSync(process.electronApp.pid); }); }; async function runServe(options, server) { options.debug && logger.warn(`debug mode`); const buildOptions = getBuildOptions(options); const buildCounts = [0, buildOptions.length > 1 ? 0 : 1]; for (let i = 0; i < buildOptions.length; i++) { const { __NAME__: name, ignoreWatch, onSuccess: _onSuccess, watchFiles, ...tsupOptions } = buildOptions[i]; logger.info(`${name} build`); const onSuccess = async (config, signal) => { if (_onSuccess) { if (typeof _onSuccess === "string") await execa(_onSuccess); else if (typeof _onSuccess === "function") await _onSuccess(config, signal); } if (buildCounts[i] <= 0) { buildCounts[i]++; logger.info(`${name} build success`); if (buildCounts[0] === 1 && buildCounts[1] === 1) { logger.info("startup electron"); await startup(options); } return; } logger.success(`${name} rebuild success`); if (name === "main") { logger.info("restart electron"); await startup(options); } else { logger.info("reload page"); server.ws.send({ type: "full-reload" }); } }; await build({ onSuccess, ...tsupOptions, watch: watchFiles ?? (options.recommended ? [`electron/${name}`] : true), ignoreWatch: (Array.isArray(ignoreWatch) ? ignoreWatch : []).concat([ ".history", ".temp", ".tmp", ".cache", "dist" ]) }); } } async function runBuild(options) { const buildOptions = getBuildOptions(options); for (let i = 0; i < buildOptions.length; i++) await build(buildOptions[i]); } //#endregion //#region src/index.ts const isDev = process.env.NODE_ENV === "development"; function getPkg() { const pkgFile = path.resolve(process.cwd(), "package.json"); if (!fs.existsSync(pkgFile)) throw new Error("Main file is not specified, and no package.json found"); const pkg = readJson(pkgFile); if (!pkg.main) throw new Error("Main file is not specified, please check package.json"); return pkg; } function preMergeOptions(options) { const format = getPkg().type === "module" ? "esm" : "cjs"; const electron$1 = { target: format === "esm" ? "node18.18" : "node16", format, shims: true, clean: true, dts: false, treeshake: !!isDev, outExtensions({ format: format$1 }) { return { js: format$1 === "es" ? ".mjs" : ".js" }; } }; const opts = merge({ recommended: true, debug: false, external: ["electron"], main: { ...electron$1 }, preload: { ...electron$1 }, builder: false }, cloneDeep(options)); ["main", "preload"].forEach((prop) => { const opt = opts[prop]; const fmt = opt.format; opt.format = ["cjs", "esm"].includes(fmt) ? [fmt] : [format]; const entry = opt.entry; if (entry === void 0) { const filePath = `electron/${prop}/index.ts`; if (fs.existsSync(path.join(process.cwd(), filePath))) opt.entry = [filePath]; } else if (typeof entry === "string") opt.entry = [entry]; if (isDev) opt.sourcemap ??= true; else opt.minify ??= true; const external = opt.external || opts.external || ["electron"]; opt.external = [...new Set(["electron"].concat(external))]; }); return opts; } function geNumberBooleanValue(value) { if (typeof value !== "string" || value.trim() === "") return; if (["true", "false"].includes(value)) return value === "true"; const v = Number(value); return Number.isNaN(v) ? void 0 : v; } function getBooleanValue(value) { if (typeof value !== "string" || value.trim() === "") return; if (["true", "false"].includes(value)) return value === "true"; if (["1", "0"].includes(value)) return value === "1"; } /** * A simple vite plugin for electron * @param options */ function useElectronPlugin(options) { const opts = preMergeOptions(options); let isServer = false; let resolvedConfig; return { name: PLUGIN_NAME, config(config, env) { isServer = env.command === "serve"; let outDir = config?.build?.outDir || "dist"; opts.main ||= {}; opts.preload ||= {}; if (opts.recommended) { opts.main.outDir = path.join(outDir, "main"); opts.preload.outDir = path.join(outDir, "preload"); outDir = path.join(outDir, "renderer"); } else { opts.main.outDir ||= path.join("dist-electron", "main"); opts.preload.outDir ||= path.join("dist-electron", "preload"); } return { build: { outDir } }; }, configResolved(config) { opts.debug = getBooleanValue(config.env.VITE_ELECTRON_DEBUG) ?? opts.debug; opts.inspect = geNumberBooleanValue(config.env.VITE_ELECTRON_INSPECT) ?? opts.inspect; opts.builder = getBooleanValue(config.env.VITE_ELECTRON_BUILDER) ?? opts.builder; resolvedConfig = config; }, configureServer(server) { if (!server || !server.httpServer) return; server.httpServer.on("listening", async () => { const env = { NODE_ENV: server.config.mode || "development", VITE_DEV_SERVER_URL: resolveServerUrl(server) }; ["main", "preload"].forEach((prop) => { opts[prop].env = env; }); await runServe(opts, server); }); }, async closeBundle() { if (isServer) return; await runBuild(opts); if (opts.recommended && opts.builder) await runElectronBuilder(opts, resolvedConfig); } }; } var src_default = useElectronPlugin; //#endregion export { src_default as default, useElectronPlugin };