electron-incremental-update 3.0.0-beta.4 → 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,24 +1,74 @@
1
- import { builtinModules, createRequire } from "node:module";
2
- import * as babel from "@babel/core";
3
- import { getPackageInfoSync, loadPackageJSON } from "local-pkg";
4
- import MagicString from "magic-string";
5
- import cp from "node:child_process";
1
+ import { builtinModules } from "node:module";
6
2
  import fs from "node:fs";
7
3
  import path from "node:path";
8
- import { build, createFilter, createLogger, mergeConfig, normalizePath, version } from "vite";
9
4
  import { isCI } from "ci-info";
10
- import { createPackage } from "@electron/asar";
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 crypto from "node:crypto";
12
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";
13
15
  import { generate } from "selfsigned";
14
-
16
+ //#region src/vite/startup.ts
17
+ /**
18
+ * Filter error messages from stdout/stderr during startup
19
+ * @param args - Startup arguments
20
+ * @param filter - Filter function to determine which messages to show
21
+ */
22
+ async function filterErrorMessageStartup(filter) {
23
+ const elec = process.electronApp;
24
+ elec.stdout?.addListener("data", (data) => {
25
+ console.log(data.toString().trimEnd());
26
+ });
27
+ elec.stderr?.addListener("data", (data) => {
28
+ const message = data.toString();
29
+ if (filter(message)) console.error(message);
30
+ });
31
+ }
32
+ /**
33
+ * Fix Windows character encoding by setting code page to UTF-8
34
+ * @param fn - Function to wrap with encoding fix
35
+ * @returns Wrapped function with Windows encoding fix
36
+ */
37
+ function fixWinCharEncoding(fn) {
38
+ return (async (...args) => {
39
+ if (process.platform === "win32") (await import("node:child_process")).spawnSync("chcp", ["65001"]);
40
+ await fn(...args);
41
+ });
42
+ }
43
+ //#endregion
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");
52
+ }
53
+ /**
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
59
+ */
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));
62
+ }
63
+ //#endregion
15
64
  //#region src/utils/version.ts
65
+ const REG_VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([a-z0-9]+)(?:\.(\d+))?)?$/i;
16
66
  /**
17
67
  * Parse version string to {@link Version}, like `0.2.0-beta.1`
18
68
  * @param version version string
19
69
  */
20
70
  function parseVersion(version) {
21
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-([a-z0-9.-]+))?/i.exec(version);
71
+ const match = REG_VERSION.exec(version);
22
72
  if (!match) throw new TypeError(`invalid version: ${version}`);
23
73
  const [major, minor, patch] = match.slice(1, 4).map(Number);
24
74
  const ret = {
@@ -29,9 +79,8 @@ function parseVersion(version) {
29
79
  stageVersion: -1
30
80
  };
31
81
  if (match[4]) {
32
- let [stage, _v] = match[4].split(".");
33
- ret.stage = stage;
34
- ret.stageVersion = Number(_v) || -1;
82
+ ret.stage = match[4];
83
+ ret.stageVersion = match[5] === void 0 ? -1 : Number(match[5]);
35
84
  }
36
85
  if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch) || Number.isNaN(ret.stageVersion)) throw new TypeError(`Invalid version: ${version}`);
37
86
  return ret;
@@ -42,7 +91,7 @@ const is = (j) => !!(j && j.minimumVersion && j.signature && j.version);
42
91
  * @param json any variable
43
92
  */
44
93
  function isUpdateJSON(json) {
45
- return is(json) && is(json?.beta);
94
+ return json && is(json) && is(json.beta);
46
95
  }
