electrobun 1.18.1 → 1.18.4-beta.10

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 (64) hide show
  1. package/README.md +37 -14
  2. package/dash.config.ts +28 -0
  3. package/dist/api/browser/global.d.ts +6 -0
  4. package/dist/api/browser/index.ts +162 -44
  5. package/dist/api/{bun → config}/ElectrobunConfig.ts +109 -40
  6. package/dist/api/config/validate.test.ts +31 -0
  7. package/dist/api/config/validate.ts +19 -0
  8. package/dist/api/preload/.generated/compiled.ts +8 -0
  9. package/dist/api/{bun/preload → preload}/build.ts +6 -6
  10. package/dist/api/{bun/preload → preload}/encryption.ts +11 -5
  11. package/dist/api/{bun/preload → preload}/globals.d.ts +8 -0
  12. package/dist/api/{bun/preload → preload}/index.ts +18 -8
  13. package/dist/api/{bun/preload → preload}/webviewTag.ts +32 -3
  14. package/dist/api/{bun/preload → preload}/wgpuTag.ts +33 -2
  15. package/dist/api/{bun → sdks/bun}/__tests__/ffi-contract.test.ts +6 -9
  16. package/dist/api/{bun → sdks/bun}/core/BrowserView.ts +96 -55
  17. package/dist/api/{bun → sdks/bun}/core/BrowserWindow.ts +20 -30
  18. package/dist/api/{bun → sdks/bun}/core/BuildConfig.ts +32 -5
  19. package/dist/api/{bun → sdks/bun}/core/GpuWindow.ts +20 -7
  20. package/dist/api/sdks/bun/core/Socket.ts +22 -0
  21. package/dist/api/{bun → sdks/bun}/core/Tray.ts +33 -32
  22. package/dist/api/{bun → sdks/bun}/core/Updater.ts +10 -10
  23. package/dist/api/{bun → sdks/bun}/core/Utils.ts +3 -5
  24. package/dist/api/{bun → sdks/bun}/core/WGPUView.ts +43 -13
  25. package/dist/api/{bun → sdks/bun}/index.ts +39 -10
  26. package/dist/api/{bun → sdks/bun}/proc/native.ts +942 -746
  27. package/dist/api/{bun → sdks/bun}/webGPU.ts +1 -1
  28. package/dist/api/{bun → sdks/bun}/webgpuAdapter.ts +40 -33
  29. package/dist/api/shared/build-dependencies.test.ts +48 -0
  30. package/dist/api/shared/build-dependencies.ts +46 -0
  31. package/dist/api/shared/go-version.ts +3 -0
  32. package/dist/api/shared/odin-version.ts +6 -0
  33. package/dist/api/shared/rust-version.ts +3 -0
  34. package/dist/go-sdk/callbacks.go +260 -0
  35. package/dist/go-sdk/electrobun.go +2021 -0
  36. package/dist/main.js +35 -28
  37. package/dist/odin-sdk/electrobun/electrobun.odin +2337 -0
  38. package/dist/preload-full.js +948 -0
  39. package/dist/preload-sandboxed.js +111 -0
  40. package/dist/rust-sdk/electrobun.rs +2396 -0
  41. package/dist/zig-sdk/electrobun.zig +2005 -0
  42. package/package.json +5 -29
  43. package/src/cli/index.ts +868 -654
  44. package/bin/electrobun.cjs +0 -169
  45. package/dist/api/bun/core/Socket.ts +0 -205
  46. package/dist/api/bun/core/windowIds.ts +0 -5
  47. package/dist/api/bun/preload/.generated/compiled.ts +0 -8
  48. package/dist/api/shared/bun-version.ts +0 -3
  49. /package/dist/api/{bun/preload → preload}/dragRegions.ts +0 -0
  50. /package/dist/api/{bun/preload → preload}/events.ts +0 -0
  51. /package/dist/api/{bun/preload → preload}/index-sandboxed.ts +0 -0
  52. /package/dist/api/{bun/preload → preload}/internalRpc.ts +0 -0
  53. /package/dist/api/{bun/preload → preload}/overlaySync.ts +0 -0
  54. /package/dist/api/{bun → sdks/bun}/core/ApplicationMenu.ts +0 -0
  55. /package/dist/api/{bun → sdks/bun}/core/ContextMenu.ts +0 -0
  56. /package/dist/api/{bun → sdks/bun}/core/Paths.ts +0 -0
  57. /package/dist/api/{bun → sdks/bun}/core/menuRoles.ts +0 -0
  58. /package/dist/api/{bun → sdks/bun}/events/ApplicationEvents.ts +0 -0
  59. /package/dist/api/{bun → sdks/bun}/events/event.ts +0 -0
  60. /package/dist/api/{bun → sdks/bun}/events/eventEmitter.ts +0 -0
  61. /package/dist/api/{bun → sdks/bun}/events/trayEvents.ts +0 -0
  62. /package/dist/api/{bun → sdks/bun}/events/webviewEvents.ts +0 -0
  63. /package/dist/api/{bun → sdks/bun}/events/windowEvents.ts +0 -0
  64. /package/dist/api/{bun → sdks/bun}/proc/linux.md +0 -0
package/src/cli/index.ts CHANGED
@@ -19,8 +19,11 @@ import { execSync } from "child_process";
19
19
  import * as readline from "readline";
20
20
  import { OS, ARCH } from "../shared/platform";
21
21
  import { DEFAULT_CEF_VERSION_STRING } from "../shared/cef-version";
22
- import { BUN_VERSION } from "../shared/bun-version";
22
+ import { RUST_VERSION } from "../shared/rust-version";
23
+ import { GO_VERSION } from "../shared/go-version";
24
+ import { ODIN_VERSION } from "../shared/odin-version";
23
25
  import { ELECTROBUN_VERSION } from "../shared/electrobun-version";
