electrobun 1.16.0 → 1.17.0-beta.0

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.
@@ -1,3 +1,3 @@
1
1
  // Default Bun version shipped with this Electrobun release.
2
2
  // All platforms use the same version. Update this when bumping Bun.
3
- export const BUN_VERSION = "1.3.9";
3
+ export const BUN_VERSION = "1.3.11";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrobun",
3
- "version": "1.16.0",
3
+ "version": "1.17.0-beta.0",
4
4
  "description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
5
5
  "license": "MIT",
6
6
  "author": "Blackboard Technologies Inc.",
@@ -14,7 +14,8 @@
14
14
  "exports": {
15
15
  ".": "./dist/api/bun/index.ts",
16
16
  "./bun": "./dist/api/bun/index.ts",
17
- "./view": "./dist/api/browser/index.ts"
17
+ "./view": "./dist/api/browser/index.ts",
18
+ "./carrot": "./dist/api/carrot/bun.ts"
18
19
  },
19
20
  "type": "module",
20
21
  "bin": {
package/src/cli/index.ts CHANGED
@@ -482,35 +482,41 @@ async function ensureBunBinary(
482
482
  targetOS: "macos" | "win" | "linux",
483
483
  targetArch: "arm64" | "x64",
484
484
  bunVersion?: string,
485
+ bunnyBun?: string,
485
486
  ): Promise<string> {
486
- if (!bunVersion) {
487
+ const effectiveVersion = bunnyBun || bunVersion;
488
+ if (!effectiveVersion) {
487
489
  return getPlatformPaths(targetOS, targetArch).BUN_BINARY;
488
490
  }
489
491
 
490
492
  const binExt = targetOS === "win" ? ".exe" : "";
491
- const overrideDir = join(ELECTROBUN_CACHE_PATH, "bun-override", `${targetOS}-${targetArch}`);
493
+ const cacheSubdir = bunnyBun ? "bunny-bun-override" : "bun-override";
494
+ const overrideDir = join(ELECTROBUN_CACHE_PATH, cacheSubdir, `${targetOS}-${targetArch}`);
492
495
  const overrideBinary = join(overrideDir, `bun${binExt}`);
493
496
  const versionFile = join(overrideDir, ".bun-version");
494
497
 
495
498
  // Check if already downloaded with matching version
496
499
  if (existsSync(overrideBinary) && existsSync(versionFile)) {
497
500
  const cachedVersion = readFileSync(versionFile, "utf8").trim();
498
- if (cachedVersion === bunVersion) {
501
+ if (cachedVersion === effectiveVersion) {
499
502
  console.log(
500
- `Custom Bun ${bunVersion} already cached for ${targetOS}-${targetArch}`,
503
+ `${bunnyBun ? "Bunny" : "Custom"} Bun ${effectiveVersion} already cached for ${targetOS}-${targetArch}`,
501
504
  );
502
505
  return overrideBinary;
503
506
  }
504
- // Version mismatch - remove stale cache
505
507
  console.log(
506
- `Cached Bun version "${cachedVersion}" does not match requested "${bunVersion}", re-downloading...`,
508
+ `Cached Bun version "${cachedVersion}" does not match requested "${effectiveVersion}", re-downloading...`,
507
509
  );
508
510
  rmSync(overrideDir, { recursive: true, force: true });
509
511
  } else if (existsSync(overrideDir)) {
510
512
  rmSync(overrideDir, { recursive: true, force: true });
511
513
  }
512
514
 
513
- await downloadCustomBun(bunVersion, targetOS, targetArch);
515
+ if (bunnyBun) {
516
+ await downloadBunnyBun(bunnyBun, targetOS, targetArch);
517
+ } else {
518
+ await downloadCustomBun(effectiveVersion, targetOS, targetArch);
519
+ }
514
520
  return overrideBinary;
515
521
  }
516
522
 
@@ -664,6 +670,123 @@ async function downloadCustomBun(
664
670
  }
665
671
  }
666
672
 
673
+ /**
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.
677
+ */
678
+ async function downloadBunnyBun(
679
+ releaseTag: string,
680
+ platformOS: "macos" | "win" | "linux",
681
+ 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";
696
+ }
697
+
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
+ }
788
+ }
789
+
667
790
  async function ensureCEFDependencies(
668
791
  targetOS?: "macos" | "win" | "linux",
669
792
  targetArch?: "arm64" | "x64",
@@ -1357,6 +1480,7 @@ const defaultConfig = {
1357
1480
  cefVersion: undefined as string | undefined, // Override CEF version: "CEF_VERSION+chromium-CHROMIUM_VERSION"
1358
1481
  wgpuVersion: undefined as string | undefined, // Override Dawn (WebGPU) version: "0.2.3" or "v0.2.3-beta.0"
1359
1482
  bunVersion: undefined as string | undefined, // Override Bun runtime version: "1.4.2"
1483
+ bunnyBun: undefined as string | undefined, // Use Electrobunny's Bun fork: "bunny-bun-abc1234" (release tag from blackboardsh/bun)
1360
1484
  locales: undefined as string[] | "*" | undefined, // ICU locales subset (Linux/Windows)
1361
1485
  mac: {
1362
1486
  codesign: false,
@@ -1398,6 +1522,16 @@ const defaultConfig = {
1398
1522
  copy: undefined as Record<string, string> | undefined,
1399
1523
  watch: undefined as string[] | undefined,
1400
1524
  watchIgnore: undefined as string[] | undefined,
1525
+ carrot: undefined as {
1526
+ id: string;
1527
+ name: string;
1528
+ description?: string;
1529
+ mode?: "window" | "background";
1530
+ permissions?: Record<string, unknown>;
1531
+ dependencies?: Record<string, string>;
1532
+ remoteUIs?: Record<string, { entrypoint: string; [key: string]: unknown }>;
1533
+ carrotOnly?: boolean;
1534
+ } | undefined,
1401
1535
  },
1402
1536
  runtime: {} as Record<string, unknown>,
1403
1537
  scripts: {
@@ -2013,21 +2147,38 @@ Categories=Utility;Application;
2013
2147
  );
2014
2148
  }
2015
2149
 
2150
+ const isCarrotOnly = config.build.carrot?.carrotOnly === true;
2151
+
2016
2152
  // build macos bundle
2017
2153
  // Use display name (with spaces) for macOS bundle folders, sanitized name for other platforms
2154
+ let appBundleFolderPath: string;
2155
+ let appBundleFolderContentsPath: string;
2156
+ let appBundleMacOSPath: string;
2157
+ let appBundleFolderResourcesPath: string;
2158
+ let appBundleFolderFrameworksPath: string;
2159
+ let appBundleAppCodePath: string;
2018
2160
  const bundleName =
2019
2161
  targetOS === "macos" ? macOSBundleDisplayName : appFileName;
2020
- const {
2021
- appBundleFolderPath,
2022
- appBundleFolderContentsPath,
2023
- appBundleMacOSPath,
2024
- appBundleFolderResourcesPath,
2025
- appBundleFolderFrameworksPath,
2026
- } = createAppBundle(bundleName, buildFolder, targetOS);
2027
-
2028
- const appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
2029
2162
 
2030
- mkdirSync(appBundleAppCodePath, { recursive: true });
2163
+ if (isCarrotOnly) {
2164
+ // For carrot-only builds, create a minimal output structure for bun/view builds
2165
+ appBundleFolderPath = join(buildFolder, "carrot-build");
2166
+ appBundleFolderContentsPath = appBundleFolderPath;
2167
+ appBundleMacOSPath = appBundleFolderPath;
2168
+ appBundleFolderResourcesPath = appBundleFolderPath;
2169
+ appBundleFolderFrameworksPath = appBundleFolderPath;
2170
+ appBundleAppCodePath = join(appBundleFolderPath, "app");
2171
+ mkdirSync(appBundleAppCodePath, { recursive: true });
2172
+ } else {
2173
+ const bundle = createAppBundle(bundleName, buildFolder, targetOS);
2174
+ appBundleFolderPath = bundle.appBundleFolderPath;
2175
+ appBundleFolderContentsPath = bundle.appBundleFolderContentsPath;
2176
+ appBundleMacOSPath = bundle.appBundleMacOSPath;
2177
+ appBundleFolderResourcesPath = bundle.appBundleFolderResourcesPath;
2178
+ appBundleFolderFrameworksPath = bundle.appBundleFolderFrameworksPath;
2179
+ appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
2180
+ mkdirSync(appBundleAppCodePath, { recursive: true });
2181
+ }
2031
2182
 
2032
2183
  // const bundledBunPath = join(appBundleMacOSPath, 'bun');
2033
2184
  // cpSync(bunPath, bundledBunPath);
@@ -2038,6 +2189,10 @@ Categories=Utility;Application;
2038
2189
 
2039
2190
  // We likely want to let users configure this for different environments (eg: dev, canary, stable) and/or
2040
2191
  // provide methods to help segment data in those folders based on channel/environment
2192
+
2193
+ let InfoPlistContents = "";
2194
+
2195
+ if (!isCarrotOnly) {
2041
2196
  // Generate usage descriptions from entitlements
2042
2197
  const usageDescriptions = generateUsageDescriptions(
2043
2198
  config.build.mac.entitlements || {},
@@ -2048,7 +2203,7 @@ Categories=Utility;Application;
2048
2203
  config.app.identifier,
2049
2204
  );
2050
2205
 
2051
- const InfoPlistContents = `<?xml version="1.0" encoding="UTF-8"?>
2206
+ InfoPlistContents = `<?xml version="1.0" encoding="UTF-8"?>
2052
2207
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2053
2208
  <plist version="1.0">
2054
2209
  <dict>
@@ -2191,6 +2346,7 @@ Categories=Utility;Application;
2191
2346
  currentTarget.os,
2192
2347
  currentTarget.arch,
2193
2348
  config.build.bunVersion,
2349
+ config.build.bunnyBun,
2194
2350
  );
2195
2351
  // Note: .bin/bun binary in node_modules is a symlink to the versioned one in another place
2196
2352
  // in node_modules, so we have to dereference here to get the actual binary in the bundle.
@@ -2788,6 +2944,7 @@ Categories=Utility;Application;
2788
2944
  });