47
96
  /**
48
97
  * Default function to generate `UpdateJSON`
@@ -64,141 +113,424 @@ function defaultVersionJsonGenerator(existingJson, signature, version, minimumVe
64
113
  }
65
114
  return existingJson;
66
115
  }
67
-
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
+ }
68
127
  //#endregion
69
128
  //#region src/vite/constant.ts
70
- const id = "electron-incremental-updater";
129
+ const id = "electron-incremental-update";
71
130
  const bytecodeId = `${id}-bytecode`;
72
131
  const log = createLogger("info", { prefix: `[${id}]` });
73
132
  const bytecodeLog = createLogger("info", { prefix: `[${bytecodeId}]` });
74
-
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
+ ];
75
144
  //#endregion
76
145
  //#region src/vite/bytecode/code.ts
77
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";
78
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";
79
-
80
148
  //#endregion
81
149
  //#region src/vite/bytecode/utils.ts
82
- const electronModule = getPackageInfoSync("electron");
83
- const electronMajorVersion = parseVersion(electronModule.version).major;
84
150
  const useStrict = "'use strict';";
85
151
  const bytecodeModuleLoader = "__loader__.js";
86
- function getElectronPath() {
87
- const electronModulePath = electronModule.rootPath;
88
- let electronExecPath = process.env.ELECTRON_EXEC_PATH || "";
89
- if (!electronExecPath) {
90
- if (!electronModulePath) throw new Error("Electron is not installed");
91
- const pathFile = path.join(electronModulePath, "path.txt");
92
- let executablePath;
93
- if (fs.existsSync(pathFile)) executablePath = fs.readFileSync(pathFile, "utf-8");
94
- if (executablePath) {
95
- electronExecPath = path.join(electronModulePath, "dist", executablePath);
96
- process.env.ELECTRON_EXEC_PATH = electronExecPath;
97
- } else throw new Error("Electron executable file is not existed");
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
+ };
160
+ }
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}`;
98
198
  }
99
- return electronExecPath;
100
199
  }
101
- function getBytecodeCompilerPath() {
102
- const scriptPath = path.join(electronModule.rootPath, "EIU_bytenode.cjs");
103
- if (!fs.existsSync(scriptPath)) fs.writeFileSync(scriptPath, bytecodeGeneratorScript);
104
- return scriptPath;
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
+ };
105
225
  }
106
- function toRelativePath(filename, importer) {
107
- const relPath = path.posix.relative(path.dirname(importer), filename);
108
- return relPath.startsWith(".") ? relPath : `./${relPath}`;
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
+ });
109
238
  }
110
- const logErr = (...args) => bytecodeLog.error(args.join(" "), { timestamp: true });
111
- function compileToBytecode(code, electronPath = getElectronPath()) {
112
- let data = Buffer.from([]);
113
- const bytecodePath = getBytecodeCompilerPath();
114
- return new Promise((resolve, reject) => {
115
- const proc = cp.spawn(electronPath, [bytecodePath], {
116
- env: { ELECTRON_RUN_AS_NODE: "1" },
117
- stdio: [
118
- "pipe",
119
- "pipe",
120
- "pipe",
121
- "ipc"
122
- ]
123
- });
124
- if (proc.stdin) {
125
- proc.stdin.write(code);
126
- proc.stdin.end();
127
- }
128
- if (proc.stdout) {
129
- proc.stdout.on("data", (chunk) => data = Buffer.concat([data, chunk]));
130
- proc.stdout.on("error", (err) => logErr(err));
131
- proc.stdout.on("end", () => resolve(data));
132
- }
133
- if (proc.stderr) {
134
- proc.stderr.on("data", (chunk) => logErr("Error: ", chunk.toString()));
135
- proc.stderr.on("error", (err) => logErr("Error: ", err));
136
- }
137
- proc.addListener("error", (err) => logErr(err));
138
- proc.on("error", (err) => reject(err));
139
- proc.on("exit", () => resolve(data));
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) {
262
+ const parent = path.parent;
263
+ const node = path.node;
264
+ if (parent.type === "CallExpression") {
265
+ if (parent.callee.type === "Identifier" && parent.callee.name === "require") return;
266
+ if (parent.callee.type === "Import") return;
267
+ }
268
+ if (parent.type.startsWith("Export")) return;
269
+ if (parent.type.startsWith("Import")) return;
270
+ if (parent.type === "ObjectMethod" && parent.key === node) {
271
+ parent.computed = true;
272
+ path.replaceWith(createObfuscatedStringCall(node.value, options.offset));
273
+ state.hasTransformed = true;
274
+ return;
275
+ }
276
+ if (parent.type === "ObjectProperty" && parent.key === node) {
277
+ parent.computed = true;
278
+ path.replaceWith(createObfuscatedStringCall(node.value, options.offset));
279
+ state.hasTransformed = true;
280
+ return;
281
+ }
282
+ if (!node.value.trim()) return;
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
+ ]
140
315
  });
141
316
  }
142
- function convertArrowFunctionAndTemplate(code) {
143
- const result = babel.transform(code, { plugins: ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-template-literals"] });
317
+ //#endregion
318
+ //#region src/vite/bytecode/index.ts
319
+ function getBytecodeLoaderBlock(chunkFileName) {
320
+ const loaderFileName = path.posix.relative(path.posix.dirname(chunkFileName), bytecodeModuleLoader);
321
+ return `require("${loaderFileName.startsWith(".") ? loaderFileName : `./${loaderFileName}`}")`;
322
+ }
323
+ function bytecodePlugin(env, minify, isESM, options) {
324
+ const { enable, preload = false, electronPath, beforeCompile } = options;
325
+ if (!enable) return null;
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 });
328
+ return null;
329
+ }
330
+ if (isESM) throw new Error("`bytecodePlugin` requires CommonJS. Set \"type\": \"commonjs\" in package.json and use .cjs extensions");
331
+ let hasJsChunks = false;
144
332
  return {
145
- code: result?.code || code,
146
- map: result?.map
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({
337
+ type: "asset",
338
+ source: `${bytecodeModuleLoaderCode}\n`,
339
+ name: "Bytecode Loader",
340
+ fileName: bytecodeModuleLoader
341
+ });
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;
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];
364
+ }));
365
+ }
147
366
  };
148
367
  }
149
- const decodeFn = ";function _0xstr_(a,b){return String.fromCharCode.apply(0,a.map(function(x){return x-b}))};";
150
- function obfuscateString(input, offset = ~~(Math.random() * 16) + 1) {
151
- return `_0xstr_([${input.split("").map((c) => `0x${(c.charCodeAt(0) + offset).toString(16)}`).join(",")}],${offset})`;
368
+ //#endregion
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
+ };
152
381
  }
153
- /**
154
- * Obfuscate string
155
- * @param code source code
156
- * @param sourcemap whether to generate sourcemap
157
- * @param offset custom offset
158
- */
159
- function convertLiteral(code, sourcemap, offset) {
160
- const s = new MagicString(code);
161
- let hasTransformed = false;
162
- const ast = babel.parse(code, { ast: true });
163
- if (!ast) throw new Error("Cannot parse code");
164
- babel.traverse(ast, { StringLiteral(path) {
165
- const parent = path.parent;
166
- const node = path.node;
167
- if (parent.type === "CallExpression") {
168
- if (parent.callee.type === "Identifier" && parent.callee.name === "require") return;
169
- if (parent.callee.type === "Import") return;
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;
387
+ }
388
+ function getNextPatchVersion(version) {
389
+ const parsed = parseVersion(version);
390
+ return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
391
+ }
392
+ function isValidVersion(version) {
393
+ try {
394
+ parseVersion(version);
395
+ return true;
396
+ } catch {
397
+ return false;
398
+ }
399
+ }
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
+ }
415
+ try {
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;
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
170
438
  }
171
- if (parent.type.startsWith("Export")) return;
172
- if (parent.type.startsWith("Import")) return;
173
- if (parent.type === "ObjectMethod" && parent.key === node) return;
174
- if (parent.type === "ObjectProperty" && parent.key === node) {
175
- const result = `[${obfuscateString(node.value, offset)}]`;
176
- const start = node.start;
177
- const end = node.end;
178
- if (start && end) {
179
- s.overwrite(start, end, result);
180
- hasTransformed = true;
439
+ };
440
+ }
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
+ }
468
+ }
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);
181
486
  }
182
- return;
487
+ };
488
+ }
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");
183
509
  }
