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

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";
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";
10
8
  import crypto from "node:crypto";
11
9
  import zlib from "node:zlib";
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,35 @@ function fixWinCharEncoding(fn) {
61
40
  await fn(...args);
62
41
  });
63
42
  }
64
-
65
- //#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
43
  //#endregion
73
- //#region src/vite/utils/file.ts
74
- /**
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")
78
- */
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]}`;
44
+ //#region src/utils/crypto.ts
45
+ function hashBuffer(data, length) {
46
+ const hash = crypto.createHash("SHA256").update(data).digest("binary");
47
+ return Buffer.from(hash).subarray(0, length);
48
+ }
49
+ function aesEncrypt(plainText, key, iv) {
50
+ const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
51
+ return cipher.update(plainText, "utf8", "base64url") + cipher.final("base64url");
92
52
  }
93
53
  /**
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
54
+ * Default function to generate asar signature, returns generated signature
55
+ * @param buffer file buffer
56
+ * @param privateKey primary key
57
+ * @param cert certificate
58
+ * @param version target version
98
59
  */
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
- }
60
+ function defaultSignature(buffer, privateKey, cert, version) {
61
+ return aesEncrypt(`${crypto.createSign("RSA-SHA256").update(buffer).sign(crypto.createPrivateKey(privateKey), "base64")}%${version}`, hashBuffer(cert, 32), hashBuffer(buffer, 16));
105
62
  }
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
63
  //#endregion
113
64
  //#region src/utils/version.ts
65
+ const REG_VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([a-z0-9]+)(?:\.(\d+))?)?$/i;
114
66
  /**
115
67
  * Parse version string to {@link Version}, like `0.2.0-beta.1`
116
68
  * @param version version string
117
69
  */
118
70
  function parseVersion(version) {
119
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-([a-z0-9.-]+))?/i.exec(version);
71
+ const match = REG_VERSION.exec(version);
120
72
  if (!match) throw new TypeError(`invalid version: ${version}`);
121
73
  const [major, minor, patch] = match.slice(1, 4).map(Number);
122
74
  const ret = {
@@ -127,9 +79,8 @@ function parseVersion(version) {
127
79
  stageVersion: -1
128
80
  };
129
81
  if (match[4]) {
130
- let [stage, _v] = match[4].split(".");
131
- ret.stage = stage;
132
- ret.stageVersion = Number(_v) || -1;
82
+ ret.stage = match[4];
83
+ ret.stageVersion = match[5] === void 0 ? -1 : Number(match[5]);
133
84
  }
134
85
  if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch) || Number.isNaN(ret.stageVersion)) throw new TypeError(`Invalid version: ${version}`);
135
86
  return ret;
@@ -140,7 +91,7 @@ const is = (j) => !!(j && j.minimumVersion && j.signature && j.version);
140
91
  * @param json any variable
141
92
  */
142
93
  function isUpdateJSON(json) {
143
- return is(json) && is(json?.beta);
94
+ return json && is(json) && is(json.beta);
144
95
  }
145
96
  /**
146
97
  * Default function to generate `UpdateJSON`
@@ -162,74 +113,152 @@ function defaultVersionJsonGenerator(existingJson, signature, version, minimumVe
162
113
  }
163
114
  return existingJson;
164
115
  }
165
-
116
+ //#endregion
117
+ //#region src/utils/zip.ts
118
+ /**
119
+ * Default function to compress file using brotli
120
+ * @param buffer uncompressed file buffer
121
+ */
122
+ async function defaultZipFile(buffer) {
123
+ return new Promise((resolve, reject) => {
124
+ zlib.brotliCompress(buffer, (err, buffer) => err ? reject(err) : resolve(buffer));
125
+ });
126
+ }
127
+ //#endregion
128
+ //#region src/vite/constant.ts
129
+ const id = "electron-incremental-update";
130
+ const bytecodeId = `${id}-bytecode`;
131
+ const log = createLogger("info", { prefix: `[${id}]` });
132
+ const bytecodeLog = createLogger("info", { prefix: `[${bytecodeId}]` });
133
+ const defaultExternal = [
134
+ ...builtinModules,
135
+ "electron",
136
+ "electron/common",
137
+ "electron/main",
138
+ "electron/renderer",
139
+ "electron/utility",
140
+ /^node:/,
141
+ /.*\.(node|dll|dylib|so)$/,
142
+ "original-fs"
143
+ ];
144
+ //#endregion
145
+ //#region src/vite/bytecode/code.ts
146
+ 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";
147
+ 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
148
  //#endregion
167
149
  //#region src/vite/bytecode/utils.ts
168
- const electronModule = getPackageInfoSync("electron");
169
- const electronMajorVersion = parseVersion(electronModule.version).major;
170
150
  const useStrict = "'use strict';";
171
151
  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;
152
+ async function resolvePaths(customPath) {
153
+ if (!customPath || !process.CACHED_ELECTRON_PATH) process.CACHED_ELECTRON_PATH = (await import("electron")).default;
154
+ if (!process.CACHED_BYTECODE_COMPILER_PATH) process.CACHED_BYTECODE_COMPILER_PATH = path.join(path.dirname(process.CACHED_ELECTRON_PATH), "EIU_bytenode.cjs");
155
+ if (!fs.existsSync(process.CACHED_BYTECODE_COMPILER_PATH)) fs.writeFileSync(process.CACHED_BYTECODE_COMPILER_PATH, bytecodeGeneratorScript);
156
+ return {
157
+ electronPath: customPath || process.CACHED_ELECTRON_PATH,
158
+ bytecodePath: process.CACHED_BYTECODE_COMPILER_PATH
159
+ };
186
160
  }
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;
161
+ async function compileToBytecode(code, name, customElectronPath) {
162
+ try {
163
+ const { bytecodePath, electronPath } = await resolvePaths(customElectronPath);
164
+ return await new Promise((resolve, reject) => {
165
+ const proc = cp.spawn(electronPath, [bytecodePath], {
166
+ env: {
167
+ ...process.env,
168
+ ELECTRON_RUN_AS_NODE: "1"
169
+ },
170
+ stdio: [
171
+ "pipe",
172
+ "pipe",
173
+ "pipe",
174
+ "ipc"
175
+ ]
176
+ });
177
+ const stdoutChunks = [];
178
+ const stderrChunks = [];
179
+ if (proc.stdin) {
180
+ proc.stdin.write(code);
181
+ proc.stdin.end();
182
+ }
183
+ if (proc.stdout) proc.stdout.on("data", (chunk_2) => stdoutChunks.push(chunk_2)).on("error", (err) => reject(err));
184
+ if (proc.stderr) proc.stderr.on("data", (chunk_3) => stderrChunks.push(chunk_3)).on("error", (err_1) => reject(err_1));
185
+ proc.on("error", (err_2) => reject(err_2));
186
+ proc.on("close", (exitCode) => {
187
+ const stdout = Buffer.concat(stdoutChunks);
188
+ const errorMessage = Buffer.concat(stderrChunks).toString("utf-8");
189
+ if (exitCode !== 0 || stdout.length === 0) {
190
+ reject(/* @__PURE__ */ new Error(`Bytecode generation process exited with code ${exitCode}. Error: ${errorMessage}`));
191
+ return;
192
+ }
193
+ resolve(stdout);
194
+ });
195
+ });
196
+ } catch (e) {
197
+ return `Failed to generate bytecode of [${name}], ${e}`;
198
+ }
191
199
  }
192
- function toRelativePath(filename, importer) {
193
- const relPath = path.posix.relative(path.dirname(importer), filename);
194
- return relPath.startsWith(".") ? relPath : `./${relPath}`;
200
+ const decodeFnBody = babel.parse(";function _0xstr_(a,b){return String.fromCharCode.apply(0,a.map(function(x){return x-b}))};")?.program.body ?? [];
201
+ function createObfuscatedStringCall(input, offset) {
202
+ const resolvedOffset = offset ?? ~~(Math.random() * 16) + 1;
203
+ const elements = input.split("").map((char) => {
204
+ const value = char.codePointAt(0) + resolvedOffset;
205
+ const node = babel.types.numericLiteral(value);
206
+ node.extra = {
207
+ raw: `0x${value.toString(16)}`,
208
+ rawValue: value
209
+ };
210
+ return node;
211
+ });
212
+ return babel.types.callExpression(babel.types.identifier("_0xstr_"), [babel.types.arrayExpression(elements), babel.types.numericLiteral(resolvedOffset)]);
213
+ }
214
+ function createPrepareContext(bytecodeFileNames) {
215
+ const requireRewrites = {};
216
+ for (const fileName of bytecodeFileNames) {
217
+ if (!fileName.endsWith(".js") && !fileName.endsWith(".cjs")) continue;
218
+ const baseName = path.posix.basename(fileName);
219
+ requireRewrites[baseName] = `${baseName}c`;
220
+ }
221
+ return {
222
+ requireRewrites,
223
+ hasRequireRewrites: Object.keys(requireRewrites).length > 0
224
+ };
195
225
  }
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}`);
226
+ function rewriteRequirePath(requirePath, requireRewrites) {
227
+ const baseName = path.posix.basename(requirePath);
228
+ const newBaseName = requireRewrites[baseName];
229
+ if (!newBaseName) return;
230
+ return `${requirePath.slice(0, -baseName.length)}${newBaseName}`;
231
+ }
232
+ function rewriteSimpleRequireCalls(code, requireRewrites) {
233
+ if (!code.includes("require(")) return code;
234
+ return code.replace(/\brequire\(\s*(['"])([^'"]+)\1\s*\)/g, (match, quote, requirePath) => {
235
+ const newRequirePath = rewriteRequirePath(requirePath, requireRewrites);
236
+ return newRequirePath ? `require(${quote}${newRequirePath}${quote})` : match;
237
+ });
226
238
  }
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) {
239
+ function obfuscateStringsPlugin(_, options) {
240
+ function transformProperty(path) {
241
+ const node = path.node;
242
+ if (node.computed || !babel.types.isIdentifier(node.property)) return;
243
+ if (path.findParent((parent) => parent.isFunctionDeclaration() && babel.types.isIdentifier(parent.node.id, { name: "_0xstr_" }))) return;
244
+ node.computed = true;
245
+ node.property = babel.types.stringLiteral(node.property.name);
246
+ }
247
+ return { visitor: {
248
+ MemberExpression(path) {
249
+ transformProperty(path);
250
+ },
251
+ OptionalMemberExpression(path) {
252
+ transformProperty(path);
253
+ },
254
+ ObjectProperty(path, state) {
255
+ const key = path.node.key;
256
+ if (path.node.computed || path.node.shorthand || !babel.types.isIdentifier(key) || key.name === "__proto__") return;
257
+ path.node.computed = true;
258
+ path.node.key = createObfuscatedStringCall(key.name, options.offset);
259
+ state.hasTransformed = true;
260
+ },
261
+ StringLiteral(path, state) {
233
262
  const parent = path.parent;
234
263
  const node = path.node;
235
264
  if (parent.type === "CallExpression") {
@@ -239,409 +268,347 @@ function prepare(code, offset) {
239
268
  if (parent.type.startsWith("Export")) return;
240
269
  if (parent.type.startsWith("Import")) return;
241
270
  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;
271
+ parent.computed = true;
272
+ path.replaceWith(createObfuscatedStringCall(node.value, options.offset));
273
+ state.hasTransformed = true;
246
274
  return;
247
275
  }
248
276
  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;
277
+ parent.computed = true;
278
+ path.replaceWith(createObfuscatedStringCall(node.value, options.offset));
279
+ state.hasTransformed = true;
253
280
  return;
254
281
  }
255
282
  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})`;
283
+ path.replaceWith(createObfuscatedStringCall(node.value, options.offset));
284
+ state.hasTransformed = true;
285
+ },
286
+ Program: { exit(path, state) {
287
+ if (!state.hasTransformed) return;
288
+ path.unshiftContainer("body", decodeFnBody.map((node) => babel.types.cloneNode(node)));
289
+ } }
290
+ } };
291
+ }
292
+ function rewriteRequirePlugin(_, options) {
293
+ return { visitor: { CallExpression(path) {
294
+ if (!babel.types.isIdentifier(path.node.callee, { name: "require" }) || path.node.arguments.length === 0) return;
295
+ const arg = path.node.arguments[0];
296
+ if (!babel.types.isStringLiteral(arg)) return;
297
+ const newRequirePath = rewriteRequirePath(arg.value, options.requireRewrites);
298
+ if (newRequirePath) path.node.arguments[0] = babel.types.stringLiteral(newRequirePath);
299
+ } } };
300
+ }
301
+ function prepare(code, minify, context, offset) {
302
+ if (!code.includes("\"") && !code.includes("'") && !code.includes("`") && !code.includes("=>") && !/\.[A-Za-z_$]/.test(code) && !/[{,]\s*[A-Za-z_$][\w$]*\s*:/.test(code)) return { code };
303
+ if (context.hasRequireRewrites && !code.includes("`") && !code.includes("=>")) {
304
+ const codeWithoutSimpleRequires = code.replace(/\brequire\(\s*(['"])([^'"]+)\1\s*\)/g, "");
305
+ if (!codeWithoutSimpleRequires.includes("\"") && !codeWithoutSimpleRequires.includes("'")) return { code: rewriteSimpleRequireCalls(code, context.requireRewrites) };
306
+ }
307
+ return babel.transform(code, {
308
+ minified: minify,
309
+ plugins: [
310
+ "@babel/plugin-transform-arrow-functions",
311
+ "@babel/plugin-transform-template-literals",
312
+ [obfuscateStringsPlugin, { offset }],
313
+ [rewriteRequirePlugin, { requireRewrites: context.requireRewrites }]
314
+ ]
315
+ });
265
316
  }
266
-
267
317
  //#endregion
268
318
  //#region src/vite/bytecode/index.ts
269
319
  function getBytecodeLoaderBlock(chunkFileName) {
270
- return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`;
271
- }
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;
320
+ const loaderFileName = path.posix.relative(path.posix.dirname(chunkFileName), bytecodeModuleLoader);
321
+ return `require("${loaderFileName.startsWith(".") ? loaderFileName : `./${loaderFileName}`}")`;
288
322
  }
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) {
323
+ function bytecodePlugin(env, minify, isESM, options) {
301
324
  const { enable, preload = false, electronPath, beforeCompile } = options;
302
325
  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 });
326
+ if (env === "preload" && !preload) {
327
+ 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
328
  return null;
306
329
  }
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 = [];
330
+ if (isESM) throw new Error("`bytecodePlugin` requires CommonJS. Set \"type\": \"commonjs\" in package.json and use .cjs extensions");
331
+ let hasJsChunks = false;
311
332
  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({
333
+ name: bytecodeId,
334
+ async generateBundle(outputOptions, bundle) {
335
+ hasJsChunks = Object.values(bundle).some((file) => file.type === "chunk" && (file.fileName.endsWith(".js") || file.fileName.endsWith(".cjs")));
336
+ if (hasJsChunks) this.emitFile({
324
337
  type: "asset",
325
338
  source: `${bytecodeModuleLoaderCode}\n`,
326
- name: "Bytecode Loader File",
339
+ name: "Bytecode Loader",
327
340
  fileName: bytecodeModuleLoader
328
341
  });
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);
342
+ if (!hasJsChunks) return;
343
+ const outputDir = outputOptions.dir ?? (outputOptions.file && path.dirname(outputOptions.file));
344
+ const prepareContext = createPrepareContext(Object.values(bundle).filter((f) => f.type === "chunk" && !f.isEntry).map((c) => path.posix.basename(c.fileName)));
345
+ await Promise.all(Object.entries(bundle).map(async ([fileName, item]) => {
346
+ if (item.type !== "chunk" || fileName === "__loader__.js") return;
347
+ const chunk = item;
348
+ const bytecodeFileName = `${fileName}c`;
349
+ const absPath = outputDir ? path.join(outputDir, fileName) : fileName;
350
+ let code = prepare(chunk.code, minify, prepareContext)?.code || chunk.code;
351
+ if (beforeCompile) {
352
+ const hookResult = await beforeCompile(code, absPath);
353
+ if (hookResult) code = hookResult;
377
354
  }
355
+ const bytecode = await compileToBytecode(code, absPath, electronPath);
356
+ if (typeof bytecode === "string") throw new TypeError(bytecode);
357
+ this.emitFile({
358
+ type: "asset",
359
+ source: bytecode,
360
+ fileName: bytecodeFileName
361
+ });
362
+ if (chunk.isEntry) chunk.code = `${useStrict}\n${getBytecodeLoaderBlock(fileName)}\nmodule.exports=require("./${path.posix.basename(fileName)}c");\n`;
363
+ else delete bundle[fileName];
378
364
  }));
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
365
  }
388
366
  };
389
367
  }
390
-
391
368
  //#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 || {});
369
+ //#region src/vite/local-dev-update.ts
370
+ const LOCAL_DEV_SIGNATURE = "local-dev";
371
+ function resolveLocalDevUpdateOptions(root, options) {
372
+ if (!options) return;
373
+ const resolvedOptions = options === true ? {} : options;
374
+ return {
375
+ baseDir: path.resolve(root, resolvedOptions.baseDir ?? "release/local-update"),
376
+ installedAsarPath: path.resolve(root, "DEV.asar"),
377
+ packageJsonPath: resolvedOptions.packageJsonPath ? path.resolve(root, resolvedOptions.packageJsonPath) : void 0,
378
+ chunkSize: resolvedOptions.chunkSize,
379
+ chunkDelay: resolvedOptions.chunkDelay
380
+ };
417
381
  }
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
- }));
382
+ async function resolveLocalDevUpdatePackage(fallbackPkg, localDevUpdate) {
383
+ if (!localDevUpdate?.packageJsonPath) return fallbackPkg;
384
+ const pkg = JSON.parse(await readFile(localDevUpdate.packageJsonPath, "utf-8"));
385
+ if (!pkg.name || !pkg.version || !pkg.main) throw new Error("localDevUpdate.packageJsonPath must contain name, version and main fields");
386
+ return pkg;
428
387
  }
429
- function pidTree(tree) {
430
- const command = process.platform === "darwin" ? `pgrep -P ${tree.pid}` : `ps -o pid --no-headers --ppid ${tree.ppid}`;
388
+ function getNextPatchVersion(version) {
389
+ const parsed = parseVersion(version);
390
+ return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
391
+ }
392
+ function isValidVersion(version) {
431
393
  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;
394
+ parseVersion(version);
395
+ return true;
396
+ } catch {
397
+ return false;
398
+ }
439
399
  }
440
- function killTree(tree) {
441
- if (tree.children) for (const child of tree.children) killTree(child);
400
+ function readValidAsarVersion(asarPath) {
401
+ try {
402
+ const version = extractFile(asarPath, "version").toString("utf-8").trim();
403
+ return isValidVersion(version) ? version : void 0;
404
+ } catch {
405
+ return;
406
+ }
407
+ }
408
+ async function installPendingAsar(installedAsarPath) {
409
+ const pendingAsarPath = `${installedAsarPath}.tmp`;
410
+ const version = readValidAsarVersion(pendingAsarPath);
411
+ if (!version) {
412
+ await rm(pendingAsarPath, { force: true });
413
+ return false;
414
+ }
442
415
  try {
443
- process.kill(tree.pid);
416
+ await copyFile(pendingAsarPath, installedAsarPath);
417
+ await rm(pendingAsarPath, { force: true });
418
+ log.info(`Installed pending local dev update ${version}`, { timestamp: true });
419
+ return true;
420
+ } catch (error) {
421
+ if (error.code === "ENOENT") return false;
422
+ throw error;
423
+ }
424
+ }
425
+ async function readExistingUpdateJSON(versionPath, version) {
426
+ try {
427
+ const json = JSON.parse(await readFile(versionPath, "utf-8"));
428
+ if (isUpdateJSON(json)) return json;
444
429
  } catch {}
430
+ return {
431
+ version,
432
+ minimumVersion: "0.0.0",
433
+ signature: LOCAL_DEV_SIGNATURE,
434
+ beta: {
435
+ version,
436
+ minimumVersion: "0.0.0",
437
+ signature: LOCAL_DEV_SIGNATURE
438
+ }
439
+ };
445
440
  }
446
-
447
- //#endregion
448
- //#region src/vite/electron/core.ts
449
- function build$1(isESM, options) {
450
- return build(resolveViteConfig(isESM, options));
441
+ async function prepareLocalDevUpdateResource({ root, pkg, buildAsarOption, versionPath, minimumVersion, keepInstalledVersion, localDevUpdate }) {
442
+ const installedPendingUpdate = await installPendingAsar(localDevUpdate.installedAsarPath);
443
+ const installedVersion = readValidAsarVersion(localDevUpdate.installedAsarPath) ?? pkg.version;
444
+ const targetVersion = keepInstalledVersion && installedPendingUpdate ? installedVersion : getNextPatchVersion(installedVersion);
445
+ const workDir = await mkdtemp(path.join(tmpdir(), "eiu-local-dev-update-"));
446
+ const stagedElectronDistPath = path.join(workDir, "dist-electron");
447
+ const resolvedVersionPath = path.join(localDevUpdate.baseDir, versionPath);
448
+ const asarPath = path.join(localDevUpdate.baseDir, `${pkg.name}.asar`);
449
+ const gzipPath = path.join(localDevUpdate.baseDir, `${pkg.name}-${targetVersion}.asar.gz`);
450
+ try {
451
+ await mkdir(localDevUpdate.baseDir, { recursive: true });
452
+ await mkdir(path.dirname(resolvedVersionPath), { recursive: true });
453
+ await cp$1(path.resolve(root, buildAsarOption.electronDistPath), stagedElectronDistPath, { recursive: true });
454
+ await writeFile(path.join(stagedElectronDistPath, "version"), targetVersion, "utf-8");
455
+ await createPackage(stagedElectronDistPath, asarPath);
456
+ await writeFile(gzipPath, await buildAsarOption.generateGzipFile(await readFile(asarPath)));
457
+ const updateJSON = defaultVersionJsonGenerator(await readExistingUpdateJSON(resolvedVersionPath, targetVersion), LOCAL_DEV_SIGNATURE, targetVersion, minimumVersion);
458
+ if (!isUpdateJSON(updateJSON)) throw new Error("Invalid local dev update json");
459
+ await writeFile(resolvedVersionPath, JSON.stringify(updateJSON, null, 2), "utf-8");
460
+ log.info(`Prepared local dev update ${targetVersion}`, { timestamp: true });
461
+ return targetVersion;
462
+ } finally {
463
+ await rm(workDir, {
464
+ recursive: true,
465
+ force: true
466
+ });
467
+ }
451
468
  }
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)));
469
+ function createLocalDevUpdateOnstart(args) {
470
+ let restartRequested = false;
471
+ let startupArgs;
472
+ let managedElectronApp;
473
+ function createStartupArgs(onstartArgs) {
474
+ return {
475
+ ...onstartArgs,
476
+ startup(argv, options, customElectronPkg) {
477
+ const env = {
478
+ ...process.env,
479
+ ...options?.env
480
+ };
481
+ delete env.ELECTRON_RUN_AS_NODE;
482
+ return onstartArgs.startup(argv, {
483
+ ...options,
484
+ env
485
+ }, customElectronPkg);
486
+ }
487
+ };
459
488
  }
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
- });
489
+ async function start(onstartArgs, options = {}) {
490
+ const safeStartupArgs = createStartupArgs(onstartArgs);
491
+ startupArgs = safeStartupArgs;
492
+ await prepareLocalDevUpdateResource({
493
+ ...args,
494
+ keepInstalledVersion: options.keepInstalledVersion
495
+ });
496
+ if (args.userOnstart) await args.userOnstart(safeStartupArgs);
497
+ else await safeStartupArgs.startup();
498
+ const electronApp = process.electronApp;
499
+ removeManagedListeners();
500
+ managedElectronApp = electronApp;
501
+ managedElectronApp?.removeListener("exit", process.exit);
502
+ managedElectronApp?.on("message", handleMessage);
503
+ managedElectronApp?.once("exit", handleExit);
504
+ }
505
+ function handleMessage(message) {
506
+ if (message === "eiu:restart") {
507
+ restartRequested = true;
508
+ managedElectronApp?.send?.("eiu:restart-ready");
503
509
  }
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
- }));
510
+ }
511
+ function handleExit() {
512
+ if (restartRequested) {
513
+ restartRequested = false;
514
+ setTimeout(() => {
515
+ if (startupArgs) start(startupArgs, { keepInstalledVersion: true });
516
+ });
517
+ return;
521
518
  }
522
- }];
519
+ process.exit();
520
+ }
521
+ function removeManagedListeners() {
522
+ managedElectronApp?.removeListener("message", handleMessage);
523
+ managedElectronApp?.removeListener("exit", handleExit);
524
+ }
525
+ return (onstartArgs) => start(onstartArgs);
523
526
  }
527
+ //#endregion
528
+ //#region src/vite/utils/file.ts
524
529
  /**
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')
530
+ * Convert byte size to human-readable format
531
+ * @param size - Size in bytes
532
+ * @returns Human-readable size string (e.g., "1.23 MB")
530
533
  */
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"
534
+ function readableSize(size) {
535
+ const units = [
536
+ "B",
537
+ "KB",
538
+ "MB",
539
+ "GB"
547
540
  ];
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);
541
+ let i = 0;
542
+ while (size >= 1024 && i < units.length - 1) {
543
+ size /= 1024;
544
+ i++;
556
545
  }
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\//;
546
+ return `${size.toFixed(2)} ${units[i]}`;
547
+ }
577
548
  /**
578
- * During dev, we exclude the `cjs` npm-pkg from bundle, mush like Vite :)
549
+ * Copy file/directory, skipping if target exists
550
+ * @param from - Source path
551
+ * @param to - Destination path
552
+ * @param skipIfExist - Skip copy if destination exists
579
553
  */
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
- };
554
+ function copyAndSkipIfExist(from, to, skipIfExist) {
555
+ if (!skipIfExist || !fs.existsSync(to)) try {
556
+ fs.cpSync(from, to, { recursive: true });
557
+ } catch (error) {
558
+ log.warn(`Copy failed: ${error}`, { timestamp: true });
559
+ }
610
560
  }
611
-
612
561
  //#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
- }
562
+ //#region src/vite/utils/build.ts
622
563
  /**
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
564
+ * Build asar file and update package
565
+ * @param options - Asar build options
566
+ * @returns Buffer of the built asar file
628
567
  */
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));
568
+ async function buildAsar(root, { version, asarOutputPath, electronDistPath, rendererDistPath, gzipPath, generateGzipFile }) {
569
+ electronDistPath = path.resolve(root, electronDistPath);
570
+ asarOutputPath = path.resolve(root, asarOutputPath);
571
+ rendererDistPath = path.resolve(root, rendererDistPath);
572
+ gzipPath = path.resolve(root, gzipPath);
573
+ const rPath = path.join(electronDistPath, "renderer");
574
+ await fs.promises.cp(rendererDistPath, rPath, { recursive: true });
575
+ fs.writeFileSync(path.join(electronDistPath, "version"), version);
576
+ await fs.promises.mkdir(path.dirname(asarOutputPath), { recursive: true });
577
+ await createPackage(electronDistPath, asarOutputPath);
578
+ const buf = await generateGzipFile(fs.readFileSync(asarOutputPath));
579
+ await fs.promises.mkdir(path.dirname(gzipPath), { recursive: true });
580
+ fs.writeFileSync(gzipPath, buf);
581
+ log.info(`Build update asar to '${gzipPath}' [${readableSize(buf.length)}]`, { timestamp: true });
582
+ return buf;
631
583
  }
632
-
633
- //#endregion
634
- //#region src/utils/zip.ts
635
584
  /**
636
- * Default function to compress file using brotli
637
- * @param buffer uncompressed file buffer
585
+ * Build update.json file with signature and version information
586
+ * @param options - Version build options
587
+ * @param asarBuffer - Buffer of the asar file to sign
638
588
  */
639
- async function defaultZipFile(buffer) {
640
- return new Promise((resolve, reject) => {
641
- zlib.brotliCompress(buffer, (err, buffer) => err ? reject(err) : resolve(buffer));
642
- });
589
+ async function buildUpdateJson({ versionPath, privateKey, cert, version, minimumVersion, generateSignature, generateUpdateJson }, asarBuffer) {
590
+ let _json = {
591
+ beta: {
592
+ minimumVersion: version,
593
+ signature: "",
594
+ version
595
+ },
596
+ minimumVersion: version,
597
+ signature: "",
598
+ version
599
+ };
600
+ if (fs.existsSync(versionPath)) try {
601
+ const oldVersionJson = JSON.parse(fs.readFileSync(versionPath, "utf-8"));
602
+ if (isUpdateJSON(oldVersionJson)) _json = oldVersionJson;
603
+ else log.warn("Old version json is invalid, ignore it", { timestamp: true });
604
+ } catch {}
605
+ const sig = await generateSignature(asarBuffer, privateKey, cert, version);
606
+ _json = await generateUpdateJson(_json, sig, version, minimumVersion);
607
+ if (!isUpdateJSON(_json)) throw new Error("Invalid update json");
608
+ await fs.promises.mkdir(path.dirname(versionPath), { recursive: true });
609
+ fs.writeFileSync(versionPath, JSON.stringify(_json, null, 2));
610
+ log.info(`build update json to '${versionPath}'`, { timestamp: true });
643
611
  }
644
-
645
612
  //#endregion
646
613
  //#region src/vite/utils/key.ts
647
614
  /**
@@ -708,31 +675,32 @@ function parseSubjects(subject) {
708
675
  value
709
676
  }));
710
677
  }
711
-
712
678
  //#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 = {
679
+ //#region src/vite/core.ts
680
+ async function resolveUpdaterOption(root, pkg, options = {}, resolveSignatureKeys = true) {
681
+ 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 = "release/version.json" } = {}, keys: { privateKeyPath = "keys/private.pem", certPath = "keys/cert.pem", keyLength = 2048, certInfo: { subject = {
716
682
  commonName: pkg.name,
717
683
  organizationName: `org.${pkg.name}`
718
684
  }, 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({
685
+ const { privateKey, cert } = resolveSignatureKeys ? await parseKeys({
728
686
  keyLength,
729
- privateKeyPath,
730
- certPath,
687
+ privateKeyPath: path.resolve(root, privateKeyPath),
688
+ certPath: path.resolve(root, certPath),
731
689
  subject,
732
690
  days
733
- });
691
+ }) : {
692
+ privateKey: "",
693
+ cert: ""
694
+ };
734
695
  return {
735
- buildAsarOption,
696
+ buildAsarOption: {
697
+ version: pkg.version,
698
+ asarOutputPath,
699
+ gzipPath,
700
+ electronDistPath,
701
+ rendererDistPath,
702
+ generateGzipFile
703
+ },
736
704
  buildVersionOption: {
737
705
  version: pkg.version,
738
706
  minimumVersion,
@@ -742,269 +710,230 @@ async function parseUpdaterOption(pkg, options = {}) {
742
710
  generateSignature,
743
711
  generateUpdateJson
744
712
  },
745
- cert,
746
713
  entryOutDir
747
714
  };
748
715
  }
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";
716
+ function resolveEntryName(files) {
717
+ if (typeof files === "string") return path.parse(files).name;
718
+ if (Array.isArray(files)) {
719
+ const [firstInput] = files;
720
+ if (!firstInput) throw new Error("`options.main.files` must contain at least one main entry");
721
+ return path.parse(firstInput).name;
804
722
  }
805
- log.info(`Using "${mainFilePath}" as main file`, { timestamp: true });
806
- return mainFilePath.replace(/\.[cm]?ts$/, ".js");
723
+ const firstEntry = Object.entries(files)[0];
724
+ if (!firstEntry) throw new Error("`options.main.files` must contain at least one main entry");
725
+ return firstEntry[0];
807
726
  }
808
- function parseVersionPath(versionPath) {
727
+ function normalizeVersionPath(versionPath) {
809
728
  versionPath = normalizePath(versionPath);
810
729
  if (!versionPath.startsWith("./")) versionPath = `./${versionPath}`;
811
730
  return new URL(versionPath, "file://").pathname.slice(1);
812
731
  }
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);
732
+ async function createElectronOptions(options, context) {
733
+ const { entry, main, preload, sourcemap = context.isDev || !!process.env.VSCODE_DEBUG, minify = !context.isDev, buildVersionJson, notBundle = true, external, updater, bytecode, localDevUpdate } = options;
734
+ const pkg = context.packageJson;
869
735
  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
736
  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;
737
+ const finalExternal = [...defaultExternal];
738
+ if (external === true) finalExternal.push(...Object.keys(pkg.dependencies || {}));
739
+ else if (Array.isArray(external)) finalExternal.push(...external);
740
+ const bytecodeOptions = typeof bytecode === "object" ? {
741
+ ...bytecode,
742
+ enable: bytecode.enable ?? true
743
+ } : bytecode === true ? { enable: true } : void 0;
873
744
  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);
745
+ const resolvedLocalDevUpdate = context.isDev ? resolveLocalDevUpdateOptions(context.root, localDevUpdate) : void 0;
746
+ const updatePkg = await resolveLocalDevUpdatePackage(pkg, resolvedLocalDevUpdate);
747
+ const { buildAsarOption, buildVersionOption, entryOutDir } = await resolveUpdaterOption(context.root, updatePkg, updater, !resolvedLocalDevUpdate);
748
+ const mainFileName = `${resolveEntryName(main.files)}.${isESM ? "mjs" : "js"}`;
749
+ log.info(`Using "${mainFileName}" as main file`, { timestamp: true });
875
750
  log.info(`Clear cache files`, { timestamp: true });
876
751
  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(() => {});
752
+ buildAsarOption.rendererDistPath,
753
+ buildAsarOption.electronDistPath,
754
+ entryOutDir
755
+ ].map((p) => fs.promises.rm(path.resolve(context.root, p), {
756
+ recursive: true,
757
+ force: true
758
+ }))).catch(() => {});
759
+ const outputNames = {
760
+ entryFileNames: `[name].${isESM ? "mjs" : "js"}`,
761
+ chunkFileNames: `[name].${isESM ? "mjs" : "js"}`,
762
+ assetFileNames: "[name].[ext]"
763
+ };
764
+ const versionPath = normalizeVersionPath(normalizePath(buildVersionOption.versionPath));
765
+ const mainOnstart = resolvedLocalDevUpdate ? createLocalDevUpdateOnstart({
766
+ root: context.root,
767
+ pkg: updatePkg,
768
+ buildAsarOption,
769
+ versionPath,
770
+ minimumVersion: buildVersionOption.minimumVersion,
771
+ localDevUpdate: resolvedLocalDevUpdate,
772
+ userOnstart: main.onstart
773
+ }) : main.onstart;
890
774
  const define = {
891
775
  __EIU_ASAR_BASE_NAME__: JSON.stringify(path.basename(buildAsarOption.asarOutputPath)),
892
776
  __EIU_ELECTRON_DIST_PATH__: JSON.stringify(normalizePath(buildAsarOption.electronDistPath)),
893
777
  __EIU_ENTRY_DIST_PATH__: JSON.stringify(normalizePath(entryOutDir)),
894
- __EIU_IS_DEV__: JSON.stringify(!isBuild),
778
+ __EIU_IS_DEV__: JSON.stringify(context.isDev),
895
779
  __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)))
780
+ __EIU_LOCAL_DEV_UPDATE__: JSON.stringify(!!resolvedLocalDevUpdate),
781
+ __EIU_LOCAL_DEV_UPDATE_ASAR_PATH__: JSON.stringify(resolvedLocalDevUpdate?.installedAsarPath ?? ""),
782
+ __EIU_LOCAL_DEV_UPDATE_CHUNK_DELAY__: JSON.stringify(resolvedLocalDevUpdate?.chunkDelay) ?? "undefined",
783
+ __EIU_LOCAL_DEV_UPDATE_CHUNK_SIZE__: JSON.stringify(resolvedLocalDevUpdate?.chunkSize) ?? "undefined",
784
+ __EIU_LOCAL_DEV_UPDATE_DIR__: JSON.stringify(resolvedLocalDevUpdate?.baseDir ?? ""),
785
+ __EIU_MAIN_FILE__: JSON.stringify(mainFileName),
786
+ __EIU_SIGNATURE_CERT__: JSON.stringify(buildVersionOption.cert),
787
+ __EIU_VERSION_PATH__: JSON.stringify(versionPath)
899
788
  };
900
789
  const _electronOptions = [{
901
- entry: _main.files,
902
- onstart: _main.onstart,
903
- vite: mergeConfig({
904
- plugins: [!isBuild && useNotBundle && notBundle(), bytecodeOptions && bytecodePlugin("main", isESM, bytecodeOptions)],
790
+ name: "main",
791
+ input: main.files,
792
+ onstart: mainOnstart,
793
+ notBundle,
794
+ plugins: [isESM && esmShim(), bytecodeOptions && bytecodePlugin("main", minify, isESM, bytecodeOptions)],
795
+ options: mergeConfig({
905
796
  build: {
906
797
  sourcemap,
907
798
  minify,
908
799
  outDir: `${buildAsarOption.electronDistPath}/main`,
909
800
  rolldownOptions: {
910
801
  external: finalExternal,
911
- platform: "node",
912
802
  output: {
913
- polyfillRequire: false,
914
- exports: "named"
803
+ format: isESM ? "es" : "cjs",
804
+ polyfillRequire: isESM,
805
+ ...outputNames
915
806
  }
916
807
  }
917
808
  },
918
809
  define
919
- }, _main.vite ?? {})
810
+ }, main.options ?? {})
920
811
  }];
921
- if (_preload?.files) _electronOptions.push({
812
+ if (preload?.files) _electronOptions.push({
813
+ name: "preload",
922
814
  onstart(args) {
923
815
  args.reload();
924
816
  },
925
- vite: mergeConfig({
926
- plugins: [bytecodeOptions && bytecodePlugin("preload", isESM, bytecodeOptions)],
817
+ notBundle,
818
+ input: preload.files,
819
+ plugins: [isESM && esmShim(), bytecodeOptions && bytecodePlugin("preload", minify, isESM, bytecodeOptions)],
820
+ options: mergeConfig({
927
821
  build: {
928
822
  sourcemap: sourcemap ? "inline" : void 0,
929
823
  minify,
930
824
  outDir: `${buildAsarOption.electronDistPath}/preload`,
931
825
  rolldownOptions: {
932
826
  external: finalExternal,
933
- input: _preload.files,
934
827
  output: {
935
828
  format: "cjs",
936
829
  codeSplitting: false,
937
830
  polyfillRequire: false,
938
- entryFileNames: `[name].${isESM ? "mjs" : "js"}`,
939
- chunkFileNames: `[name].${isESM ? "mjs" : "js"}`,
940
- assetFileNames: "[name].[ext]"
831
+ ...outputNames
941
832
  }
942
833
  }
943
834
  },
944
835
  define
945
- }, _preload?.vite ?? {})
836
+ }, preload?.options ?? {})
946
837
  });
947
838
  _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
- }
839
+ name: "entry",
840
+ input: entry.files,
841
+ async onstart(args) {
842
+ if (mainOnstart) await mainOnstart(args);
843
+ else await args.startup();
844
+ },
845
+ notBundle,
846
+ plugins: [
847
+ isESM && esmShim(),
848
+ bytecodeOptions && bytecodePlugin("entry", minify, isESM, bytecodeOptions),
849
+ {
850
+ name: `${id}:entry`,
851
+ async closeBundle() {
852
+ log.info(`Build entry to '${entryOutDir}'`, { timestamp: true });
853
+ await entry.postBuild?.({
854
+ isBuild: !context.isDev,
855
+ getPathFromEntryOutputDir(...paths) {
856
+ return path.join(entryOutDir, ...paths);
857
+ },
858
+ copyToEntryOutputDir({ from, to = path.basename(from), skipIfExist = true }) {
859
+ if (!fs.existsSync(from)) {
860
+ log.warn(`${from} not found`, { timestamp: true });
861
+ return;
980
862
  }
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);
863
+ copyAndSkipIfExist(from, path.join(entryOutDir, to), skipIfExist);
864
+ },
865
+ copyModules() {
866
+ console.warn("`copyModules()` is deprecated. Will do nothing");
988
867
  }
989
- }
868
+ });
869
+ if (context.isDev) return;
870
+ const buffer = await buildAsar(context.root, buildAsarOption);
871
+ if (!buildVersionJson && !isCI) log.warn("No `buildVersionJson` option setup, skip build version json. Only build in CI by default", { timestamp: true });
872
+ else await buildUpdateJson(buildVersionOption, buffer);
990
873
  }
991
- ],
874
+ }
875
+ ],
876
+ options: mergeConfig({
992
877
  build: {
993
878
  sourcemap,
994
879
  minify,
995
880
  outDir: entryOutDir,
996
881
  rolldownOptions: {
997
882
  external: finalExternal,
998
- platform: "node",
999
- output: { polyfillRequire: false }
883
+ output: {
884
+ format: isESM ? "es" : "cjs",
885
+ polyfillRequire: isESM,
886
+ ...outputNames
887
+ }
1000
888
  }
1001
889
  },
1002
890
  define
1003
- }, _entry.vite || {})
891
+ }, entry.options || {})
892
+ });
893
+ return _electronOptions;
894
+ }
895
+ /**
896
+ * Base on `vite-plugin-electron/multi-env`
897
+ * - integrate with updater
898
+ * - no `renderer` config
899
+ * - remove old output file
900
+ * - externalize dependencies
901
+ * - auto restart when entry file changes
902
+ *
903
+ * You can override all the environment configs, except output directories (use `options.updater.paths.electronDistPath` instead)
904
+ *
905
+ * @example
906
+ * ```ts
907
+ * import { defineConfig } from 'vite'
908
+ * import { electronWithUpdater } from 'electron-incremental-update/vite'
909
+ *
910
+ * export default defineConfig(async ({ command }) => {
911
+ * const isBuild = command === 'build'
912
+ * return {
913
+ * plugins: [
914
+ * electronWithUpdater({
915
+ * isBuild,
916
+ * main: {
917
+ * files: ['./electron/main/index.ts', './electron/main/worker.ts'],
918
+ * },
919
+ * preload: {
920
+ * files: './electron/preload/index.ts',
921
+ * },
922
+ * updater: {
923
+ * // options
924
+ * }
925
+ * }),
926
+ * ],
927
+ * }
928
+ * })
929
+ * ```
930
+ */
931
+ async function electronWithUpdater(options) {
932
+ return electronPluginFactory((context) => {
933
+ process.CACHED_ELECTRON_OPTIONS ??= createElectronOptions(options, context);
934
+ return process.CACHED_ELECTRON_OPTIONS;
1004
935
  });
1005
- return electron(isESM, normalizePath(path.resolve(root)), _electronOptions);
1006
936
  }
1007
-
1008
937
  //#endregion
1009
938
  //#region src/vite/define.ts
1010
939
  /**
@@ -1017,8 +946,6 @@ async function electronWithUpdater(options) {
1017
946
  * export default defineElectronConfig({
1018
947
  * main: {
1019
948
  * 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
949
  * },
1023
950
  * preload: {
1024
951
  * files: './electron/preload/index.ts',
@@ -1039,21 +966,16 @@ async function electronWithUpdater(options) {
1039
966
  * ```
1040
967
  */
1041
968
  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
- };
969
+ const electronPlugin = electronWithUpdater(options);
970
+ const result = options.renderer ?? {};
971
+ result.plugins ??= [];
972
+ result.plugins.push(electronPlugin);
973
+ const rendererDistPath = options.updater?.paths?.rendererDistPath;
974
+ if (rendererDistPath) {
975
+ result.build ??= {};
976
+ result.build.outDir = rendererDistPath;
977
+ }
978
+ return result;
1056
979
  }
1057
-
1058
980
  //#endregion
1059
- export { debugStartup, electronWithUpdater as default, electronWithUpdater, defineElectronConfig, filterErrorMessageStartup, fixWinCharEncoding };
981
+ export { electronWithUpdater as default, electronWithUpdater, defineElectronConfig, filterErrorMessageStartup, fixWinCharEncoding };