ee-bin 5.0.0-beta.1 → 5.0.0-beta.5

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.
Files changed (73) hide show
  1. package/dist/cjs/config/bin_default.js +69 -29
  2. package/dist/cjs/config/bin_default.js.map +1 -1
  3. package/dist/cjs/index.js +125 -4
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/lib/extend.js +49 -15
  6. package/dist/cjs/lib/extend.js.map +1 -1
  7. package/dist/cjs/lib/helpers.js +71 -22
  8. package/dist/cjs/lib/helpers.js.map +1 -1
  9. package/dist/cjs/lib/utils.js +71 -30
  10. package/dist/cjs/lib/utils.js.map +1 -1
  11. package/dist/cjs/plugins/bundle_registry_plugin.js +156 -0
  12. package/dist/cjs/plugins/bundle_registry_plugin.js.map +1 -0
  13. package/dist/cjs/tools/encrypt.js +190 -29
  14. package/dist/cjs/tools/encrypt.js.map +1 -1
  15. package/dist/cjs/tools/iconGen.js +118 -30
  16. package/dist/cjs/tools/iconGen.js.map +1 -1
  17. package/dist/cjs/tools/incrUpdater.js +95 -33
  18. package/dist/cjs/tools/incrUpdater.js.map +1 -1
  19. package/dist/cjs/tools/move.js +71 -11
  20. package/dist/cjs/tools/move.js.map +1 -1
  21. package/dist/cjs/tools/serve.js +406 -81
  22. package/dist/cjs/tools/serve.js.map +1 -1
  23. package/dist/cjs/types/config.js +13 -0
  24. package/dist/cjs/types/config.js.map +1 -0
  25. package/dist/esm/config/bin_default.d.ts +19 -147
  26. package/dist/esm/config/bin_default.d.ts.map +1 -1
  27. package/dist/esm/config/bin_default.js +69 -29
  28. package/dist/esm/config/bin_default.js.map +1 -1
  29. package/dist/esm/index.d.ts +20 -0
  30. package/dist/esm/index.d.ts.map +1 -1
  31. package/dist/esm/index.js +125 -4
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/lib/extend.d.ts +33 -0
  34. package/dist/esm/lib/extend.d.ts.map +1 -1
  35. package/dist/esm/lib/extend.js +49 -15
  36. package/dist/esm/lib/extend.js.map +1 -1
  37. package/dist/esm/lib/helpers.d.ts +44 -3
  38. package/dist/esm/lib/helpers.d.ts.map +1 -1
  39. package/dist/esm/lib/helpers.js +71 -22
  40. package/dist/esm/lib/helpers.js.map +1 -1
  41. package/dist/esm/lib/utils.d.ts +57 -3
  42. package/dist/esm/lib/utils.d.ts.map +1 -1
  43. package/dist/esm/lib/utils.js +71 -30
  44. package/dist/esm/lib/utils.js.map +1 -1
  45. package/dist/esm/plugins/bundle_registry_plugin.d.ts +33 -0
  46. package/dist/esm/plugins/bundle_registry_plugin.d.ts.map +1 -0
  47. package/dist/esm/plugins/bundle_registry_plugin.js +156 -0
  48. package/dist/esm/plugins/bundle_registry_plugin.js.map +1 -0
  49. package/dist/esm/tools/encrypt.d.ts +37 -1
  50. package/dist/esm/tools/encrypt.d.ts.map +1 -1
  51. package/dist/esm/tools/encrypt.js +190 -29
  52. package/dist/esm/tools/encrypt.js.map +1 -1
  53. package/dist/esm/tools/iconGen.d.ts +27 -1
  54. package/dist/esm/tools/iconGen.d.ts.map +1 -1
  55. package/dist/esm/tools/iconGen.js +118 -30
  56. package/dist/esm/tools/iconGen.js.map +1 -1
  57. package/dist/esm/tools/incrUpdater.d.ts +60 -13
  58. package/dist/esm/tools/incrUpdater.d.ts.map +1 -1
  59. package/dist/esm/tools/incrUpdater.js +95 -33
  60. package/dist/esm/tools/incrUpdater.js.map +1 -1
  61. package/dist/esm/tools/move.d.ts +41 -0
  62. package/dist/esm/tools/move.d.ts.map +1 -1
  63. package/dist/esm/tools/move.js +71 -11
  64. package/dist/esm/tools/move.js.map +1 -1
  65. package/dist/esm/tools/serve.d.ts +162 -25
  66. package/dist/esm/tools/serve.d.ts.map +1 -1
  67. package/dist/esm/tools/serve.js +406 -81
  68. package/dist/esm/tools/serve.js.map +1 -1
  69. package/dist/esm/types/config.d.ts +211 -0
  70. package/dist/esm/types/config.d.ts.map +1 -0
  71. package/dist/esm/types/config.js +13 -0
  72. package/dist/esm/types/config.js.map +1 -0
  73. package/package.json +16 -13
@@ -1,4 +1,25 @@
1
1
  "use strict";