2789
2945
  }
2790
2946
  }
2947
+ } // end if (!isCarrotOnly)
2791
2948
 
2792
2949
  // transpile developer's bun code
2793
2950
  const bunDestFolder = join(appBundleAppCodePath, "bun");
@@ -2874,10 +3031,112 @@ Categories=Utility;Application;
2874
3031
  cpSync(source, destination, { recursive: true, dereference: true });
2875
3032
  }
2876
3033
 
2877
- buildIcons(appBundleFolderResourcesPath, appBundleFolderPath);
3034
+ if (!isCarrotOnly) {
3035
+ buildIcons(appBundleFolderResourcesPath, appBundleFolderPath);
3036
+ }
2878
3037
 
2879
- // Run postBuild script
2880
- runHook("postBuild");
3038
+ // Build carrot artifact if configured (before postBuild so the hook can add custom views)
3039
+ let carrotBuildDir: string | null = null;
3040
+ if (config.build.carrot) {
3041
+ const carrotConfig = config.build.carrot;
3042
+ carrotBuildDir = join(buildFolder, "carrot", carrotConfig.id);
3043
+
3044
+ if (existsSync(carrotBuildDir)) {
3045
+ rmSync(carrotBuildDir, { recursive: true });
3046
+ }
3047
+ mkdirSync(carrotBuildDir, { recursive: true });
3048
+
3049
+ // Copy the bun bundle as worker.js
3050
+ const bunOutputDir = join(appBundleAppCodePath, "bun");
3051
+ // The output filename matches the entrypoint name (e.g., worker.ts → worker.js)
3052
+ const bunEntryName = basename(config.build.bun.entrypoint).replace(/\.ts$/, ".js");
3053
+ const workerSrc = join(bunOutputDir, bunEntryName);
3054
+ if (existsSync(workerSrc)) {
3055
+ cpSync(workerSrc, join(carrotBuildDir, "worker.js"));
3056
+ }
3057
+
3058
+ // Copy views
3059
+ const viewsSrc = join(appBundleAppCodePath, "views");
3060
+ if (existsSync(viewsSrc)) {
3061
+ const viewsDest = join(carrotBuildDir, "views");
3062
+ cpSync(viewsSrc, viewsDest, { recursive: true });
3063
+ }
3064
+
3065
+ // Copy static assets
3066
+ for (const relSource in config.build.copy) {
3067
+ const destRel = config.build.copy[relSource]!;
3068
+ const builtAsset = join(appBundleAppCodePath, destRel);
3069
+ if (existsSync(builtAsset)) {
3070
+ const carrotDest = join(carrotBuildDir, destRel);
3071
+ mkdirSync(dirname(carrotDest), { recursive: true });
3072
+ cpSync(builtAsset, carrotDest, { recursive: true });
3073
+ }
3074
+ }
3075
+
3076
+ // Build remote UIs if configured
3077
+ if (carrotConfig.remoteUIs) {
3078
+ for (const remoteUIName in carrotConfig.remoteUIs) {
3079
+ const remoteUIConfig = carrotConfig.remoteUIs[remoteUIName]!;
3080
+ const remoteUISource = join(projectRoot, remoteUIConfig.entrypoint);
3081
+ if (!existsSync(remoteUISource)) {
3082
+ console.error(`Remote UI entrypoint not found: ${remoteUISource}`);
3083
+ continue;
3084
+ }
3085
+ const remoteUIDestFolder = join(carrotBuildDir, "remote-ui", remoteUIName);
3086
+ mkdirSync(remoteUIDestFolder, { recursive: true });
3087
+
3088
+ const { entrypoint: _entrypoint, ...remoteUIBuildOptions } = remoteUIConfig;
3089
+ const remoteUIBuildResult = await Bun.build({
3090
+ ...remoteUIBuildOptions,
3091
+ entrypoints: [remoteUISource],
3092
+ outdir: remoteUIDestFolder,
3093
+ target: "browser",
3094
+ });
3095
+
3096
+ if (!remoteUIBuildResult.success) {
3097
+ console.error(`Failed to build remote UI: ${remoteUIName}`);
3098
+ printBuildLogs(remoteUIBuildResult.logs);
3099
+ }
3100
+ }
3101
+ }
3102
+
3103
+ // Write carrot.json manifest
3104
+ const carrotManifest = {
3105
+ id: carrotConfig.id,
3106
+ name: carrotConfig.name,
3107
+ version: config.app.version,
3108
+ description: carrotConfig.description || config.app.description || "",
3109
+ mode: carrotConfig.mode || "window",
3110
+ permissions: carrotConfig.permissions || {},
3111
+ dependencies: carrotConfig.dependencies || {},
3112
+ worker: { relativePath: "worker.js" },
3113
+ view: existsSync(viewsSrc) ? { relativePath: "views/index.html" } : undefined,
3114
+ };
3115
+ writeFileSync(
3116
+ join(carrotBuildDir, "carrot.json"),
3117
+ JSON.stringify(carrotManifest, null, 2),
3118
+ );
3119
+
3120
+ console.log(`Carrot built: ${carrotConfig.id} v${config.app.version}`);
3121
+ }
3122
+
3123
+ // Run postBuild script — with carrot dir available if configured
3124
+ runHook("postBuild", carrotBuildDir ? { ELECTROBUN_CARROT_DIR: carrotBuildDir } : {});
3125
+
3126
+ // Compress carrot artifact for non-dev builds
3127
+ if (config.build.carrot && carrotBuildDir && buildEnvironment !== "dev") {
3128
+ const artifactName = `${config.build.carrot.id}-${config.app.version}.tar.zst`;
3129
+ mkdirSync(artifactFolder, { recursive: true });
3130
+ const artifactPath = join(artifactFolder, artifactName);
3131
+
3132
+ execSync(`tar -C "${carrotBuildDir}" -cf - . | zstd -o "${artifactPath}"`, { stdio: "pipe" });
3133
+ const size = statSync(artifactPath).size;
3134
+ console.log(`Carrot artifact: ${artifactPath} (${(size / 1024).toFixed(0)} KB)`);
3135
+ }
3136
+
3137
+ if (isCarrotOnly) {
3138
+ return;
3139
+ }
2881
3140
 
2882
3141
  // Pack app resources into ASAR archive if enabled
2883
3142
  if (config.build.useAsar) {
@@ -3113,6 +3372,7 @@ Categories=Utility;Application;
3113
3372
  ? { cefVersion: config.build?.cefVersion ?? DEFAULT_CEF_VERSION_STRING }
3114
3373
  : {}),
3115
3374
  bunVersion: config.build?.bunVersion ?? BUN_VERSION,
3375
+ ...(config.build?.bunnyBun ? { bunnyBun: config.build.bunnyBun } : {}),
3116
3376
  };
3117
3377
 
3118
3378
  // Include chromiumFlags only if the developer defined them