electrobun 1.18.1 → 1.18.4-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.
package/src/cli/index.ts CHANGED
@@ -111,6 +111,9 @@ function getPlatformPaths(
111
111
  BUN_BINARY: join(platformDistDir, "bun") + binExt,
112
112
  LAUNCHER_DEV: join(platformDistDir, "electrobun") + binExt,
113
113
  LAUNCHER_RELEASE: join(platformDistDir, "launcher") + binExt,
114
+ CORE_MACOS: join(platformDistDir, "libElectrobunCore.dylib"),
115
+ CORE_WIN: join(platformDistDir, "ElectrobunCore.dll"),
116
+ CORE_LINUX: join(platformDistDir, "libElectrobunCore.so"),
114
117
  NATIVE_WRAPPER_MACOS: join(platformDistDir, "libNativeWrapper.dylib"),
115
118
  NATIVE_WRAPPER_WIN: join(platformDistDir, "libNativeWrapper.dll"),
116
119
  NATIVE_WRAPPER_LINUX: join(platformDistDir, "libNativeWrapper.so"),
@@ -134,6 +137,8 @@ function getPlatformPaths(
134
137
  // These work with existing package.json and development workflow
135
138
  MAIN_JS: join(sharedDistDir, "main.js"),
136
139
  API_DIR: join(sharedDistDir, "api"),
140
+ PRELOAD_FULL_JS: join(sharedDistDir, "preload-full.js"),
141
+ PRELOAD_SANDBOXED_JS: join(sharedDistDir, "preload-sandboxed.js"),
137
142
  };
138
143
  }
139
144
 