2
+ /**
3
+ * Dev/Build/Start Manager — ee-bin's core dispatcher
4
+ *
5
+ * The ServeProcess class manages the full dev/build/start/exec lifecycle and is the
6
+ * most complex module in ee-bin. Core responsibilities:
7
+ * 1. dev — Start frontend dev server + Electron process, optional watch mode with auto-rebuild
8
+ * 2. build — Bundle Electron code + execute electron-builder platform build commands
9
+ * 3. start — Start Electron in production mode
10
+ * 4. exec — Execute user-defined custom commands
11
+ *
12
+ * Process management strategy:
13
+ * - execProcess only tracks async ChildProcess instances (sync executions are already
14
+ * complete, so there's no process to manage)
15
+ * - SIGINT/SIGTERM signal handlers close all child processes and restore package.json main field
16
+ * - In watch mode, debounce + tree-kill terminate the old Electron process before restarting
17
+ *
18
+ * Bundling strategy:
19
+ * - bundle mode: esbuild bundles into a single file + virtual registry plugin
20
+ * - copy mode: directly copies the entire electron/ directory (for non-bundling scenarios)
21
+ * - After bundling, switches package.json main field to point to ./public/electron/main.js
22
+ */
2
23
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
24
  if (k2 === undefined) k2 = k;
4
25
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -41,20 +62,26 @@ const helpers_js_1 = require("../lib/helpers.js");
41
62
  const path_1 = __importDefault(require("path"));
42
63
  const fs_1 = __importDefault(require("fs"));
43
64
  const esbuild_1 = require("esbuild");
44
- const chokidar_1 = __importDefault(require("chokidar"));
65
+ const globby_1 = require("globby");
66
+ const chokidar_1 = require("chokidar");
45
67
  const tree_kill_1 = __importDefault(require("tree-kill"));
46
68
  const process_1 = __importDefault(require("process"));
47
69
  const cross_spawn_1 = __importStar(require("cross-spawn"));
48
70
  const utils_js_1 = require("../lib/utils.js");
71
+ const bundle_registry_plugin_js_1 = require("../plugins/bundle_registry_plugin.js");
49
72
  const log = (0, helpers_js_1.createDebug)('ee-bin:serve');