26
+ import { assertNoLegacyBunMainProcessConfig } from "../config/validate";
24
27
  import {
25
28
  getAppFileName,
26
29
  getBundleFileName,
@@ -74,6 +77,8 @@ const indexOfElectrobun = process.argv.findIndex((arg) =>
74
77
  );
75
78
  const commandArg = process.argv[indexOfElectrobun + 1] || "build";
76
79
 
80
+ type MainProcess = "cottontail" | "zig" | "rust" | "go" | "odin";
81
+
77
82
  // Walk up from projectRoot to find electrobun in node_modules (supports hoisted monorepo layouts)
78
83
  function resolveElectrobunDir(): string {
79
84
  let dir = projectRoot;
@@ -108,9 +113,12 @@ function getPlatformPaths(
108
113
 
109
114
  return {
110
115
  // Platform-specific binaries (from dist-OS-ARCH/)
111
- BUN_BINARY: join(platformDistDir, "bun") + binExt,
116
+ COTTONTAIL_BINARY: join(platformDistDir, "cottontail") + binExt,
112
117
  LAUNCHER_DEV: join(platformDistDir, "electrobun") + binExt,
113
118
  LAUNCHER_RELEASE: join(platformDistDir, "launcher") + binExt,
119
+ CORE_MACOS: join(platformDistDir, "libElectrobunCore.dylib"),
120
+ CORE_WIN: join(platformDistDir, "ElectrobunCore.dll"),
121
+ CORE_LINUX: join(platformDistDir, "libElectrobunCore.so"),
114
122
  NATIVE_WRAPPER_MACOS: join(platformDistDir, "libNativeWrapper.dylib"),
115
123
  NATIVE_WRAPPER_WIN: join(platformDistDir, "libNativeWrapper.dll"),
116
124
  NATIVE_WRAPPER_LINUX: join(platformDistDir, "libNativeWrapper.so"),
@@ -134,6 +142,8 @@ function getPlatformPaths(
134
142
  // These work with existing package.json and development workflow
135
143
  MAIN_JS: join(sharedDistDir, "main.js"),
136
144
  API_DIR: join(sharedDistDir, "api"),
145
+ PRELOAD_FULL_JS: join(sharedDistDir, "preload-full.js"),
146
+ PRELOAD_SANDBOXED_JS: join(sharedDistDir, "preload-sandboxed.js"),
137
147
  };
138
148
  }
139
149
 
@@ -141,6 +151,452 @@ function getPlatformPaths(
141
151
  // @ts-expect-error - reserved for future use
142
152
  const _PATHS = getPlatformPaths(OS, ARCH);
143
153
 
154
+ function getVendoredZigBinaryPath(): string {
155
+ return join(
156
+ ELECTROBUN_DEP_PATH,
157
+ "vendors",
158
+ "zig",
159
+ OS === "win" ? "zig.exe" : "zig",
160
+ );
161
+ }
162
+
163
+ function getZigTarget(
164
+ targetOS: "macos" | "win" | "linux",
165
+ targetArch: "arm64" | "x64",
166
+ ): string {
167
+ if (targetOS === "win") {
168
+ return "x86_64-windows";
169
+ }
170
+ if (targetOS === "linux") {
171
+ return targetArch === "arm64" ? "aarch64-linux" : "x86_64-linux";
172
+ }
173
+ return targetArch === "arm64" ? "aarch64-macos" : "x86_64-macos";
174
+ }
175
+
176
+ function getRustTarget(
177
+ targetOS: "macos" | "win" | "linux",
178
+ targetArch: "arm64" | "x64",
179
+ ): string {
180
+ if (targetOS === "win") {
181
+ return targetArch === "arm64"
182
+ ? "aarch64-pc-windows-msvc"
183
+ : "x86_64-pc-windows-msvc";
184
+ }
185
+ if (targetOS === "linux") {
186
+ return targetArch === "arm64"
187
+ ? "aarch64-unknown-linux-gnu"
188
+ : "x86_64-unknown-linux-gnu";
189
+ }
190
+ return targetArch === "arm64"
191
+ ? "aarch64-apple-darwin"
192
+ : "x86_64-apple-darwin";
193
+ }
194
+
195
+ function getVendoredRustBinaryPath(): string {
196
+ return join(
197
+ ELECTROBUN_DEP_PATH,
198
+ "vendors",
199
+ "rust",
200
+ "bin",
201
+ OS === "win" ? "rustc.exe" : "rustc",
202
+ );
203
+ }
204
+
205
+ function getGoTarget(
206
+ targetOS: "macos" | "win" | "linux",
207
+ targetArch: "arm64" | "x64",
208
+ ): { goOS: string; goArch: string } {
209
+ return {
210
+ goOS:
211
+ targetOS === "macos" ? "darwin" : targetOS === "win" ? "windows" : "linux",
212
+ goArch: targetArch === "arm64" ? "arm64" : "amd64",
213
+ };
214
+ }
215
+
216
+ function getVendoredGoBinaryPath(): string {
217
+ return join(
218
+ ELECTROBUN_DEP_PATH,
219
+ "vendors",
220
+ "go",
221
+ "bin",
222
+ OS === "win" ? "go.exe" : "go",
223
+ );
224
+ }
225
+
226
+ function getVendoredOdinBinaryPath(): string {
227
+ return join(
228
+ ELECTROBUN_DEP_PATH,
229
+ "vendors",
230
+ "odin",
231
+ OS === "win" ? "odin.exe" : "odin",
232
+ );
233
+ }
234
+
235
+ function getCEFHelperNames(): string[] {
236
+ const baseName = "main";
237
+ return [
238
+ `${baseName} Helper`,
239
+ `${baseName} Helper (Alerts)`,
240
+ `${baseName} Helper (GPU)`,
241
+ `${baseName} Helper (Plugin)`,
242
+ `${baseName} Helper (Renderer)`,
243
+ ];
244
+ }
245
+
246
+ async function buildZigMainExecutable(options: {
247
+ entrypoint: string;
248
+ buildFolder: string;
249
+ targetOS: "macos" | "win" | "linux";
250
+ targetArch: "arm64" | "x64";
251
+ buildEnvironment: "dev" | "canary" | "stable";
252
+ }) {
253
+ const zigBinary = getVendoredZigBinaryPath();
254
+ if (!existsSync(zigBinary)) {
255
+ throw new Error(
256
+ `Vendored Zig compiler not found at ${zigBinary}. Rebuild electrobun/package so vendors/zig is available.`,
257
+ );
258
+ }
259
+
260
+ const zigSdkPath = join(ELECTROBUN_DEP_PATH, "dist", "zig-sdk", "electrobun.zig");
261
+ if (!existsSync(zigSdkPath)) {
262
+ throw new Error(`Electrobun Zig SDK not found at ${zigSdkPath}`);
263
+ }
264
+
265
+ const binExt = options.targetOS === "win" ? ".exe" : "";
266
+ const tempBuildDir = join(
267
+ options.buildFolder,
268
+ ".electrobun-zig-main",
269
+ `${options.targetOS}-${options.targetArch}`,
270
+ );
271
+ const relativeZigSdkPath = path.relative(tempBuildDir, zigSdkPath) || ".";
272
+ const relativeEntrypointPath = path.relative(tempBuildDir, options.entrypoint) || ".";
273
+ const zigOutBin = join(tempBuildDir, "zig-out", "bin", "main" + binExt);
274
+ const buildScriptPath = join(tempBuildDir, "build.zig");
275
+ mkdirSync(tempBuildDir, { recursive: true });
276
+
277
+ const buildScript = `const std = @import("std");
278
+
279
+ pub fn build(b: *std.Build) void {
280
+ const target = b.standardTargetOptions(.{});
281
+ const optimize = b.standardOptimizeOption(.{});
282
+
283
+ const electrobun = b.createModule(.{
284
+ .root_source_file = b.path(${JSON.stringify(relativeZigSdkPath)}),
285
+ });
286
+
287
+ const exe = b.addExecutable(.{
288
+ .name = "main",
289
+ .root_source_file = b.path(${JSON.stringify(relativeEntrypointPath)}),
290
+ .target = target,
291
+ .optimize = optimize,
292
+ });
293
+
294
+ exe.root_module.addImport("electrobun", electrobun);
295
+ exe.linkLibC();
296
+ b.installArtifact(exe);
297
+ }
298
+ `;
299
+ writeFileSync(buildScriptPath, buildScript, "utf8");
300
+
301
+ const zigArgs = [
302
+ "build",
303
+ `-Dtarget=${getZigTarget(options.targetOS, options.targetArch)}`,
304
+ ];
305
+
306
+ if (options.targetOS === "win") {
307
+ zigArgs.push("-Dcpu=baseline");
308
+ }
309
+
310
+ if (options.buildEnvironment !== "dev") {
311
+ zigArgs.push("-Doptimize=ReleaseSmall");
312
+ }
313
+
314
+ const result = Bun.spawnSync([zigBinary, ...zigArgs], {
315
+ cwd: tempBuildDir,
316
+ stdio: ["ignore", "pipe", "pipe"],
317
+ });
318
+
319
+ if (result.exitCode !== 0) {
320
+ const stdout = result.stdout ? new TextDecoder().decode(result.stdout) : "";
321
+ const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : "";
322
+ if (stdout.trim()) {
323
+ console.error(stdout);
324
+ }
325
+ if (stderr.trim()) {
326
+ console.error(stderr);
327
+ }
328
+ throw new Error("Build failed: zig main process compilation failed");
329
+ }
330
+
331
+ if (!existsSync(zigOutBin)) {
332
+ throw new Error(`Zig main process binary was not produced at ${zigOutBin}`);
333
+ }
334
+
335
+ return zigOutBin;
336
+ }
337
+
338
+ async function buildRustMainExecutable(options: {
339
+ entrypoint: string;
340
+ buildFolder: string;
341
+ targetOS: "macos" | "win" | "linux";
342
+ targetArch: "arm64" | "x64";
343
+ buildEnvironment: "dev" | "canary" | "stable";
344
+ }) {
345
+ const rustBinary = getVendoredRustBinaryPath();
346
+ if (!existsSync(rustBinary)) {
347
+ throw new Error(
348
+ `Vendored Rust compiler not found at ${rustBinary}. Rebuild electrobun/package so vendors/rust is available.`,
349
+ );
350
+ }
351
+
352
+ const rustSdkPath = join(ELECTROBUN_DEP_PATH, "dist", "rust-sdk", "electrobun.rs");
353
+ if (!existsSync(rustSdkPath)) {
354
+ throw new Error(`Electrobun Rust SDK not found at ${rustSdkPath}`);
355
+ }
356
+
357
+ const targetTriple = getRustTarget(options.targetOS, options.targetArch);
358
+ const hostTriple = getRustTarget(OS, ARCH);
359
+ if (targetTriple !== hostTriple) {
360
+ throw new Error(
361
+ `Rust main process cross-compilation is not supported yet. Vendored rustc is ${hostTriple}, but the build target is ${targetTriple}.`,
362
+ );
363
+ }
364
+
365
+ const binExt = options.targetOS === "win" ? ".exe" : "";
366
+ const tempBuildDir = join(
367
+ options.buildFolder,
368
+ ".electrobun-rust-main",
369
+ `${options.targetOS}-${options.targetArch}`,
370
+ );
371
+ const rustOutBin = join(tempBuildDir, "main" + binExt);
372
+ const wrapperPath = join(tempBuildDir, "main.rs");
373
+ mkdirSync(tempBuildDir, { recursive: true });
374
+
375
+ const wrapperSource = `#[path = ${JSON.stringify(rustSdkPath)}]
376
+ pub mod electrobun;
377
+
378
+ #[path = ${JSON.stringify(options.entrypoint)}]
379
+ mod user_main;
380
+
381
+ fn main() {
382
+ user_main::main();
383
+ }
384
+ `;
385
+ writeFileSync(wrapperPath, wrapperSource, "utf8");
386
+
387
+ const rustArgs = [
388
+ "--edition=2021",
389
+ wrapperPath,
390
+ "--target",
391
+ targetTriple,
392
+ "-o",
393
+ rustOutBin,
394
+ ];
395
+
396
+ if (options.buildEnvironment === "dev") {
397
+ rustArgs.push("-C", "opt-level=2", "-C", "debuginfo=0");
398
+ } else {
399
+ rustArgs.push("-C", "opt-level=z", "-C", "strip=symbols");
400
+ }
401
+
402
+ const result = Bun.spawnSync([rustBinary, ...rustArgs], {
403
+ cwd: tempBuildDir,
404
+ stdio: ["ignore", "pipe", "pipe"],
405
+ });
406
+
407
+ if (result.exitCode !== 0) {
408
+ const stdout = result.stdout ? new TextDecoder().decode(result.stdout) : "";
409
+ const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : "";
410
+ if (stdout.trim()) {
411
+ console.error(stdout);
412
+ }
413
+ if (stderr.trim()) {
414
+ console.error(stderr);
415
+ }
416
+ throw new Error("Build failed: rust main process compilation failed");
417
+ }
418
+
419
+ if (!existsSync(rustOutBin)) {
420
+ throw new Error(`Rust main process binary was not produced at ${rustOutBin}`);
421
+ }
422
+
423
+ return rustOutBin;
424
+ }
425
+
426
+ async function buildGoMainExecutable(options: {
427
+ entrypoint: string;
428
+ buildFolder: string;
429
+ targetOS: "macos" | "win" | "linux";
430
+ targetArch: "arm64" | "x64";
431
+ buildEnvironment: "dev" | "canary" | "stable";
432
+ }) {
433
+ const goBinary = getVendoredGoBinaryPath();
434
+ if (!existsSync(goBinary)) {
435
+ throw new Error(
436
+ `Vendored Go compiler not found at ${goBinary}. Rebuild electrobun/package so vendors/go is available.`,
437
+ );
438
+ }
439
+
440
+ const goSdkPath = join(ELECTROBUN_DEP_PATH, "dist", "go-sdk");
441
+ if (!existsSync(goSdkPath)) {
442
+ throw new Error(`Electrobun Go SDK not found at ${goSdkPath}`);
443
+ }
444
+
445
+ const target = getGoTarget(options.targetOS, options.targetArch);
446
+ const host = getGoTarget(OS, ARCH);
447
+ if (target.goOS !== host.goOS || target.goArch !== host.goArch) {
448
+ throw new Error(
449
+ `Go main process cross-compilation is not supported yet. Vendored Go is ${host.goOS}-${host.goArch}, but the build target is ${target.goOS}-${target.goArch}.`,
450
+ );
451
+ }
452
+
453
+ const binExt = options.targetOS === "win" ? ".exe" : "";
454
+ const tempBuildDir = join(
455
+ options.buildFolder,
456
+ ".electrobun-go-main",
457
+ `${options.targetOS}-${options.targetArch}`,
458
+ );
459
+ const goOutBin = join(tempBuildDir, "main" + binExt);
460
+ const goPath = join(tempBuildDir, "gopath");
461
+ const goSrcPath = join(goPath, "src");
462
+ const sdkDestPath = join(goSrcPath, "electrobun");
463
+ const appDestPath = join(goSrcPath, "electrobun-app");
464
+ const entrypointDir = statSync(options.entrypoint).isDirectory()
465
+ ? options.entrypoint
466
+ : dirname(options.entrypoint);
467
+
468
+ rmSync(tempBuildDir, { recursive: true, force: true });
469
+ mkdirSync(goSrcPath, { recursive: true });
470
+ cpSync(goSdkPath, sdkDestPath, { recursive: true, dereference: true });
471
+ cpSync(entrypointDir, appDestPath, { recursive: true, dereference: true });
472
+
473
+ const goArgs = ["build", "-o", goOutBin];
474
+ if (options.buildEnvironment !== "dev") {
475
+ goArgs.push("-ldflags=-s -w");
476
+ }
477
+ goArgs.push("electrobun-app");
478
+
479
+ const result = Bun.spawnSync([goBinary, ...goArgs], {
480
+ cwd: tempBuildDir,
481
+ env: {
482
+ ...process.env,
483
+ CGO_ENABLED: "1",
484
+ GO111MODULE: "off",
485
+ GOARCH: target.goArch,
486
+ GOOS: target.goOS,
487
+ GOPATH: goPath,
488
+ GOROOT: join(ELECTROBUN_DEP_PATH, "vendors", "go"),
489
+ GOTOOLCHAIN: "local",
490
+ ...(options.targetOS === "win"
491
+ ? { CC: `${getVendoredZigBinaryPath()} cc` }
492
+ : {}),
493
+ },
494
+ stdio: ["ignore", "pipe", "pipe"],
495
+ });
496
+
497
+ if (result.exitCode !== 0) {
498
+ const stdout = result.stdout ? new TextDecoder().decode(result.stdout) : "";
499
+ const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : "";
500
+ if (stdout.trim()) {
501
+ console.error(stdout);
502
+ }
503
+ if (stderr.trim()) {
504
+ console.error(stderr);
505
+ }
506
+ throw new Error("Build failed: go main process compilation failed");
507
+ }
508
+
509
+ if (!existsSync(goOutBin)) {
510
+ throw new Error(`Go main process binary was not produced at ${goOutBin}`);
511
+ }
512
+
513
+ return goOutBin;
514
+ }
515
+
516
+ async function buildOdinMainExecutable(options: {
517
+ entrypoint: string;
518
+ buildFolder: string;
519
+ targetOS: "macos" | "win" | "linux";
520
+ targetArch: "arm64" | "x64";
521
+ buildEnvironment: "dev" | "canary" | "stable";
522
+ }) {
523
+ const odinBinary = getVendoredOdinBinaryPath();
524
+ if (!existsSync(odinBinary)) {
525
+ throw new Error(
526
+ `Vendored Odin compiler not found at ${odinBinary}. Rebuild electrobun/package so vendors/odin is available.`,
527
+ );
528
+ }
529
+
530
+ // dist/odin-sdk is exposed as the "electrobun_sdk" collection; the SDK itself
531
+ // is the "electrobun" package inside it (import "electrobun_sdk:electrobun").
532
+ const odinSdkCollectionPath = join(ELECTROBUN_DEP_PATH, "dist", "odin-sdk");
533
+ if (!existsSync(join(odinSdkCollectionPath, "electrobun", "electrobun.odin"))) {
534
+ throw new Error(`Electrobun Odin SDK not found at ${odinSdkCollectionPath}`);
535
+ }
536
+
537
+ // Odin can cross-compile object code, but linking still requires the target
538
+ // platform's linker/SDK, so mirror the Rust/Go host-only policy.
539
+ if (options.targetOS !== OS || options.targetArch !== ARCH) {
540
+ throw new Error(
541
+ `Odin main process cross-compilation is not supported yet. Vendored Odin targets ${OS}-${ARCH}, but the build target is ${options.targetOS}-${options.targetArch}.`,
542
+ );
543
+ }
544
+
545
+ const binExt = options.targetOS === "win" ? ".exe" : "";
546
+ const tempBuildDir = join(
547
+ options.buildFolder,
548
+ ".electrobun-odin-main",
549
+ `${options.targetOS}-${options.targetArch}`,
550
+ );
551
+ const odinOutBin = join(tempBuildDir, "main" + binExt);
552
+ mkdirSync(tempBuildDir, { recursive: true });
553
+
554
+ // Odin compiles a package (directory); accept a file entrypoint for config
555
+ // symmetry with the other languages and compile its containing package.
556
+ const entrypointDir = statSync(options.entrypoint).isDirectory()
557
+ ? options.entrypoint
558
+ : dirname(options.entrypoint);
559
+
560
+ const odinArgs = [
561
+ "build",
562
+ entrypointDir,
563
+ `-out:${odinOutBin}`,
564
+ `-collection:electrobun_sdk=${odinSdkCollectionPath}`,
565
+ ];
566
+
567
+ if (options.buildEnvironment !== "dev") {
568
+ odinArgs.push("-o:size");
569
+ }
570
+
571
+ const result = Bun.spawnSync([odinBinary, ...odinArgs], {
572
+ cwd: projectRoot,
573
+ stdio: ["ignore", "pipe", "pipe"],
574
+ });
575
+
576
+ if (result.exitCode !== 0) {
577
+ const stdout = result.stdout ? new TextDecoder().decode(result.stdout) : "";
578
+ const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : "";
579
+ if (stdout.trim()) {
580
+ console.error(stdout);
581
+ }
582
+ if (stderr.trim()) {
583
+ console.error(stderr);
584
+ }
585
+ if (options.targetOS === "win") {
586
+ console.error(
587
+ "Note: Odin on Windows requires the MSVC toolchain (link.exe from Visual Studio Build Tools). See the Odin main process docs for prerequisites.",
588
+ );
589
+ }
590
+ throw new Error("Build failed: odin main process compilation failed");
591
+ }
592
+
593
+ if (!existsSync(odinOutBin)) {
594
+ throw new Error(`Odin main process binary was not produced at ${odinOutBin}`);
595
+ }
596
+
597
+ return odinOutBin;
598
+ }
599
+
144
600
  async function ensureCoreDependencies(
145
601
  targetOS?: "macos" | "win" | "linux",
146
602
  targetArch?: "arm64" | "x64",
@@ -154,7 +610,7 @@ async function ensureCoreDependencies(
154
610
 
155
611
  // Check platform-specific binaries
156
612
  const requiredBinaries = [
157
- platformPaths.BUN_BINARY,
613
+ platformPaths.COTTONTAIL_BINARY,
158
614
  platformPaths.BSDIFF,
159
615
  platformPaths.BSPATCH,
160
616
  ];
@@ -298,7 +754,7 @@ async function ensureCoreDependencies(
298
754
 
299
755
  // Verify extraction completed successfully - check platform-specific binaries only
300
756
  const requiredBinaries = [
301
- platformPaths.BUN_BINARY,
757
+ platformPaths.COTTONTAIL_BINARY,
302
758
  platformPaths.BSDIFF,
303
759
  platformPaths.BSPATCH,
304
760
  platformPaths.ZSTD,
@@ -339,452 +795,56 @@ async function ensureCoreDependencies(
339
795
  "Development fallback: copying main.js from platform-specific download to shared dist/",
340
796
  );
341
797
  mkdirSync(sharedDistPath, { recursive: true });
342
- cpSync(extractedMainJs, sharedMainJs, { dereference: true });
343
- }
344
-
345
- console.log(
346
- `Core dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`,
347
- );
348
- } catch (error: any) {
349
- console.error(
350
- `Failed to download core dependencies for ${platformOS}-${platformArch}:`,
351
- error.message,
352
- );
353
- console.error(
354
- "Please ensure you have an internet connection and the release exists.",
355
- );
356
- process.exit(1);
357
- }
358
- }
359
-
360
- /**
361
- * Returns the effective CEF directory path. When a custom cefVersion is set,
362
- * CEF files are stored in node_modules/.electrobun-cache/ which survives
363
- * both dist rebuilds and bun install (which replaces node_modules/electrobun).
364
- * When using the default version, returns the standard dist-{platform}/cef/ path.
365
- */
366
- function getEffectiveCEFDir(
367
- platformOS: "macos" | "win" | "linux",
368
- platformArch: "arm64" | "x64",
369
- cefVersion?: string,
370
- ): string {
371
- if (cefVersion) {
372
- return join(ELECTROBUN_CACHE_PATH, "cef-override", `${platformOS}-${platformArch}`);
373
- }
374
- return getPlatformPaths(platformOS, platformArch).CEF_DIR;
375
- }
376
-
377
- /**
378
- * Returns the effective WGPU directory path. WGPU files are stored in
379
- * node_modules/.electrobun-cache/ to survive dist rebuilds and bun install.
380
- */
381
- function getEffectiveWGPUDir(
382
- platformOS: "macos" | "win" | "linux",
383
- platformArch: "arm64" | "x64",
384
- ): string {
385
- return join(
386
- projectRoot,
387
- "node_modules",
388
- ".electrobun-cache",
389
- "wgpu",
390
- `${platformOS}-${platformArch}`,
391
- );
392
- }
393
-
394
- /**
395
- * Trims an ICU .dat file to only include the specified locales.
396
- * Uses icupkg (from ICU tools) to list and remove unwanted locale data.
397
- */
398
- async function trimICUData(
399
- source: string,
400
- dest: string,
401
- locales: string[],
402
- ): Promise<void> {
403
- // Copy the full .dat file first
404
- cpSync(source, dest);
405
-
406
- // Try to find icupkg in PATH or common locations
407
- let icupkgPath = "icupkg";
408
- try {
409
- execSync(`${icupkgPath} --help`, { stdio: "ignore" });
410
- } catch {
411
- // icupkg not available, skip trimming
412
- throw new Error(
413
- "icupkg not found in PATH. Install ICU tools to enable locale trimming.",
414
- );
415
- }
416
-
417
- // List all items in the .dat file
418
- const listOutput = execSync(`${icupkgPath} -l "${dest}"`, {
419
- encoding: "utf-8",
420
- });
421
- const allItems = listOutput.split("\n").filter((line) => line.trim());
422
-
423
- // Locale-specific directories in ICU data
424
- const localeDirs = [
425
- "brkitr/",
426
- "coll/",
427
- "curr/",
428
- "lang/",
429
- "locales/",
430
- "rbnf/",
431
- "region/",
432
- "unit/",
433
- "zone/",
434
- ];
435
-
436
- const toRemove = allItems.filter((item) => {
437
- // Only consider items in locale-specific directories
438
- const isLocaleItem = localeDirs.some((dir) => item.startsWith(dir));
439
- if (!isLocaleItem) return false;
440
-
441
- // Extract the basename (after the last /)
442
- const basename = item.split("/").pop() || "";
443
- // Remove file extension for matching
444
- const name = basename.replace(/\.res$/, "");
445
-
446
- // Keep items matching requested locales (exact match or with region suffix)
447
- return !locales.some(
448
- (l) =>
449
- name === l ||
450
- name === "root" ||
451
- name.startsWith(`${l}_`) ||
452
- name.startsWith(`${l}-`),
453
- );
454
- });
455
-
456
- if (toRemove.length > 0) {
457
- // Write removal list to temp file
458
- const { tmpdir } = await import("os");
459
- const removeListPath = join(tmpdir(), "icu-remove.txt");
460
- writeFileSync(removeListPath, toRemove.join("\n"));
461
-
462
- try {
463
- execSync(`${icupkgPath} -r "@${removeListPath}" "${dest}"`, {
464
- stdio: "inherit",
465
- });
466
- } finally {
467
- try {
468
- unlinkSync(removeListPath);
469
- } catch {
470
- // ignore cleanup errors
471
- }
472
- }
473
- }
474
- }
475
-
476
- /**
477
- * Ensures the correct Bun binary is available for bundling. When a custom
478
- * bunVersion is specified in the config, downloads that version from GitHub
479
- * releases and caches it. Otherwise returns the default binary path.
480
- */
481
- async function ensureBunBinary(
482
- targetOS: "macos" | "win" | "linux",
483
- targetArch: "arm64" | "x64",
484
- bunVersion?: string,
485
- bunnyBun?: string,
486
- ): Promise<string> {
487
- const effectiveVersion = bunnyBun || bunVersion;
488
- if (!effectiveVersion) {
489
- return getPlatformPaths(targetOS, targetArch).BUN_BINARY;
490
- }
491
-
492
- const binExt = targetOS === "win" ? ".exe" : "";
493
- const cacheSubdir = bunnyBun ? "bunny-bun-override" : "bun-override";
494
- const overrideDir = join(ELECTROBUN_CACHE_PATH, cacheSubdir, `${targetOS}-${targetArch}`);
495
- const overrideBinary = join(overrideDir, `bun${binExt}`);
496
- const versionFile = join(overrideDir, ".bun-version");
497
-
498
- // Check if already downloaded with matching version
499
- if (existsSync(overrideBinary) && existsSync(versionFile)) {
500
- const cachedVersion = readFileSync(versionFile, "utf8").trim();
501
- if (cachedVersion === effectiveVersion) {
502
- console.log(
503
- `${bunnyBun ? "Bunny" : "Custom"} Bun ${effectiveVersion} already cached for ${targetOS}-${targetArch}`,
504
- );
505
- return overrideBinary;
506
- }
507
- console.log(
508
- `Cached Bun version "${cachedVersion}" does not match requested "${effectiveVersion}", re-downloading...`,
509
- );
510
- rmSync(overrideDir, { recursive: true, force: true });
511
- } else if (existsSync(overrideDir)) {
512
- rmSync(overrideDir, { recursive: true, force: true });
513
- }
514
-
515
- if (bunnyBun) {
516
- await downloadBunnyBun(bunnyBun, targetOS, targetArch);
517
- } else {
518
- await downloadCustomBun(effectiveVersion, targetOS, targetArch);
519
- }
520
- return overrideBinary;
521
- }
522
-
523
- /**
524
- * Downloads a specific Bun version from GitHub releases for a custom version
525
- * override. The binary is cached in node_modules/.electrobun-cache/bun-override/
526
- * so it survives dist rebuilds and bun install.
527
- */
528
- async function downloadCustomBun(
529
- bunVersion: string,
530
- platformOS: "macos" | "win" | "linux",
531
- platformArch: "arm64" | "x64",
532
- ) {
533
- // Map to GitHub release asset names
534
- let bunUrlSegment: string;
535
- let bunDirName: string;
536
-
537
- if (platformOS === "win") {
538
- bunUrlSegment = "bun-windows-x64-baseline.zip";
539
- bunDirName = "bun-windows-x64-baseline";
540
- } else if (platformOS === "macos") {
541
- bunUrlSegment =
542
- platformArch === "arm64"
543
- ? "bun-darwin-aarch64.zip"
544
- : "bun-darwin-x64.zip";
545
- bunDirName =
546
- platformArch === "arm64" ? "bun-darwin-aarch64" : "bun-darwin-x64";
547
- } else if (platformOS === "linux") {
548
- bunUrlSegment =
549
- platformArch === "arm64" ? "bun-linux-aarch64.zip" : "bun-linux-x64.zip";
550
- bunDirName =
551
- platformArch === "arm64" ? "bun-linux-aarch64" : "bun-linux-x64";
552
- } else {
553
- throw new Error(`Unsupported platform for custom Bun: ${platformOS}`);
554
- }
555
-
556
- const binExt = platformOS === "win" ? ".exe" : "";
557
- const overrideDir = join(ELECTROBUN_CACHE_PATH, "bun-override", `${platformOS}-${platformArch}`);
558
- const overrideBinary = join(overrideDir, `bun${binExt}`);
559
- const bunUrl = `https://github.com/oven-sh/bun/releases/download/bun-v${bunVersion}/${bunUrlSegment}`;
560
-
561
- console.log(`Using custom Bun version: ${bunVersion}`);
562
- console.log(`Downloading from: ${bunUrl}`);
563
-
564
- mkdirSync(overrideDir, { recursive: true });
565
-
566
- const tempZipPath = join(overrideDir, "temp.zip");
567
-
568
- try {
569
- console.log(`Downloading custom Bun...`);
570
- const response = await fetch(bunUrl);
571
- if (!response.ok) {
572
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
573
- }
574
-
575
- const contentLength = response.headers.get("content-length");
576
- const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
577
- const fileStream = createWriteStream(tempZipPath);
578
- let downloadedSize = 0;
579
- let lastReportedPercent = -1;
580
-
581
- if (response.body) {
582
- const reader = response.body.getReader();
583
- while (true) {
584
- const { done, value } = await reader.read();
585
- if (done) break;
586
-
587
- const chunk = Buffer.from(value);
588
- fileStream.write(chunk);
589
- downloadedSize += chunk.length;
590
-
591
- if (totalSize > 0) {
592
- const percent = Math.round((downloadedSize / totalSize) * 100);
593
- const percentTier = Math.floor(percent / 10) * 10;
594
- if (percentTier > lastReportedPercent && percentTier <= 100) {
595
- console.log(
596
- ` Progress: ${percentTier}% (${Math.round(downloadedSize / 1024 / 1024)}MB/${Math.round(totalSize / 1024 / 1024)}MB)`,
597
- );
598
- lastReportedPercent = percentTier;
599
- }
600
- }
601
- }
602
- }
603
-
604
- await new Promise((resolve, reject) => {
605
- fileStream.end((error: any) => {
606
- if (error) reject(error);
607
- else resolve(void 0);
608
- });
609
- });
610
-
611
- console.log(
612
- `Download completed (${Math.round(downloadedSize / 1024 / 1024)}MB), extracting...`,
613
- );
614
-
615
- // Extract zip file
616
- if (platformOS === "win") {
617
- execSync(
618
- `powershell -command "Expand-Archive -Path '${tempZipPath}' -DestinationPath '${overrideDir}' -Force"`,
619
- { stdio: "inherit" },
620
- );
621
- } else {
622
- execSync(`unzip -o ${escapePathForTerminal(tempZipPath)} -d ${escapePathForTerminal(overrideDir)}`, {
623
- stdio: "inherit",
624
- });
625
- }
626
-
627
- // Move binary from extracted subdirectory to override dir root
628
- const extractedBinary = join(overrideDir, bunDirName, `bun${binExt}`);
629
- if (existsSync(extractedBinary)) {
630
- renameSync(extractedBinary, overrideBinary);
631
- } else {
632
- throw new Error(
633
- `Bun binary not found after extraction at ${extractedBinary}`,
634
- );
635
- }
636
-
637
- // Set execute permissions on non-Windows
638
- if (platformOS !== "win") {
639
- execSync(`chmod +x ${escapePathForTerminal(overrideBinary)}`);
798
+ cpSync(extractedMainJs, sharedMainJs, { dereference: true });
640
799
  }
641
800
 
642
- // Write version stamp
643
- writeFileSync(join(overrideDir, ".bun-version"), bunVersion);
644
-
645
- // Clean up
646
- if (existsSync(tempZipPath)) unlinkSync(tempZipPath);
647
- const extractedDir = join(overrideDir, bunDirName);
648
- if (existsSync(extractedDir))
649
- rmSync(extractedDir, { recursive: true, force: true });
650
-
651
801
  console.log(
652
- `Custom Bun ${bunVersion} for ${platformOS}-${platformArch} set up successfully`,
802
+ `Core dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`,
653
803
  );
654
804
  } catch (error: any) {
655
- // Clean up on failure
656
- if (existsSync(overrideDir)) {
657
- try {
658
- rmSync(overrideDir, { recursive: true, force: true });
659
- } catch {}
660
- }
661
-
662
805
  console.error(
663
- `Failed to set up custom Bun ${bunVersion} for ${platformOS}-${platformArch}:`,
806
+ `Failed to download core dependencies for ${platformOS}-${platformArch}:`,
664
807
  error.message,
665
808
  );
666
809
  console.error(
667
- `\nVerify the Bun version string and that it exists at: https://github.com/oven-sh/bun/releases`,
810
+ "Please ensure you have an internet connection and the release exists.",
668
811
  );
669
812
  process.exit(1);
670
813
  }
671
814
  }
672
815
 
673
816
  /**
674
- * Downloads Electrobunny's Bun fork from blackboardsh/bun GitHub releases.
675
- * Release assets follow the same naming convention as oven-sh/bun:
676
- * bun-darwin-aarch64.zip, bun-linux-x64.zip, etc.
817
+ * Returns the effective CEF directory path. When a custom cefVersion is set,
818
+ * CEF files are stored in node_modules/.electrobun-cache/ which survives
819
+ * both dist rebuilds and package reinstalls.
820
+ * When using the default version, returns the standard dist-{platform}/cef/ path.
677
821
  */
678
- async function downloadBunnyBun(
679
- releaseTag: string,
822
+ function getEffectiveCEFDir(
680
823
  platformOS: "macos" | "win" | "linux",
681
824
  platformArch: "arm64" | "x64",
682
- ) {
683
- let assetName: string;
684
- let dirName: string;
685
-
686
- // Asset names match the CI artifact names from blackboardsh/bun
687
- if (platformOS === "win") {
688
- assetName = "bun-windows-x64.zip";
689
- dirName = "bun-windows-x64";
690
- } else if (platformOS === "macos") {
691
- assetName = platformArch === "arm64" ? "bun-darwin-arm64.zip" : "bun-darwin-x64.zip";
692
- dirName = platformArch === "arm64" ? "bun-darwin-arm64" : "bun-darwin-x64";
693
- } else {
694
- assetName = platformArch === "arm64" ? "bun-linux-arm64.zip" : "bun-linux-x64.zip";
695
- dirName = platformArch === "arm64" ? "bun-linux-arm64" : "bun-linux-x64";
825
+ cefVersion?: string,
826
+ ): string {
827
+ if (cefVersion) {
828
+ return join(ELECTROBUN_CACHE_PATH, "cef-override", `${platformOS}-${platformArch}`);
696
829
  }
830
+ return getPlatformPaths(platformOS, platformArch).CEF_DIR;
831
+ }
697
832
 
698
- const binExt = platformOS === "win" ? ".exe" : "";
699
- const overrideDir = join(ELECTROBUN_CACHE_PATH, "bunny-bun-override", `${platformOS}-${platformArch}`);
700
- const overrideBinary = join(overrideDir, `bun${binExt}`);
701
- const bunUrl = `https://github.com/blackboardsh/bun/releases/download/${releaseTag}/${assetName}`;
702
-
703
- console.log(`Using Bunny Bun: ${releaseTag}`);
704
- console.log(`Downloading from: ${bunUrl}`);
705
-
706
- mkdirSync(overrideDir, { recursive: true });
707
- const tempZipPath = join(overrideDir, "temp.zip");
708
-
709
- try {
710
- const response = await fetch(bunUrl);
711
- if (!response.ok) {
712
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
713
- }
714
-
715
- const contentLength = response.headers.get("content-length");
716
- const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
717
- const fileStream = createWriteStream(tempZipPath);
718
- let downloadedSize = 0;
719
- let lastReportedPercent = -1;
720
-
721
- if (response.body) {
722
- const reader = response.body.getReader();
723
- while (true) {
724
- const { done, value } = await reader.read();
725
- if (done) break;
726
- const chunk = Buffer.from(value);
727
- fileStream.write(chunk);
728
- downloadedSize += chunk.length;
729
- if (totalSize > 0) {
730
- const percent = Math.round((downloadedSize / totalSize) * 100);
731
- const percentTier = Math.floor(percent / 10) * 10;
732
- if (percentTier > lastReportedPercent && percentTier <= 100) {
733
- console.log(` Progress: ${percentTier}% (${Math.round(downloadedSize / 1024 / 1024)}MB/${Math.round(totalSize / 1024 / 1024)}MB)`);
734
- lastReportedPercent = percentTier;
735
- }
736
- }
737
- }
738
- }
739
-
740
- await new Promise((resolve, reject) => {
741
- fileStream.end((error: any) => { if (error) reject(error); else resolve(void 0); });
742
- });
743
-
744
- console.log(`Download completed (${Math.round(downloadedSize / 1024 / 1024)}MB), extracting...`);
745
-
746
- if (platformOS === "win") {
747
- execSync(`powershell -command "Expand-Archive -Path '${tempZipPath}' -DestinationPath '${overrideDir}' -Force"`, { stdio: "inherit" });
748
- } else {
749
- execSync(`unzip -o ${escapePathForTerminal(tempZipPath)} -d ${escapePathForTerminal(overrideDir)}`, { stdio: "inherit" });
750
- }
751
-
752
- // Move binary from extracted subdirectory
753
- const extractedBinary = join(overrideDir, dirName, `bun${binExt}`);
754
- if (existsSync(extractedBinary)) {
755
- renameSync(extractedBinary, overrideBinary);
756
- } else {
757
- throw new Error(`Bun binary not found after extraction at ${extractedBinary}`);
758
- }
759
-
760
- if (platformOS !== "win") {
761
- execSync(`chmod +x ${escapePathForTerminal(overrideBinary)}`);
762
- }
763
-
764
- // Also extract ICU data if present
765
- const extractedDir = join(overrideDir, dirName);
766
- if (existsSync(extractedDir)) {
767
- for (const file of readdirSync(extractedDir)) {
768
- if (file.endsWith(".dat")) {
769
- renameSync(join(extractedDir, file), join(overrideDir, file));
770
- }
771
- }
772
- }
773
-
774
- writeFileSync(join(overrideDir, ".bun-version"), releaseTag);
775
-
776
- if (existsSync(tempZipPath)) unlinkSync(tempZipPath);
777
- if (existsSync(extractedDir)) rmSync(extractedDir, { recursive: true, force: true });
778
-
779
- console.log(`Bunny Bun ${releaseTag} for ${platformOS}-${platformArch} set up successfully`);
780
- } catch (error: any) {
781
- if (existsSync(overrideDir)) {
782
- try { rmSync(overrideDir, { recursive: true, force: true }); } catch {}
783
- }
784
- console.error(`Failed to set up Bunny Bun ${releaseTag} for ${platformOS}-${platformArch}:`, error.message);
785
- console.error(`\nVerify the release tag exists at: https://github.com/blackboardsh/bun/releases`);
786
- process.exit(1);
787
- }
833
+ /**
834
+ * Returns the effective WGPU directory path. WGPU files are stored in
835
+ * node_modules/.electrobun-cache/ to survive dist rebuilds and package reinstalls.
836
+ */
837
+ function getEffectiveWGPUDir(
838
+ platformOS: "macos" | "win" | "linux",
839
+ platformArch: "arm64" | "x64",
840
+ ): string {
841
+ return join(
842
+ projectRoot,
843
+ "node_modules",
844
+ ".electrobun-cache",
845
+ "wgpu",
846
+ `${platformOS}-${platformArch}`,
847
+ );
788
848
  }
789
849
 
790
850
  async function ensureCEFDependencies(
@@ -1306,7 +1366,7 @@ async function downloadAndExtractCustomCEF(
1306
1366
  console.log(`Using custom CEF version: ${cefVersion}`);
1307
1367
  console.log(`Downloading from: ${cefUrl}`);
1308
1368
 
1309
- // Store custom CEF in .electrobun-cache so it survives dist rebuilds and bun install
1369
+ // Store custom CEF in .electrobun-cache so it survives dist rebuilds and package reinstalls.
1310
1370
  const cefDir = getEffectiveCEFDir(platformOS, platformArch, cefVersion);
1311
1371
  console.log(`Caching custom CEF to ${cefDir}`);
1312
1372
  mkdirSync(cefDir, { recursive: true });
@@ -1470,7 +1530,7 @@ type FileAssociation = {
1470
1530
  };
1471
1531
 
1472
1532
  // Default values merged with user's electrobun.config.ts
1473
- // For the user-facing type, see ElectrobunConfig in src/bun/ElectrobunConfig.ts
1533
+ // For the user-facing type, see ElectrobunConfig in src/config/ElectrobunConfig.ts
1474
1534
  const defaultConfig = {
1475
1535
  app: {
1476
1536
  name: "MyApp",
@@ -1483,13 +1543,11 @@ const defaultConfig = {
1483
1543
  build: {
1484
1544
  buildFolder: "build",
1485
1545
  artifactFolder: "artifacts",
1546
+ mainProcess: "cottontail" as MainProcess,
1486
1547
  useAsar: false,
1487
1548
  asarUnpack: undefined as string[] | undefined, // Glob patterns for files to exclude from ASAR (e.g., ["*.node", "*.dll"])
1488
1549
  cefVersion: undefined as string | undefined, // Override CEF version: "CEF_VERSION+chromium-CHROMIUM_VERSION"
1489
1550
  wgpuVersion: undefined as string | undefined, // Override Dawn (WebGPU) version: "0.2.3" or "v0.2.3-beta.0"
1490
- bunVersion: undefined as string | undefined, // Override Bun runtime version: "1.4.2"
1491
- bunnyBun: undefined as string | undefined, // Use Electrobunny's Bun fork: "bunny-bun-abc1234" (release tag from blackboardsh/bun)
1492
- locales: undefined as string[] | "*" | undefined, // ICU locales subset (Linux/Windows)
1493
1551
  mac: {
1494
1552
  codesign: false,
1495
1553
  createDmg: true,
@@ -1499,7 +1557,7 @@ const defaultConfig = {
1499
1557
  entitlements: {
1500
1558
  // This entitlement is required for Electrobun apps with a hardened runtime (required for notarization) to run on macos
1501
1559
  "com.apple.security.cs.allow-jit": true,
1502
- // Required for bun runtime to work with dynamic code execution and JIT compilation when signed
1560
+ // Required for Cottontail/JSC dynamic code execution and JIT compilation when signed
1503
1561
  "com.apple.security.cs.allow-unsigned-executable-memory": true,
1504
1562
  "com.apple.security.cs.disable-library-validation": true,
1505
1563
  } as Record<string, boolean | string>,
@@ -1521,9 +1579,21 @@ const defaultConfig = {
1521
1579
  defaultRenderer: undefined as "native" | "cef" | undefined,
1522
1580
  chromiumFlags: undefined as Record<string, string | boolean> | undefined,
1523
1581
  },
1524
- bun: {
1582
+ cottontail: {
1525
1583
  entrypoint: "src/bun/index.ts",
1526
1584
  },
1585
+ zig: {
1586
+ entrypoint: "src/zig/main.zig",
1587
+ },
1588
+ rust: {
1589
+ entrypoint: "src/rust/main.rs",
1590
+ },
1591
+ go: {
1592
+ entrypoint: "src/go/main.go",
1593
+ },
1594
+ odin: {
1595
+ entrypoint: "src/odin/main.odin",
1596
+ },
1527
1597
  views: undefined as
1528
1598
  | Record<string, { entrypoint: string; [key: string]: unknown }>
1529
1599
  | undefined,
@@ -1535,7 +1605,6 @@ const defaultConfig = {
1535
1605
  name: string;
1536
1606
  description?: string;
1537
1607
  mode?: "window" | "background";
1538
- permissions?: Record<string, unknown>;
1539
1608
  dependencies?: Record<string, string>;
1540
1609
  // Map of remote UI ID → config. Two flavors:
1541
1610
  // 1. { name, entrypoint, ...bunBuildOpts } — electrobun builds it via Bun.build
@@ -1548,6 +1617,24 @@ const defaultConfig = {
1548
1617
  path?: string;
1549
1618
  [key: string]: unknown;
1550
1619
  }>;
1620
+ slateUIs?: Record<string, {
1621
+ name?: string;
1622
+ entrypoint?: string;
1623
+ path?: string;
1624
+ [key: string]: unknown;
1625
+ }>;
1626
+ contributions?: {
1627
+ fileActivators?: Array<{
1628
+ baseName?: string;
1629
+ nodeType?: "file" | "dir" | "any";
1630
+ slate: {
1631
+ type: string;
1632
+ name?: string;
1633
+ icon?: string;
1634
+ config?: Record<string, unknown>;
1635
+ };
1636
+ }>;
1637
+ };
1551
1638
  carrotOnly?: boolean;
1552
1639
  } | undefined,
1553
1640
  },
@@ -2054,8 +2141,8 @@ ${utiDecls}
2054
2141
  console.log("");
2055
2142
  console.log("📦 Next steps:");
2056
2143
  console.log(` cd ${projectName}`);
2057
- console.log(" bun install");
2058
- console.log(" bun start");
2144
+ console.log(" npm install");
2145
+ console.log(" npm start");
2059
2146
  console.log("");
2060
2147
  console.log("🎉 Happy building with Electrobun!");
2061
2148
  console.log("");
@@ -2066,7 +2153,7 @@ ${utiDecls}
2066
2153
  console.log(
2067
2154
  "Different architecture, different APIs. Do not use Electron patterns.",
2068
2155
  );
2069
- console.log("Docs: https://docs.electrobunny.ai/electrobun/llms.txt");
2156
+ console.log("Docs: https://framework.blackboard.sh/electrobun/");
2070
2157
  console.log(
2071
2158
  "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
2072
2159
  );
@@ -2078,8 +2165,10 @@ ${utiDecls}
2078
2165
  // Get environment
2079
2166
  const envArg =
2080
2167
  process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
2081
- const buildEnvironment = ["dev", "canary", "stable"].includes(envArg)
2082
- ? envArg
2168
+ const buildEnvironment: "dev" | "canary" | "stable" = ["dev", "canary", "stable"].includes(
2169
+ envArg,
2170
+ )
2171
+ ? (envArg as "dev" | "canary" | "stable")
2083
2172
  : "dev";
2084
2173
 
2085
2174
  try {
@@ -2110,7 +2199,7 @@ ${utiDecls}
2110
2199
 
2111
2200
  async function runBuild(
2112
2201
  config: Awaited<ReturnType<typeof getConfig>>,
2113
- buildEnvironment: string,
2202
+ buildEnvironment: "dev" | "canary" | "stable",
2114
2203
  ) {
2115
2204
  // Determine current platform as default target
2116
2205
  const currentTarget = { os: OS, arch: ARCH };
@@ -2158,10 +2247,10 @@ ${utiDecls}
2158
2247
  if (!hookScript) return;
2159
2248
 
2160
2249
  console.log(`Running ${String(hookName)} script:`, hookScript);
2161
- // Use host platform's bun binary for running scripts, not target platform's
2250
+ // Hooks execute with the host Cottontail runtime.
2162
2251
  const hostPaths = getPlatformPaths(OS, ARCH);
2163
2252
 
2164
- const result = Bun.spawnSync([hostPaths.BUN_BINARY, hookScript], {
2253
+ const result = Bun.spawnSync([hostPaths.COTTONTAIL_BINARY, hookScript], {
2165
2254
  stdio: ["ignore", "inherit", "inherit"],
2166
2255
  cwd: projectRoot,
2167
2256
  env: {
@@ -2189,7 +2278,10 @@ ${utiDecls}
2189
2278
  new TextDecoder().decode(result.stderr as Uint8Array),
2190
2279
  );
2191
2280
  }
2192
- console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);
2281
+ console.error(
2282
+ "Tried to run with Cottontail at:",
2283
+ hostPaths.COTTONTAIL_BINARY,
2284
+ );
2193
2285
  console.error("Script path:", hookScript);
2194
2286
  console.error("Working directory:", projectRoot);
2195
2287
  throw new Error("Build failed: hook script failed");
@@ -2392,20 +2484,52 @@ Categories=Utility;Application;
2392
2484
 
2393
2485
  // refresh build folder
2394
2486
  if (existsSync(buildFolder)) {
2395
- rmSync(buildFolder, { recursive: true });
2487
+ rmSync(buildFolder, { recursive: true, force: true });
2396
2488
  }
2397
2489
  mkdirSync(buildFolder, { recursive: true });
2398
- // bundle bun to build/bun
2399
- const bunConfig = config.build.bun;
2400
- const bunSource = join(projectRoot, bunConfig.entrypoint);
2401
2490
 
2402
- if (!existsSync(bunSource)) {
2491
+ const mainProcess = config.build.mainProcess ?? "cottontail";
2492
+ const cottontailConfig = config.build.cottontail;
2493
+ const cottontailSource = join(projectRoot, cottontailConfig.entrypoint);
2494
+ const zigConfig = config.build.zig;
2495
+ const zigSource = join(projectRoot, zigConfig.entrypoint);
2496
+ const rustConfig = config.build.rust;
2497
+ const rustSource = join(projectRoot, rustConfig.entrypoint);
2498
+ const goConfig = config.build.go;
2499
+ const goSource = join(projectRoot, goConfig.entrypoint);
2500
+ const odinConfig = config.build.odin;
2501
+ const odinSource = join(projectRoot, odinConfig.entrypoint);
2502
+
2503
+ if (mainProcess === "cottontail") {
2504
+ if (!existsSync(cottontailSource)) {
2505
+ throw new Error(
2506
+ `failed to bundle ${cottontailSource} because it doesn't exist.\n You need a config.build.cottontail.entrypoint source file to build.`,
2507
+ );
2508
+ }
2509
+ } else if (mainProcess === "zig" && !existsSync(zigSource)) {
2510
+ throw new Error(
2511
+ `failed to compile ${zigSource} because it doesn't exist.\n You need a config.build.zig.entrypoint source file to build.`,
2512
+ );
2513
+ } else if (mainProcess === "rust" && !existsSync(rustSource)) {
2514
+ throw new Error(
2515
+ `failed to compile ${rustSource} because it doesn't exist.\n You need a config.build.rust.entrypoint source file to build.`,
2516
+ );
2517
+ } else if (mainProcess === "go" && !existsSync(goSource)) {
2518
+ throw new Error(
2519
+ `failed to compile ${goSource} because it doesn't exist.\n You need a config.build.go.entrypoint source file to build.`,
2520
+ );
2521
+ } else if (mainProcess === "odin" && !existsSync(odinSource)) {
2403
2522
  throw new Error(
2404
- `failed to bundle ${bunSource} because it doesn't exist.\n You need a config.build.bun.entrypoint source file to build.`,
2523
+ `failed to compile ${odinSource} because it doesn't exist.\n You need a config.build.odin.entrypoint source file to build.`,
2405
2524
  );
2406
2525
  }
2407
2526
 
2408
2527
  const isCarrotOnly = config.build.carrot?.carrotOnly === true;
2528
+ if (config.build.carrot && mainProcess !== "cottontail") {
2529
+ throw new Error(
2530
+ `build.carrot is not supported with build.mainProcess = "${mainProcess}" yet.`,
2531
+ );
2532
+ }
2409
2533
 
2410
2534
  // build macos bundle
2411
2535
  // Use display name (with spaces) for macOS bundle folders, sanitized name for other platforms
@@ -2419,7 +2543,7 @@ Categories=Utility;Application;
2419
2543
  targetOS === "macos" ? macOSBundleDisplayName : appFileName;
2420
2544
 
2421
2545
  if (isCarrotOnly) {
2422
- // For carrot-only builds, create a minimal output structure for bun/view builds
2546
+ // For carrot-only builds, create a minimal output structure for main/view builds
2423
2547
  appBundleFolderPath = join(buildFolder, "carrot-build");
2424
2548
  appBundleFolderContentsPath = appBundleFolderPath;
2425
2549
  appBundleMacOSPath = appBundleFolderPath;
@@ -2438,8 +2562,40 @@ Categories=Utility;Application;
2438
2562
  mkdirSync(appBundleAppCodePath, { recursive: true });
2439
2563
  }
2440
2564
 
2441
- // const bundledBunPath = join(appBundleMacOSPath, 'bun');
2442
- // cpSync(bunPath, bundledBunPath);
2565
+ let nativeMainBinarySourcePath: string | null = null;
2566
+ if (mainProcess === "zig") {
2567
+ nativeMainBinarySourcePath = await buildZigMainExecutable({
2568
+ entrypoint: zigSource,
2569
+ buildFolder,
2570
+ targetOS,
2571
+ targetArch: targetARCH,
2572
+ buildEnvironment,
2573
+ });
2574
+ } else if (mainProcess === "rust") {
2575
+ nativeMainBinarySourcePath = await buildRustMainExecutable({
2576
+ entrypoint: rustSource,
2577
+ buildFolder,
2578
+ targetOS,
2579
+ targetArch: targetARCH,
2580
+ buildEnvironment,
2581
+ });
2582
+ } else if (mainProcess === "go") {
2583
+ nativeMainBinarySourcePath = await buildGoMainExecutable({
2584
+ entrypoint: goSource,
2585
+ buildFolder,
2586
+ targetOS,
2587
+ targetArch: targetARCH,
2588
+ buildEnvironment,
2589
+ });
2590
+ } else if (mainProcess === "odin") {
2591
+ nativeMainBinarySourcePath = await buildOdinMainExecutable({
2592
+ entrypoint: odinSource,
2593
+ buildFolder,
2594
+ targetOS,
2595
+ targetArch: targetARCH,
2596
+ buildEnvironment,
2597
+ });
2598
+ }
2443
2599
 
2444
2600
  // Note: for sandboxed apps, MacOS will use the CFBundleIdentifier to create a unique container for the app,
2445
2601
  // mirroring folders like Application Support, Caches, etc. in the user's Library folder that the sandboxed app
@@ -2498,59 +2654,27 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
2498
2654
  join(appBundleFolderContentsPath, "Info.plist"),
2499
2655
  InfoPlistContents,
2500
2656
  );
2501
- // in dev builds the log file is a named pipe so we can stream it back to the terminal
2502
- // in canary/stable builds it'll be a regular log file
2503
- // const LauncherContents = `#!/bin/bash
2504
- // # change directory from whatever open was or double clicking on the app to the dir of the bin in the app bundle
2505
- // cd "$(dirname "$0")"/
2506
-
2507
- // # Define the log file path
2508
- // LOG_FILE="$HOME/${logPath}"
2509
-
2510
- // # Ensure the directory exists
2511
- // mkdir -p "$(dirname "$LOG_FILE")"
2512
-
2513
- // if [[ ! -p $LOG_FILE ]]; then
2514
- // mkfifo $LOG_FILE
2515
- // fi
2516
-
2517
- // # Execute bun and redirect stdout and stderr to the log file
2518
- // ./bun ../Resources/app/bun/index.js >"$LOG_FILE" 2>&1
2519
- // `;
2520
-
2521
- // // Launcher binary
2522
- // // todo (yoav): This will likely be a zig compiled binary in the future
2523
- // Bun.write(join(appBundleMacOSPath, 'MyApp'), LauncherContents);
2524
- // chmodSync(join(appBundleMacOSPath, 'MyApp'), '755');
2525
- // const zigLauncherBinarySource = join(projectRoot, 'node_modules', 'electrobun', 'src', 'launcher', 'zig-out', 'bin', 'launcher');
2526
- // const zigLauncherDestination = join(appBundleMacOSPath, 'MyApp');
2527
- // const destLauncherFolder = dirname(zigLauncherDestination);
2528
- // if (!existsSync(destLauncherFolder)) {
2529
- // // console.info('creating folder: ', destFolder);
2530
- // mkdirSync(destLauncherFolder, {recursive: true});
2531
- // }
2532
- // cpSync(zigLauncherBinarySource, zigLauncherDestination, {recursive: true, dereference: true});
2533
- // Copy zig launcher for all platforms
2534
- const bunCliLauncherBinarySource = targetPaths.LAUNCHER_RELEASE;
2535
- const bunCliLauncherDestination =
2657
+ // Copy the Zig launcher for all platforms.
2658
+ const launcherBinarySource = targetPaths.LAUNCHER_RELEASE;
2659
+ const launcherDestination =
2536
2660
  join(appBundleMacOSPath, "launcher") + targetBinExt;
2537
- const destLauncherFolder = dirname(bunCliLauncherDestination);
2661
+ const destLauncherFolder = dirname(launcherDestination);
2538
2662
  if (!existsSync(destLauncherFolder)) {
2539
2663
  mkdirSync(destLauncherFolder, { recursive: true });
2540
2664
  }
2541
2665
 
2542
- cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
2666
+ cpSync(launcherBinarySource, launcherDestination, {
2543
2667
  recursive: true,
2544
2668
  dereference: true,
2545
2669
  });
2546
2670
 
2547
2671
  // On Windows, ensure launcher has .exe extension
2548
- // Bun's cpSync on Windows may create files without .exe despite the destination path having it
2672
+ // cpSync may create files without .exe despite the destination path having it.
2549
2673
  if (targetOS === "win") {
2550
2674
  const launcherWithoutExt = join(appBundleMacOSPath, "launcher");
2551
2675
 
2552
2676
  // Use PowerShell to force rename and add .exe extension
2553
- // This bypasses Bun's PATHEXT behavior that treats launcher and launcher.exe as the same
2677
+ // because PATHEXT can treat launcher and launcher.exe as the same path.
2554
2678
  try {
2555
2679
  execSync(
2556
2680
  `powershell -Command "if (Test-Path '${launcherWithoutExt}') { Rename-Item -Path '${launcherWithoutExt}' -NewName 'launcher.exe' -Force }"`,
@@ -2595,7 +2719,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
2595
2719
  const rceditDir = dirname(rceditPkgPath);
2596
2720
  const rceditX64 = join(rceditDir, "bin", "rcedit-x64.exe");
2597
2721
  const rceditExe = existsSync(rceditX64) ? rceditX64 : join(rceditDir, "bin", "rcedit.exe");
2598
- execFileSync(rceditExe, [bunCliLauncherDestination, "--set-icon", iconPath]);
2722
+ execFileSync(rceditExe, [launcherDestination, "--set-icon", iconPath]);
2599
2723
  console.log(`Successfully embedded icon into launcher.exe`);
2600
2724
 
2601
2725
  // Clean up temp ICO file
@@ -2610,105 +2734,98 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
2610
2734
  }
2611
2735
  }
2612
2736
 
2613
- cpSync(targetPaths.MAIN_JS, join(appBundleFolderResourcesPath, "main.js"), {
2737
+ cpSync(targetPaths.PRELOAD_FULL_JS, join(appBundleFolderResourcesPath, "preload-full.js"), {
2614
2738
  dereference: true,
2615
2739
  });
2616
-
2617
- // Bun runtime binary
2618
- // todo (yoav): this only works for the current architecture
2619
- const bunBinarySourcePath = await ensureBunBinary(
2620
- currentTarget.os,
2621
- currentTarget.arch,
2622
- config.build.bunVersion,
2623
- config.build.bunnyBun,
2740
+ cpSync(
2741
+ targetPaths.PRELOAD_SANDBOXED_JS,
2742
+ join(appBundleFolderResourcesPath, "preload-sandboxed.js"),
2743
+ {
2744
+ dereference: true,
2745
+ },
2624
2746
  );
2625
- // Note: .bin/bun binary in node_modules is a symlink to the versioned one in another place
2626
- // in node_modules, so we have to dereference here to get the actual binary in the bundle.
2627
- const bunBinaryDestInBundlePath =
2628
- join(appBundleMacOSPath, "bun") + targetBinExt;
2629
- const destFolder2 = dirname(bunBinaryDestInBundlePath);
2630
- if (!existsSync(destFolder2)) {
2631
- // console.info('creating folder: ', destFolder);
2632
- mkdirSync(destFolder2, { recursive: true });
2633
- }
2634
- cpSync(bunBinarySourcePath, bunBinaryDestInBundlePath, {
2635
- dereference: true,
2636
- });
2637
2747
 
2638
- // Copy ICU data file if it exists (Linux/Windows external ICU builds)
2639
- // ICU version varies per platform WebKit build, so detect the filename dynamically
2640
- const bunDir = dirname(bunBinarySourcePath);
2641
- const icuDataFileName = readdirSync(bunDir).find((f) => /^icudt\d+l\.dat$/.test(f));
2642
- const icuDataSource = icuDataFileName ? join(bunDir, icuDataFileName) : "";
2643
- if (icuDataFileName && existsSync(icuDataSource) && targetOS !== "macos") {
2644
- const icuDataDest = join(appBundleMacOSPath, icuDataFileName);
2645
-
2646
- const locales = config.build?.locales;
2647
- if (locales && locales !== "*" && Array.isArray(locales) && locales.length > 0) {
2648
- // Trim ICU data to specified locales using icupkg
2649
- try {
2650
- await trimICUData(icuDataSource, icuDataDest, locales);
2651
- const originalSize = statSync(icuDataSource).size;
2652
- const trimmedSize = statSync(icuDataDest).size;
2653
- console.log(
2654
- `Trimmed ICU data: ${(originalSize / 1024 / 1024).toFixed(1)}MB → ${(trimmedSize / 1024 / 1024).toFixed(1)}MB (locales: ${locales.join(", ")})`,
2655
- );
2656
- } catch (error) {
2657
- console.warn(`Warning: Failed to trim ICU data, copying full file: ${error}`);
2658
- cpSync(icuDataSource, icuDataDest);
2659
- }
2660
- } else {
2661
- cpSync(icuDataSource, icuDataDest);
2662
- console.log(`Copied ICU data file: ${icuDataFileName}`);
2663
- }
2664
- }
2748
+ if (mainProcess === "cottontail") {
2749
+ cpSync(targetPaths.MAIN_JS, join(appBundleFolderResourcesPath, "main.js"), {
2750
+ dereference: true,
2751
+ });
2665
2752
 
2666
- // Embed icon into bun.exe on Windows
2667
- if (targetOS === "win" && config.build.win?.icon) {
2668
- const iconSourcePath =
2669
- config.build.win.icon.startsWith("/") ||
2670
- config.build.win.icon.match(/^[a-zA-Z]:/)
2671
- ? config.build.win.icon
2672
- : join(projectRoot, config.build.win.icon);
2753
+ const cottontailBinarySourcePath = targetPaths.COTTONTAIL_BINARY;
2754
+ if (!existsSync(cottontailBinarySourcePath)) {
2755
+ throw new Error(
2756
+ `Cottontail runtime is missing for ${currentTarget.os}-${currentTarget.arch}: ${cottontailBinarySourcePath}`,
2757
+ );
2758
+ }
2759
+ const cottontailBinaryDestInBundlePath =
2760
+ join(appBundleMacOSPath, "cottontail") + targetBinExt;
2761
+ mkdirSync(dirname(cottontailBinaryDestInBundlePath), {
2762
+ recursive: true,
2763
+ });
2764
+ cpSync(cottontailBinarySourcePath, cottontailBinaryDestInBundlePath, {
2765
+ dereference: true,
2766
+ });
2673
2767
 
2674
- if (existsSync(iconSourcePath)) {
2675
- console.log(`Embedding icon into bun.exe: ${iconSourcePath}`);
2676
- try {
2677
- let iconPath = iconSourcePath;
2768
+ // Embed the application icon into the packaged runtime on Windows.
2769
+ if (targetOS === "win" && config.build.win?.icon) {
2770
+ const iconSourcePath =
2771
+ config.build.win.icon.startsWith("/") ||
2772
+ config.build.win.icon.match(/^[a-zA-Z]:/)
2773
+ ? config.build.win.icon
2774
+ : join(projectRoot, config.build.win.icon);
2678
2775
 
2679
- // Convert PNG to ICO if needed
2680
- if (iconSourcePath.toLowerCase().endsWith(".png")) {
2681
- const pngToIco = (await import("png-to-ico")).default;
2682
- const tempIcoPath = join(buildFolder, "temp-bun-icon.ico");
2683
- const icoBuffer = await pngToIco(iconSourcePath);
2684
- writeFileSync(tempIcoPath, new Uint8Array(icoBuffer));
2685
- iconPath = tempIcoPath;
2686
- console.log(
2687
- `Converted PNG to ICO format for bun.exe: ${tempIcoPath}`,
2688
- );
2689
- }
2776
+ if (existsSync(iconSourcePath)) {
2777
+ console.log(`Embedding icon into cottontail.exe: ${iconSourcePath}`);
2778
+ try {
2779
+ let iconPath = iconSourcePath;
2690
2780
 
2691
- // Use rcedit to embed the icon into bun.exe
2692
- const { execFileSync } = await import("child_process");
2693
- const rceditPkgPath = require.resolve("rcedit/package.json");
2694
- const rceditDir = dirname(rceditPkgPath);
2695
- const rceditX64 = join(rceditDir, "bin", "rcedit-x64.exe");
2696
- const rceditExe = existsSync(rceditX64) ? rceditX64 : join(rceditDir, "bin", "rcedit.exe");
2697
- execFileSync(rceditExe, [bunBinaryDestInBundlePath, "--set-icon", iconPath]);
2698
- console.log(`Successfully embedded icon into bun.exe`);
2781
+ if (iconSourcePath.toLowerCase().endsWith(".png")) {
2782
+ const pngToIco = (await import("png-to-ico")).default;
2783
+ const tempIcoPath = join(
2784
+ buildFolder,
2785
+ "temp-cottontail-icon.ico",
2786
+ );
2787
+ const icoBuffer = await pngToIco(iconSourcePath);
2788
+ writeFileSync(tempIcoPath, new Uint8Array(icoBuffer));
2789
+ iconPath = tempIcoPath;
2790
+ console.log(
2791
+ `Converted PNG to ICO format for cottontail.exe: ${tempIcoPath}`,
2792
+ );
2793
+ }
2699
2794
 
2700
- // Clean up temp ICO file
2701
- if (iconPath !== iconSourcePath && existsSync(iconPath)) {
2702
- unlinkSync(iconPath);
2795
+ const { execFileSync } = await import("child_process");
2796
+ const rceditPkgPath = require.resolve("rcedit/package.json");
2797
+ const rceditDir = dirname(rceditPkgPath);
2798
+ const rceditX64 = join(rceditDir, "bin", "rcedit-x64.exe");
2799
+ const rceditExe = existsSync(rceditX64) ? rceditX64 : join(rceditDir, "bin", "rcedit.exe");
2800
+ execFileSync(rceditExe, [
2801
+ cottontailBinaryDestInBundlePath,
2802
+ "--set-icon",
2803
+ iconPath,
2804
+ ]);
2805
+ console.log(`Successfully embedded icon into cottontail.exe`);
2806
+
2807
+ if (iconPath !== iconSourcePath && existsSync(iconPath)) {
2808
+ unlinkSync(iconPath);
2809
+ }
2810
+ } catch (error) {
2811
+ console.warn(
2812
+ `Warning: Failed to embed icon into cottontail.exe: ${error}`,
2813
+ );
2703
2814
  }
2704
- } catch (error) {
2705
- console.warn(`Warning: Failed to embed icon into bun.exe: ${error}`);
2706
2815
  }
2707
2816
  }
2817
+ } else if (nativeMainBinarySourcePath) {
2818
+ cpSync(nativeMainBinarySourcePath, join(appBundleMacOSPath, "main") + targetBinExt, {
2819
+ dereference: true,
2820
+ });
2708
2821
  }
2709
2822
 
2710
2823
  // copy native wrapper dynamic library
2711
2824
  if (targetOS === "macos") {
2825
+ cpSync(targetPaths.CORE_MACOS, join(appBundleMacOSPath, "libElectrobunCore.dylib"), {
2826
+ dereference: true,
2827
+ });
2828
+
2712
2829
  const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_MACOS;
2713
2830
  const nativeWrapperMacosDestination = join(
2714
2831
  appBundleMacOSPath,
@@ -2718,6 +2835,10 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
2718
2835
  dereference: true,
2719
2836
  });
2720
2837
  } else if (targetOS === "win") {
2838
+ cpSync(targetPaths.CORE_WIN, join(appBundleMacOSPath, "ElectrobunCore.dll"), {
2839
+ dereference: true,
2840
+ });
2841
+
2721
2842
  const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_WIN;
2722
2843
  const nativeWrapperMacosDestination = join(
2723
2844
  appBundleMacOSPath,
@@ -2737,6 +2858,9 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
2737
2858
  } else if (targetOS === "linux") {
2738
2859
  // Choose the appropriate native wrapper based on bundleCEF setting
2739
2860
  const useCEF = config.build.linux?.bundleCEF;
2861
+ cpSync(targetPaths.CORE_LINUX, join(appBundleMacOSPath, "libElectrobunCore.so"), {
2862
+ dereference: true,
2863
+ });
2740
2864
  const nativeWrapperLinuxSource = useCEF
2741
2865
  ? targetPaths.NATIVE_WRAPPER_LINUX_CEF
2742
2866
  : targetPaths.NATIVE_WRAPPER_LINUX;
@@ -2822,13 +2946,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
2822
2946
  });
2823
2947
 
2824
2948
  // cef helpers
2825
- const cefHelperNames = [
2826
- "bun Helper",
2827
- "bun Helper (Alerts)",
2828
- "bun Helper (GPU)",
2829
- "bun Helper (Plugin)",
2830
- "bun Helper (Renderer)",
2831
- ];
2949
+ const cefHelperNames = getCEFHelperNames();
2832
2950
 
2833
2951
  const helperSourcePath = targetPaths.CEF_HELPER_MACOS;
2834
2952
  cefHelperNames.forEach((helperName) => {
@@ -2909,13 +3027,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
2909
3027
  }
2910
3028
 
2911
3029
  // Copy CEF helper processes with different names
2912
- const cefHelperNames = [
2913
- "bun Helper",
2914
- "bun Helper (Alerts)",
2915
- "bun Helper (GPU)",
2916
- "bun Helper (Plugin)",
2917
- "bun Helper (Renderer)",
2918
- ];
3030
+ const cefHelperNames = getCEFHelperNames();
2919
3031
 
2920
3032
  const helperSourcePath = targetPaths.CEF_HELPER_WIN;
2921
3033
  if (existsSync(helperSourcePath)) {
@@ -3047,13 +3159,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3047
3159
  });
3048
3160
 
3049
3161
  // Copy CEF helper processes with different names
3050
- const cefHelperNames = [
3051
- "bun Helper",
3052
- "bun Helper (Alerts)",
3053
- "bun Helper (GPU)",
3054
- "bun Helper (Plugin)",
3055
- "bun Helper (Renderer)",
3056
- ];
3162
+ const cefHelperNames = getCEFHelperNames();
3057
3163
 
3058
3164
  const helperSourcePath = targetPaths.CEF_HELPER_LINUX;
3059
3165
  if (existsSync(helperSourcePath)) {
@@ -3222,22 +3328,25 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3222
3328
  }
3223
3329
  } // end if (!isCarrotOnly)
3224
3330
 
3225
- // transpile developer's bun code
3226
- const bunDestFolder = join(appBundleAppCodePath, "bun");
3227
- // Build bun-javascript ts files
3228
- const { entrypoint: _bunEntrypoint, ...bunBuildOptions } = bunConfig;
3229
- const buildResult = await Bun.build({
3230
- ...bunBuildOptions,
3231
- entrypoints: [bunSource],
3232
- outdir: bunDestFolder,
3233
- // minify: true, // todo (yoav): add minify in canary and prod builds
3234
- target: "bun",
3235
- });
3331
+ if (mainProcess === "cottontail") {
3332
+ // Keep the existing app/bun layout as a source-compatible SDK detail.
3333
+ const mainDestFolder = join(appBundleAppCodePath, "bun");
3334
+ const {
3335
+ entrypoint: _cottontailEntrypoint,
3336
+ ...cottontailBuildOptions
3337
+ } = cottontailConfig;
3338
+ const buildResult = await Bun.build({
3339
+ ...cottontailBuildOptions,
3340
+ entrypoints: [cottontailSource],
3341
+ outdir: mainDestFolder,
3342
+ target: "bun",
3343
+ });
3236
3344
 
3237
- if (!buildResult.success) {
3238
- console.error("failed to build", bunSource);
3239
- printBuildLogs(buildResult.logs);
3240
- throw new Error("Build failed: bun build failed");
3345
+ if (!buildResult.success) {
3346
+ console.error("failed to build", cottontailSource);
3347
+ printBuildLogs(buildResult.logs);
3348
+ throw new Error("Build failed: Cottontail main-process bundle failed");
3349
+ }
3241
3350
  }
3242
3351
 
3243
3352
  // transpile developer's view code
@@ -3322,11 +3431,14 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3322
3431
  }
3323
3432
  mkdirSync(carrotBuildDir, { recursive: true });
3324
3433
 
3325
- // Copy the bun bundle as worker.js
3326
- const bunOutputDir = join(appBundleAppCodePath, "bun");
3434
+ // Copy the Cottontail main-process bundle as worker.js.
3435
+ const mainOutputDir = join(appBundleAppCodePath, "bun");
3327
3436
  // The output filename matches the entrypoint name (e.g., worker.ts → worker.js)
3328
- const bunEntryName = basename(config.build.bun.entrypoint).replace(/\.ts$/, ".js");
3329
- const workerSrc = join(bunOutputDir, bunEntryName);
3437
+ const mainEntryName = basename(cottontailConfig.entrypoint).replace(
3438
+ /\.ts$/,
3439
+ ".js",
3440
+ );
3441
+ const workerSrc = join(mainOutputDir, mainEntryName);
3330
3442
  if (existsSync(workerSrc)) {
3331
3443
  cpSync(workerSrc, join(carrotBuildDir, "worker.js"));
3332
3444
  }
@@ -3402,6 +3514,55 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3402
3514
  }
3403
3515
  }
3404
3516
 
3517
+ // Build slate UIs if configured.
3518
+ // slateUIs mirrors remoteUIs, but points at an ESM module entry file instead of an HTML page.
3519
+ const resolvedSlateUIs: Record<string, { name: string; path: string }> = {};
3520
+ if (carrotConfig.slateUIs) {
3521
+ for (const slateUIName in carrotConfig.slateUIs) {
3522
+ const slateUIConfig = carrotConfig.slateUIs[slateUIName]!;
3523
+ const label = slateUIConfig.name || slateUIName;
3524
+
3525
+ if (slateUIConfig.entrypoint) {
3526
+ const slateUISource = join(projectRoot, slateUIConfig.entrypoint);
3527
+ if (!existsSync(slateUISource)) {
3528
+ console.error(`Slate UI entrypoint not found: ${slateUISource}`);
3529
+ continue;
3530
+ }
3531
+ const slateUIDestFolder = join(carrotBuildDir, "slate-ui", slateUIName);
3532
+ mkdirSync(slateUIDestFolder, { recursive: true });
3533
+
3534
+ const { entrypoint: _entrypoint, name: _name, path: _path, ...slateUIBuildOptions } = slateUIConfig;
3535
+ const slateUIBuildResult = await Bun.build({
3536
+ ...slateUIBuildOptions,
3537
+ entrypoints: [slateUISource],
3538
+ outdir: slateUIDestFolder,
3539
+ target: "browser",
3540
+ format: "esm",
3541
+ });
3542
+
3543
+ if (!slateUIBuildResult.success) {
3544
+ console.error(`Failed to build slate UI: ${slateUIName}`);
3545
+ printBuildLogs(slateUIBuildResult.logs);
3546
+ continue;
3547
+ }
3548
+
3549
+ resolvedSlateUIs[slateUIName] = {
3550
+ name: label,
3551
+ path: `slate-ui/${slateUIName}/index.js`,
3552
+ };
3553
+ } else if (slateUIConfig.path) {
3554
+ resolvedSlateUIs[slateUIName] = {
3555
+ name: label,
3556
+ path: slateUIConfig.path,
3557
+ };
3558
+ } else {
3559
+ console.warn(
3560
+ `Slate UI "${slateUIName}" has neither entrypoint nor path; skipping.`,
3561
+ );
3562
+ }
3563
+ }
3564
+ }
3565
+
3405
3566
  // Write carrot.json manifest
3406
3567
  const carrotManifest = {
3407
3568
  id: carrotConfig.id,
@@ -3409,7 +3570,6 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3409
3570
  version: config.app.version,
3410
3571
  description: carrotConfig.description || config.app.description || "",
3411
3572
  mode: carrotConfig.mode || "window",
3412
- permissions: carrotConfig.permissions || {},
3413
3573
  dependencies: Object.fromEntries(
3414
3574
  Object.entries(carrotConfig.dependencies || {}).map(([id, spec]) => [
3415
3575
  id,
@@ -3419,9 +3579,15 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3419
3579
  : spec,
3420
3580
  ]),
3421
3581
  ),
3582
+ contributions:
3583
+ carrotConfig.contributions &&
3584
+ Object.keys(carrotConfig.contributions).length > 0
3585
+ ? carrotConfig.contributions
3586
+ : undefined,
3422
3587
  worker: { relativePath: "worker.js" },
3423
3588
  view: existsSync(viewsSrc) ? { relativePath: "views/index.html" } : undefined,
3424
3589
  remoteUIs: Object.keys(resolvedRemoteUIs).length > 0 ? resolvedRemoteUIs : undefined,
3590
+ slateUIs: Object.keys(resolvedSlateUIs).length > 0 ? resolvedSlateUIs : undefined,
3425
3591
  };
3426
3592
  writeFileSync(
3427
3593
  join(carrotBuildDir, "carrot.json"),
@@ -3637,7 +3803,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3637
3803
  // so we're being explicit here.
3638
3804
  hash = Bun.hash.wyhash(archiveBytes, 43770n).toString(36);
3639
3805
  } else {
3640
- // Fallback for older Bun versions - use a simple hash of file paths
3806
+ // Fallback for runtimes without Bun.Archive compatibility.
3641
3807
  console.warn("Bun.Archive not available, using fallback hash method");
3642
3808
  const fileList = Object.keys(bundleFiles).sort().join("\n");
3643
3809
  hash = Bun.hash.wyhash(fileList).toString(36);
@@ -3645,9 +3811,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3645
3811
  console.timeEnd("Generate Bundle hash");
3646
3812
  }
3647
3813
 
3648
- // const bunVersion = execSync(`${bunBinarySourcePath} --version`).toString().trim();
3649
-
3650
- // version.json inside the app bundle
3814
+ // version.json inside the app bundle
3651
3815
  const versionJsonContent = JSON.stringify({
3652
3816
  version: config.app.version,
3653
3817
  // The first tar file does not include this, it gets hashed,
@@ -3676,14 +3840,16 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3676
3840
  const bundlesCEF = platformConfig?.bundleCEF ?? false;
3677
3841
 
3678
3842
  const buildJsonObj: Record<string, unknown> = {
3843
+ mainProcess,
3679
3844
  defaultRenderer: platformConfig?.defaultRenderer ?? "native",
3680
3845
  availableRenderers: bundlesCEF ? ["native", "cef"] : ["native"],
3681
3846
  runtime: config.runtime ?? {},
3682
3847
  ...(bundlesCEF
3683
3848
  ? { cefVersion: config.build?.cefVersion ?? DEFAULT_CEF_VERSION_STRING }
3684
3849
  : {}),
3685
- bunVersion: config.build?.bunVersion ?? BUN_VERSION,
3686
- ...(config.build?.bunnyBun ? { bunnyBun: config.build.bunnyBun } : {}),
3850
+ ...(mainProcess === "rust" ? { rustVersion: RUST_VERSION } : {}),
3851
+ ...(mainProcess === "go" ? { goVersion: GO_VERSION } : {}),
3852
+ ...(mainProcess === "odin" ? { odinVersion: ODIN_VERSION } : {}),
3687
3853
  };
3688
3854
 
3689
3855
  // Include chromiumFlags only if the developer defined them
@@ -3720,9 +3886,6 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3720
3886
  console.log("skipping codesign");
3721
3887
  }
3722
3888
 
3723
- // codesign
3724
- // NOTE: Codesigning fails in dev mode (when using a single-file-executable bun cli as the launcher)
3725
- // see https://github.com/oven-sh/bun/issues/7208
3726
3889
  if (shouldNotarize) {
3727
3890
  notarizeAndStaple(appBundleFolderPath, config);
3728
3891
  } else {
@@ -3760,10 +3923,9 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3760
3923
  // Tar the app bundle for all platforms
3761
3924
  createTar(tarPath, buildFolder, [basename(appBundleFolderPath)]);
3762
3925
 
3763
- // Remove the app bundle folder after tarring (except on Linux where it might be needed for dev)
3764
- if (targetOS !== "linux" || buildEnvironment !== "dev") {
3765
- rmSync(appBundleFolderPath, { recursive: true });
3766
- }
3926
+ // This branch only runs for non-dev release packaging, so the temp app bundle
3927
+ // can always be removed after the tarball is produced.
3928
+ rmSync(appBundleFolderPath, { recursive: true });
3767
3929
 
3768
3930
  // generate bsdiff
3769
3931
  // https://storage.googleapis.com/eggbun-static/electrobun-playground/canary/ElectrobunPlayground-canary.app.tar.zst
@@ -4422,6 +4584,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
4422
4584
  console.log(
4423
4585
  "\n[electrobun dev] Shutting down gracefully... (press Ctrl+C again to force quit)",
4424
4586
  );
4587
+ handle.kill();
4425
4588
  } else {
4426
4589
  console.log("\n[electrobun dev] Force quitting...");
4427
4590
  try {
@@ -4442,9 +4605,34 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
4442
4605
  // Collect watch directories from config entrypoints
4443
4606
  const watchDirs = new Set<string>();
4444
4607
 
4445
- // Bun entrypoint directory
4446
- if (config.build.bun?.entrypoint) {
4447
- watchDirs.add(join(projectRoot, dirname(config.build.bun.entrypoint)));
4608
+ // Cottontail entrypoint directory
4609
+ if (
4610
+ config.build.mainProcess === "cottontail" &&
4611
+ config.build.cottontail?.entrypoint
4612
+ ) {
4613
+ watchDirs.add(
4614
+ join(projectRoot, dirname(config.build.cottontail.entrypoint)),
4615
+ );
4616
+ }
4617
+
4618
+ // Zig entrypoint directory
4619
+ if (config.build.mainProcess === "zig" && config.build.zig?.entrypoint) {
4620
+ watchDirs.add(join(projectRoot, dirname(config.build.zig.entrypoint)));
4621
+ }
4622
+
4623
+ // Rust entrypoint directory
4624
+ if (config.build.mainProcess === "rust" && config.build.rust?.entrypoint) {
4625
+ watchDirs.add(join(projectRoot, dirname(config.build.rust.entrypoint)));
4626
+ }
4627
+
4628
+ // Go entrypoint directory
4629
+ if (config.build.mainProcess === "go" && config.build.go?.entrypoint) {
4630
+ watchDirs.add(join(projectRoot, dirname(config.build.go.entrypoint)));
4631
+ }
4632
+
4633
+ // Odin entrypoint directory
4634
+ if (config.build.mainProcess === "odin" && config.build.odin?.entrypoint) {
4635
+ watchDirs.add(join(projectRoot, dirname(config.build.odin.entrypoint)));
4448
4636
  }
4449
4637
 
4450
4638
  // View entrypoint directories
@@ -4526,17 +4714,37 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
4526
4714
  );
4527
4715
 
4528
4716
  function shouldIgnore(fullPath: string): boolean {
4717
+ const resolvedFullPath = path.resolve(fullPath);
4718
+ const pathSegments = resolvedFullPath.split(path.sep).filter(Boolean);
4719
+ const genericIgnoredSegments = new Set([
4720
+ "node_modules",
4721
+ path.basename(buildDir),
4722
+ path.basename(artifactDir),
4723
+ ".electrobun-cache",
4724
+ ]);
4725
+ if (pathSegments.some((segment) => genericIgnoredSegments.has(segment))) {
4726
+ return true;
4727
+ }
4529
4728
  // Check built-in ignore dirs
4530
4729
  if (
4531
4730
  ignoreDirs.some(
4532
- (ignored) =>
4533
- fullPath.startsWith(ignored + "/") || fullPath === ignored,
4731
+ (ignored) => {
4732
+ const relativeToIgnored = path.relative(ignored, resolvedFullPath);
4733
+ return (
4734
+ relativeToIgnored === "" ||
4735
+ (!relativeToIgnored.startsWith("..") &&
4736
+ !path.isAbsolute(relativeToIgnored))
4737
+ );
4738
+ },
4534
4739
  )
4535
4740
  ) {
4536
4741
  return true;
4537
4742
  }
4538
4743
  // Check user-configured watchIgnore globs (match against project-relative path)
4539
- const relativePath = fullPath.replace(projectRoot + "/", "");
4744
+ const relativePath = path
4745
+ .relative(projectRoot, resolvedFullPath)
4746
+ .split(path.sep)
4747
+ .join("/");
4540
4748
  if (ignoreGlobs.some((glob) => glob.match(relativePath))) {
4541
4749
  return true;
4542
4750
  }
@@ -4756,6 +4964,8 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
4756
4964
  }
4757
4965
  }
4758
4966
 
4967
+ assertNoLegacyBunMainProcessConfig(loadedConfig);
4968
+
4759
4969
  // todo (yoav): write a deep clone fn
4760
4970
  return {
4761
4971
  ...defaultConfig,
@@ -4783,9 +4993,21 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
4783
4993
  ...defaultConfig.build.linux,
4784
4994
  ...(loadedConfig?.build?.linux || {}),
4785
4995
  },
4786
- bun: {
4787
- ...defaultConfig.build.bun,
4788
- ...(loadedConfig?.build?.bun || {}),
4996
+ cottontail: {
4997
+ ...defaultConfig.build.cottontail,
4998
+ ...(loadedConfig?.build?.cottontail || {}),
4999
+ },
5000
+ zig: {
5001
+ ...defaultConfig.build.zig,
5002
+ ...(loadedConfig?.build?.zig || {}),
5003
+ },
5004
+ rust: {
5005
+ ...defaultConfig.build.rust,
5006
+ ...(loadedConfig?.build?.rust || {}),
5007
+ },
5008
+ go: {
5009
+ ...defaultConfig.build.go,
5010
+ ...(loadedConfig?.build?.go || {}),
4789
5011
  },
4790
5012
  },
4791
5013
  runtime: {
@@ -5084,13 +5306,9 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
5084
5306
  }
5085
5307
 
5086
5308
  // Sign CEF helper apps (they're in the main Frameworks directory, not inside CEF framework)
5087
- const cefHelperApps = [
5088
- "bun Helper.app",
5089
- "bun Helper (GPU).app",
5090
- "bun Helper (Plugin).app",
5091
- "bun Helper (Alerts).app",
5092
- "bun Helper (Renderer).app",
5093
- ];
5309
+ const cefHelperApps = getCEFHelperNames().map(
5310
+ (helperName) => `${helperName}.app`,
5311
+ );
5094
5312
 
5095
5313
  for (const helperApp of cefHelperApps) {
5096
5314
  const helperPath = join(frameworksPath, helperApp);
@@ -5193,7 +5411,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
5193
5411
  }
5194
5412
  }
5195
5413
 
5196
- // Sign .node native modules in Resources/app/bun
5414
+ // Sign native modules in the source-compatible Resources/app/bun path.
5197
5415
  const resourcesPath = join(contentsPath, "Resources", "app", "bun");
5198
5416
  if (existsSync(resourcesPath)) {
5199
5417
  console.log("Signing native modules in Resources/app/bun...");
@@ -5256,10 +5474,6 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
5256
5474
  }
5257
5475
 
5258
5476
  let fileToNotarize = appOrDmgPath;
5259
- // codesign
5260
- // NOTE: Codesigning fails in dev mode (when using a single-file-executable bun cli as the launcher)
5261
- // see https://github.com/oven-sh/bun/issues/7208
5262
- // if (shouldNotarize) {
5263
5477
  console.log("notarizing...");
5264
5478
  const zipPath = appOrDmgPath + ".zip";
5265
5479
  // if (appOrDmgPath.endsWith('.app')) {