@@ -141,6 +146,135 @@ function getPlatformPaths(
141
146
  // @ts-expect-error - reserved for future use
142
147
  const _PATHS = getPlatformPaths(OS, ARCH);
143
148
 
149
+ function getVendoredZigBinaryPath(): string {
150
+ return join(
151
+ ELECTROBUN_DEP_PATH,
152
+ "vendors",
153
+ "zig",
154
+ OS === "win" ? "zig.exe" : "zig",
155
+ );
156
+ }
157
+
158
+ function getZigTarget(
159
+ targetOS: "macos" | "win" | "linux",
160
+ targetArch: "arm64" | "x64",
161
+ ): string {
162
+ if (targetOS === "win") {
163
+ return "x86_64-windows";
164
+ }
165
+ if (targetOS === "linux") {
166
+ return targetArch === "arm64" ? "aarch64-linux" : "x86_64-linux";
167
+ }
168
+ return targetArch === "arm64" ? "aarch64-macos" : "x86_64-macos";
169
+ }
170
+
171
+ function getCEFHelperBaseName(mainProcess: "bun" | "zig"): string {
172
+ return mainProcess === "zig" ? "main" : "bun";
173
+ }
174
+
175
+ function getCEFHelperNames(mainProcess: "bun" | "zig"): string[] {
176
+ const baseName = getCEFHelperBaseName(mainProcess);
177
+ return [
178
+ `${baseName} Helper`,
179
+ `${baseName} Helper (Alerts)`,
180
+ `${baseName} Helper (GPU)`,
181
+ `${baseName} Helper (Plugin)`,
182
+ `${baseName} Helper (Renderer)`,
183
+ ];
184
+ }
185
+
186
+ async function buildZigMainExecutable(options: {
187
+ entrypoint: string;
188
+ buildFolder: string;
189
+ targetOS: "macos" | "win" | "linux";
190
+ targetArch: "arm64" | "x64";
191
+ buildEnvironment: "dev" | "canary" | "stable";
192
+ }) {
193
+ const zigBinary = getVendoredZigBinaryPath();
194
+ if (!existsSync(zigBinary)) {
195
+ throw new Error(
196
+ `Vendored Zig compiler not found at ${zigBinary}. Rebuild electrobun/package so vendors/zig is available.`,
197
+ );
198
+ }
199
+
200
+ const zigSdkPath = join(ELECTROBUN_DEP_PATH, "dist", "zig-sdk", "electrobun.zig");
201
+ if (!existsSync(zigSdkPath)) {
202
+ throw new Error(`Electrobun Zig SDK not found at ${zigSdkPath}`);
203
+ }
204
+
205
+ const binExt = options.targetOS === "win" ? ".exe" : "";
206
+ const tempBuildDir = join(
207
+ options.buildFolder,
208
+ ".electrobun-zig-main",
209
+ `${options.targetOS}-${options.targetArch}`,
210
+ );
211
+ const relativeZigSdkPath = path.relative(tempBuildDir, zigSdkPath) || ".";
212
+ const relativeEntrypointPath = path.relative(tempBuildDir, options.entrypoint) || ".";
213
+ const zigOutBin = join(tempBuildDir, "zig-out", "bin", "main" + binExt);
214
+ const buildScriptPath = join(tempBuildDir, "build.zig");
215
+ mkdirSync(tempBuildDir, { recursive: true });
216
+
217
+ const buildScript = `const std = @import("std");
218
+
219
+ pub fn build(b: *std.Build) void {
220
+ const target = b.standardTargetOptions(.{});
221
+ const optimize = b.standardOptimizeOption(.{});
222
+
223
+ const electrobun = b.createModule(.{
224
+ .root_source_file = b.path(${JSON.stringify(relativeZigSdkPath)}),
225
+ });
226
+
227
+ const exe = b.addExecutable(.{
228
+ .name = "main",
229
+ .root_source_file = b.path(${JSON.stringify(relativeEntrypointPath)}),
230
+ .target = target,
231
+ .optimize = optimize,
232
+ });
233
+
234
+ exe.root_module.addImport("electrobun", electrobun);
235
+ exe.linkLibC();
236
+ b.installArtifact(exe);
237
+ }
238
+ `;
239
+ writeFileSync(buildScriptPath, buildScript, "utf8");
240
+
241
+ const zigArgs = [
242
+ "build",
243
+ `-Dtarget=${getZigTarget(options.targetOS, options.targetArch)}`,
244
+ ];
245
+
246
+ if (options.targetOS === "win") {
247
+ zigArgs.push("-Dcpu=baseline");
248
+ }
249
+
250
+ if (options.buildEnvironment !== "dev") {
251
+ zigArgs.push("-Doptimize=ReleaseSmall");
252
+ }
253
+
254
+ const result = Bun.spawnSync([zigBinary, ...zigArgs], {
255
+ cwd: tempBuildDir,
256
+ stdio: ["ignore", "pipe", "pipe"],
257
+ });
258
+
259
+ if (result.exitCode !== 0) {
260
+ const stdout = result.stdout ? new TextDecoder().decode(result.stdout) : "";
261
+ const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : "";
262
+ if (stdout.trim()) {
263
+ console.error(stdout);
264
+ }
265
+ if (stderr.trim()) {
266
+ console.error(stderr);
267
+ }
268
+ throw new Error("Build failed: zig main process compilation failed");
269
+ }
270
+
271
+ if (!existsSync(zigOutBin)) {
272
+ throw new Error(`Zig main process binary was not produced at ${zigOutBin}`);
273
+ }
274
+
275
+ return zigOutBin;
276
+ }
277
+
144
278
  async function ensureCoreDependencies(
145
279
  targetOS?: "macos" | "win" | "linux",
146
280
  targetArch?: "arm64" | "x64",
@@ -1483,6 +1617,7 @@ const defaultConfig = {
1483
1617
  build: {
1484
1618
  buildFolder: "build",
1485
1619
  artifactFolder: "artifacts",
1620
+ mainProcess: "bun" as "bun" | "zig",
1486
1621
  useAsar: false,
1487
1622
  asarUnpack: undefined as string[] | undefined, // Glob patterns for files to exclude from ASAR (e.g., ["*.node", "*.dll"])
1488
1623
  cefVersion: undefined as string | undefined, // Override CEF version: "CEF_VERSION+chromium-CHROMIUM_VERSION"
@@ -1524,6 +1659,9 @@ const defaultConfig = {
1524
1659
  bun: {
1525
1660
  entrypoint: "src/bun/index.ts",
1526
1661
  },
1662
+ zig: {
1663
+ entrypoint: "src/zig/main.zig",
1664
+ },
1527
1665
  views: undefined as
1528
1666
  | Record<string, { entrypoint: string; [key: string]: unknown }>
1529
1667
  | undefined,
@@ -1548,6 +1686,24 @@ const defaultConfig = {
1548
1686
  path?: string;
1549
1687
  [key: string]: unknown;
1550
1688
  }>;
1689
+ slateUIs?: Record<string, {
1690
+ name?: string;
1691
+ entrypoint?: string;
1692
+ path?: string;
1693
+ [key: string]: unknown;
1694
+ }>;
1695
+ contributions?: {
1696
+ fileActivators?: Array<{
1697
+ baseName?: string;
1698
+ nodeType?: "file" | "dir" | "any";
1699
+ slate: {
1700
+ type: string;
1701
+ name?: string;
1702
+ icon?: string;
1703
+ config?: Record<string, unknown>;
1704
+ };
1705
+ }>;
1706
+ };
1551
1707
  carrotOnly?: boolean;
1552
1708
  } | undefined,
1553
1709
  },
@@ -2078,8 +2234,10 @@ ${utiDecls}
2078
2234
  // Get environment
2079
2235
  const envArg =
2080
2236
  process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
2081
- const buildEnvironment = ["dev", "canary", "stable"].includes(envArg)
2082
- ? envArg
2237
+ const buildEnvironment: "dev" | "canary" | "stable" = ["dev", "canary", "stable"].includes(
2238
+ envArg,
2239
+ )
2240
+ ? (envArg as "dev" | "canary" | "stable")
2083
2241
  : "dev";
2084
2242
 
2085
2243
  try {
@@ -2110,7 +2268,7 @@ ${utiDecls}
2110
2268
 
2111
2269
  async function runBuild(
2112
2270
  config: Awaited<ReturnType<typeof getConfig>>,
2113
- buildEnvironment: string,
2271
+ buildEnvironment: "dev" | "canary" | "stable",
2114
2272
  ) {
2115
2273
  // Determine current platform as default target
2116
2274
  const currentTarget = { os: OS, arch: ARCH };
@@ -2392,20 +2550,34 @@ Categories=Utility;Application;
2392
2550
 
2393
2551
  // refresh build folder
2394
2552
  if (existsSync(buildFolder)) {
2395
- rmSync(buildFolder, { recursive: true });
2553
+ rmSync(buildFolder, { recursive: true, force: true });
2396
2554
  }
2397
2555
  mkdirSync(buildFolder, { recursive: true });
2398
- // bundle bun to build/bun
2556
+
2557
+ const mainProcess = config.build.mainProcess ?? "bun";
2399
2558
  const bunConfig = config.build.bun;
2400
2559
  const bunSource = join(projectRoot, bunConfig.entrypoint);
2560
+ const zigConfig = config.build.zig;
2561
+ const zigSource = join(projectRoot, zigConfig.entrypoint);
2401
2562
 
2402
- if (!existsSync(bunSource)) {
2563
+ if (mainProcess === "bun") {
2564
+ if (!existsSync(bunSource)) {
2565
+ throw new Error(
2566
+ `failed to bundle ${bunSource} because it doesn't exist.\n You need a config.build.bun.entrypoint source file to build.`,
2567
+ );
2568
+ }
2569
+ } else if (!existsSync(zigSource)) {
2403
2570
  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.`,
2571
+ `failed to compile ${zigSource} because it doesn't exist.\n You need a config.build.zig.entrypoint source file to build.`,
2405
2572
  );
2406
2573
  }
2407
2574
 
2408
2575
  const isCarrotOnly = config.build.carrot?.carrotOnly === true;
2576
+ if (config.build.carrot && mainProcess === "zig") {
2577
+ throw new Error(
2578
+ `build.carrot is not supported with build.mainProcess = "zig" yet.`,
2579
+ );
2580
+ }
2409
2581
 
2410
2582
  // build macos bundle
2411
2583
  // Use display name (with spaces) for macOS bundle folders, sanitized name for other platforms
@@ -2438,6 +2610,17 @@ Categories=Utility;Application;
2438
2610
  mkdirSync(appBundleAppCodePath, { recursive: true });
2439
2611
  }
2440
2612
 
2613
+ let zigMainBinarySourcePath: string | null = null;
2614
+ if (mainProcess === "zig") {
2615
+ zigMainBinarySourcePath = await buildZigMainExecutable({
2616
+ entrypoint: zigSource,
2617
+ buildFolder,
2618
+ targetOS,
2619
+ targetArch: targetARCH,
2620
+ buildEnvironment,
2621
+ });
2622
+ }
2623
+
2441
2624
  // const bundledBunPath = join(appBundleMacOSPath, 'bun');
2442
2625
  // cpSync(bunPath, bundledBunPath);
2443
2626
 
@@ -2610,105 +2793,121 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
2610
2793
  }
2611
2794
  }
2612
2795
 
2613
- cpSync(targetPaths.MAIN_JS, join(appBundleFolderResourcesPath, "main.js"), {
2796
+ cpSync(targetPaths.PRELOAD_FULL_JS, join(appBundleFolderResourcesPath, "preload-full.js"), {
2614
2797
  dereference: true,
2615
2798
  });
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,
2799
+ cpSync(
2800
+ targetPaths.PRELOAD_SANDBOXED_JS,
2801
+ join(appBundleFolderResourcesPath, "preload-sandboxed.js"),
2802
+ {
2803
+ dereference: true,
2804
+ },
2624
2805
  );
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
2806
 
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
- }
2807
+ if (mainProcess === "bun") {
2808
+ cpSync(targetPaths.MAIN_JS, join(appBundleFolderResourcesPath, "main.js"), {
2809
+ dereference: true,
2810
+ });
2665
2811
 
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);
2812
+ // Bun runtime binary
2813
+ // todo (yoav): this only works for the current architecture
2814
+ const bunBinarySourcePath = await ensureBunBinary(
2815
+ currentTarget.os,
2816
+ currentTarget.arch,
2817
+ config.build.bunVersion,
2818
+ config.build.bunnyBun,
2819
+ );
2820
+ // Note: .bin/bun binary in node_modules is a symlink to the versioned one in another place
2821
+ // in node_modules, so we have to dereference here to get the actual binary in the bundle.
2822
+ const bunBinaryDestInBundlePath =
2823
+ join(appBundleMacOSPath, "bun") + targetBinExt;
2824
+ const destFolder2 = dirname(bunBinaryDestInBundlePath);
2825
+ if (!existsSync(destFolder2)) {
2826
+ mkdirSync(destFolder2, { recursive: true });
2827
+ }
2828
+ cpSync(bunBinarySourcePath, bunBinaryDestInBundlePath, {
2829
+ dereference: true,
2830
+ });
2673
2831
 
2674
- if (existsSync(iconSourcePath)) {
2675
- console.log(`Embedding icon into bun.exe: ${iconSourcePath}`);
2676
- try {
2677
- let iconPath = iconSourcePath;
2832
+ // Copy ICU data file if it exists (Linux/Windows external ICU builds)
2833
+ // ICU version varies per platform WebKit build, so detect the filename dynamically
2834
+ const bunDir = dirname(bunBinarySourcePath);
2835
+ const icuDataFileName = readdirSync(bunDir).find((f) => /^icudt\d+l\.dat$/.test(f));
2836
+ const icuDataSource = icuDataFileName ? join(bunDir, icuDataFileName) : "";
2837
+ if (icuDataFileName && existsSync(icuDataSource) && targetOS !== "macos") {
2838
+ const icuDataDest = join(appBundleMacOSPath, icuDataFileName);
2678
2839
 
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;
2840
+ const locales = config.build?.locales;
2841
+ if (locales && locales !== "*" && Array.isArray(locales) && locales.length > 0) {
2842
+ try {
2843
+ await trimICUData(icuDataSource, icuDataDest, locales);
2844
+ const originalSize = statSync(icuDataSource).size;
2845
+ const trimmedSize = statSync(icuDataDest).size;
2686
2846
  console.log(
2687
- `Converted PNG to ICO format for bun.exe: ${tempIcoPath}`,
2847
+ `Trimmed ICU data: ${(originalSize / 1024 / 1024).toFixed(1)}MB → ${(trimmedSize / 1024 / 1024).toFixed(1)}MB (locales: ${locales.join(", ")})`,
2688
2848
  );
2849
+ } catch (error) {
2850
+ console.warn(`Warning: Failed to trim ICU data, copying full file: ${error}`);
2851
+ cpSync(icuDataSource, icuDataDest);
2689
2852
  }
2853
+ } else {
2854
+ cpSync(icuDataSource, icuDataDest);
2855
+ console.log(`Copied ICU data file: ${icuDataFileName}`);
2856
+ }
2857
+ }
2690
2858
 
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`);
2859
+ // Embed icon into bun.exe on Windows
2860
+ if (targetOS === "win" && config.build.win?.icon) {
2861
+ const iconSourcePath =
2862
+ config.build.win.icon.startsWith("/") ||
2863
+ config.build.win.icon.match(/^[a-zA-Z]:/)
2864
+ ? config.build.win.icon
2865
+ : join(projectRoot, config.build.win.icon);
2699
2866
 
2700
- // Clean up temp ICO file
2701
- if (iconPath !== iconSourcePath && existsSync(iconPath)) {
2702
- unlinkSync(iconPath);
2867
+ if (existsSync(iconSourcePath)) {
2868
+ console.log(`Embedding icon into bun.exe: ${iconSourcePath}`);
2869
+ try {
2870
+ let iconPath = iconSourcePath;
2871
+
2872
+ if (iconSourcePath.toLowerCase().endsWith(".png")) {
2873
+ const pngToIco = (await import("png-to-ico")).default;
2874
+ const tempIcoPath = join(buildFolder, "temp-bun-icon.ico");
2875
+ const icoBuffer = await pngToIco(iconSourcePath);
2876
+ writeFileSync(tempIcoPath, new Uint8Array(icoBuffer));
2877
+ iconPath = tempIcoPath;
2878
+ console.log(
2879
+ `Converted PNG to ICO format for bun.exe: ${tempIcoPath}`,
2880
+ );
2881
+ }
2882
+
2883
+ const { execFileSync } = await import("child_process");
2884
+ const rceditPkgPath = require.resolve("rcedit/package.json");
2885
+ const rceditDir = dirname(rceditPkgPath);
2886
+ const rceditX64 = join(rceditDir, "bin", "rcedit-x64.exe");
2887
+ const rceditExe = existsSync(rceditX64) ? rceditX64 : join(rceditDir, "bin", "rcedit.exe");
2888
+ execFileSync(rceditExe, [bunBinaryDestInBundlePath, "--set-icon", iconPath]);
2889
+ console.log(`Successfully embedded icon into bun.exe`);
2890
+
2891
+ if (iconPath !== iconSourcePath && existsSync(iconPath)) {
2892
+ unlinkSync(iconPath);
2893
+ }
2894
+ } catch (error) {
2895
+ console.warn(`Warning: Failed to embed icon into bun.exe: ${error}`);
2703
2896
  }
2704
- } catch (error) {
2705
- console.warn(`Warning: Failed to embed icon into bun.exe: ${error}`);
2706
2897
  }
2707
2898
  }
2899
+ } else if (zigMainBinarySourcePath) {
2900
+ cpSync(zigMainBinarySourcePath, join(appBundleMacOSPath, "main") + targetBinExt, {
2901
+ dereference: true,
2902
+ });
2708
2903
  }
2709
2904
 
2710
2905
  // copy native wrapper dynamic library
2711
2906
  if (targetOS === "macos") {
2907
+ cpSync(targetPaths.CORE_MACOS, join(appBundleMacOSPath, "libElectrobunCore.dylib"), {
2908
+ dereference: true,
2909
+ });
2910
+
2712
2911
  const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_MACOS;
2713
2912
  const nativeWrapperMacosDestination = join(
2714
2913
  appBundleMacOSPath,
@@ -2718,6 +2917,10 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
2718
2917
  dereference: true,
2719
2918
  });
2720
2919
  } else if (targetOS === "win") {
2920
+ cpSync(targetPaths.CORE_WIN, join(appBundleMacOSPath, "ElectrobunCore.dll"), {
2921
+ dereference: true,
2922
+ });
2923
+
2721
2924
  const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_WIN;
2722
2925
  const nativeWrapperMacosDestination = join(
2723
2926
  appBundleMacOSPath,
@@ -2737,6 +2940,9 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
2737
2940
  } else if (targetOS === "linux") {
2738
2941
  // Choose the appropriate native wrapper based on bundleCEF setting
2739
2942
  const useCEF = config.build.linux?.bundleCEF;
2943
+ cpSync(targetPaths.CORE_LINUX, join(appBundleMacOSPath, "libElectrobunCore.so"), {
2944
+ dereference: true,
2945
+ });
2740
2946
  const nativeWrapperLinuxSource = useCEF
2741
2947
  ? targetPaths.NATIVE_WRAPPER_LINUX_CEF
2742
2948
  : targetPaths.NATIVE_WRAPPER_LINUX;
@@ -2822,13 +3028,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
2822
3028
  });
2823
3029
 
2824
3030
  // 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
- ];
3031
+ const cefHelperNames = getCEFHelperNames(mainProcess);
2832
3032
 
2833
3033
  const helperSourcePath = targetPaths.CEF_HELPER_MACOS;
2834
3034
  cefHelperNames.forEach((helperName) => {
@@ -2909,13 +3109,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
2909
3109
  }
2910
3110
 
2911
3111
  // 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
- ];
3112
+ const cefHelperNames = getCEFHelperNames(mainProcess);
2919
3113
 
2920
3114
  const helperSourcePath = targetPaths.CEF_HELPER_WIN;
2921
3115
  if (existsSync(helperSourcePath)) {
@@ -3047,13 +3241,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3047
3241
  });
3048
3242
 
3049
3243
  // 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
- ];
3244
+ const cefHelperNames = getCEFHelperNames(mainProcess);
3057
3245
 
3058
3246
  const helperSourcePath = targetPaths.CEF_HELPER_LINUX;
3059
3247
  if (existsSync(helperSourcePath)) {
@@ -3222,22 +3410,22 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3222
3410
  }
3223
3411
  } // end if (!isCarrotOnly)
3224
3412
 
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
- });
3413
+ if (mainProcess === "bun") {
3414
+ // transpile developer's bun code
3415
+ const bunDestFolder = join(appBundleAppCodePath, "bun");
3416
+ const { entrypoint: _bunEntrypoint, ...bunBuildOptions } = bunConfig;
3417
+ const buildResult = await Bun.build({
3418
+ ...bunBuildOptions,
3419
+ entrypoints: [bunSource],
3420
+ outdir: bunDestFolder,
3421
+ target: "bun",
3422
+ });
3236
3423
 
3237
- if (!buildResult.success) {
3238
- console.error("failed to build", bunSource);
3239
- printBuildLogs(buildResult.logs);
3240
- throw new Error("Build failed: bun build failed");
3424
+ if (!buildResult.success) {
3425
+ console.error("failed to build", bunSource);
3426
+ printBuildLogs(buildResult.logs);
3427
+ throw new Error("Build failed: bun build failed");
3428
+ }
3241
3429
  }
3242
3430
 
3243
3431
  // transpile developer's view code
@@ -3402,6 +3590,55 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3402
3590
  }
3403
3591
  }
3404
3592
 
3593
+ // Build slate UIs if configured.
3594
+ // slateUIs mirrors remoteUIs, but points at an ESM module entry file instead of an HTML page.
3595
+ const resolvedSlateUIs: Record<string, { name: string; path: string }> = {};
3596
+ if (carrotConfig.slateUIs) {
3597
+ for (const slateUIName in carrotConfig.slateUIs) {
3598
+ const slateUIConfig = carrotConfig.slateUIs[slateUIName]!;
3599
+ const label = slateUIConfig.name || slateUIName;
3600
+
3601
+ if (slateUIConfig.entrypoint) {
3602
+ const slateUISource = join(projectRoot, slateUIConfig.entrypoint);
3603
+ if (!existsSync(slateUISource)) {
3604
+ console.error(`Slate UI entrypoint not found: ${slateUISource}`);
3605
+ continue;
3606
+ }
3607
+ const slateUIDestFolder = join(carrotBuildDir, "slate-ui", slateUIName);
3608
+ mkdirSync(slateUIDestFolder, { recursive: true });
3609
+
3610
+ const { entrypoint: _entrypoint, name: _name, path: _path, ...slateUIBuildOptions } = slateUIConfig;
3611
+ const slateUIBuildResult = await Bun.build({
3612
+ ...slateUIBuildOptions,
3613
+ entrypoints: [slateUISource],
3614
+ outdir: slateUIDestFolder,
3615
+ target: "browser",
3616
+ format: "esm",
3617
+ });
3618
+
3619
+ if (!slateUIBuildResult.success) {
3620
+ console.error(`Failed to build slate UI: ${slateUIName}`);
3621
+ printBuildLogs(slateUIBuildResult.logs);
3622
+ continue;
3623
+ }
3624
+
3625
+ resolvedSlateUIs[slateUIName] = {
3626
+ name: label,
3627
+ path: `slate-ui/${slateUIName}/index.js`,
3628
+ };
3629
+ } else if (slateUIConfig.path) {
3630
+ resolvedSlateUIs[slateUIName] = {
3631
+ name: label,
3632
+ path: slateUIConfig.path,
3633
+ };
3634
+ } else {
3635
+ console.warn(
3636
+ `Slate UI "${slateUIName}" has neither entrypoint nor path; skipping.`,
3637
+ );
3638
+ }
3639
+ }
3640
+ }
3641
+
3405
3642
  // Write carrot.json manifest
3406
3643
  const carrotManifest = {
3407
3644
  id: carrotConfig.id,
@@ -3419,9 +3656,15 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3419
3656
  : spec,
3420
3657
  ]),
3421
3658
  ),
3659
+ contributions:
3660
+ carrotConfig.contributions &&
3661
+ Object.keys(carrotConfig.contributions).length > 0
3662
+ ? carrotConfig.contributions
3663
+ : undefined,
3422
3664
  worker: { relativePath: "worker.js" },
3423
3665
  view: existsSync(viewsSrc) ? { relativePath: "views/index.html" } : undefined,
3424
3666
  remoteUIs: Object.keys(resolvedRemoteUIs).length > 0 ? resolvedRemoteUIs : undefined,
3667
+ slateUIs: Object.keys(resolvedSlateUIs).length > 0 ? resolvedSlateUIs : undefined,
3425
3668
  };
3426
3669
  writeFileSync(
3427
3670
  join(carrotBuildDir, "carrot.json"),
@@ -3676,13 +3919,16 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3676
3919
  const bundlesCEF = platformConfig?.bundleCEF ?? false;
3677
3920
 
3678
3921
  const buildJsonObj: Record<string, unknown> = {
3922
+ mainProcess,
3679
3923
  defaultRenderer: platformConfig?.defaultRenderer ?? "native",
3680
3924
  availableRenderers: bundlesCEF ? ["native", "cef"] : ["native"],
3681
3925
  runtime: config.runtime ?? {},
3682
3926
  ...(bundlesCEF
3683
3927
  ? { cefVersion: config.build?.cefVersion ?? DEFAULT_CEF_VERSION_STRING }
3684
3928
  : {}),
3685
- bunVersion: config.build?.bunVersion ?? BUN_VERSION,
3929
+ ...(mainProcess === "bun"
3930
+ ? { bunVersion: config.build?.bunVersion ?? BUN_VERSION }
3931
+ : {}),
3686
3932
  ...(config.build?.bunnyBun ? { bunnyBun: config.build.bunnyBun } : {}),
3687
3933
  };
3688
3934
 
@@ -3760,10 +4006,9 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
3760
4006
  // Tar the app bundle for all platforms
3761
4007
  createTar(tarPath, buildFolder, [basename(appBundleFolderPath)]);
3762
4008
 
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
- }
4009
+ // This branch only runs for non-dev release packaging, so the temp app bundle
4010
+ // can always be removed after the tarball is produced.
4011
+ rmSync(appBundleFolderPath, { recursive: true });
3767
4012
 
3768
4013
  // generate bsdiff
3769
4014
  // https://storage.googleapis.com/eggbun-static/electrobun-playground/canary/ElectrobunPlayground-canary.app.tar.zst
@@ -4443,10 +4688,15 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
4443
4688
  const watchDirs = new Set<string>();
4444
4689
 
4445
4690
  // Bun entrypoint directory
4446
- if (config.build.bun?.entrypoint) {
4691
+ if (config.build.mainProcess !== "zig" && config.build.bun?.entrypoint) {
4447
4692
  watchDirs.add(join(projectRoot, dirname(config.build.bun.entrypoint)));
4448
4693
  }
4449
4694
 
4695
+ // Zig entrypoint directory
4696
+ if (config.build.mainProcess === "zig" && config.build.zig?.entrypoint) {
4697
+ watchDirs.add(join(projectRoot, dirname(config.build.zig.entrypoint)));
4698
+ }
4699
+
4450
4700
  // View entrypoint directories
4451
4701
  if (config.build.views) {
4452
4702
  for (const viewConfig of Object.values(config.build.views)) {
@@ -4526,17 +4776,37 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
4526
4776
  );
4527
4777
 
4528
4778
  function shouldIgnore(fullPath: string): boolean {
4779
+ const resolvedFullPath = path.resolve(fullPath);
4780
+ const pathSegments = resolvedFullPath.split(path.sep).filter(Boolean);
4781
+ const genericIgnoredSegments = new Set([
4782
+ "node_modules",
4783
+ path.basename(buildDir),
4784
+ path.basename(artifactDir),
4785
+ ".electrobun-cache",
4786
+ ]);
4787
+ if (pathSegments.some((segment) => genericIgnoredSegments.has(segment))) {
4788
+ return true;
4789
+ }
4529
4790
  // Check built-in ignore dirs
4530
4791
  if (
4531
4792
  ignoreDirs.some(
4532
- (ignored) =>
4533
- fullPath.startsWith(ignored + "/") || fullPath === ignored,
4793
+ (ignored) => {
4794
+ const relativeToIgnored = path.relative(ignored, resolvedFullPath);
4795
+ return (
4796
+ relativeToIgnored === "" ||
4797
+ (!relativeToIgnored.startsWith("..") &&
4798
+ !path.isAbsolute(relativeToIgnored))
4799
+ );
4800
+ },
4534
4801
  )
4535
4802
  ) {
4536
4803
  return true;
4537
4804
  }
4538
4805
  // Check user-configured watchIgnore globs (match against project-relative path)
4539
- const relativePath = fullPath.replace(projectRoot + "/", "");
4806
+ const relativePath = path
4807
+ .relative(projectRoot, resolvedFullPath)
4808
+ .split(path.sep)
4809
+ .join("/");
4540
4810
  if (ignoreGlobs.some((glob) => glob.match(relativePath))) {
4541
4811
  return true;
4542
4812
  }
@@ -4787,6 +5057,10 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
4787
5057
  ...defaultConfig.build.bun,
4788
5058
  ...(loadedConfig?.build?.bun || {}),
4789
5059
  },
5060
+ zig: {
5061
+ ...defaultConfig.build.zig,
5062
+ ...(loadedConfig?.build?.zig || {}),
5063
+ },
4790
5064
  },
4791
5065
  runtime: {
4792
5066
  ...defaultConfig.runtime,
@@ -5084,13 +5358,10 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
5084
5358
  }
5085
5359
 
5086
5360
  // 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
- ];
5361
+ const mainProcess = config.build.mainProcess ?? "bun";
5362
+ const cefHelperApps = getCEFHelperNames(mainProcess).map(
5363
+ (helperName) => `${helperName}.app`,
5364
+ );
5094
5365
 
5095
5366
  for (const helperApp of cefHelperApps) {
5096
5367
  const helperPath = join(frameworksPath, helperApp);