73
+ /** Maximum buffer size for child processes (1GB), prevents large-output build commands from being truncated */
74
+ const MAX_BUFFER = 1024 * 1024 * 1024;
75
+ const ELECTRON_DIR = './electron';
76
+ const BUNDLE_DIR = './public/electron';
77
+ const PKG_PATH = './package.json';
50
78
  class ServeProcess {
51
79
  constructor() {
52
80
  this.execProcess = {};
53
- this.electronDir = './electron';
54
- this.bundleDir = './public/electron';
55
- this.pkgPath = './package.json';
81
+ this.originalPkgMain = undefined;
56
82
  this._init();
57
83
  }
84
+ /** Register SIGINT/SIGTERM signal handlers to ensure child processes are closed and config is restored on exit */
58
85
  _init() {
59
86
  process_1.default.on('SIGINT', () => {
60
87
  console.log(helpers_js_1.chalk.blue('[ee-bin] ') + 'Received SIGINT. Closing processes...');
@@ -65,28 +92,47 @@ class ServeProcess {
65
92
  this._closeProcess();
66
93
  });
67
94
  }
95
+ /**
96
+ * Close all child processes, restore package.json, then exit
97
+ *
98
+ * Flow: kill all child processes → restore pkgMain → sleep 500ms → process.exit(0)
99
+ * NOTE: The 500ms sleep is a compromise. The ideal approach would be to listen for each
100
+ * child process's exit event before exiting, but that's complex to implement (multi-process
101
+ * racing, nested processes, etc.). If a child process doesn't close in time, it may be orphaned.
102
+ */
68
103
  async _closeProcess() {
69
- const currentProcess = [];
70
104
  const keys = Object.keys(this.execProcess);
71
105
  for (const key of keys) {
72
106
  const p = this.execProcess[key];
73
107
  if (p && p.pid) {
74
- currentProcess.push({ name: key, pid: p.pid });
108
+ (0, tree_kill_1.default)(p.pid);
109
+ log('Kill %s server, pid: %d', helpers_js_1.chalk.blue(key), p.pid);
75
110
  }
76
111
  }
112
+ this._restorePkgMain();
77
113
  await this.sleep(500);
78
- for (const p of currentProcess) {
79
- (0, tree_kill_1.default)(p.pid);
80
- log('Kill %s server, pid: %d', helpers_js_1.chalk.blue(p.name), p.pid);
81
- }
82
114
  process_1.default.exit(0);
83
115
  }
84
- dev(options = {}) {
116
+ /**
117
+ * Dev mode — start frontend dev server + Electron process
118
+ *
119
+ * Complete flow:
120
+ * 1. Set NODE_ENV=dev
121
+ * 2. Load config, parse command names to start
122
+ * 3. If electron command is included:
123
+ * a. First bundle Electron code (via esbuild)
124
+ * b. Switch package.json main field
125
+ * c. If electron.watch=true, watch electron/ directory for changes
126
+ * → on change: debounce → re-bundle → kill old process → re-spawn
127
+ * 4. multiExec starts all commands (frontend + Electron)
128
+ */
129
+ async dev(options = {}) {
85
130
  process_1.default.env.NODE_ENV = 'dev';
86
131
  const { config, serve } = options;
87
132
  const binCfg = (0, utils_js_1.loadConfig)(config);
88
133
  const binCmd = 'dev';
89
- const binCmdConfig = (binCfg[binCmd] || {});
134
+ const binCmdConfig = binCfg.dev;
135
+ // Default to starting all commands defined in dev config when none specified
90
136
  let command = serve;
91
137
  if (!command) {
92
138
  command = Object.keys(binCmdConfig).join(',');
@@ -97,52 +143,64 @@ class ServeProcess {
97
143
  command: command || '',
98
144
  };
99
145
  const cmds = (0, helpers_js_1.formatCmds)(command || '');
100
- if (cmds.indexOf('electron') !== -1) {
146
+ if (cmds.includes('electron')) {
101
147
  const electronConfig = binCmdConfig.electron;
102
- const debugging = (0, utils_js_1.getArgumentByName)('debuger', electronConfig?.args) === 'true';
103
- this._switchPkgMain(debugging);
148
+ // Electron process needs bundled code, so bundle first before starting
149
+ await this.bundle(binCfg.build.electron);
150
+ this._switchPkgMain();
151
+ // Watch mode: monitor electron directory for changes, auto-rebuild + restart
104
152
  if (electronConfig?.watch) {
105
153
  let debounceTimer = null;
106
154
  const cmd = 'electron';
107
- const watcher = chokidar_1.default.watch([this.electronDir], { persistent: true });
155
+ const watcher = (0, chokidar_1.watch)([ELECTRON_DIR], { persistent: true });
108
156
  watcher.on('change', async (f) => {
109
157
  console.log(helpers_js_1.chalk.blue('[ee-bin] [dev] ') + `File [${helpers_js_1.chalk.cyan(f)}] has been changed`);
158
+ // Debounce: rapid successive file changes only trigger one rebuild
110
159
  if (debounceTimer) {
111
160
  clearTimeout(debounceTimer);
112
161
  }
113
162
  debounceTimer = setTimeout(async () => {
114
- console.log(helpers_js_1.chalk.blue('[ee-bin] [dev] ') + `Restart ${cmd}`);
115
- this.bundle(binCfg.build?.electron);
116
- const subProcess = this.execProcess[cmd];
117
- if (subProcess && subProcess.pid) {
118
- (0, tree_kill_1.default)(subProcess.pid, 'SIGKILL', (err) => {
119
- if (err) {
120
- console.log(helpers_js_1.chalk.red('[ee-bin] [dev] ') + `Restart failed, error: ${err}`);
121
- process_1.default.exit(-1);
122
- }
123
- delete this.execProcess[cmd];
124
- const onlyElectronOpt = {
125
- binCmd,
126
- binCmdConfig,
127
- command: cmd,
128
- };
129
- this.multiExec(onlyElectronOpt);
130
- });
163
+ try {
164
+ console.log(helpers_js_1.chalk.blue('[ee-bin] [dev] ') + `Restart ${cmd}`);
165
+ await this.bundle(binCfg.build.electron);
166
+ const subProcess = this.execProcess[cmd];
167
+ if (subProcess && subProcess.pid) {
168
+ // Kill old Electron process (SIGKILL for forced termination), then re-spawn on success
169
+ (0, tree_kill_1.default)(subProcess.pid, 'SIGKILL', (err) => {
170
+ if (err) {
171
+ console.log(helpers_js_1.chalk.red('[ee-bin] [dev] ') + `Restart failed, error: ${err}`);
172
+ process_1.default.exit(-1);
173
+ }
174
+ delete this.execProcess[cmd];
175
+ const onlyElectronOpt = {
176
+ binCmd,
177
+ binCmdConfig,
178
+ command: cmd,
179
+ };
180
+ this.multiExec(onlyElectronOpt);
181
+ });
182
+ }
183
+ }
184
+ catch (e) {
185
+ console.log(helpers_js_1.chalk.red('[ee-bin] [dev] ') + `Re-bundle failed: ${e instanceof Error ? e.message : e}`);
131
186
  }
132
187
  }, electronConfig.delay);
133
188
  });
134
189
  }
135
- this.bundle(binCfg.build?.electron);
136
190
  }
137
191
  this.multiExec(opt);
138
192
  }
139
- start(options = {}) {
193
+ /**
194
+ * Production start — directly run the Electron process (no bundling)
195
+ * Prerequisite: the project has already been built via the build command
196
+ */
197
+ async start(options = {}) {
140
198
  process_1.default.env.NODE_ENV = 'prod';
141
199
  const { config } = options;
142
200
  const binCfg = (0, utils_js_1.loadConfig)(config);
143
201
  const binCmd = 'start';
144
202
  const binCmdConfig = {
145
- start: binCfg[binCmd],
203
+ start: binCfg.start,
146
204
  };
147
205
  const opt = {
148
206
  binCmd,
@@ -151,28 +209,45 @@ class ServeProcess {
151
209
  };
152
210
  this.multiExec(opt);
153
211
  }
212
+ /** Helper: sleep for the specified number of milliseconds */
154
213
  sleep(ms) {
155
214
  return new Promise((resolve) => setTimeout(resolve, ms));
156
215
  }
157
- build(options = {}) {
216
+ /**
217
+ * Build mode — bundle Electron code + execute platform build commands
218
+ *
219
+ * Complete flow:
220
+ * 1. Set NODE_ENV=prod (or user-specified environment)
221
+ * 2. If cmds includes 'electron': bundle first → remove electron from command list → switch pkgMain
222
+ * 3. multiExec executes remaining commands (e.g. frontend, win64, mac, etc.)
223
+ * 4. After build completes, restore package.json main field
224
+ *
225
+ * The 'electron' command only triggers bundling, not an Electron process launch,
226
+ * so it's removed from the command list and not processed by multiExec
227
+ */
228
+ async build(options = {}) {
158
229
  const { config, env } = options;
159
230
  let { cmds } = options;
160
231
  process_1.default.env.NODE_ENV = env || 'prod';
161
232
  const binCfg = (0, utils_js_1.loadConfig)(config);
162
233
  const binCmd = 'build';
163
- const binCmdConfig = (binCfg[binCmd] || {});
234
+ // build.electron is BundleConfig; other keys are ExecConfig.
235
+ // electron is always removed from commands before passing to multiExec, so this cast is safe
236
+ const binCmdConfig = binCfg.build;
164
237
  if (!cmds || cmds === '') {
165
238
  const tip = helpers_js_1.chalk.bgYellow('Warning') + ' Please modify the ' + helpers_js_1.chalk.blue('build') + ' property in the bin file';
166
239
  console.log(tip);
167
240
  return;
168
241
  }
169
242
  const commands = (0, helpers_js_1.formatCmds)(cmds);
170
- if (commands.indexOf('electron') !== -1) {
171
- this.bundle(binCfg.build?.electron);
243
+ if (commands.includes('electron')) {
244
+ await this.bundle(binCfg.build.electron);
245
+ // Remove 'electron' from the command list — it only triggers bundling,
246
+ // not a subprocess launch
172
247
  const index = commands.indexOf('electron');
173
248
  commands.splice(index, 1);
174
249
  cmds = commands.join(',');
175
- this._switchPkgMain(false);
250
+ this._switchPkgMain();
176
251
  }
177
252
  const opt = {
178
253
  binCmd,
@@ -180,12 +255,15 @@ class ServeProcess {
180
255
  command: cmds || '',
181
256
  };
182
257
  this.multiExec(opt);
258
+ // Restore package.json after build completes (dev mode restores on SIGINT/SIGTERM)
259
+ this._restorePkgMain();
183
260
  }
261
+ /** Execute user-defined custom commands from the "exec" config section */
184
262
  exec(options = {}) {
185
263
  const { config, cmds } = options;
186
264
  const binCfg = (0, utils_js_1.loadConfig)(config);
187
265
  const binCmd = 'exec';
188
- const binCmdConfig = (binCfg[binCmd] || {});
266
+ const binCmdConfig = binCfg.exec;
189
267
  const opt = {
190
268
  binCmd,
191
269
  binCmdConfig,
@@ -193,6 +271,15 @@ class ServeProcess {
193
271
  };
194
272
  this.multiExec(opt);
195
273
  }
274
+ /**
275
+ * Execute multiple commands — iterate the command list and start a subprocess for each
276
+ *
277
+ * Design decisions:
278
+ * - Frontend file:// protocol is skipped in dev mode (frontend is already served via HTTP)
279
+ * - sync mode uses crossSpawnSync for blocking execution; result is not stored in execProcess (process already complete)
280
+ * - async mode uses crossSpawn for non-blocking execution; stored in execProcess for later kill
281
+ * - async processes listen for exit events; in dev mode, a message is logged when a process exits
282
+ */
196
283
  multiExec(opt) {
197
284
  const { binCmd, binCmdConfig, command } = opt;
198
285
  const commands = (0, helpers_js_1.formatCmds)(command || '');
@@ -201,36 +288,47 @@ class ServeProcess {
201
288
  if (!cfg) {
202
289
  continue;
203
290
  }
291
+ // In dev mode, skip frontend startup when protocol is 'file://'
292
+ // (frontend files are already served via HTTP dev server, no separate file:// process needed)
204
293
  if (binCmd === 'dev' && cmd === 'frontend' && cfg.protocol === 'file://') {
205
294
  continue;
206
295
  }
207
296
  console.log(helpers_js_1.chalk.blue(`[ee-bin] [${binCmd}] `) + `Run ${helpers_js_1.chalk.green(cmd)} command`);
208
297
  console.log(helpers_js_1.chalk.blue(`[ee-bin] [${binCmd}] `) + helpers_js_1.chalk.magenta('Config:'), JSON.stringify(cfg));
209
298
  const execDir = path_1.default.join(process_1.default.cwd(), cfg.directory);
210
- const execArgs = helpers_js_1.is.string(cfg.args) ? [cfg.args] : (cfg.args || []);
299
+ const execArgs = (0, utils_js_1.toArray)(cfg.args);
211
300
  const stdioOpt = cfg.stdio || 'inherit';
212
301
  if (cfg.sync) {
213
- this.execProcess[cmd] = (0, cross_spawn_1.sync)(cfg.cmd, execArgs, {
302
+ // Sync execution: blocks until the command completes, no process tracking needed
303
+ const syncResult = (0, cross_spawn_1.sync)(cfg.cmd, execArgs, {
214
304
  stdio: stdioOpt,
215
305
  cwd: execDir,
216
- maxBuffer: 1024 * 1024 * 1024,
306
+ maxBuffer: MAX_BUFFER,
217
307
  });
308
+ if (syncResult.error) {
309
+ throw new Error(`[ee-bin] [${binCmd}] Command "${cfg.cmd}" failed to spawn: ${syncResult.error.message}`);
310
+ }
311
+ if (syncResult.status !== 0 && syncResult.status !== null) {
312
+ throw new Error(`[ee-bin] [${binCmd}] Command "${cfg.cmd} ${execArgs.join(' ')}" exited with code ${syncResult.status}`);
313
+ }
218
314
  }
219
315
  else {
220
- this.execProcess[cmd] = (0, cross_spawn_1.default)(cfg.cmd, execArgs, {
316
+ // Async execution: starts a child process and tracks it for SIGINT/SIGTERM kill
317
+ const childProc = (0, cross_spawn_1.default)(cfg.cmd, execArgs, {
221
318
  stdio: stdioOpt,
222
319
  cwd: execDir,
223
- maxBuffer: 1024 * 1024 * 1024,
320
+ maxBuffer: MAX_BUFFER,
224
321
  });
225
- }
226
- console.log(helpers_js_1.chalk.blue(`[ee-bin] [${binCmd}] `) +
227
- 'The ' +
228
- helpers_js_1.chalk.green(`${cmd}`) +
229
- ` command is ${cfg.sync ? 'run completed' : 'running'}`);
230
- if (!cfg.sync) {
231
- this.execProcess[cmd].on('exit', () => {
322
+ this.execProcess[cmd] = childProc;
323
+ childProc.on('error', (err) => {
324
+ console.log(helpers_js_1.chalk.red(`[ee-bin] [${binCmd}] `) + `Command "${cmd}" failed to spawn: ${err.message}`);
325
+ delete this.execProcess[cmd];
326
+ });
327
+ childProc.on('exit', () => {
232
328
  if (binCmd === 'dev') {
233
329
  console.log(helpers_js_1.chalk.blue(`[ee-bin] [${binCmd}] `) + `The ${helpers_js_1.chalk.green(cmd)} process is exiting`);
330
+ // On Windows, Electron exit doesn't always terminate the parent process,
331
+ // so remind the user to press Ctrl+C
234
332
  if (process_1.default.platform === 'win32' && cmd === 'electron') {
235
333
  console.log(helpers_js_1.chalk.blue(`[ee-bin] [${binCmd}] `) + helpers_js_1.chalk.green('Press "CTRL+C" to exit'));
236
334
  }
@@ -239,45 +337,272 @@ class ServeProcess {
239
337
  console.log(helpers_js_1.chalk.blue(`[ee-bin] [${binCmd}] `) + `The ${helpers_js_1.chalk.green(cmd)} command has been executed and exited`);
240
338
  });
241
339
  }
340
+ console.log(helpers_js_1.chalk.blue(`[ee-bin] [${binCmd}] `) +
341
+ 'The ' +
342
+ helpers_js_1.chalk.green(`${cmd}`) +
343
+ ` command is ${cfg.sync ? 'run completed' : 'running'}`);
242
344
  }
243
345
  }
244
- bundle(bundleConfig) {
346
+ /**
347
+ * Bundle Electron main process code
348
+ *
349
+ * Two modes:
350
+ * - 'bundle': Use esbuild + bundleRegistryPlugin to bundle into a single file
351
+ * - 'copy': Directly copy the entire electron/ directory to public/electron/
352
+ *
353
+ * Clears the output directory (rm outdir) before bundling to ensure a clean build
354
+ */
355
+ async bundle(bundleConfig) {
245
356
  if (!bundleConfig)
246
357
  return;
247
- const bundleType = bundleConfig.bundleType;
248
- if (bundleType === 'copy') {
249
- const srcResource = path_1.default.join(process_1.default.cwd(), this.electronDir);
250
- const destResource = path_1.default.join(process_1.default.cwd(), this.bundleDir);
251
- (0, utils_js_1.rm)(destResource);
252
- (0, helpers_js_1.copyDirSync)(srcResource, destResource);
358
+ const cwd = process_1.default.cwd();
359
+ const outdir = path_1.default.join(cwd, BUNDLE_DIR);
360
+ // Clean output directory to ensure fresh build results
361
+ (0, utils_js_1.rm)(outdir);
362
+ if (bundleConfig.bundleType === 'copy') {
363
+ (0, helpers_js_1.copyDirSync)(path_1.default.join(cwd, ELECTRON_DIR), outdir);
253
364
  }
254
365
  else {
255
- const type = bundleConfig.type || 'javascript';
256
- const esbuildOptions = bundleConfig[type];
257
- if (esbuildOptions) {
258
- log('esbuild options:%O', esbuildOptions);
259
- (0, esbuild_1.buildSync)(esbuildOptions);
260
- }
366
+ await this._bundleWithRegistry(bundleConfig);
261
367
  }
262
368
  }
263
- _switchPkgMain(isDebugger = false) {
264
- let mainFile = 'main.js';
265
- const pkgPath = path_1.default.join(process_1.default.cwd(), this.pkgPath);
266
- const pkg = (0, utils_js_1.readJsonSync)(pkgPath);
267
- const maints = path_1.default.join(process_1.default.cwd(), this.electronDir, 'main.ts');
268
- if (fs_1.default.existsSync(maints)) {
269
- mainFile = 'main.ts';
369
+ /**
370
+ * Resolve the esbuild options shared by the main bundle and the per-file copy transpile.
371
+ *
372
+ * Both the bundled main.js and the separately-transpiled jobs/copy files must use identical
373
+ * compilation settings, otherwise main process code and job code would diverge (e.g. main.js
374
+ * minified but jobs not, or different module format / target / define). This method centralizes
375
+ * everything that should be consistent; callers add only the mode-specific keys (bundle,
376
+ * entryPoints/outfile, externals, plugins, banner, logLevel).
377
+ *
378
+ * sourcemap auto mode: dev → inline (debuggable), prod → off (smaller output).
379
+ */
380
+ _resolveBaseBuildOptions(bundleConfig) {
381
+ const isDev = process_1.default.env.NODE_ENV === 'dev' || process_1.default.env.NODE_ENV === 'local';
382
+ let sourcemap;
383
+ if (bundleConfig.sourcemap === 'inline' || bundleConfig.sourcemap === true) {
384
+ sourcemap = 'inline';
270
385
  }
271
- if (isDebugger && mainFile === 'main.js') {
272
- pkg.main = this.electronDir + '/' + mainFile;
273
- (0, utils_js_1.writeJsonSync)(pkgPath, pkg);
386
+ else if (bundleConfig.sourcemap === 'external') {
387
+ sourcemap = true;
274
388
  }
275
389
  else {
276
- const bundleMainPath = this.bundleDir + '/' + 'main.js';
277
- if (pkg.main !== bundleMainPath) {
278
- pkg.main = bundleMainPath;
279
- (0, utils_js_1.writeJsonSync)(pkgPath, pkg);
390
+ sourcemap = isDev ? 'inline' : false;
391
+ }
392
+ const format = bundleConfig.format || 'cjs';
393
+ return {
394
+ platform: 'node',
395
+ target: 'node20',
396
+ format,
397
+ sourcemap,
398
+ minify: bundleConfig.minify ?? false,
399
+ keepNames: bundleConfig.keepNames ?? false,
400
+ ...(bundleConfig.drop ? { drop: bundleConfig.drop } : {}),
401
+ ...(bundleConfig.legalComments ? { legalComments: bundleConfig.legalComments } : {}),
402
+ define: {
403
+ ...(bundleConfig.define || {}),
404
+ },
405
+ };
406
+ }
407
+ /**
408
+ * Bundle Electron code using esbuild + registry plugin
409
+ *
410
+ * esbuild configuration strategy:
411
+ * - entryPoints: Virtual module 'app:bundle-entry' (generated by the plugin)
412
+ * - format: Default cjs (recommended for Electron), optional esm
413
+ * - sourcemap: Auto-inferred (dev→inline, prod→off), user can override
414
+ * - external: Framework externals (ee-core/electron/better-sqlite3 etc.) + user-defined
415
+ * - banner: Injects process.env.EE_BUNDLED = "true" so ee-core detects bundle mode
416
+ * - packages: 'external' tells esbuild to automatically exclude all node_modules packages
417
+ *
418
+ * Post-bundle steps:
419
+ * 1. Rename output file (app_bundle-entry.js → main.js)
420
+ * 2. Copy non-bundlable files (jobs directory, preload/bridge.js, user-defined copy targets)
421
+ */
422
+ async _bundleWithRegistry(bundleConfig) {
423
+ const cwd = process_1.default.cwd();
424
+ const controllerDir = path_1.default.join(cwd, ELECTRON_DIR, 'controller');
425
+ const configDir = path_1.default.join(cwd, ELECTRON_DIR, 'config');
426
+ const mainJsPath = path_1.default.join(cwd, ELECTRON_DIR, 'main.js');
427
+ const mainTsPath = path_1.default.join(cwd, ELECTRON_DIR, 'main.ts');
428
+ // Detect TypeScript entry (affects esbuild resolution and output format inference)
429
+ const isTypeScript = fs_1.default.existsSync(mainTsPath);
430
+ const entryMain = isTypeScript ? mainTsPath : mainJsPath;
431
+ const outdir = path_1.default.join(cwd, BUNDLE_DIR);
432
+ const outfile = path_1.default.join(outdir, 'main.js');
433
+ // Framework internal externals: these packages must be loaded from node_modules at runtime,
434
+ // not bundled into main.js. Reasons:
435
+ // - ee-core: child_process.fork() needs its entry point as a real file on disk
436
+ // - electron/better-sqlite3: native modules that cannot be bundled by esbuild
437
+ // - pino-roll/pino-pretty: rely on fs operations that don't work after bundling
438
+ const frameworkExternal = [
439
+ 'ee-core',
440
+ 'ee-bin',
441
+ 'electron',
442
+ 'better-sqlite3',
443
+ 'proxy-agent',
444
+ 'pino-roll',
445
+ 'pino-pretty',
446
+ ];
447
+ const userExternal = bundleConfig.external || [];
448
+ const plugin = (0, bundle_registry_plugin_js_1.bundleRegistryPlugin)(controllerDir, entryMain, configDir);
449
+ const options = {
450
+ // Shared compilation settings (format/target/sourcemap/minify/keepNames/drop/legalComments/define)
451
+ // — kept identical to the per-file copy transpile so main.js and jobs/copy code never diverge
452
+ ...this._resolveBaseBuildOptions(bundleConfig),
453
+ // Mode-specific: bundle everything reachable from the virtual entry into a single file
454
+ entryPoints: ['app:bundle-entry'],
455
+ bundle: true,
456
+ // packages: 'external' tells esbuild to treat all npm packages as external
457
+ // (already refined by the explicit external list above)
458
+ packages: 'external',
459
+ outdir,
460
+ external: [
461
+ ...frameworkExternal,
462
+ ...userExternal,
463
+ ],
464
+ // Banner injects EE_BUNDLED marker: ee-core checks this value to use the registry
465
+ // instead of filesystem scanning when in bundle mode
466
+ banner: {
467
+ js: 'process.env.EE_BUNDLED = "true";',
468
+ },
469
+ plugins: [plugin],
470
+ logLevel: 'info',
471
+ };
472
+ log('_bundleWithRegistry options:%O', options);
473
+ await (0, esbuild_1.build)(options);
474
+ // esbuild replaces ':' in virtual module name 'app:bundle-entry' with '_',
475
+ // so the output file is named 'app_bundle-entry.js' — rename it to 'main.js'
476
+ const bundleEntryFile = path_1.default.join(outdir, 'app_bundle-entry.js');
477
+ if (fs_1.default.existsSync(bundleEntryFile)) {
478
+ fs_1.default.renameSync(bundleEntryFile, path_1.default.join(outdir, 'main.js'));
479
+ }
480
+ // Also rename the sourcemap file if it exists
481
+ const bundleEntryMap = path_1.default.join(outdir, 'app_bundle-entry.js.map');
482
+ if (fs_1.default.existsSync(bundleEntryMap)) {
483
+ fs_1.default.renameSync(bundleEntryMap, path_1.default.join(outdir, 'main.js.map'));
484
+ }
485
+ // Copy non-bundlable files (child_process.fork and BrowserWindow preload need separate files)
486
+ await this._copyUnbundledFiles(cwd, outdir, bundleConfig);
487
+ console.log(helpers_js_1.chalk.blue('[ee-bin] ') + `Bundle output: ${outfile}`);
488
+ }
489
+ /**
490
+ * Copy a directory or single file from electron/ to the bundle output WITH per-file transpilation.
491
+ *
492
+ * Script files (.ts/.js/.mts/.cts/.tsx/.jsx) are compiled to Node-loadable .js using
493
+ * esbuild with bundle:false, so their imports stay as runtime require()/import calls:
494
+ * - relative imports (./foo) resolve to the sibling transpiled .js
495
+ * - ee-core/* and other packages resolve from node_modules at runtime
496
+ * Non-script files (e.g. .json) are copied verbatim. Directory structure is preserved.
497
+ *
498
+ * @param src Absolute path to a source directory or file
499
+ * @param dest Absolute path to the destination directory (for a dir src) or file (for a file src)
500
+ * @param baseOptions Shared esbuild options from _resolveBaseBuildOptions — same compilation
501
+ * settings (format/target/minify/define/...) as the main bundle, so copied
502
+ * code stays consistent with main.js. bundle:false is forced for per-file output.
503
+ */
504
+ async _transpileDir(src, dest, baseOptions) {
505
+ if (!fs_1.default.existsSync(src))
506
+ return;
507
+ const scriptExts = new Set(['.ts', '.js', '.mts', '.cts', '.tsx', '.jsx']);
508
+ const transpileFile = async (srcFile, destFile) => {
509
+ const ext = path_1.default.extname(srcFile);
510
+ if (!scriptExts.has(ext)) {
511
+ // Non-script asset (e.g. .json): copy verbatim
512
+ fs_1.default.mkdirSync(path_1.default.dirname(destFile), { recursive: true });
513
+ fs_1.default.copyFileSync(srcFile, destFile);
514
+ return;
280
515
  }
516
+ // Output as sibling .js, preserving the directory structure. Per-file transpile (bundle:false)
517
+ // so imports stay as runtime require()/import calls resolved on disk.
518
+ await (0, esbuild_1.build)({
519
+ ...baseOptions,
520
+ entryPoints: [srcFile],
521
+ outfile: destFile.slice(0, -ext.length) + '.js',
522
+ bundle: false,
523
+ logLevel: 'silent',
524
+ });
525
+ };
526
+ // Single file: dest is the target file path
527
+ if (fs_1.default.statSync(src).isFile()) {
528
+ await transpileFile(src, dest);
529
+ return;
530
+ }
531
+ // Directory: walk every file, preserving the relative structure under dest
532
+ const entries = (0, globby_1.globbySync)('**/*', { cwd: src, onlyFiles: true });
533
+ for (const rel of entries) {
534
+ await transpileFile(path_1.default.join(src, rel), path_1.default.join(dest, rel));
535
+ }
536
+ }
537
+ /**
538
+ * Copy non-bundlable files — two-tier strategy
539
+ *
540
+ * 1. preload/bridge.js (BrowserWindow preload script must be a separate file, loaded directly by Electron)
541
+ * 2. Copy targets: framework defaults (jobs/) + user-defined (bundleConfig.copy), all handled
542
+ * per-file by _transpileDir — script files transpiled to CJS .js (so Node's require()/
543
+ * child_process.fork() can load them), other files copied verbatim, structure preserved
544
+ */
545
+ async _copyUnbundledFiles(cwd, outdir, bundleConfig) {
546
+ // Shared esbuild options (format/target/minify/define/...) so unbundled output stays
547
+ // consistent with main.js. Per-file transpile (bundle:false) is forced inside _transpileDir.
548
+ const baseOptions = this._resolveBaseBuildOptions(bundleConfig);
549
+ // preload/bridge.*: BrowserWindow's preload script is loaded directly from disk by Electron.
550
+ // It cannot be bundled into main.js (bundled path would be wrong, and Electron requires
551
+ // preload scripts to be separate files). The source may be .ts/.js/.mts/... — resolve whichever
552
+ // exists and transpile it to bridge.js (a plain copy would break for TypeScript sources).
553
+ const bridgeMatches = (0, globby_1.globbySync)('preload/bridge.{ts,js,mts,cts,tsx,jsx}', { cwd: path_1.default.join(cwd, ELECTRON_DIR) });
554
+ if (bridgeMatches.length > 0) {
555
+ const bridgeRel = bridgeMatches[0];
556
+ const bridgeSrc = path_1.default.join(cwd, ELECTRON_DIR, bridgeRel);
557
+ // dest mirrors the source basename so _transpileDir derives the sibling .js correctly
558
+ const bridgeDest = path_1.default.join(outdir, bridgeRel);
559
+ await this._transpileDir(bridgeSrc, bridgeDest, baseOptions);
560
+ }
561
+ // Copy targets kept out of main.js. jobs/ is a framework default (its files run in forked
562
+ // child processes loaded by ee-core's require()-based loader, which cannot execute .ts), then
563
+ // user-defined entries from bundleConfig.copy. De-duplicated so an explicit 'jobs' won't run twice.
564
+ // Script files are transpiled with the SAME esbuild options as main.js (format/target/minify/
565
+ // define/...), other files (assets, .json) copied verbatim — all handled per-file by _transpileDir.
566
+ const copyTargets = [...new Set(['jobs', ...(bundleConfig.copy || [])])];
567
+ for (const target of copyTargets) {
568
+ const src = path_1.default.join(cwd, ELECTRON_DIR, target);
569
+ const dest = path_1.default.join(outdir, target);
570
+ if (!fs_1.default.existsSync(src))
571
+ continue;
572
+ await this._transpileDir(src, dest, baseOptions);
573
+ console.log(helpers_js_1.chalk.blue('[ee-bin] ') + `Copied: ${dest}`);
574
+ }
575
+ }
576
+ /**
577
+ * Switch package.json main field
578
+ *
579
+ * After bundling, Electron needs to point to ./public/electron/main.js instead of
580
+ * ./electron/main.js, because the bundle output is in the public/electron/ directory.
581
+ * The original value is saved before switching so it can be restored later.
582
+ */
583
+ _switchPkgMain() {
584
+ const pkgPath = path_1.default.join(process_1.default.cwd(), PKG_PATH);
585
+ const pkg = (0, utils_js_1.readJsonSync)(pkgPath);
586
+ const bundleMainPath = BUNDLE_DIR + '/main.js';
587
+ if (pkg.main !== bundleMainPath) {
588
+ this.originalPkgMain = pkg.main;
589
+ pkg.main = bundleMainPath;
590
+ (0, utils_js_1.writeJsonSync)(pkgPath, pkg);
591
+ }
592
+ }
593
+ /**
594
+ * Restore package.json main field
595
+ *
596
+ * Restores the original main value after build completes or on SIGINT/SIGTERM,
597
+ * preventing package.json from being permanently modified (which would break dev mode)
598
+ */
599
+ _restorePkgMain() {
600
+ if (this.originalPkgMain !== undefined) {
601
+ const pkgPath = path_1.default.join(process_1.default.cwd(), PKG_PATH);
602
+ const pkg = (0, utils_js_1.readJsonSync)(pkgPath);
603
+ pkg.main = this.originalPkgMain;
604
+ (0, utils_js_1.writeJsonSync)(pkgPath, pkg);
605
+ this.originalPkgMain = undefined;
281
606
  }
282
607
  }
283
608
  }