184
- if (!node.value.trim()) return;
185
- const result = obfuscateString(node.value, offset);
186
- const start = node.start;
187
- const end = node.end;
188
- if (start && end) {
189
- s.overwrite(start, end, result);
190
- hasTransformed = true;
510
+ }
511
+ function handleExit() {
512
+ if (restartRequested) {
513
+ restartRequested = false;
514
+ setTimeout(() => {
515
+ if (startupArgs) start(startupArgs, { keepInstalledVersion: true });
516
+ });
517
+ return;
191
518
  }
192
- } });
193
- if (hasTransformed) s.append("\n").append(decodeFn);
194
- return {
195
- code: s.toString(),
196
- map: sourcemap ? s.generateMap({ hires: true }) : void 0
197
- };
519
+ process.exit();
520
+ }
521
+ function removeManagedListeners() {
522
+ managedElectronApp?.removeListener("message", handleMessage);
523
+ managedElectronApp?.removeListener("exit", handleExit);
524
+ }
525
+ return (onstartArgs) => start(onstartArgs);
198
526
  }
199
-
200
527
  //#endregion
201
- //#region src/vite/utils.ts
528
+ //#region src/vite/utils/file.ts
529
+ /**
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")
533
+ */
202
534
  function readableSize(size) {
203
535
  const units = [
204
536
  "B",
@@ -213,6 +545,12 @@ function readableSize(size) {
213
545
  }
214
546
  return `${size.toFixed(2)} ${units[i]}`;
215
547
  }
548
+ /**
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
553
+ */
216
554
  function copyAndSkipIfExist(from, to, skipIfExist) {
217
555
  if (!skipIfExist || !fs.existsSync(to)) try {
218
556
  fs.cpSync(from, to, { recursive: true });
@@ -220,18 +558,34 @@ function copyAndSkipIfExist(from, to, skipIfExist) {
220
558
  log.warn(`Copy failed: ${error}`, { timestamp: true });
221
559
  }
222
560
  }
223
-
224
561
  //#endregion
225
- //#region src/vite/build.ts
226
- async function buildAsar({ version, asarOutputPath, gzipPath, electronDistPath, rendererDistPath, generateGzipFile }) {
227
- await fs.promises.rename(rendererDistPath, path.join(electronDistPath, "renderer"));
562
+ //#region src/vite/utils/build.ts
563
+ /**
564
+ * Build asar file and update package
565
+ * @param options - Asar build options
566
+ * @returns Buffer of the built asar file
567
+ */
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 });
228
575
  fs.writeFileSync(path.join(electronDistPath, "version"), version);
576
+ await fs.promises.mkdir(path.dirname(asarOutputPath), { recursive: true });
229
577
  await createPackage(electronDistPath, asarOutputPath);
230
578
  const buf = await generateGzipFile(fs.readFileSync(asarOutputPath));
579
+ await fs.promises.mkdir(path.dirname(gzipPath), { recursive: true });
231
580
  fs.writeFileSync(gzipPath, buf);
232
581
  log.info(`Build update asar to '${gzipPath}' [${readableSize(buf.length)}]`, { timestamp: true });
233
582
  return buf;
234
583
  }
