electron-incremental-update 3.0.0-beta.5 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vite.mjs CHANGED
@@ -1,51 +1,30 @@
1
- import { builtinModules, createRequire } from "node:module";
2
- import { isCI } from "ci-info";
3
- import { getPackageInfoSync, loadPackageJSON } from "local-pkg";
1
+ import { builtinModules } from "node:module";
4
2
  import fs from "node:fs";
5
3
  import path from "node:path";
6
- import { build, createLogger, mergeConfig, normalizePath, version } from "vite";
7
- import MagicString from "magic-string";
8
- import * as babel from "@babel/core";
9
- import cp from "node:child_process";
10
- import crypto from "node:crypto";
4
+ import { isCI } from "ci-info";
5
+ import { createLogger, mergeConfig, normalizePath } from "vite";
6
+ import { electronPluginFactory } from "vite-plugin-electron/multi-env";
7
+ import { esmShim } from "vite-plugin-electron/plugin";
11
8
  import zlib from "node:zlib";
9
+ import crypto from "node:crypto";
10
+ import cp from "node:child_process";
11
+ import * as babel from "@babel/core";
12
+ import { copyFile, cp as cp$1, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
14
+ import { createPackage, extractFile } from "@electron/asar";
12
15
  import { generate } from "selfsigned";
13
- import { createPackage } from "@electron/asar";
14
-
15
16
  //#region src/vite/startup.ts
16
17
  /**
17
- * Debug mode startup function
18
- * Automatically starts Electron in VSCode debug mode
19
- * @param args - Startup arguments
20
- */
21
- async function debugStartup(args) {
22
- if (process.env.VSCODE_DEBUG) console.log("[startup] Electron App");
23
- else await args.startup();
24
- }
25
- /**
26
18
  * Filter error messages from stdout/stderr during startup
27
19
  * @param args - Startup arguments
28
20
  * @param filter - Filter function to determine which messages to show
29
21
  */
30
- async function filterErrorMessageStartup(args, filter) {
31
- const stdio = process.platform === "linux" ? [
32
- "inherit",
33
- "pipe",
34
- "pipe",
35
- "ignore",
36
- "ipc"
37
- ] : [
38
- "inherit",
39
- "pipe",
40
- "pipe",
41
- "ipc"
42
- ];
43
- await args.startup(void 0, { stdio });
22
+ async function filterErrorMessageStartup(filter) {
44
23
  const elec = process.electronApp;
45
- elec.stdout.addListener("data", (data) => {
24
+ elec.stdout?.addListener("data", (data) => {
46
25
  console.log(data.toString().trimEnd());
47
26
  });
48
- elec.stderr.addListener("data", (data) => {
27
+ elec.stderr?.addListener("data", (data) => {
49
28
  const message = data.toString();
50
29
  if (filter(message)) console.error(message);
51
30
  });
@@ -61,62 +40,51 @@ function fixWinCharEncoding(fn) {
61
40
  await fn(...args);
62
41
  });
63
42
  }
64
-
65
43
  //#endregion
66
- //#region src/vite/constant.ts
67
- const id = "electron-incremental-updater";
68
- const bytecodeId = `${id}-bytecode`;
69
- const log = createLogger("info", { prefix: `[${id}]` });
70
- const bytecodeLog = createLogger("info", { prefix: `[${bytecodeId}]` });
71
-
72
- //#endregion
73
- //#region src/vite/utils/file.ts
44
+ //#region src/utils/compress.ts
74
45
  /**
75
- * Convert byte size to human-readable format
76
- * @param size - Size in bytes
77
- * @returns Human-readable size string (e.g., "1.23 MB")
46
+ * Default function to compress file using brotli
47
+ * @param buffer uncompressed file buffer
78
48
  */
79
- function readableSize(size) {
80
- const units = [
81
- "B",
82
- "KB",
83
- "MB",
84
- "GB"
85
- ];
86
- let i = 0;
87
- while (size >= 1024 && i < units.length - 1) {
88
- size /= 1024;
89
- i++;
90
- }
91
- return `${size.toFixed(2)} ${units[i]}`;
49
+ async function defaultCompressFile(buffer) {
50
+ return new Promise((resolve, reject) => {
51
+ zlib.brotliCompress(buffer, (err, buffer) => err ? reject(err) : resolve(buffer));
52
+ });
53
+ }
54
+ //#endregion
55
+ //#region src/utils/crypto.ts
56
+ function hashBuffer(data, length) {
57
+ const hash = crypto.createHash("SHA256").update(data).digest("binary");
58
+ return Buffer.from(hash).subarray(0, length);
59
+ }
60
+ function aesEncrypt(plainText, key, iv) {
61
+ const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
62
+ return cipher.update(plainText, "utf8", "base64url") + cipher.final("base64url");
92
63
  }
93
64
  /**
94
- * Copy file/directory, skipping if target exists
95
- * @param from - Source path
96
- * @param to - Destination path
97
- * @param skipIfExist - Skip copy if destination exists
65
+ * Default function to generate asar signature, returns generated signature
66
+ * @param buffer file buffer
67
+ * @param privateKey primary key
68
+ * @param cert certificate
69
+ * @param version target version
98
70
  */
99
- function copyAndSkipIfExist(from, to, skipIfExist) {
100
- if (!skipIfExist || !fs.existsSync(to)) try {
101
- fs.cpSync(from, to, { recursive: true });
102
- } catch (error) {
103
- log.warn(`Copy failed: ${error}`, { timestamp: true });
104
- }
71
+ function defaultSignature(buffer, privateKey, cert, version) {
72
+ return aesEncrypt(`${crypto.createSign("RSA-SHA256").update(buffer).sign(crypto.createPrivateKey(privateKey), "base64")}%${version}`, hashBuffer(cert, 32), hashBuffer(buffer, 16));
105
73
  }
106
-
107
- //#endregion
108
- //#region src/vite/bytecode/code.ts
109
- const bytecodeGeneratorScript = "const vm = require('vm')\nconst v8 = require('v8')\nconst wrap = require('module').wrap\nv8.setFlagsFromString('--no-lazy')\nv8.setFlagsFromString('--no-flush-bytecode')\nlet code = ''\nprocess.stdin.setEncoding('utf-8')\nprocess.stdin.on('readable', () => {\n const data = process.stdin.read()\n if (data !== null) {\n code += data\n }\n})\nprocess.stdin.on('end', () => {\n try {\n if (typeof code !== 'string') {\n throw new Error('javascript code must be string.')\n }\n const script = new vm.Script(wrap(code), { produceCachedData: true })\n const bytecodeBuffer = script.createCachedData()\n process.stdout.write(bytecodeBuffer)\n } catch (error) {\n console.error(error)\n }\n})\n";
110
- const bytecodeModuleLoaderCode = "\"use strict\";\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst vm = require(\"vm\");\nconst v8 = require(\"v8\");\nconst Module = require(\"module\");\nv8.setFlagsFromString(\"--no-lazy\");\nv8.setFlagsFromString(\"--no-flush-bytecode\");\nconst FLAG_HASH_OFFSET = 12;\nconst SOURCE_HASH_OFFSET = 8;\nlet dummyBytecode;\nfunction setFlagHashHeader(bytecodeBuffer) {\n if (!dummyBytecode) {\n const script = new vm.Script(\"\", {\n produceCachedData: true\n });\n dummyBytecode = script.createCachedData();\n }\n dummyBytecode.slice(FLAG_HASH_OFFSET, FLAG_HASH_OFFSET + 4).copy(bytecodeBuffer, FLAG_HASH_OFFSET);\n};\nfunction getSourceHashHeader(bytecodeBuffer) {\n return bytecodeBuffer.slice(SOURCE_HASH_OFFSET, SOURCE_HASH_OFFSET + 4);\n};\nfunction buffer2Number(buffer) {\n let ret = 0;\n ret |= buffer[3] << 24;\n ret |= buffer[2] << 16;\n ret |= buffer[1] << 8;\n ret |= buffer[0];\n return ret;\n};\nModule._extensions[\".jsc\"] = Module._extensions[\".cjsc\"] = function (module, filename) {\n const bytecodeBuffer = fs.readFileSync(filename);\n if (!Buffer.isBuffer(bytecodeBuffer)) {\n throw new Error(\"BytecodeBuffer must be a buffer object.\");\n }\n setFlagHashHeader(bytecodeBuffer);\n const length = buffer2Number(getSourceHashHeader(bytecodeBuffer));\n let dummyCode = \"\";\n if (length > 1) {\n dummyCode = \"\\\"\" + \"\\u200b\".repeat(length - 2) + \"\\\"\";\n }\n const script = new vm.Script(dummyCode, {\n filename: filename,\n lineOffset: 0,\n displayErrors: true,\n cachedData: bytecodeBuffer\n });\n if (script.cachedDataRejected) {\n throw new Error(\"Invalid or incompatible cached data (cachedDataRejected)\");\n }\n const require = function (id) {\n return module.require(id);\n };\n require.resolve = function (request, options) {\n return Module._resolveFilename(request, module, false, options);\n };\n if (process.mainModule) {\n require.main = process.mainModule;\n }\n require.extensions = Module._extensions;\n require.cache = Module._cache;\n const compiledWrapper = script.runInThisContext({\n filename: filename,\n lineOffset: 0,\n columnOffset: 0,\n displayErrors: true\n });\n const dirname = path.dirname(filename);\n const args = [module.exports, require, module, filename, dirname, process, global];\n return compiledWrapper.apply(module.exports, args);\n};\n";
111
-
112
74
  //#endregion
113
75
  //#region src/utils/version.ts
76
+ const REG_VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([a-z0-9]+)(?:\.(\d+))?)?$/i;
114
77
  /**
115
78
  * Parse version string to {@link Version}, like `0.2.0-beta.1`
79
+ *
80
+ * **Supported format**: `major.minor.patch[-stage[.stageVersion]]`
81
+ *
82
+ * Build metadata (`+build`) and complex semver prerelease identifiers
83
+ * (e.g. `1.0.0-beta.1.2`) are not supported yet.
116
84
  * @param version version string
117
85
  */
118
86
  function parseVersion(version) {
119
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-([a-z0-9.-]+))?/i.exec(version);
87
+ const match = REG_VERSION.exec(version);
120
88
  if (!match) throw new TypeError(`invalid version: ${version}`);
121
89
  const [major, minor, patch] = match.slice(1, 4).map(Number);
122
90
  const ret = {
@@ -127,9 +95,8 @@ function parseVersion(version) {
127
95
  stageVersion: -1
128
96
  };
129
97
  if (match[4]) {
130
- let [stage, _v] = match[4].split(".");
131
- ret.stage = stage;
132
- ret.stageVersion = Number(_v) || -1;
98
+ ret.stage = match[4];
99
+ ret.stageVersion = match[5] === void 0 ? -1 : Number(match[5]);
133
100
  }
134
101
  if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch) || Number.isNaN(ret.stageVersion)) throw new TypeError(`Invalid version: ${version}`);
135
102
  return ret;
@@ -140,7 +107,7 @@ const is = (j) => !!(j && j.minimumVersion && j.signature && j.version);
140
107
  * @param json any variable
141
108
  */
142
109
  function isUpdateJSON(json) {
143
- return is(json) && is(json?.beta);
110
+ return json && is(json) && is(json.beta);
144
111
  }
145
112
  /**
146
113
  * Default function to generate `UpdateJSON`
@@ -162,74 +129,141 @@ function defaultVersionJsonGenerator(existingJson, signature, version, minimumVe
162
129
  }
163
130
  return existingJson;
164
131
  }
165
-
132
+ //#endregion
133
+ //#region src/vite/constant.ts
134
+ const id = "electron-incremental-update";
135
+ const bytecodeId = `${id}-bytecode`;
136
+ const log = createLogger("info", { prefix: `[${id}]` });
137
+ const bytecodeLog = createLogger("info", { prefix: `[${bytecodeId}]` });
138
+ const defaultExternal = [
139
+ ...builtinModules,
140
+ "electron",
141
+ "electron/common",
142
+ "electron/main",
143
+ "electron/renderer",
144
+ "electron/utility",
145
+ /^node:/,
146
+ /.*\.(node|dll|dylib|so)$/,
147
+ "original-fs"
148
+ ];
149
+ //#endregion
150
+ //#region src/vite/bytecode/code.ts
151
+ const bytecodeGeneratorScript = "const vm = require('vm')\nconst v8 = require('v8')\nconst wrap = require('module').wrap\nv8.setFlagsFromString('--no-lazy')\nv8.setFlagsFromString('--no-flush-bytecode')\nlet code = ''\nprocess.stdin.setEncoding('utf-8')\nprocess.stdin.on('readable', () => {\n const data = process.stdin.read()\n if (data !== null) {\n code += data\n }\n})\nprocess.stdin.on('end', () => {\n try {\n if (typeof code !== 'string') {\n throw new Error('javascript code must be string.')\n }\n const script = new vm.Script(wrap(code), { produceCachedData: true })\n const bytecodeBuffer = script.createCachedData()\n process.stdout.write(bytecodeBuffer)\n } catch (error) {\n console.error(error)\n }\n})\n";
152
+ const bytecodeModuleLoaderCode = "\"use strict\";\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst vm = require(\"vm\");\nconst v8 = require(\"v8\");\nconst Module = require(\"module\");\nv8.setFlagsFromString(\"--no-lazy\");\nv8.setFlagsFromString(\"--no-flush-bytecode\");\nconst FLAG_HASH_OFFSET = 12;\nconst SOURCE_HASH_OFFSET = 8;\nlet dummyBytecode;\nfunction setFlagHashHeader(bytecodeBuffer) {\n if (!dummyBytecode) {\n const script = new vm.Script(\"\", {\n produceCachedData: true\n });\n dummyBytecode = script.createCachedData();\n }\n dummyBytecode.slice(FLAG_HASH_OFFSET, FLAG_HASH_OFFSET + 4).copy(bytecodeBuffer, FLAG_HASH_OFFSET);\n};\nfunction getSourceHashHeader(bytecodeBuffer) {\n return bytecodeBuffer.slice(SOURCE_HASH_OFFSET, SOURCE_HASH_OFFSET + 4);\n};\nfunction buffer2Number(buffer) {\n let ret = 0;\n ret |= buffer[3] << 24;\n ret |= buffer[2] << 16;\n ret |= buffer[1] << 8;\n ret |= buffer[0];\n return ret;\n};\nModule._extensions[\".jsc\"] = Module._extensions[\".cjsc\"] = function (module, filename) {\n const bytecodeBuffer = fs.readFileSync(filename);\n if (!Buffer.isBuffer(bytecodeBuffer)) {\n throw new Error(\"BytecodeBuffer must be a buffer object.\");\n }\n setFlagHashHeader(bytecodeBuffer);\n const length = buffer2Number(getSourceHashHeader(bytecodeBuffer));\n let dummyCode = \"\";\n if (length > 1) {\n dummyCode = \"\\\"\" + \"\\u200b\".repeat(length - 2) + \"\\\"\";\n }\n const script = new vm.Script(dummyCode, {\n filename: filename,\n lineOffset: 0,\n displayErrors: true,\n cachedData: bytecodeBuffer\n });\n if (script.cachedDataRejected) {\n throw new Error(\"Invalid or incompatible cached data (cachedDataRejected)\");\n }\n const require = function (id) {\n return module.require(id);\n };\n require.resolve = function (request, options) {\n return Module._resolveFilename(request, module, false, options);\n };\n if (process.mainModule) {\n require.main = process.mainModule;\n }\n require.extensions = Module._extensions;\n require.cache = Module._cache;\n const compiledWrapper = script.runInThisContext({\n filename: filename,\n lineOffset: 0,\n columnOffset: 0,\n displayErrors: true\n });\n const dirname = path.dirname(filename);\n const args = [module.exports, require, module, filename, dirname, process, global];\n return compiledWrapper.apply(module.exports, args);\n};\n";
166
153
  //#endregion
167
154
  //#region src/vite/bytecode/utils.ts
168
- const electronModule = getPackageInfoSync("electron");
169
- const electronMajorVersion = parseVersion(electronModule.version).major;
170
155
  const useStrict = "'use strict';";
171
156
  const bytecodeModuleLoader = "__loader__.js";
172
- function getElectronPath() {
173
- const electronModulePath = electronModule.rootPath;
174
- let electronExecPath = process.env.ELECTRON_EXEC_PATH || "";
175
- if (!electronExecPath) {
176
- if (!electronModulePath) throw new Error("Electron is not installed");
177
- const pathFile = path.join(electronModulePath, "path.txt");
178
- let executablePath;
179
- if (fs.existsSync(pathFile)) executablePath = fs.readFileSync(pathFile, "utf-8");
180
- if (executablePath) {
181
- electronExecPath = path.join(electronModulePath, "dist", executablePath);
182
- process.env.ELECTRON_EXEC_PATH = electronExecPath;
183
- } else throw new Error("Electron executable file is not existed");
184
- }
185
- return electronExecPath;
157
+ async function resolvePaths(customPath) {
158
+ if (!customPath || !process.CACHED_ELECTRON_PATH) process.CACHED_ELECTRON_PATH = (await import("electron")).default;
159
+ if (!process.CACHED_BYTECODE_COMPILER_PATH) process.CACHED_BYTECODE_COMPILER_PATH = path.join(path.dirname(process.CACHED_ELECTRON_PATH), "EIU_bytenode.cjs");
160
+ if (!fs.existsSync(process.CACHED_BYTECODE_COMPILER_PATH)) fs.writeFileSync(process.CACHED_BYTECODE_COMPILER_PATH, bytecodeGeneratorScript);
161
+ return {
162
+ electronPath: customPath || process.CACHED_ELECTRON_PATH,
163
+ bytecodePath: process.CACHED_BYTECODE_COMPILER_PATH
164
+ };
186
165
  }
187
- function getBytecodeCompilerPath() {
188
- const scriptPath = path.join(electronModule.rootPath, "EIU_bytenode.cjs");
189
- if (!fs.existsSync(scriptPath)) fs.writeFileSync(scriptPath, bytecodeGeneratorScript);
190
- return scriptPath;
166
+ async function compileToBytecode(code, name, customElectronPath) {
167
+ try {
168
+ const { bytecodePath, electronPath } = await resolvePaths(customElectronPath);
169
+ return await new Promise((resolve, reject) => {
170
+ const proc = cp.spawn(electronPath, [bytecodePath], {
171
+ env: {
172
+ ...process.env,
173
+ ELECTRON_RUN_AS_NODE: "1"
174
+ },
175
+ stdio: [
176
+ "pipe",
177
+ "pipe",
178
+ "pipe",
179
+ "ipc"
180
+ ]
181
+ });
182
+ const stdoutChunks = [];
183
+ const stderrChunks = [];
184
+ if (proc.stdin) {
185
+ proc.stdin.write(code);
186
+ proc.stdin.end();
187
+ }
188
+ if (proc.stdout) proc.stdout.on("data", (chunk_2) => stdoutChunks.push(chunk_2)).on("error", (err) => reject(err));
189
+ if (proc.stderr) proc.stderr.on("data", (chunk_3) => stderrChunks.push(chunk_3)).on("error", (err_1) => reject(err_1));
190
+ proc.on("error", (err_2) => reject(err_2));
191
+ proc.on("close", (exitCode) => {
192
+ const stdout = Buffer.concat(stdoutChunks);
193
+ const errorMessage = Buffer.concat(stderrChunks).toString("utf-8");
194
+ if (exitCode !== 0 || stdout.length === 0) {
195
+ reject(/* @__PURE__ */ new Error(`Bytecode generation process exited with code ${exitCode}. Error: ${errorMessage}`));
196
+ return;
197
+ }
198
+ resolve(stdout);
199
+ });
200
+ });
201
+ } catch (e) {
202
+ return `Failed to generate bytecode of [${name}], ${e}`;
203
+ }
191
204
  }
