@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
JavaScript
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 };