584
+ /**
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
588
+ */
235
589
  async function buildUpdateJson({ versionPath, privateKey, cert, version, minimumVersion, generateSignature, generateUpdateJson }, asarBuffer) {
236
590
  let _json = {
237
591
  beta: {
@@ -251,388 +605,20 @@ async function buildUpdateJson({ versionPath, privateKey, cert, version, minimum
251
605
  const sig = await generateSignature(asarBuffer, privateKey, cert, version);
252
606
  _json = await generateUpdateJson(_json, sig, version, minimumVersion);
253
607
  if (!isUpdateJSON(_json)) throw new Error("Invalid update json");
608
+ await fs.promises.mkdir(path.dirname(versionPath), { recursive: true });
254
609
  fs.writeFileSync(versionPath, JSON.stringify(_json, null, 2));
255
610
  log.info(`build update json to '${versionPath}'`, { timestamp: true });
256
611
  }
257
-
258
- //#endregion
259
- //#region src/vite/bytecode/index.ts
260
- function getBytecodeLoaderBlock(chunkFileName) {
261
- return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`;
262
- }
263
- /**
264
- * Compile to v8 bytecode to protect source code.
265
- */
266
- function bytecodePlugin(env, options) {
267
- const { enable, preload = false, electronPath, beforeCompile } = options;
268
- if (!enable) return null;
269
- if (!preload && env === "preload") {
270
- 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 });
271
- return null;
272
- }
273
- const filter = createFilter(/\.(m?[jt]s|[jt]sx)$/);
274
- let config;
275
- let bytecodeRequired = false;
276
- let bytecodeFiles = [];
277
- return {
278
- name: `${bytecodeId}-${env}`,
279
- apply: "build",
280
- enforce: "post",
281
- configResolved(resolvedConfig) {
282
- config = resolvedConfig;
283
- },
284
- transform(code, id) {
285
- if (!filter(id)) return convertLiteral(code, !!config.build.sourcemap);
286
- },
287
- generateBundle(options) {
288
- if (options.format !== "es" && bytecodeRequired) this.emitFile({
289
- type: "asset",
290
- source: `${bytecodeModuleLoaderCode}\n`,
291
- name: "Bytecode Loader File",
292
- fileName: bytecodeModuleLoader
293
- });
294
- },
295
- renderChunk(code, chunk, options) {
296
- if (options.format === "es") {
297
- bytecodeLog.warn("`bytecodePlugin` does not support ES module, please set \"build.rollupOptions.output.format\" option to \"cjs\"", { timestamp: true });
298
- return null;
299
- }
300
- if (chunk.type === "chunk") {
301
- bytecodeRequired = true;
302
- return convertArrowFunctionAndTemplate(code);
303
- }
304
- return null;
305
- },
306
- async writeBundle(options, output) {
307
- if (options.format === "es" || !bytecodeRequired) return;
308
- const outDir = options.dir;
309
- bytecodeFiles = [];
310
- const bundles = Object.keys(output);
311
- const chunks = Object.values(output).filter((chunk) => chunk.type === "chunk" && chunk.fileName !== bytecodeModuleLoader);
312
- const bytecodeChunks = new Set(chunks.map((chunk) => chunk.fileName));
313
- const pattern = chunks.filter((chunk) => !chunk.isEntry).map((chunk) => path.basename(chunk.fileName)).map((chunk) => `(${chunk})`).join("|");
314
- const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, "g") : null;
315
- await Promise.all(bundles.map(async (name) => {
316
- const chunk = output[name];
317
- if (chunk.type === "chunk") {
318
- let _code = chunk.code;
319
- const chunkFilePath = path.resolve(outDir, name);
320
- if (beforeCompile) {
321
- const cbResult = await beforeCompile(_code, chunkFilePath);
322
- if (cbResult) _code = cbResult;
323
- }
324
- if (bytecodeRE && _code.match(bytecodeRE)) {
325
- let match;
326
- const s = new MagicString(_code);
327
- while (match = bytecodeRE.exec(_code)) {
328
- const [prefix, chunkName] = match;
329
- const len = prefix.length + chunkName.length;
330
- s.overwrite(match.index, match.index + len, `${prefix + chunkName}c`, { contentOnly: true });
331
- }
332
- _code = s.toString();
333
- }
334
- if (bytecodeChunks.has(name)) {
335
- const bytecodeBuffer = await compileToBytecode(_code, electronPath);
336
- fs.writeFileSync(`${chunkFilePath}c`, bytecodeBuffer);
337
- if (chunk.isEntry) {
338
- const code = `${useStrict}\n${getBytecodeLoaderBlock(chunk.fileName)}\nmodule.exports=${`require("./${`${path.basename(name)}c`}");`}\n`;
339
- fs.writeFileSync(chunkFilePath, code);
340
- } else fs.unlinkSync(chunkFilePath);
341
- bytecodeFiles.push({
342
- name: `${name}c`,
343
- size: bytecodeBuffer.length
344
- });
345
- } else {
346
- if (chunk.isEntry) {
347
- let hasBytecodeMoudle = false;
348
- const idsToHandle = new Set([...chunk.imports, ...chunk.dynamicImports]);
349
- for (const moduleId of idsToHandle) {
350
- if (bytecodeChunks.has(moduleId)) {
351
- hasBytecodeMoudle = true;
352
- break;
353
- }
354
- const moduleInfo = this.getModuleInfo(moduleId);
355
- if (moduleInfo) {
356
- const { importers, dynamicImporters } = moduleInfo;
357
- for (const importerId of importers) idsToHandle.add(importerId);
358
- for (const importerId of dynamicImporters) idsToHandle.add(importerId);
359
- }
360
- }
361
- const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName);
362
- _code = hasBytecodeMoudle ? _code.replace(new RegExp(`(${useStrict})|("use strict";)`), `${useStrict}\n${bytecodeLoaderBlock}`) : _code;
363
- }
364
- fs.writeFileSync(chunkFilePath, _code);
365
- }
366
- }
367
- }));
368
- },
369
- closeBundle() {
370
- const outDir = `${normalizePath(path.relative(config.root, path.resolve(config.root, config.build.outDir)))}/`;
371
- bytecodeFiles.forEach((file) => {
372
- bytecodeLog.info(`${outDir}${file.name} [${readableSize(file.size)}]`, { timestamp: true });
373
- });
374
- bytecodeLog.info(`${bytecodeFiles.length} bundles compiled into bytecode.`, { timestamp: true });
375
- bytecodeFiles = [];
376
- }
377
- };
378
- }
379
-
380
- //#endregion
381
- //#region src/vite/electron/utils.ts
382
- /** Resolve the default Vite's `InlineConfig` for build Electron-Main */
383
- function resolveViteConfig(isESM, options) {
384
- return mergeConfig({
385
- configFile: false,
386
- publicDir: false,
387
- build: {
388
- lib: options.entry && {
389
- entry: options.entry,
390
- formats: isESM ? ["es"] : ["cjs"],
391
- fileName: () => "[name].js"
392
- },
393
- outDir: "dist-electron",
394
- emptyOutDir: false
395
- },
396
- resolve: {
397
- conditions: ["node"],
398
- mainFields: [
399
- "module",
400
- "jsnext:main",
401
- "jsnext"
402
- ]
403
- },
404
- define: { "process.env": "process.env" }
405
- }, options?.vite || {});
406
- }
407
- /**
408
- * Inspired `tree-kill`, implemented based on sync-api. #168
409
- * @see https://github.com/pkrumins/node-tree-kill/blob/v1.2.2/index.js
410
- */
411
- function treeKillSync(pid) {
412
- if (process.platform === "win32") cp.execSync(`taskkill /pid ${pid} /T /F`);
413
- else killTree(pidTree({
414
- pid,
415
- ppid: process.pid
416
- }));
417
- }
418
- function pidTree(tree) {
419
- const command = process.platform === "darwin" ? `pgrep -P ${tree.pid}` : `ps -o pid --no-headers --ppid ${tree.ppid}`;
420
- try {
421
- const childs = cp.execSync(command, { encoding: "utf8" }).match(/\d+/g)?.map((id) => +id);
422
- if (childs) tree.children = childs.map((cid) => pidTree({
423
- pid: cid,
424
- ppid: tree.pid
425
- }));
426
- } catch {}
427
- return tree;
428
- }
429
- function killTree(tree) {
430
- if (tree.children) for (const child of tree.children) killTree(child);
431
- try {
432
- process.kill(tree.pid);
433
- } catch {}
434
- }
435
-
436
612
  //#endregion
437
- //#region src/vite/electron/core.ts
438
- function build$1(isESM, options) {
439
- return build(resolveViteConfig(isESM, options));
440
- }
441
- function electron(isESM, root, options) {
442
- const optionsArray = Array.isArray(options) ? options : [options];
443
- let userConfig;
444
- let configEnv;
445
- 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\`.`);
446
- async function parallelBuild(options) {
447
- await Promise.all(options.map(build$1.bind(build$1, isESM)));
448
- }
449
- return [{
450
- name: "vite-plugin-electron:dev",
451
- apply: "serve",
452
- configResolved(config) {
453
- 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`);
454
- },
455
- configureServer(server) {
456
- server.httpServer?.once("listening", () => {
457
- Object.assign(process.env, { VITE_DEV_SERVER_URL: server.resolvedUrls?.local[0] });
458
- const entryCount = optionsArray.length;
459
- let closeBundleCount = 0;
460
- parallelBuild(optionsArray.map((options) => {
461
- options.vite ??= {};
462
- options.vite.mode ??= server.config.mode;
463
- options.vite.root ??= server.config.root;
464
- options.vite.envDir ??= server.config.envDir;
465
- options.vite.envPrefix ??= server.config.envPrefix;
466
- const defaultArgs = [options.vite.root || ".", "--no-sandbox"];
467
- options.vite.build ??= {};
468
- if (!Object.keys(options.vite.build).includes("watch")) options.vite.build.watch = {};
469
- options.vite.build.minify ??= false;
470
- options.vite.plugins ??= [];
471
- options.vite.plugins.push({
472
- name: ":startup",
473
- closeBundle() {
474
- if (++closeBundleCount < entryCount) return;
475
- if (options.onstart) options.onstart.call(this, {
476
- async startup(args = defaultArgs, ...opt) {
477
- await startup(args, ...opt);
478
- },
479
- reload() {
480
- if (process.electronApp) {
481
- (server.hot || server.ws).send({ type: "full-reload" });
482
- startup.send("electron-vite&type=hot-reload");
483
- } else startup(defaultArgs);
484
- }
485
- });
486
- else startup(defaultArgs);
487
- }
488
- });
489
- return options;
490
- }));
491
- });
492
- }
493
- }, {
494
- name: "vite-plugin-electron:prod",
495
- apply: "build",
496
- config(config, env) {
497
- userConfig = config;
498
- configEnv = env;
499
- config.base ??= "./";
500
- },
501
- async closeBundle() {
502
- await parallelBuild(optionsArray.map((options) => {
503
- options.vite ??= {};
504
- options.vite.mode ??= configEnv.mode;
505
- options.vite.root ??= userConfig.root;
506
- options.vite.envDir ??= userConfig.envDir;
507
- options.vite.envPrefix ??= userConfig.envPrefix;
508
- return options;
509
- }));
510
- }
511
- }];
512
- }
513
- /**
514
- * Electron App startup function.
515
- * It will mount the Electron App child-process to `process.electronApp`.
516
- * @param argv default value `['.', '--no-sandbox']`
517
- * @param options options for `child_process.spawn`
518
- * @param customElectronPkg custom electron package name (default: 'electron')
519
- */
520
- const startup = async (argv = [".", "--no-sandbox"], options, customElectronPkg) => {
521
- const { spawn } = await import("node:child_process");
522
- const electron = await import(customElectronPkg ?? "electron");
523
- const electronPath = electron.default ?? electron;
524
- await startup.exit();
525
- const stdio = process.platform === "linux" ? [
526
- "inherit",
527
- "inherit",
528
- "inherit",
529
- "ignore",
530
- "ipc"
531
- ] : [
532
- "inherit",
533
- "inherit",
534
- "inherit",
535
- "ipc"
536
- ];
537
- process.electronApp = spawn(electronPath, argv, {
538
- stdio,
539
- ...options
540
- });
541
- process.electronApp.once("exit", process.exit);
542
- if (!startup.hookedProcessExit) {
543
- startup.hookedProcessExit = true;
544
- process.once("exit", startup.exit);
545
- }
546
- };
547
- startup.send = (message) => {
548
- if (process.electronApp) process.electronApp.send?.(message);
549
- };
550
- startup.hookedProcessExit = false;
551
- startup.exit = async () => {
552
- if (process.electronApp) await new Promise((resolve) => {
553
- process.electronApp.removeAllListeners();
554
- process.electronApp.once("exit", resolve);
555
- treeKillSync(process.electronApp.pid);
556
- });
557
- };
558
-
559
- //#endregion
560
- //#region src/vite/electron/plugin.ts
561
- /**
562
- * @see https://github.com/vitejs/vite/blob/v4.4.7/packages/vite/src/node/utils.ts#L140
563
- */
564
- const bareImportRE = /^(?![a-zA-Z]:)[\w@](?!.*:\/\/)/;
565
- const nodeModulesRE = /\/node_modules\//;
566
- /**
567
- * During dev, we exclude the `cjs` npm-pkg from bundle, mush like Vite :)
568
- */
569
- function notBundle(options = {}) {
570
- const externalIds = /* @__PURE__ */ new Set();
571
- return {
572
- name: "vite-plugin-electron:not-bundle",
573
- enforce: "pre",
574
- apply: "serve",
575
- resolveId: {
576
- filter: { id: bareImportRE },
577
- async handler(source, importer) {
578
- if (!importer || importer.includes("node_modules/")) return;
579
- if (externalIds.has(source)) return {
580
- id: source,
581
- external: true
582
- };
583
- const id = (await this.resolve(source, importer, { skipSelf: true }))?.id;
584
- if (!id || !nodeModulesRE.test(id) || options.filter?.(id) === false) return;
585
- try {
586
- createRequire(importer).resolve(source);
587
- } catch {
588
- return;
589
- }
590
- externalIds.add(source);
591
- return {
592
- id: source,
593
- external: true,
594
- moduleSideEffects: false
595
- };
596
- }
597
- }
598
- };
599
- }
600
-
601
- //#endregion
602
- //#region src/utils/crypto.ts
603
- function hashBuffer(data, length) {
604
- const hash = crypto.createHash("SHA256").update(data).digest("binary");
605
- return Buffer.from(hash).subarray(0, length);
606
- }
607
- function aesEncrypt(plainText, key, iv) {
608
- const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
609
- return cipher.update(plainText, "utf8", "base64url") + cipher.final("base64url");
610
- }
611
- /**
612
- * Default function to generate asar signature, returns generated signature
613
- * @param buffer file buffer
614
- * @param privateKey primary key
615
- * @param cert certificate
616
- * @param version target version
617
- */
618
- function defaultSignature(buffer, privateKey, cert, version) {
619
- return aesEncrypt(`${crypto.createSign("RSA-SHA256").update(buffer).sign(crypto.createPrivateKey(privateKey), "base64")}%${version}`, hashBuffer(cert, 32), hashBuffer(buffer, 16));
620
- }
621
-
622
- //#endregion
623
- //#region src/utils/zip.ts
613
+ //#region src/vite/utils/key.ts
624
614
  /**
625
- * Default function to compress file using brotli
626
- * @param buffer uncompressed file buffer
615
+ * Generate a new key pair for signing
616
+ * @param keyLength - Key length in bits
617
+ * @param subject - Certificate subject
618
+ * @param days - Validity period in days
619
+ * @param privateKeyPath - Output path for private key
620
+ * @param certPath - Output path for certificate
627
621
  */
628
- async function defaultZipFile(buffer) {
629
- return new Promise((resolve, reject) => {
630
- zlib.brotliCompress(buffer, (err, buffer) => err ? reject(err) : resolve(buffer));
631
- });
632
- }
633
-
634
- //#endregion
635
- //#region src/vite/key.ts
636
622
  async function generateKeyPair(keyLength, subject, days, privateKeyPath, certPath) {
637
623
  const privateKeyDir = path.dirname(privateKeyPath);
638
624
  if (!fs.existsSync(privateKeyDir)) fs.mkdirSync(privateKeyDir, { recursive: true });
@@ -650,6 +636,11 @@ async function generateKeyPair(keyLength, subject, days, privateKeyPath, certPat
650
636
  fs.writeFileSync(privateKeyPath, privateKey.replace(/\r\n?/g, "\n"));
651
637
  fs.writeFileSync(certPath, cert.replace(/\r\n?/g, "\n"));
652
638
  }
639
+ /**
640
+ * Parse and load keys, generating new ones if they don't exist
641
+ * @param options - Key parsing options
642
+ * @returns Object containing private key and certificate strings
643
+ */
653
644
  async function parseKeys({ keyLength, privateKeyPath, certPath, subject, days }) {
654
645
  const keysDir = path.dirname(privateKeyPath);
655
646
  let privateKey = process.env.UPDATER_PK;
@@ -673,37 +664,43 @@ async function parseKeys({ keyLength, privateKeyPath, certPath, subject, days })
673
664
  cert
674
665
  };
675
666
  }
667
+ /**
668
+ * Convert DistinguishedName object to CertSubject array
669
+ * @param subject - Distinguished name object
670
+ * @returns Certificate subject array
671
+ */
676
672
  function parseSubjects(subject) {
677
673
  return Object.entries(subject).filter(([_, value]) => !!value).map(([name, value]) => ({
678
674
  name,
679
675
  value
680
676
  }));
681
677
  }
682
-
683
678
  //#endregion
684
- //#region src/vite/option.ts
685
- async function parseUpdaterOption(pkg, options = {}) {
686
- 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 = {
687
682
  commonName: pkg.name,
688
683
  organizationName: `org.${pkg.name}`
689
684
  }, days = 3650 } = {} } = {}, overrideGenerator: { generateGzipFile = defaultZipFile, generateSignature = defaultSignature, generateUpdateJson = defaultVersionJsonGenerator } = {} } = options;
690
- const buildAsarOption = {
691
- version: pkg.version,
692
- asarOutputPath,
693
- gzipPath,
694
- electronDistPath,
695
- rendererDistPath,
696
- generateGzipFile
697
- };
698
- const { privateKey, cert } = await parseKeys({
685
+ const { privateKey, cert } = resolveSignatureKeys ? await parseKeys({
699
686
  keyLength,
700
- privateKeyPath,
701
- certPath,
687
+ privateKeyPath: path.resolve(root, privateKeyPath),
688
+ certPath: path.resolve(root, certPath),
702
689
  subject,
703
690
  days
704
- });
691
+ }) : {
692
+ privateKey: "",
693
+ cert: ""
694
+ };
705
695
  return {
706
- buildAsarOption,
696
+ buildAsarOption: {
697
+ version: pkg.version,
698
+ asarOutputPath,
699
+ gzipPath,
700
+ electronDistPath,
701
+ rendererDistPath,
702
+ generateGzipFile
703
+ },
707
704
  buildVersionOption: {
708
705
  version: pkg.version,
709
706
  minimumVersion,
@@ -713,289 +710,230 @@ async function parseUpdaterOption(pkg, options = {}) {
713
710
  generateSignature,
714
711
  generateUpdateJson
715
712
  },
716
- cert,
717
713
  entryOutDir
718
714
  };
719
715
  }
720
-
721
- //#endregion
722
- //#region src/vite/core.ts
723
- /**
724
- * Startup function for debug
725
- * @see {@link https://github.com/electron-vite/electron-vite-vue/blob/main/vite.config.ts electron-vite-vue template}
726
- * @example
727
- * import { debugStartup, buildElectronPluginOptions } from 'electron-incremental-update/vite'
728
- * const options = buildElectronPluginOptions({
729
- * // ...
730
- * main: {
731
- * // ...
732
- * startup: debugStartup
733
- * },
734
- * })
735
- */
736
- const debugStartup = async (args) => {
737
- if (process.env.VSCODE_DEBUG) console.log("[startup] Electron App");
738
- else await args.startup();
739
- };
740
- /**
741
- * Startup function to filter unwanted error message
742
- * @see {@link https://github.com/electron/electron/issues/46903#issuecomment-2848483520 reference}
743
- * @example
744
- * import { filterErrorMessageStartup, buildElectronPluginOptions } from 'electron-incremental-update/vite'
745
- * const options = buildElectronPluginOptions({
746
- * // ...
747
- * main: {
748
- * // ...
749
- * startup: args => filterErrorMessageStartup(
750
- * args,
751
- * // ignore error message when function returns false
752
- * msg => !/"code":-32601/.test(msg)
753
- * )
754
- * },
755
- * })
756
- */
757
- async function filterErrorMessageStartup(args, filter) {
758
- const stdio = process.platform === "linux" ? [
759
- "inherit",
760
- "pipe",
761
- "pipe",
762
- "ignore",
763
- "ipc"
764
- ] : [
765
- "inherit",
766
- "pipe",
767
- "pipe",
768
- "ipc"
769
- ];
770
- await args.startup(void 0, { stdio });
771
- const elec = process.electronApp;
772
- elec.stdout.addListener("data", (data) => {
773
- console.log(data.toString().trimEnd());
774
- });
775
- elec.stderr.addListener("data", (data) => {
776
- const message = data.toString();
777
- if (filter(message)) console.error(message);
778
- });
779
- }
780
- /**
781
- * Startup function util to fix Windows terminal charset
782
- * @example
783
- * import { debugStartup, fixWinCharEncoding, buildElectronPluginOptions } from 'electron-incremental-update/vite'
784
- * const options = buildElectronPluginOptions({
785
- * // ...
786
- * main: {
787
- * // ...
788
- * startup: fixWinCharEncoding(debugStartup)
789
- * },
790
- * })
791
- */
792
- function fixWinCharEncoding(fn) {
793
- return (async (...args) => {
794
- if (process.platform === "win32") (await import("node:child_process")).spawnSync("chcp", ["65001"]);
795
- await fn(...args);
796
- });
797
- }
798
- function getMainFileBaseName(options) {
799
- let mainFilePath;
800
- if (typeof options === "string") mainFilePath = path.basename(options);
801
- else if (Array.isArray(options)) mainFilePath = path.basename(options[0]);
802
- else {
803
- if (!(options?.index ?? options?.main)) throw new Error(`\`options.main.files\` (${options}) must have "index" or "main" key, like \`{ index: "./electron/main/index.ts" }\``);
804
- 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;
805
722
  }
806
- log.info(`Using "${mainFilePath}" as main file`, { timestamp: true });
807
- 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];
808
726
  }
809
- function parseVersionPath(versionPath) {
727
+ function normalizeVersionPath(versionPath) {
810
728
  versionPath = normalizePath(versionPath);
811
729
  if (!versionPath.startsWith("./")) versionPath = `./${versionPath}`;
812
730
  return new URL(versionPath, "file://").pathname.slice(1);
813
731
  }
814
- const defaultExternal = [
815
- ...builtinModules,
816
- "electron",
817
- /^node:/,
818
- /.*\.(node|dll|dylib|so)$/,
819
- "original-fs"
820
- ];
821
- /**
822
- * Base on `./electron/simple`
823
- * - integrate with updater
824
- * - no `renderer` config
825
- * - remove old output file
826
- * - externalize dependencies
827
- * - auto restart when entry file changes
828
- * - other configs in {@link https://github.com/electron-vite/electron-vite-vue/blob/main/vite.config.ts electron-vite-vue template}
829
- *
830
- * You can override all the vite configs, except output directories (use `options.updater.paths.electronDistPath` instead)
831
- *
832
- * @example
833
- * ```ts
834
- * import { defineConfig } from 'vite'
835
- * import { debugStartup, electronWithUpdater } from 'electron-incremental-update/vite'
836
- *
837
- * export default defineConfig(async ({ command }) => {
838
- * const isBuild = command === 'build'
839
- * return {
840
- * plugins: [
841
- * electronWithUpdater({
842
- * isBuild,
843
- * main: {
844
- * files: ['./electron/main/index.ts', './electron/main/worker.ts'],
845
- * // see https://github.com/electron-vite/electron-vite-vue/blob/85ed267c4851bf59f32888d766c0071661d4b94c/vite.config.ts#L22-L28
846
- * onstart: debugStartup,
847
- * },
848
- * preload: {
849
- * files: './electron/preload/index.ts',
850
- * },
851
- * updater: {
852
- * // options
853
- * }
854
- * }),
855
- * ],
856
- * server: process.env.VSCODE_DEBUG && (() => {
857
- * const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL)
858
- * return {
859
- * host: url.hostname,
860
- * port: +url.port,
861
- * }
862
- * })(),
863
- * }
864
- * })
865
- * ```
866
- */
867
- async function electronWithUpdater(options) {
868
- 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;
869
- 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;
870
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");
871
736
  const isESM = pkg.type === "module";
872
- const finalExternal = [...defaultExternal, ...isBuild || _entry.postBuild ? [] : external || Object.keys(pkg.dependencies || {})];
873
- 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;
874
744
  if (isESM && bytecodeOptions?.enable) throw new Error("`bytecodePlugin` does not support ES module, please remove \"type\": \"module\" in package.json");
875
- 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 });
876
750
  log.info(`Clear cache files`, { timestamp: true });
877
- await Promise.all([fs.promises.rm(buildAsarOption.electronDistPath, {
878
- recursive: true,
879
- force: true
880
- }), fs.promises.rm(entryOutDir, {
751
+ await Promise.all([
752
+ buildAsarOption.rendererDistPath,
753
+ buildAsarOption.electronDistPath,
754
+ entryOutDir
755
+ ].map((p) => fs.promises.rm(path.resolve(context.root, p), {
881
756
  recursive: true,
882
757
  force: true
883
- })]).catch(() => {});
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;
884
774
  const define = {
885
775
  __EIU_ASAR_BASE_NAME__: JSON.stringify(path.basename(buildAsarOption.asarOutputPath)),
886
776
  __EIU_ELECTRON_DIST_PATH__: JSON.stringify(normalizePath(buildAsarOption.electronDistPath)),
887
777
  __EIU_ENTRY_DIST_PATH__: JSON.stringify(normalizePath(entryOutDir)),
888
- __EIU_IS_DEV__: JSON.stringify(!isBuild),
778
+ __EIU_IS_DEV__: JSON.stringify(context.isDev),
889
779
  __EIU_IS_ESM__: JSON.stringify(isESM),
890
- __EIU_MAIN_FILE__: JSON.stringify(getMainFileBaseName(_main.files)),
891
- __EIU_SIGNATURE_CERT__: JSON.stringify(cert),
892
- __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)
893
788
  };
894
789
  const _electronOptions = [{
895
- entry: _main.files,
896
- onstart: _main.onstart,
897
- vite: mergeConfig({
898
- plugins: [!isBuild && useNotBundle && notBundle(), bytecodeOptions && bytecodePlugin("main", 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({
899
796
  build: {
900
797
  sourcemap,
901
798
  minify,
902
799
  outDir: `${buildAsarOption.electronDistPath}/main`,
903
800
  rolldownOptions: {
904
801
  external: finalExternal,
905
- platform: "node",
906
- output: { polyfillRequire: false }
802
+ output: {
803
+ format: isESM ? "es" : "cjs",
804
+ polyfillRequire: isESM,
805
+ ...outputNames
806
+ }
907
807
  }
908
808
  },
909
809
  define
910
- }, _main.vite ?? {})
810
+ }, main.options ?? {})
911
811
  }];
912
- if (_preload?.files) _electronOptions.push({
812
+ if (preload?.files) _electronOptions.push({
813
+ name: "preload",
913
814
  onstart(args) {
914
815
  args.reload();
915
816
  },
916
- vite: mergeConfig({
917
- plugins: [bytecodeOptions && bytecodePlugin("preload", bytecodeOptions)],
817
+ notBundle,
818
+ input: preload.files,
819
+ plugins: [isESM && esmShim(), bytecodeOptions && bytecodePlugin("preload", minify, isESM, bytecodeOptions)],
820
+ options: mergeConfig({
918
821
  build: {
919
822
  sourcemap: sourcemap ? "inline" : void 0,
920
823
  minify,
921
824
  outDir: `${buildAsarOption.electronDistPath}/preload`,
922
825
  rolldownOptions: {
923
826
  external: finalExternal,
924
- input: _preload.files,
925
827
  output: {
926
828
  format: "cjs",
927
- inlineDynamicImports: true,
829
+ codeSplitting: false,
928
830
  polyfillRequire: false,
929
- entryFileNames: `[name].${isESM ? "mjs" : "js"}`,
930
- chunkFileNames: `[name].${isESM ? "mjs" : "js"}`,
931
- assetFileNames: "[name].[ext]"
831
+ ...outputNames
932
832
  }
933
833
  }
934
834
  },
935
835
  define
936
- }, _preload?.vite ?? {})
836
+ }, preload?.options ?? {})
937
837
  });
938
838
  _electronOptions.push({
939
- entry: _entry.files,
940
- vite: mergeConfig({
941
- plugins: [
942
- bytecodeOptions && bytecodePlugin("main", bytecodeOptions),
943
- !isBuild && useNotBundle && notBundle(),
944
- {
945
- name: `${id}:entry`,
946
- enforce: "post",
947
- async closeBundle() {
948
- log.info(`Build entry to '${entryOutDir}'`, { timestamp: true });
949
- await _entry.postBuild?.({
950
- isBuild,
951
- getPathFromEntryOutputDir(...paths) {
952
- return path.join(entryOutDir, ...paths);
953
- },
954
- copyToEntryOutputDir({ from, to = path.basename(from), skipIfExist = true }) {
955
- if (!fs.existsSync(from)) {
956
- log.warn(`${from} not found`, { timestamp: true });
957
- return;
958
- }
959
- copyAndSkipIfExist(from, path.join(entryOutDir, to), skipIfExist);
960
- },
961
- copyModules({ modules, skipIfExist = true }) {
962
- const nodeModulesPath = path.join(entryOutDir, "node_modules");
963
- for (const m of modules) {
964
- const { rootPath } = getPackageInfoSync(m) || {};
965
- if (!rootPath) {
966
- log.warn(`Package '${m}' not found`, { timestamp: true });
967
- continue;
968
- }
969
- copyAndSkipIfExist(rootPath, path.join(nodeModulesPath, m), skipIfExist);
970
- }
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;
971
862
  }
972
- });
973
- if (isBuild) try {
974
- const buffer = await buildAsar(buildAsarOption);
975
- if (!buildVersionJson && !isCI) log.warn("No `buildVersionJson` option setup, skip build version json. Only build in CI by default", { timestamp: true });
976
- else await buildUpdateJson(buildVersionOption, buffer);
977
- } catch (error) {
978
- console.error(error);
863
+ copyAndSkipIfExist(from, path.join(entryOutDir, to), skipIfExist);
864
+ },
865
+ copyModules() {
866
+ console.warn("`copyModules()` is deprecated. Will do nothing");
979
867
  }
980
- }
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);
981
873
  }
982
- ],
874
+ }
875
+ ],
876
+ options: mergeConfig({
983
877
  build: {
984
878
  sourcemap,
985
879
  minify,
986
880
  outDir: entryOutDir,
987
881
  rolldownOptions: {
988
882
  external: finalExternal,
989
- platform: "node",
990
- output: { polyfillRequire: false }
883
+ output: {
884
+ format: isESM ? "es" : "cjs",
885
+ polyfillRequire: isESM,
886
+ ...outputNames
887
+ }
991
888
  }
992
889
  },
993
890
  define
994
- }, _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;
995
935
  });
996
- return electron(isESM, normalizePath(path.resolve(root)), _electronOptions);
997
936
  }
998
-
999
937
  //#endregion
1000
938
  //#region src/vite/define.ts
1001
939
  /**
@@ -1008,8 +946,6 @@ async function electronWithUpdater(options) {
1008
946
  * export default defineElectronConfig({
1009
947
  * main: {
1010
948
  * files: ['./electron/main/index.ts', './electron/main/worker.ts'],
1011
- * // see https://github.com/electron-vite/electron-vite-vue/blob/85ed267c4851bf59f32888d766c0071661d4b94c/vite.config.ts#L22-L28
1012
- * onstart: debugStartup,
1013
949
  * },
1014
950
  * preload: {
1015
951
  * files: './electron/preload/index.ts',
@@ -1030,21 +966,16 @@ async function electronWithUpdater(options) {
1030
966
  * ```
1031
967
  */
1032
968
  function defineElectronConfig(options) {
1033
- return ({ command }) => {
1034
- options.isBuild ??= command === "build";
1035
- const electronPlugin = electronWithUpdater(options);
1036
- const result = options.renderer ?? {};
1037
- result.plugins ??= [];
1038
- result.plugins.push(electronPlugin);
1039
- result.root = options.root;
1040
- const rendererDistPath = options.updater?.paths?.rendererDistPath;
1041
- if (rendererDistPath) {
1042
- result.build ??= {};
1043
- result.build.outDir = rendererDistPath;
1044
- }
1045
- return result;
1046
- };
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;
1047
979
  }
1048
-
1049
980
  //#endregion
1050
- export { convertLiteral, debugStartup, electronWithUpdater as default, electronWithUpdater, defineElectronConfig, filterErrorMessageStartup, fixWinCharEncoding };
981
+ export { electronWithUpdater as default, electronWithUpdater, defineElectronConfig, filterErrorMessageStartup, fixWinCharEncoding };