192
- function toRelativePath(filename, importer) {
193
- const relPath = path.posix.relative(path.dirname(importer), filename);
194
- return relPath.startsWith(".") ? relPath : `./${relPath}`;
205
+ const decodeFnBody = babel.parse(";function _0xstr_(a,b){return String.fromCharCode.apply(0,a.map(function(x){return x-b}))};")?.program.body ?? [];
206
+ function createObfuscatedStringCall(input, offset) {
207
+ const resolvedOffset = offset ?? ~~(Math.random() * 16) + 1;
208
+ const elements = input.split("").map((char) => {
209
+ const value = char.codePointAt(0) + resolvedOffset;
210
+ const node = babel.types.numericLiteral(value);
211
+ node.extra = {
212
+ raw: `0x${value.toString(16)}`,
213
+ rawValue: value
214
+ };
215
+ return node;
216
+ });
217
+ return babel.types.callExpression(babel.types.identifier("_0xstr_"), [babel.types.arrayExpression(elements), babel.types.numericLiteral(resolvedOffset)]);
218
+ }
219
+ function createPrepareContext(bytecodeFileNames) {
220
+ const requireRewrites = {};
221
+ for (const fileName of bytecodeFileNames) {
222
+ if (!fileName.endsWith(".js") && !fileName.endsWith(".cjs")) continue;
223
+ const baseName = path.posix.basename(fileName);
224
+ requireRewrites[baseName] = `${baseName}c`;
225
+ }
226
+ return {
227
+ requireRewrites,
228
+ hasRequireRewrites: Object.keys(requireRewrites).length > 0
229
+ };
195
230
  }
196
- function compileToBytecode(code, name, electronPath = getElectronPath()) {
197
- let data = Buffer.from([]);
198
- const bytecodePath = getBytecodeCompilerPath();
199
- return new Promise((resolve, reject) => {
200
- const proc = cp.spawn(electronPath, [bytecodePath], {
201
- env: { ELECTRON_RUN_AS_NODE: "1" },
202
- stdio: [
203
- "pipe",
204
- "pipe",
205
- "pipe",
206
- "ipc"
207
- ]
208
- });
209
- if (proc.stdin) {
210
- proc.stdin.write(code);
211
- proc.stdin.end();
212
- }
213
- if (proc.stdout) {
214
- proc.stdout.on("data", (chunk) => data = Buffer.concat([data, chunk]));
215
- proc.stdout.on("error", (err) => reject(err));
216
- proc.stdout.on("end", () => resolve(data));
217
- }
218
- if (proc.stderr) {
219
- proc.stderr.on("data", (chunk) => reject(chunk.toString()));
220
- proc.stderr.on("error", (err) => reject(err));
221
- }
222
- proc.addListener("error", (err) => reject(err));
223
- proc.on("error", (err) => reject(err));
224
- proc.on("exit", () => resolve(data));
225
- }).catch((e) => `Failed to generate bytecode of [${name}], ${e}`);
231
+ function rewriteRequirePath(requirePath, requireRewrites) {
232
+ const baseName = path.posix.basename(requirePath);
233
+ const newBaseName = requireRewrites[baseName];
234
+ if (!newBaseName) return;
235
+ return `${requirePath.slice(0, -baseName.length)}${newBaseName}`;
236
+ }
237
+ function rewriteSimpleRequireCalls(code, requireRewrites) {
238
+ if (!code.includes("require(")) return code;
239
+ return code.replace(/\brequire\(\s*(['"])([^'"]+)\1\s*\)/g, (match, quote, requirePath) => {
240
+ const newRequirePath = rewriteRequirePath(requirePath, requireRewrites);
241
+ return newRequirePath ? `require(${quote}${newRequirePath}${quote})` : match;
242
+ });
226
243
  }
227
- function prepare(code, offset) {
228
- let hasTransformed = false;
229
- const transformedCode = babel.transform(code, { plugins: [
230
- "@babel/plugin-transform-arrow-functions",
231
- "@babel/plugin-transform-template-literals",
232
- () => ({ visitor: { StringLiteral(path) {
244
+ function obfuscateStringsPlugin(_, options) {
245
+ function transformProperty(path) {
246
+ const node = path.node;
247
+ if (node.computed || !babel.types.isIdentifier(node.property)) return;
248
+ if (path.findParent((parent) => parent.isFunctionDeclaration() && babel.types.isIdentifier(parent.node.id, { name: "_0xstr_" }))) return;
249
+ node.computed = true;
250
+ node.property = babel.types.stringLiteral(node.property.name);
251
+ }
252
+ return { visitor: {
253
+ MemberExpression(path) {
254
+ transformProperty(path);
255
+ },
256
+ OptionalMemberExpression(path) {
257
+ transformProperty(path);
258
+ },
259
+ ObjectProperty(path, state) {
260
+ const key = path.node.key;
261
+ if (path.node.computed || path.node.shorthand || !babel.types.isIdentifier(key) || key.name === "__proto__") return;
262
+ path.node.computed = true;
263
+ path.node.key = createObfuscatedStringCall(key.name, options.offset);
264
+ state.hasTransformed = true;
265
+ },
266
+ StringLiteral(path, state) {
233
267
  const parent = path.parent;
234
268
  const node = path.node;
235
269
  if (parent.type === "CallExpression") {
@@ -239,409 +273,348 @@ function prepare(code, offset) {
239
273
  if (parent.type.startsWith("Export")) return;
240
274
  if (parent.type.startsWith("Import")) return;
241
275
  if (parent.type === "ObjectMethod" && parent.key === node) {
242
- const obfuscated = obfuscateString(node.value, offset);
243
- path.parentPath.node.computed = true;
244
- path.replaceWith(babel.types.identifier(obfuscated));
245
- hasTransformed = true;
276
+ parent.computed = true;
277
+ path.replaceWith(createObfuscatedStringCall(node.value, options.offset));
278
+ state.hasTransformed = true;
246
279
  return;
247
280
  }
248
281
  if (parent.type === "ObjectProperty" && parent.key === node) {
249
- const obfuscated = obfuscateString(node.value, offset);
250
- path.parentPath.node.computed = true;
251
- path.replaceWith(babel.types.identifier(obfuscated));
252
- hasTransformed = true;
282
+ parent.computed = true;
283
+ path.replaceWith(createObfuscatedStringCall(node.value, options.offset));
284
+ state.hasTransformed = true;
253
285
  return;
254
286
  }
255
287
  if (!node.value.trim()) return;
256
- path.replaceWith(babel.types.identifier(obfuscateString(node.value, offset)));
257
- hasTransformed = true;
258
- } } })
259
- ] })?.code || code;
260
- return hasTransformed ? `${transformedCode}\n${decodeFn}` : transformedCode;
261
- }
262
- const decodeFn = ";function _0xstr_(a,b){return String.fromCharCode.apply(0,a.map(function(x){return x-b}))};";
263
- function obfuscateString(input, offset = ~~(Math.random() * 16) + 1) {
264
- return `_0xstr_([${input.split("").map((c) => `0x${(c.charCodeAt(0) + offset).toString(16)}`).join(",")}],${offset})`;
288
+ path.replaceWith(createObfuscatedStringCall(node.value, options.offset));
289
+ state.hasTransformed = true;
290
+ },
291
+ Program: { exit(path, state) {
292
+ if (!state.hasTransformed) return;
293
+ path.unshiftContainer("body", decodeFnBody.map((node) => babel.types.cloneNode(node)));
294
+ } }
295
+ } };
296
+ }
297
+ function rewriteRequirePlugin(_, options) {
298
+ return { visitor: { CallExpression(path) {
299
+ if (!babel.types.isIdentifier(path.node.callee, { name: "require" }) || path.node.arguments.length === 0) return;
300
+ const arg = path.node.arguments[0];
301
+ if (!babel.types.isStringLiteral(arg)) return;
302
+ const newRequirePath = rewriteRequirePath(arg.value, options.requireRewrites);
303
+ if (newRequirePath) path.node.arguments[0] = babel.types.stringLiteral(newRequirePath);
304
+ } } };
305
+ }
306
+ function prepare(code, minify, context, offset) {
307
+ if (!code.includes("\"") && !code.includes("'") && !code.includes("`") && !code.includes("=>") && !/\.[A-Za-z_$]/.test(code) && !/[{,]\s*[A-Za-z_$][\w$]*\s*:/.test(code)) return { code };
308
+ if (context.hasRequireRewrites && !code.includes("`") && !code.includes("=>")) {
309
+ const codeWithoutSimpleRequires = code.replace(/\brequire\(\s*(['"])([^'"]+)\1\s*\)/g, "");
310
+ if (!codeWithoutSimpleRequires.includes("\"") && !codeWithoutSimpleRequires.includes("'")) return { code: rewriteSimpleRequireCalls(code, context.requireRewrites) };
311
+ }
312
+ return babel.transform(code, {
313
+ minified: minify,
314
+ plugins: [
315
+ "@babel/plugin-transform-arrow-functions",
316
+ "@babel/plugin-transform-template-literals",
317
+ [obfuscateStringsPlugin, { offset }],
318
+ [rewriteRequirePlugin, { requireRewrites: context.requireRewrites }]
319
+ ]
320
+ });
265
321
  }
266
-
267
322
  //#endregion
268
323
  //#region src/vite/bytecode/index.ts
269
324
  function getBytecodeLoaderBlock(chunkFileName) {
270
- return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`;
325
+ const loaderFileName = path.posix.relative(path.posix.dirname(chunkFileName), bytecodeModuleLoader);
326
+ return `require("${loaderFileName.startsWith(".") ? loaderFileName : `./${loaderFileName}`}")`;
271
327
  }
272
- function checkHasBytecodeChunk(getModuleInfo, imports, dynamicImports, bytecodeChunks) {
273
- const queue = [...imports, ...dynamicImports];
274
- const visited = /* @__PURE__ */ new Set();
275
- while (queue.length > 0) {
276
- const moduleId = queue.shift();
277
- if (visited.has(moduleId)) continue;
278
- visited.add(moduleId);
279
- if (bytecodeChunks.has(moduleId)) return true;
280
- const moduleInfo = getModuleInfo(moduleId);
281
- if (moduleInfo) {
282
- const { importers, dynamicImporters } = moduleInfo;
283
- for (const importerId of importers) if (!visited.has(importerId)) queue.push(importerId);
284
- for (const importerId of dynamicImporters) if (!visited.has(importerId)) queue.push(importerId);
285
- }
286
- }
287
- return false;
288
- }
289
- /**
290
- * Vite plugin that compiles JavaScript files to V8 bytecode for source code protection.
291
- *
292
- * This plugin transforms JavaScript files into bytecode format during build,
293
- * making the source code more difficult to reverse engineer. The generated bytecode
294
- * files (.jsc) can only be executed in compatible Electron environments.
295
- *
296
- * @param env - Environment type ('preload' | 'main')
297
- * @param options - Bytecode compilation options
298
- * @returns Vite plugin or null if bytecode compilation is disabled
299
- */
300
- function bytecodePlugin(env, isESM, options) {
328
+ function bytecodePlugin(env, minify, isESM, options) {
301
329
  const { enable, preload = false, electronPath, beforeCompile } = options;
302
330
  if (!enable) return null;
303
- if (!preload && env === "preload") {
304
- bytecodeLog.warn("`bytecodePlugin` is skiped in preload. To enable in preload, please manually set the \"enablePreload\" option to true and set `sandbox: false` when creating the window", { timestamp: true });
331
+ if (env === "preload" && !preload) {
332
+ if (preload === void 0) bytecodeLog.warn("`bytecodePlugin` is skipped in preload. Set `bytecode: { preload: false }` to disable this warning, or `bytecode: { preload: true }` and set `sandbox: false` in BrowserWindow to enable bytecode in preload.", { timestamp: true });
305
333
  return null;
306
334
  }
307
- if (isESM) throw new Error("`bytecodePlugin` does not support ES module, please set `\"types\": \"commonjs\"` in package.json");
308
- let config;
309
- let bytecodeRequired = false;
310
- let bytecodeFiles = [];
335
+ if (isESM) throw new Error("`bytecodePlugin` requires CommonJS. Set \"type\": \"commonjs\" in package.json and use .cjs extensions");
336
+ let hasJsChunks = false;
311
337
  return {
312
- name: `${bytecodeId}-${env}`,
313
- apply: "build",
314
- enforce: "post",
315
- configResolved(resolvedConfig) {
316
- config = resolvedConfig;
317
- },
318
- renderChunk(_, chunk) {
319
- if (chunk.type === "chunk") bytecodeRequired = true;
320
- return null;
321
- },
322
- generateBundle() {
323
- if (bytecodeRequired) this.emitFile({
338
+ name: bytecodeId,
339
+ async generateBundle(outputOptions, bundle) {
340
+ hasJsChunks = Object.values(bundle).some((file) => file.type === "chunk" && (file.fileName.endsWith(".js") || file.fileName.endsWith(".cjs")));
341
+ if (hasJsChunks) this.emitFile({
324
342
  type: "asset",
325
343
  source: `${bytecodeModuleLoaderCode}\n`,
326
- name: "Bytecode Loader File",
344
+ name: "Bytecode Loader",
327
345
  fileName: bytecodeModuleLoader
328
346
  });
329
- },
330
- async writeBundle(options, output) {
331
- if (!bytecodeRequired) return;
332
- const outDir = options.dir;
333
- bytecodeFiles = [];
334
- const bundles = Object.keys(output);
335
- const chunks = Object.values(output).filter((chunk) => chunk.type === "chunk" && chunk.fileName !== bytecodeModuleLoader);
336
- const bytecodeChunks = new Set(chunks.map((chunk) => chunk.fileName));
337
- const pattern = chunks.filter((chunk) => !chunk.isEntry).map((chunk) => path.basename(chunk.fileName)).map((chunk) => `(${chunk})`).join("|");
338
- const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, "g") : null;
339
- await Promise.all(bundles.map(async (name) => {
340
- const chunk = output[name];
341
- if (chunk.type === "chunk") {
342
- let _code = prepare(chunk.code);
343
- const chunkFilePath = path.resolve(outDir, name);
344
- if (beforeCompile) {
345
- const cbResult = await beforeCompile(_code, chunkFilePath);
346
- if (cbResult) _code = cbResult;
347
- }
348
- if (bytecodeRE && _code.match(bytecodeRE)) {
349
- let match;
350
- const s = new MagicString(_code);
351
- while (match = bytecodeRE.exec(_code)) {
352
- const [prefix, chunkName] = match;
353
- const len = prefix.length + chunkName.length;
354
- s.overwrite(match.index, match.index + len, `${prefix + chunkName}c`, { contentOnly: true });
355
- }
356
- _code = s.toString();
357
- }
358
- if (bytecodeChunks.has(name)) {
359
- const result = await compileToBytecode(_code, path.join(outDir, chunk.fileName), electronPath);
360
- if (typeof result === "string") throw new Error(result);
361
- fs.writeFileSync(`${chunkFilePath}c`, result);
362
- if (chunk.isEntry) {
363
- const code = `${useStrict}\n${getBytecodeLoaderBlock(chunk.fileName)}\nmodule.exports=${`require("./${`${path.basename(name)}c`}");`}\n`;
364
- fs.writeFileSync(chunkFilePath, code);
365
- } else fs.unlinkSync(chunkFilePath);
366
- bytecodeFiles.push({
367
- name: `${name}c`,
368
- size: result.length
369
- });
370
- } else if (chunk.isEntry) {
371
- if (checkHasBytecodeChunk(this.getModuleInfo, chunk.imports, chunk.dynamicImports, bytecodeChunks)) {
372
- const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName);
373
- _code = _code.replace(/use strict(["'];)/, `$1\n${bytecodeLoaderBlock}`);
374
- }
375
- fs.writeFileSync(chunkFilePath, _code);
376
- } else fs.writeFileSync(chunkFilePath, _code);
347
+ if (!hasJsChunks) return;
348
+ const outputDir = outputOptions.dir ?? (outputOptions.file && path.dirname(outputOptions.file));
349
+ const prepareContext = createPrepareContext(Object.values(bundle).filter((f) => f.type === "chunk" && !f.isEntry).map((c) => path.posix.basename(c.fileName)));
350
+ await Promise.all(Object.entries(bundle).map(async ([fileName, item]) => {
351
+ if (item.type !== "chunk" || fileName === "__loader__.js") return;
352
+ const chunk = item;
353
+ const bytecodeFileName = `${fileName}c`;
354
+ const absPath = outputDir ? path.join(outputDir, fileName) : fileName;
355
+ let code = prepare(chunk.code, minify, prepareContext)?.code || chunk.code;
356
+ if (beforeCompile) {
357
+ const hookResult = await beforeCompile(code, absPath);
358
+ if (hookResult) code = hookResult;
377
359
  }
360
+ const bytecode = await compileToBytecode(code, absPath, electronPath);
361
+ if (typeof bytecode === "string") throw new TypeError(bytecode);
362
+ this.emitFile({
363
+ type: "asset",
364
+ source: bytecode,
365
+ fileName: bytecodeFileName
366
+ });
367
+ if (chunk.isEntry) chunk.code = `${useStrict}\n${getBytecodeLoaderBlock(fileName)}\nmodule.exports=require("./${path.posix.basename(fileName)}c");\n`;
368
+ else delete bundle[fileName];
378
369
  }));
379
- },
380
- closeBundle() {
381
- const outDir = `${normalizePath(path.relative(config.root, path.resolve(config.root, config.build.outDir)))}/`;
382
- bytecodeFiles.forEach((file) => {
383
- bytecodeLog.info(`${outDir}${file.name} [${readableSize(file.size)}]`, { timestamp: true });
384
- });
385
- bytecodeLog.info(`${bytecodeFiles.length} bundles compiled into bytecode.`, { timestamp: true });
386
- bytecodeFiles = [];
387
370
  }
388
371
  };
389
372
  }
390
-
391
373
  //#endregion
392
- //#region src/vite/electron/utils.ts
393
- /** Resolve the default Vite's `InlineConfig` for build Electron-Main */
394
- function resolveViteConfig(isESM, options) {
395
- return mergeConfig({
396
- configFile: false,
397
- publicDir: false,
398
- build: {
399
- lib: options.entry && {
400
- entry: options.entry,
401
- formats: isESM ? ["es"] : ["cjs"],
402
- fileName: () => "[name].js"
403
- },
404
- outDir: "dist-electron",
405
- emptyOutDir: false
406
- },
407
- resolve: {
408
- conditions: ["node"],
409
- mainFields: [
410
- "module",
411
- "jsnext:main",
412
- "jsnext"
413
- ]
414
- },
415
- define: { "process.env": "process.env" }
416
- }, options?.vite || {});
374
+ //#region src/vite/local-dev-update.ts
375
+ const LOCAL_DEV_SIGNATURE = "local-dev";
376
+ function resolveLocalDevUpdateOptions(root, options) {
377
+ if (!options) return;
378
+ const resolvedOptions = options === true ? {} : options;
379
+ return {
380
+ baseDir: path.resolve(root, resolvedOptions.baseDir ?? "release/local-update"),
381
+ installedAsarPath: path.resolve(root, "DEV.asar"),
382
+ packageJsonPath: resolvedOptions.packageJsonPath ? path.resolve(root, resolvedOptions.packageJsonPath) : void 0,
383
+ chunkSize: resolvedOptions.chunkSize,
384
+ chunkDelay: resolvedOptions.chunkDelay
385
+ };
417
386
  }
418
- /**
419
- * Inspired `tree-kill`, implemented based on sync-api. #168
420
- * @see https://github.com/pkrumins/node-tree-kill/blob/v1.2.2/index.js
421
- */
422
- function treeKillSync(pid) {
423
- if (process.platform === "win32") cp.execSync(`taskkill /pid ${pid} /T /F`);
424
- else killTree(pidTree({
425
- pid,
426
- ppid: process.pid
427
- }));
387
+ async function resolveLocalDevUpdatePackage(fallbackPkg, localDevUpdate) {
388
+ if (!localDevUpdate?.packageJsonPath) return fallbackPkg;
389
+ const pkg = JSON.parse(await readFile(localDevUpdate.packageJsonPath, "utf-8"));
390
+ if (!pkg.name || !pkg.version || !pkg.main) throw new Error("localDevUpdate.packageJsonPath must contain name, version and main fields");
391
+ return pkg;
428
392
  }
429
- function pidTree(tree) {
430
- const command = process.platform === "darwin" ? `pgrep -P ${tree.pid}` : `ps -o pid --no-headers --ppid ${tree.ppid}`;
393
+ function getNextPatchVersion(version) {
394
+ const parsed = parseVersion(version);
395
+ return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
396
+ }
397
+ function isValidVersion(version) {
431
398
  try {
432
- const childs = cp.execSync(command, { encoding: "utf8" }).match(/\d+/g)?.map((id) => +id);
433
- if (childs) tree.children = childs.map((cid) => pidTree({
434
- pid: cid,
435
- ppid: tree.pid
436
- }));
437
- } catch {}
438
- return tree;
399
+ parseVersion(version);
400
+ return true;
401
+ } catch {
402
+ return false;
403
+ }
439
404
  }
440
- function killTree(tree) {
441
- if (tree.children) for (const child of tree.children) killTree(child);
405
+ function readValidAsarVersion(asarPath) {
406
+ try {
407
+ const version = extractFile(asarPath, "version").toString("utf-8").trim();
408
+ return isValidVersion(version) ? version : void 0;
409
+ } catch {
410
+ return;
411
+ }
412
+ }
413
+ async function installPendingAsar(installedAsarPath) {
414
+ const pendingAsarPath = `${installedAsarPath}.tmp`;
415
+ const version = readValidAsarVersion(pendingAsarPath);
416
+ if (!version) {
417
+ await rm(pendingAsarPath, { force: true });
418
+ return false;
419
+ }
442
420
  try {
443
- process.kill(tree.pid);
421
+ await copyFile(pendingAsarPath, installedAsarPath);
422
+ await rm(pendingAsarPath, { force: true });
423
+ log.info(`Installed pending local dev update ${version}`, { timestamp: true });
424
+ return true;
425
+ } catch (error) {
426
+ if (error.code === "ENOENT") return false;
427
+ throw error;
428
+ }
429
+ }
430
+ async function readExistingUpdateJSON(versionPath, version) {
431
+ try {
432
+ const json = JSON.parse(await readFile(versionPath, "utf-8"));
433
+ if (isUpdateJSON(json)) return json;
444
434
  } catch {}
435
+ return {
436
+ version,
437
+ minimumVersion: "0.0.0",
438
+ signature: LOCAL_DEV_SIGNATURE,
439
+ beta: {
440
+ version,
441
+ minimumVersion: "0.0.0",
442
+ signature: LOCAL_DEV_SIGNATURE
443
+ }
444
+ };
445
445
  }
446
-
447
- //#endregion
448
- //#region src/vite/electron/core.ts
449
- function build$1(isESM, options) {
450
- return build(resolveViteConfig(isESM, options));
446
+ async function prepareLocalDevUpdateResource({ root, pkg, buildAsarOption, versionPath, minimumVersion, keepInstalledVersion, localDevUpdate }) {
447
+ const installedPendingUpdate = await installPendingAsar(localDevUpdate.installedAsarPath);
448
+ const installedVersion = readValidAsarVersion(localDevUpdate.installedAsarPath) ?? pkg.version;
449
+ const targetVersion = keepInstalledVersion && installedPendingUpdate ? installedVersion : getNextPatchVersion(installedVersion);
450
+ const workDir = await mkdtemp(path.join(tmpdir(), "eiu-local-dev-update-"));
451
+ const stagedElectronDistPath = path.join(workDir, "dist-electron");
452
+ const resolvedVersionPath = path.join(localDevUpdate.baseDir, versionPath);
453
+ const asarPath = path.join(localDevUpdate.baseDir, `${pkg.name}.asar`);
454
+ const compressedPath = path.join(localDevUpdate.baseDir, `${pkg.name}-${targetVersion}.asar.br`);
455
+ try {
456
+ await mkdir(localDevUpdate.baseDir, { recursive: true });
457
+ await mkdir(path.dirname(resolvedVersionPath), { recursive: true });
458
+ await cp$1(path.resolve(root, buildAsarOption.electronDistPath), stagedElectronDistPath, { recursive: true });
459
+ await writeFile(path.join(stagedElectronDistPath, "version"), targetVersion, "utf-8");
460
+ await createPackage(stagedElectronDistPath, asarPath);
461
+ await writeFile(compressedPath, await buildAsarOption.generateCompressedFile(await readFile(asarPath)));
462
+ const updateJSON = defaultVersionJsonGenerator(await readExistingUpdateJSON(resolvedVersionPath, targetVersion), LOCAL_DEV_SIGNATURE, targetVersion, minimumVersion);
463
+ if (!isUpdateJSON(updateJSON)) throw new Error("Invalid local dev update json");
464
+ await writeFile(resolvedVersionPath, JSON.stringify(updateJSON, null, 2), "utf-8");
465
+ log.info(`Prepared local dev update ${targetVersion}`, { timestamp: true });
466
+ return targetVersion;
467
+ } finally {
468
+ await rm(workDir, {
469
+ recursive: true,
470
+ force: true
471
+ });
472
+ }
451
473
  }
452
- function electron(isESM, root, options) {
453
- const optionsArray = Array.isArray(options) ? options : [options];
454
- let userConfig;
455
- let configEnv;
456
- if (!version.startsWith("8.")) throw new Error(`[vite-plugin-electron] Vite v${version} does not support \`rolldownOptions\`, please install \`vite@>=8\` or use an earlier version of \`vite-plugin-electron\`.`);
457
- async function parallelBuild(options) {
458
- await Promise.all(options.map(build$1.bind(build$1, isESM)));
474
+ function createLocalDevUpdateOnstart(args) {
475
+ let restartRequested = false;
476
+ let startupArgs;
477
+ let managedElectronApp;
478
+ function createStartupArgs(onstartArgs) {
479
+ return {
480
+ ...onstartArgs,
481
+ startup(argv, options, customElectronPkg) {
482
+ const env = {
483
+ ...process.env,
484
+ ...options?.env
485
+ };
486
+ delete env.ELECTRON_RUN_AS_NODE;
487
+ return onstartArgs.startup(argv, {
488
+ ...options,
489
+ env
490
+ }, customElectronPkg);
491
+ }
492
+ };
459
493
  }
460
- return [{
461
- name: "vite-plugin-electron:dev",
462
- apply: "serve",
463
- configResolved(config) {
464
- if (config.root !== root) throw new Error(`Renderer's root (${config.root}) is not same as electron's root (${root}). Please setup \`root\` in electron plugin`);
465
- },
466
- configureServer(server) {
467
- server.httpServer?.once("listening", () => {
468
- Object.assign(process.env, { VITE_DEV_SERVER_URL: server.resolvedUrls?.local[0] });
469
- const entryCount = optionsArray.length;
470
- let closeBundleCount = 0;
471
- parallelBuild(optionsArray.map((options) => {
472
- options.vite ??= {};
473
- options.vite.mode ??= server.config.mode;
474
- options.vite.root ??= server.config.root;
475
- options.vite.envDir ??= server.config.envDir;
476
- options.vite.envPrefix ??= server.config.envPrefix;
477
- const defaultArgs = [options.vite.root || ".", "--no-sandbox"];
478
- options.vite.build ??= {};
479
- if (!Object.keys(options.vite.build).includes("watch")) options.vite.build.watch = {};
480
- options.vite.build.minify ??= false;
481
- options.vite.plugins ??= [];
482
- options.vite.plugins.push({
483
- name: ":startup",
484
- closeBundle() {
485
- if (++closeBundleCount < entryCount) return;
486
- if (options.onstart) options.onstart.call(this, {
487
- async startup(args = defaultArgs, ...opt) {
488
- await startup(args, ...opt);
489
- },
490
- reload() {
491
- if (process.electronApp) {
492
- (server.hot || server.ws).send({ type: "full-reload" });
493
- startup.send("electron-vite&type=hot-reload");
494
- } else startup(defaultArgs);
495
- }
496
- });
497
- else startup(defaultArgs);
498
- }
499
- });
500
- return options;
501
- }));
502
- });
494
+ async function start(onstartArgs, options = {}) {
495
+ const safeStartupArgs = createStartupArgs(onstartArgs);
496
+ startupArgs = safeStartupArgs;
497
+ await prepareLocalDevUpdateResource({
498
+ ...args,
499
+ keepInstalledVersion: options.keepInstalledVersion
500
+ });
501
+ if (args.userOnstart) await args.userOnstart(safeStartupArgs);
502
+ else await safeStartupArgs.startup();
503
+ const electronApp = process.electronApp;
504
+ removeManagedListeners();
505
+ managedElectronApp = electronApp;
506
+ managedElectronApp?.removeListener("exit", process.exit);
507
+ managedElectronApp?.on("message", handleMessage);
508
+ managedElectronApp?.once("exit", handleExit);
509
+ }
510
+ function handleMessage(message) {
511
+ if (message === "eiu:restart") {
512
+ restartRequested = true;
513
+ managedElectronApp?.send?.("eiu:restart-ready");
503
514
  }
504
- }, {
505
- name: "vite-plugin-electron:prod",
506
- apply: "build",
507
- config(config, env) {
508
- userConfig = config;
509
- configEnv = env;
510
- config.base ??= "./";
511
- },
512
- async closeBundle() {
513
- await parallelBuild(optionsArray.map((options) => {
514
- options.vite ??= {};
515
- options.vite.mode ??= configEnv.mode;
516
- options.vite.root ??= userConfig.root;
517
- options.vite.envDir ??= userConfig.envDir;
518
- options.vite.envPrefix ??= userConfig.envPrefix;
519
- return options;
520
- }));
515
+ }
516
+ function handleExit() {
517
+ if (restartRequested) {
518
+ restartRequested = false;
519
+ setTimeout(() => {
520
+ if (startupArgs) start(startupArgs, { keepInstalledVersion: true });
521
+ });
522
+ return;
521
523
  }
522
- }];
524
+ process.exit();
525
+ }
526
+ function removeManagedListeners() {
527
+ managedElectronApp?.removeListener("message", handleMessage);
528
+ managedElectronApp?.removeListener("exit", handleExit);
529
+ }
530
+ return (onstartArgs) => start(onstartArgs);
523
531
  }
532
+ //#endregion
533
+ //#region src/vite/utils/file.ts
524
534
  /**
525
- * Electron App startup function.
526
- * It will mount the Electron App child-process to `process.electronApp`.
527
- * @param argv default value `['.', '--no-sandbox']`
528
- * @param options options for `child_process.spawn`
529
- * @param customElectronPkg custom electron package name (default: 'electron')
535
+ * Convert byte size to human-readable format
536
+ * @param size - Size in bytes
537
+ * @returns Human-readable size string (e.g., "1.23 MB")
530
538
  */
531
- const startup = async (argv = [".", "--no-sandbox"], options, customElectronPkg) => {
532
- const { spawn } = await import("node:child_process");
533
- const electron = await import(customElectronPkg ?? "electron");
534
- const electronPath = electron.default ?? electron;
535
- await startup.exit();
536
- const stdio = process.platform === "linux" ? [
537
- "inherit",
538
- "inherit",
539
- "inherit",
540
- "ignore",
541
- "ipc"
542
- ] : [
543
- "inherit",
544
- "inherit",
545
- "inherit",
546
- "ipc"
539
+ function readableSize(size) {
540
+ const units = [
541
+ "B",
542
+ "KB",
543
+ "MB",
544
+ "GB"
547
545
  ];
548
- process.electronApp = spawn(electronPath, argv, {
549
- stdio,
550
- ...options
551
- });
552
- process.electronApp.once("exit", process.exit);
553
- if (!startup.hookedProcessExit) {
554
- startup.hookedProcessExit = true;
555
- process.once("exit", startup.exit);
546
+ let i = 0;
547
+ while (size >= 1024 && i < units.length - 1) {
548
+ size /= 1024;
549
+ i++;
556
550
  }
557
- };
558
- startup.send = (message) => {
559
- if (process.electronApp) process.electronApp.send?.(message);
560
- };
561
- startup.hookedProcessExit = false;
562
- startup.exit = async () => {
563
- if (process.electronApp) await new Promise((resolve) => {
564
- process.electronApp.removeAllListeners();
565
- process.electronApp.once("exit", resolve);
566
- treeKillSync(process.electronApp.pid);
567
- });
568
- };
569
-
570
- //#endregion
571
- //#region src/vite/electron/plugin.ts
572
- /**
573
- * @see https://github.com/vitejs/vite/blob/v4.4.7/packages/vite/src/node/utils.ts#L140
574
- */
575
- const bareImportRE = /^(?![a-zA-Z]:)[\w@](?!.*:\/\/)/;
576
- const nodeModulesRE = /\/node_modules\//;
551
+ return `${size.toFixed(2)} ${units[i]}`;
552
+ }
577
553
  /**
578
- * During dev, we exclude the `cjs` npm-pkg from bundle, mush like Vite :)
554
+ * Copy file/directory, skipping if target exists
555
+ * @param from - Source path
556
+ * @param to - Destination path
557
+ * @param skipIfExist - Skip copy if destination exists
579
558
  */
580
- function notBundle(options = {}) {
581
- const externalIds = /* @__PURE__ */ new Set();
582
- return {
583
- name: "vite-plugin-electron:not-bundle",
584
- enforce: "pre",
585
- apply: "serve",
586
- resolveId: {
587
- filter: { id: bareImportRE },
588
- async handler(source, importer) {
589
- if (!importer || importer.includes("node_modules/")) return;
590
- if (externalIds.has(source)) return {
591
- id: source,
592
- external: true
593
- };
594
- const id = (await this.resolve(source, importer, { skipSelf: true }))?.id;
595
- if (!id || !nodeModulesRE.test(id) || options.filter?.(id) === false) return;
596
- try {
597
- createRequire(importer).resolve(source);
598
- } catch {
599
- return;
600
- }
601
- externalIds.add(source);
602
- return {
603
- id: source,
604
- external: true,
605
- moduleSideEffects: false
606
- };
607
- }
608
- }
609
- };
559
+ function copyAndSkipIfExist(from, to, skipIfExist) {
560
+ if (!skipIfExist || !fs.existsSync(to)) try {
561
+ fs.cpSync(from, to, { recursive: true });
562
+ } catch (error) {
563
+ log.warn(`Copy failed: ${error}`, { timestamp: true });
564
+ }
610
565
  }
611
-
612
566
  //#endregion
613
- //#region src/utils/crypto.ts
614
- function hashBuffer(data, length) {
615
- const hash = crypto.createHash("SHA256").update(data).digest("binary");
616
- return Buffer.from(hash).subarray(0, length);
617
- }
618
- function aesEncrypt(plainText, key, iv) {
619
- const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
620
- return cipher.update(plainText, "utf8", "base64url") + cipher.final("base64url");
621
- }
567
+ //#region src/vite/utils/build.ts
622
568
  /**
623
- * Default function to generate asar signature, returns generated signature
624
- * @param buffer file buffer
625
- * @param privateKey primary key
626
- * @param cert certificate
627
- * @param version target version
569
+ * Build asar file and update package
570
+ * @param options - Asar build options
571
+ * @returns Buffer of the built asar file
628
572
  */
629
- function defaultSignature(buffer, privateKey, cert, version) {
630
- return aesEncrypt(`${crypto.createSign("RSA-SHA256").update(buffer).sign(crypto.createPrivateKey(privateKey), "base64")}%${version}`, hashBuffer(cert, 32), hashBuffer(buffer, 16));
573
+ async function buildAsar(root, { version, asarOutputPath, electronDistPath, rendererDistPath, compressedPath, generateCompressedFile }) {
574
+ electronDistPath = path.resolve(root, electronDistPath);
575
+ asarOutputPath = path.resolve(root, asarOutputPath);
576
+ rendererDistPath = path.resolve(root, rendererDistPath);
577
+ compressedPath = path.resolve(root, compressedPath);
578
+ const rPath = path.join(electronDistPath, "renderer");
579
+ await fs.promises.cp(rendererDistPath, rPath, { recursive: true });
580
+ fs.writeFileSync(path.join(electronDistPath, "version"), version);
581
+ await fs.promises.mkdir(path.dirname(asarOutputPath), { recursive: true });
582
+ await createPackage(electronDistPath, asarOutputPath);
583
+ const buf = await generateCompressedFile(fs.readFileSync(asarOutputPath));
584
+ await fs.promises.mkdir(path.dirname(compressedPath), { recursive: true });
585
+ fs.writeFileSync(compressedPath, buf);
586
+ log.info(`Build update asar to '${compressedPath}' [${readableSize(buf.length)}]`, { timestamp: true });
587
+ return buf;
631
588
  }
632
-
633
- //#endregion
634
- //#region src/utils/zip.ts
635
589
  /**
636
- * Default function to compress file using brotli
637
- * @param buffer uncompressed file buffer
590
+ * Build update.json file with signature and version information
591
+ * @param options - Version build options
592
+ * @param asarBuffer - Buffer of the asar file to sign
638
593
  */
639
- async function defaultZipFile(buffer) {
640
- return new Promise((resolve, reject) => {
641
- zlib.brotliCompress(buffer, (err, buffer) => err ? reject(err) : resolve(buffer));
642
- });
594
+ async function buildUpdateJson(root, { versionPath, privateKey, cert, version, minimumVersion, generateSignature, generateUpdateJson }, asarBuffer) {
595
+ const resolvedVersionPath = path.resolve(root, versionPath);
596
+ let _json = {
597
+ beta: {
598
+ minimumVersion: version,
599
+ signature: "",
600
+ version
601
+ },
602
+ minimumVersion: version,
603
+ signature: "",
604
+ version
605
+ };
606
+ if (fs.existsSync(versionPath)) try {
607
+ const oldVersionJson = JSON.parse(fs.readFileSync(versionPath, "utf-8"));
608
+ if (isUpdateJSON(oldVersionJson)) _json = oldVersionJson;
609
+ else log.warn("Old version json is invalid, ignore it", { timestamp: true });
610
+ } catch {}
611
+ const sig = await generateSignature(asarBuffer, privateKey, cert, version);
612
+ _json = await generateUpdateJson(_json, sig, version, minimumVersion);
613
+ if (!isUpdateJSON(_json)) throw new Error("Invalid update json");
614
+ await fs.promises.mkdir(path.dirname(resolvedVersionPath), { recursive: true });
615
+ fs.writeFileSync(resolvedVersionPath, JSON.stringify(_json, null, 2));
616
+ log.info(`Build update json to '${resolvedVersionPath}'`, { timestamp: true });
643
617
  }
644
-
645
618
  //#endregion
646
619
  //#region src/vite/utils/key.ts
647
620
  /**
@@ -708,31 +681,32 @@ function parseSubjects(subject) {
708
681
  value
709
682
  }));
710
683
  }
711
-
712
684
  //#endregion
713
- //#region src/vite/option.ts
714
- async function parseUpdaterOption(pkg, options = {}) {
715
- const { minimumVersion = "0.0.0", paths: { asarOutputPath = `release/${pkg.name}.asar`, gzipPath = `release/${pkg.name}-${pkg.version}.asar.gz`, entryOutDir = "dist-entry", electronDistPath = "dist-electron", rendererDistPath = "dist", versionPath = "version.json" } = {}, keys: { privateKeyPath = "keys/private.pem", certPath = "keys/cert.pem", keyLength = 2048, certInfo: { subject = {
685
+ //#region src/vite/core.ts
686
+ async function resolveUpdaterOption(root, pkg, options = {}, resolveSignatureKeys = true) {
687
+ const { minimumVersion = "0.0.0", paths: { asarOutputPath = `release/${pkg.name}.asar`, compressedPath = `release/${pkg.name}-${pkg.version}.asar.br`, entryOutDir = "dist-entry", electronDistPath = "dist-electron", rendererDistPath = "dist", versionPath = "release/version.json" } = {}, keys: { privateKeyPath = "keys/private.pem", certPath = "keys/cert.pem", keyLength = 2048, certInfo: { subject = {
716
688
  commonName: pkg.name,
717
689
  organizationName: `org.${pkg.name}`
718
- }, days = 3650 } = {} } = {}, overrideGenerator: { generateGzipFile = defaultZipFile, generateSignature = defaultSignature, generateUpdateJson = defaultVersionJsonGenerator } = {} } = options;
719
- const buildAsarOption = {
720
- version: pkg.version,
721
- asarOutputPath,
722
- gzipPath,
723
- electronDistPath,
724
- rendererDistPath,
725
- generateGzipFile
726
- };
727
- const { privateKey, cert } = await parseKeys({
690
+ }, days = 3650 } = {} } = {}, overrideGenerator: { generateCompressedFile = defaultCompressFile, generateSignature = defaultSignature, generateUpdateJson = defaultVersionJsonGenerator } = {} } = options;
691
+ const { privateKey, cert } = resolveSignatureKeys ? await parseKeys({
728
692
  keyLength,
729
- privateKeyPath,
730
- certPath,
693
+ privateKeyPath: path.resolve(root, privateKeyPath),
694
+ certPath: path.resolve(root, certPath),
731
695
  subject,
732
696
  days
733
- });
697
+ }) : {
698
+ privateKey: "",
699
+ cert: ""
700
+ };
734
701
  return {
735
- buildAsarOption,
702
+ buildAsarOption: {
703
+ version: pkg.version,
704
+ asarOutputPath,
705
+ compressedPath,
706
+ electronDistPath,
707
+ rendererDistPath,
708
+ generateCompressedFile
709
+ },
736
710
  buildVersionOption: {
737
711
  version: pkg.version,
738
712
  minimumVersion,
@@ -742,269 +716,230 @@ async function parseUpdaterOption(pkg, options = {}) {
742
716
  generateSignature,
743
717
  generateUpdateJson
744
718
  },
745
- cert,
746
719
  entryOutDir
747
720
  };
748
721
  }
749
-
750
- //#endregion
751
- //#region src/vite/utils/build.ts
752
- /**
753
- * Build asar file and update package
754
- * @param options - Asar build options
755
- * @returns Buffer of the built asar file
756
- */
757
- async function buildAsar({ version, asarOutputPath, gzipPath, electronDistPath, rendererDistPath, generateGzipFile }) {
758
- const rPath = path.join(electronDistPath, "renderer");
759
- await fs.promises.cp(rendererDistPath, rPath, { recursive: true });
760
- fs.writeFileSync(path.join(electronDistPath, "version"), version);
761
- await createPackage(electronDistPath, asarOutputPath);
762
- const buf = await generateGzipFile(fs.readFileSync(asarOutputPath));
763
- fs.writeFileSync(gzipPath, buf);
764
- log.info(`Build update asar to '${gzipPath}' [${readableSize(buf.length)}]`, { timestamp: true });
765
- return buf;
766
- }
767
- /**
768
- * Build update.json file with signature and version information
769
- * @param options - Version build options
770
- * @param asarBuffer - Buffer of the asar file to sign
771
- */
772
- async function buildUpdateJson({ versionPath, privateKey, cert, version, minimumVersion, generateSignature, generateUpdateJson }, asarBuffer) {
773
- let _json = {
774
- beta: {
775
- minimumVersion: version,
776
- signature: "",
777
- version
778
- },
779
- minimumVersion: version,
780
- signature: "",
781
- version
782
- };
783
- if (fs.existsSync(versionPath)) try {
784
- const oldVersionJson = JSON.parse(fs.readFileSync(versionPath, "utf-8"));
785
- if (isUpdateJSON(oldVersionJson)) _json = oldVersionJson;
786
- else log.warn("Old version json is invalid, ignore it", { timestamp: true });
787
- } catch {}
788
- const sig = await generateSignature(asarBuffer, privateKey, cert, version);
789
- _json = await generateUpdateJson(_json, sig, version, minimumVersion);
790
- if (!isUpdateJSON(_json)) throw new Error("Invalid update json");
791
- fs.writeFileSync(versionPath, JSON.stringify(_json, null, 2));
792
- log.info(`build update json to '${versionPath}'`, { timestamp: true });
793
- }
794
-
795
- //#endregion
796
- //#region src/vite/core.ts
797
- function getMainFileBaseName(options) {
798
- let mainFilePath;
799
- if (typeof options === "string") mainFilePath = path.basename(options);
800
- else if (Array.isArray(options)) mainFilePath = path.basename(options[0]);
801
- else {
802
- if (!(options?.index ?? options?.main)) throw new Error(`\`options.main.files\` (${options}) must have "index" or "main" key, like \`{ index: "./electron/main/index.ts" }\``);
803
- mainFilePath = options?.index ? "index.js" : "main.js";
722
+ function resolveEntryName(files) {
723
+ if (typeof files === "string") return path.parse(files).name;
724
+ if (Array.isArray(files)) {
725
+ const [firstInput] = files;
726
+ if (!firstInput) throw new Error("`options.main.files` must contain at least one main entry");
727
+ return path.parse(firstInput).name;
804
728
  }
805
- log.info(`Using "${mainFilePath}" as main file`, { timestamp: true });
806
- return mainFilePath.replace(/\.[cm]?ts$/, ".js");
729
+ const firstEntry = Object.entries(files)[0];
730
+ if (!firstEntry) throw new Error("`options.main.files` must contain at least one main entry");
731
+ return firstEntry[0];
807
732
  }
808
- function parseVersionPath(versionPath) {
733
+ function normalizeVersionPath(versionPath) {
809
734
  versionPath = normalizePath(versionPath);
810
735
  if (!versionPath.startsWith("./")) versionPath = `./${versionPath}`;
811
736
  return new URL(versionPath, "file://").pathname.slice(1);
812
737
  }
813
- const defaultExternal = [
814
- ...builtinModules,
815
- "electron",
816
- /^node:/,
817
- /.*\.(node|dll|dylib|so)$/,
818
- "original-fs"
819
- ];
820
- /**
821
- * Base on `./electron/simple`
822
- * - integrate with updater
823
- * - no `renderer` config
824
- * - remove old output file
825
- * - externalize dependencies
826
- * - auto restart when entry file changes
827
- * - other configs in {@link https://github.com/electron-vite/electron-vite-vue/blob/main/vite.config.ts electron-vite-vue template}
828
- *
829
- * You can override all the vite configs, except output directories (use `options.updater.paths.electronDistPath` instead)
830
- *
831
- * @example
832
- * ```ts
833
- * import { defineConfig } from 'vite'
834
- * import { debugStartup, electronWithUpdater } from 'electron-incremental-update/vite'
835
- *
836
- * export default defineConfig(async ({ command }) => {
837
- * const isBuild = command === 'build'
838
- * return {
839
- * plugins: [
840
- * electronWithUpdater({
841
- * isBuild,
842
- * main: {
843
- * files: ['./electron/main/index.ts', './electron/main/worker.ts'],
844
- * // see https://github.com/electron-vite/electron-vite-vue/blob/85ed267c4851bf59f32888d766c0071661d4b94c/vite.config.ts#L22-L28
845
- * onstart: debugStartup,
846
- * },
847
- * preload: {
848
- * files: './electron/preload/index.ts',
849
- * },
850
- * updater: {
851
- * // options
852
- * }
853
- * }),
854
- * ],
855
- * server: process.env.VSCODE_DEBUG && (() => {
856
- * const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL)
857
- * return {
858
- * host: url.hostname,
859
- * port: +url.port,
860
- * }
861
- * })(),
862
- * }
863
- * })
864
- * ```
865
- */
866
- async function electronWithUpdater(options) {
867
- let { isBuild, root = process.cwd(), external, entry: _entry, main: _main, preload: _preload, sourcemap = !isBuild || !!process.env.VSCODE_DEBUG, minify = isBuild, buildVersionJson, updater, bytecode, useNotBundle = true } = options;
868
- const pkg = await loadPackageJSON(root);
738
+ async function createElectronOptions(options, context) {
739
+ const { entry, main, preload, sourcemap = context.isDev || !!process.env.VSCODE_DEBUG, minify = !context.isDev, buildVersionJson, notBundle = true, external, updater, bytecode, localDevUpdate } = options;
740
+ const pkg = context.packageJson;
869
741
  if (!pkg || !pkg.version || !pkg.name || !pkg.main) throw new Error("package.json not found or invalid, must contains version, name and main field");
870
742
  const isESM = pkg.type === "module";
871
- const finalExternal = [...defaultExternal, ...isBuild || _entry.postBuild ? [] : external || Object.keys(pkg.dependencies || {})];
872
- let bytecodeOptions = typeof bytecode === "object" ? bytecode : bytecode === true ? { enable: true } : void 0;
743
+ const finalExternal = [...defaultExternal];
744
+ if (external === true) finalExternal.push(...Object.keys(pkg.dependencies || {}));
745
+ else if (Array.isArray(external)) finalExternal.push(...external);
746
+ const bytecodeOptions = typeof bytecode === "object" ? {
747
+ ...bytecode,
748
+ enable: bytecode.enable ?? true
749
+ } : bytecode === true ? { enable: true } : void 0;
873
750
  if (isESM && bytecodeOptions?.enable) throw new Error("`bytecodePlugin` does not support ES module, please remove \"type\": \"module\" in package.json");
874
- const { buildAsarOption, buildVersionOption, cert, entryOutDir } = await parseUpdaterOption(pkg, updater);
751
+ const resolvedLocalDevUpdate = context.isDev ? resolveLocalDevUpdateOptions(context.root, localDevUpdate) : void 0;
752
+ const updatePkg = await resolveLocalDevUpdatePackage(pkg, resolvedLocalDevUpdate);
753
+ const { buildAsarOption, buildVersionOption, entryOutDir } = await resolveUpdaterOption(context.root, updatePkg, updater, !resolvedLocalDevUpdate);
754
+ const mainFileName = `${resolveEntryName(main.files)}.${isESM ? "mjs" : "js"}`;
755
+ log.info(`Using "${mainFileName}" as main file`, { timestamp: true });
875
756
  log.info(`Clear cache files`, { timestamp: true });
876
757
  await Promise.all([
877
- fs.promises.rm(buildAsarOption.rendererDistPath, {
878
- recursive: true,
879
- force: true
880
- }),
881
- fs.promises.rm(buildAsarOption.electronDistPath, {
882
- recursive: true,
883
- force: true
884
- }),
885
- fs.promises.rm(entryOutDir, {
886
- recursive: true,
887
- force: true
888
- })
889
- ]).catch(() => {});
758
+ buildAsarOption.rendererDistPath,
759
+ buildAsarOption.electronDistPath,
760
+ entryOutDir
761
+ ].map((p) => fs.promises.rm(path.resolve(context.root, p), {
762
+ recursive: true,
763
+ force: true
764
+ }))).catch(() => {});
765
+ const outputNames = {
766
+ entryFileNames: `[name].${isESM ? "mjs" : "js"}`,
767
+ chunkFileNames: `[name].${isESM ? "mjs" : "js"}`,
768
+ assetFileNames: "[name].[ext]"
769
+ };
770
+ const versionPath = normalizeVersionPath(normalizePath(buildVersionOption.versionPath));
771
+ const mainOnstart = resolvedLocalDevUpdate ? createLocalDevUpdateOnstart({
772
+ root: context.root,
773
+ pkg: updatePkg,
774
+ buildAsarOption,
775
+ versionPath,
776
+ minimumVersion: buildVersionOption.minimumVersion,
777
+ localDevUpdate: resolvedLocalDevUpdate,
778
+ userOnstart: main.onstart
779
+ }) : main.onstart;
890
780
  const define = {
891
781
  __EIU_ASAR_BASE_NAME__: JSON.stringify(path.basename(buildAsarOption.asarOutputPath)),
892
782
  __EIU_ELECTRON_DIST_PATH__: JSON.stringify(normalizePath(buildAsarOption.electronDistPath)),
893
783
  __EIU_ENTRY_DIST_PATH__: JSON.stringify(normalizePath(entryOutDir)),
894
- __EIU_IS_DEV__: JSON.stringify(!isBuild),
784
+ __EIU_IS_DEV__: JSON.stringify(context.isDev),
895
785
  __EIU_IS_ESM__: JSON.stringify(isESM),
896
- __EIU_MAIN_FILE__: JSON.stringify(getMainFileBaseName(_main.files)),
897
- __EIU_SIGNATURE_CERT__: JSON.stringify(cert),
898
- __EIU_VERSION_PATH__: JSON.stringify(parseVersionPath(normalizePath(buildVersionOption.versionPath)))
786
+ __EIU_LOCAL_DEV_UPDATE__: JSON.stringify(!!resolvedLocalDevUpdate),
787
+ __EIU_LOCAL_DEV_UPDATE_ASAR_PATH__: JSON.stringify(resolvedLocalDevUpdate?.installedAsarPath ?? ""),
788
+ __EIU_LOCAL_DEV_UPDATE_CHUNK_DELAY__: JSON.stringify(resolvedLocalDevUpdate?.chunkDelay) ?? "undefined",
789
+ __EIU_LOCAL_DEV_UPDATE_CHUNK_SIZE__: JSON.stringify(resolvedLocalDevUpdate?.chunkSize) ?? "undefined",
790
+ __EIU_LOCAL_DEV_UPDATE_DIR__: JSON.stringify(resolvedLocalDevUpdate?.baseDir ?? ""),
791
+ __EIU_MAIN_FILE__: JSON.stringify(mainFileName),
792
+ __EIU_SIGNATURE_CERT__: JSON.stringify(buildVersionOption.cert),
793
+ __EIU_VERSION_PATH__: JSON.stringify(versionPath)
899
794
  };
900
795
  const _electronOptions = [{
901
- entry: _main.files,
902
- onstart: _main.onstart,
903
- vite: mergeConfig({
904
- plugins: [!isBuild && useNotBundle && notBundle(), bytecodeOptions && bytecodePlugin("main", isESM, bytecodeOptions)],
796
+ name: "main",
797
+ input: main.files,
798
+ onstart: mainOnstart,
799
+ notBundle,
800
+ plugins: [isESM && esmShim(), bytecodeOptions && bytecodePlugin("main", minify, isESM, bytecodeOptions)],
801
+ options: mergeConfig({
905
802
  build: {
906
803
  sourcemap,
907
804
  minify,
908
805
  outDir: `${buildAsarOption.electronDistPath}/main`,
909
806
  rolldownOptions: {
910
807
  external: finalExternal,
911
- platform: "node",
912
808
  output: {
913
- polyfillRequire: false,
914
- exports: "named"
809
+ format: isESM ? "es" : "cjs",
810
+ polyfillRequire: isESM,
811
+ ...outputNames
915
812
  }
916
813
  }
917
814
  },
918
815
  define
919
- }, _main.vite ?? {})
816
+ }, main.options ?? {})
920
817
  }];
921
- if (_preload?.files) _electronOptions.push({
818
+ if (preload?.files) _electronOptions.push({
819
+ name: "preload",
922
820
  onstart(args) {
923
821
  args.reload();
924
822
  },
925
- vite: mergeConfig({
926
- plugins: [bytecodeOptions && bytecodePlugin("preload", isESM, bytecodeOptions)],
823
+ notBundle,
824
+ input: preload.files,
825
+ plugins: [isESM && esmShim(), bytecodeOptions && bytecodePlugin("preload", minify, isESM, bytecodeOptions)],
826
+ options: mergeConfig({
927
827
  build: {
928
828
  sourcemap: sourcemap ? "inline" : void 0,
929
829
  minify,
930
830
  outDir: `${buildAsarOption.electronDistPath}/preload`,
931
831
  rolldownOptions: {
932
832
  external: finalExternal,
933
- input: _preload.files,
934
833
  output: {
935
834
  format: "cjs",
936
835
  codeSplitting: false,
937
836
  polyfillRequire: false,
938
- entryFileNames: `[name].${isESM ? "mjs" : "js"}`,
939
- chunkFileNames: `[name].${isESM ? "mjs" : "js"}`,
940
- assetFileNames: "[name].[ext]"
837
+ ...outputNames
941
838
  }
942
839
  }
943
840
  },
944
841
  define
945
- }, _preload?.vite ?? {})
842
+ }, preload?.options ?? {})
946
843
  });
947
844
  _electronOptions.push({
948
- entry: _entry.files,
949
- vite: mergeConfig({
950
- plugins: [
951
- bytecodeOptions && bytecodePlugin("main", isESM, bytecodeOptions),
952
- !isBuild && useNotBundle && notBundle(),
953
- {
954
- name: `${id}:entry`,
955
- enforce: "post",
956
- async closeBundle() {
957
- log.info(`Build entry to '${entryOutDir}'`, { timestamp: true });
958
- await _entry.postBuild?.({
959
- isBuild,
960
- getPathFromEntryOutputDir(...paths) {
961
- return path.join(entryOutDir, ...paths);
962
- },
963
- copyToEntryOutputDir({ from, to = path.basename(from), skipIfExist = true }) {
964
- if (!fs.existsSync(from)) {
965
- log.warn(`${from} not found`, { timestamp: true });
966
- return;
967
- }
968
- copyAndSkipIfExist(from, path.join(entryOutDir, to), skipIfExist);
969
- },
970
- copyModules({ modules, skipIfExist = true }) {
971
- const nodeModulesPath = path.join(entryOutDir, "node_modules");
972
- for (const m of modules) {
973
- const { rootPath } = getPackageInfoSync(m) || {};
974
- if (!rootPath) {
975
- log.warn(`Package '${m}' not found`, { timestamp: true });
976
- continue;
977
- }
978
- copyAndSkipIfExist(rootPath, path.join(nodeModulesPath, m), skipIfExist);
979
- }
845
+ name: "entry",
846
+ input: entry.files,
847
+ async onstart(args) {
848
+ if (mainOnstart) await mainOnstart(args);
849
+ else await args.startup();
850
+ },
851
+ notBundle,
852
+ plugins: [
853
+ isESM && esmShim(),
854
+ bytecodeOptions && bytecodePlugin("entry", minify, isESM, bytecodeOptions),
855
+ {
856
+ name: `${id}:entry`,
857
+ async closeBundle() {
858
+ log.info(`Build entry to '${entryOutDir}'`, { timestamp: true });
859
+ await entry.postBuild?.({
860
+ isBuild: !context.isDev,
861
+ getPathFromEntryOutputDir(...paths) {
862
+ return path.join(entryOutDir, ...paths);
863
+ },
864
+ copyToEntryOutputDir({ from, to = path.basename(from), skipIfExist = true }) {
865
+ if (!fs.existsSync(from)) {
866
+ log.warn(`${from} not found`, { timestamp: true });
867
+ return;
980
868
  }
981
- });
982
- if (isBuild) try {
983
- const buffer = await buildAsar(buildAsarOption);
984
- if (!buildVersionJson && !isCI) log.warn("No `buildVersionJson` option setup, skip build version json. Only build in CI by default", { timestamp: true });
985
- else await buildUpdateJson(buildVersionOption, buffer);
986
- } catch (error) {
987
- console.error(error);
869
+ copyAndSkipIfExist(from, path.join(entryOutDir, to), skipIfExist);
870
+ },
871
+ copyModules() {
872
+ console.warn("`copyModules()` is deprecated. Will do nothing");
988
873
  }
989
- }
874
+ });
875
+ if (context.isDev) return;
876
+ const buffer = await buildAsar(context.root, buildAsarOption);
877
+ if (!buildVersionJson && !isCI) log.warn("No `buildVersionJson` option setup, skip build version json. Only build in CI by default", { timestamp: true });
878
+ else await buildUpdateJson(context.root, buildVersionOption, buffer);
990
879
  }
991
- ],
880
+ }
881
+ ],
882
+ options: mergeConfig({
992
883
  build: {
993
884
  sourcemap,
994
885
  minify,
995
886
  outDir: entryOutDir,
996
887
  rolldownOptions: {
997
888
  external: finalExternal,
998
- platform: "node",
999
- output: { polyfillRequire: false }
889
+ output: {
890
+ format: isESM ? "es" : "cjs",
891
+ polyfillRequire: isESM,
892
+ ...outputNames
893
+ }
1000
894
  }
1001
895
  },
1002
896
  define
1003
- }, _entry.vite || {})
897
+ }, entry.options || {})
898
+ });
899
+ return _electronOptions;
900
+ }
901
+ /**
902
+ * Base on `vite-plugin-electron/multi-env`
903
+ * - integrate with updater
904
+ * - no `renderer` config
905
+ * - remove old output file
906
+ * - externalize dependencies
907
+ * - auto restart when entry file changes
908
+ *
909
+ * You can override all the environment configs, except output directories (use `options.updater.paths.electronDistPath` instead)
910
+ *
911
+ * @example
912
+ * ```ts
913
+ * import { defineConfig } from 'vite'
914
+ * import { electronWithUpdater } from 'electron-incremental-update/vite'
915
+ *
916
+ * export default defineConfig(async ({ command }) => {
917
+ * const isBuild = command === 'build'
918
+ * return {
919
+ * plugins: [
920
+ * electronWithUpdater({
921
+ * isBuild,
922
+ * main: {
923
+ * files: ['./electron/main/index.ts', './electron/main/worker.ts'],
924
+ * },
925
+ * preload: {
926
+ * files: './electron/preload/index.ts',
927
+ * },
928
+ * updater: {
929
+ * // options
930
+ * }
931
+ * }),
932
+ * ],
933
+ * }
934
+ * })
935
+ * ```
936
+ */
937
+ async function electronWithUpdater(options) {
938
+ return electronPluginFactory((context) => {
939
+ process.CACHED_ELECTRON_OPTIONS ??= createElectronOptions(options, context);
940
+ return process.CACHED_ELECTRON_OPTIONS;
1004
941
  });
1005
- return electron(isESM, normalizePath(path.resolve(root)), _electronOptions);
1006
942
  }
1007
-
1008
943
  //#endregion
1009
944
  //#region src/vite/define.ts
1010
945
  /**
@@ -1015,10 +950,9 @@ async function electronWithUpdater(options) {
1015
950
  * import { defineElectronConfig } from 'electron-incremental-update/vite'
1016
951
  *
1017
952
  * export default defineElectronConfig({
953
+ * // root: './apps'
1018
954
  * main: {
1019
955
  * files: ['./electron/main/index.ts', './electron/main/worker.ts'],
1020
- * // see https://github.com/electron-vite/electron-vite-vue/blob/85ed267c4851bf59f32888d766c0071661d4b94c/vite.config.ts#L22-L28
1021
- * onstart: debugStartup,
1022
956
  * },
1023
957
  * preload: {
1024
958
  * files: './electron/preload/index.ts',
@@ -1027,33 +961,23 @@ async function electronWithUpdater(options) {
1027
961
  * // options
1028
962
  * },
1029
963
  * renderer: {
1030
- * server: process.env.VSCODE_DEBUG && (() => {
1031
- * const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL)
1032
- * return {
1033
- * host: url.hostname,
1034
- * port: +url.port,
1035
- * }
1036
- * })(),
964
+ * // plugins: []
1037
965
  * }
1038
966
  * })
1039
967
  * ```
1040
968
  */
1041
969
  function defineElectronConfig(options) {
1042
- return ({ command }) => {
1043
- options.isBuild ??= command === "build";
1044
- const electronPlugin = electronWithUpdater(options);
1045
- const result = options.renderer ?? {};
1046
- result.plugins ??= [];
1047
- result.plugins.push(electronPlugin);
1048
- result.root = options.root;
1049
- const rendererDistPath = options.updater?.paths?.rendererDistPath;
1050
- if (rendererDistPath) {
1051
- result.build ??= {};
1052
- result.build.outDir = rendererDistPath;
1053
- }
1054
- return result;
1055
- };
970
+ const electronPlugin = electronWithUpdater(options);
971
+ const result = options.renderer ?? {};
972
+ result.plugins ??= [];
973
+ result.plugins.push(electronPlugin);
974
+ result.root = options.root;
975
+ const rendererDistPath = options.updater?.paths?.rendererDistPath;
976
+ if (rendererDistPath) {
977
+ result.build ??= {};
978
+ result.build.outDir = rendererDistPath;
979
+ }
980
+ return result;
1056
981
  }
1057
-
1058
982
  //#endregion
1059
- export { debugStartup, electronWithUpdater as default, electronWithUpdater, defineElectronConfig, filterErrorMessageStartup, fixWinCharEncoding };
983
+ export { electronWithUpdater as default, electronWithUpdater, defineElectronConfig, filterErrorMessageStartup, fixWinCharEncoding };