electrobun 0.0.19-beta.7 → 0.0.19-beta.71

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 (39) hide show
  1. package/BUILD.md +90 -0
  2. package/bin/electrobun.cjs +165 -0
  3. package/debug.js +5 -0
  4. package/dist/api/browser/builtinrpcSchema.ts +19 -0
  5. package/dist/api/browser/index.ts +409 -0
  6. package/dist/api/browser/rpc/webview.ts +79 -0
  7. package/dist/api/browser/stylesAndElements.ts +3 -0
  8. package/dist/api/browser/webviewtag.ts +534 -0
  9. package/dist/api/bun/core/ApplicationMenu.ts +66 -0
  10. package/dist/api/bun/core/BrowserView.ts +349 -0
  11. package/dist/api/bun/core/BrowserWindow.ts +191 -0
  12. package/dist/api/bun/core/ContextMenu.ts +67 -0
  13. package/dist/api/bun/core/Paths.ts +5 -0
  14. package/dist/api/bun/core/Socket.ts +181 -0
  15. package/dist/api/bun/core/Tray.ts +107 -0
  16. package/dist/api/bun/core/Updater.ts +395 -0
  17. package/dist/api/bun/core/Utils.ts +48 -0
  18. package/dist/api/bun/events/ApplicationEvents.ts +14 -0
  19. package/dist/api/bun/events/event.ts +29 -0
  20. package/dist/api/bun/events/eventEmitter.ts +45 -0
  21. package/dist/api/bun/events/trayEvents.ts +9 -0
  22. package/dist/api/bun/events/webviewEvents.ts +16 -0
  23. package/dist/api/bun/events/windowEvents.ts +12 -0
  24. package/dist/api/bun/index.ts +45 -0
  25. package/dist/api/bun/proc/linux.md +43 -0
  26. package/dist/api/bun/proc/native.ts +1217 -0
  27. package/dist/api/shared/platform.ts +48 -0
  28. package/dist/main.js +12 -0
  29. package/package.json +13 -7
  30. package/src/cli/index.ts +621 -203
  31. package/templates/hello-world/README.md +57 -0
  32. package/templates/hello-world/bun.lock +63 -0
  33. package/templates/hello-world/electrobun.config +17 -0
  34. package/templates/hello-world/package.json +16 -0
  35. package/templates/hello-world/src/bun/index.ts +15 -0
  36. package/templates/hello-world/src/mainview/index.css +124 -0
  37. package/templates/hello-world/src/mainview/index.html +47 -0
  38. package/templates/hello-world/src/mainview/index.ts +5 -0
  39. package/bin/electrobun +0 -0
package/src/cli/index.ts CHANGED
@@ -2,48 +2,23 @@ import { join, dirname, basename } from "path";
2
2
  import {
3
3
  existsSync,
4
4
  readFileSync,
5
+ writeFileSync,
5
6
  cpSync,
6
7
  rmdirSync,
7
8
  mkdirSync,
8
9
  createWriteStream,
9
10
  unlinkSync,
11
+ readdirSync,
10
12
  } from "fs";
11
13
  import { execSync } from "child_process";
12
14
  import tar from "tar";
13
15
  import { ZstdInit } from "@oneidentity/zstd-js/wasm";
14
- import {platform, arch} from 'os';
16
+ import { OS, ARCH } from '../shared/platform';
17
+ import { getTemplate, getTemplateNames } from './templates/embedded';
15
18
  // import { loadBsdiff, loadBspatch } from 'bsdiff-wasm';
16
19
  // MacOS named pipes hang at around 4KB
17
20
  const MAX_CHUNK_SIZE = 1024 * 2;
18
21
 
19
- // TODO: dedup with built.ts
20
- const OS: 'win' | 'linux' | 'macos' = getPlatform();
21
- const ARCH: 'arm64' | 'x64' = getArch();
22
-
23
- function getPlatform() {
24
- switch (platform()) {
25
- case "win32":
26
- return 'win';
27
- case "darwin":
28
- return 'macos';
29
- case 'linux':
30
- return 'linux';
31
- default:
32
- throw 'unsupported platform';
33
- }
34
- }
35
-
36
- function getArch() {
37
- switch (arch()) {
38
- case "arm64":
39
- return 'arm64';
40
- case "x64":
41
- return 'x64';
42
- default:
43
- throw 'unsupported arch'
44
- }
45
- }
46
-
47
22
 
48
23
  const binExt = OS === 'win' ? '.exe' : '';
49
24
 
@@ -56,48 +31,88 @@ const configPath = join(projectRoot, configName);
56
31
  const indexOfElectrobun = process.argv.findIndex((arg) =>
57
32
  arg.includes("electrobun")
58
33
  );
59
- const commandArg = process.argv[indexOfElectrobun + 1] || "launcher";
34
+ const commandArg = process.argv[indexOfElectrobun + 1] || "build";
60
35
 
61
36
  const ELECTROBUN_DEP_PATH = join(projectRoot, "node_modules", "electrobun");
62
37
 
63
38
  // When debugging electrobun with the example app use the builds (dev or release) right from the source folder
64
39
  // For developers using electrobun cli via npm use the release versions in /dist
65
40
  // This lets us not have to commit src build folders to git and provide pre-built binaries
66
- const PATHS = {
67
- BUN_BINARY: join(ELECTROBUN_DEP_PATH, "dist", "bun") + binExt,
68
- LAUNCHER_DEV: join(ELECTROBUN_DEP_PATH, "dist", "electrobun") + binExt,
69
- LAUNCHER_RELEASE: join(ELECTROBUN_DEP_PATH, "dist", "launcher") + binExt,
70
- MAIN_JS: join(ELECTROBUN_DEP_PATH, "dist", "main.js"),
71
- NATIVE_WRAPPER_MACOS: join(
72
- ELECTROBUN_DEP_PATH,
73
- "dist",
74
- "libNativeWrapper.dylib"
75
- ),
76
- NATIVE_WRAPPER_WIN: join(ELECTROBUN_DEP_PATH, "dist", "libNativeWrapper.dll"),
77
- NATIVE_WRAPPER_LINUX: join(ELECTROBUN_DEP_PATH, "dist", "libNativeWrapper.so"),
78
- WEBVIEW2LOADER_WIN: join(ELECTROBUN_DEP_PATH, "dist", "WebView2Loader.dll"),
79
- BSPATCH: join(ELECTROBUN_DEP_PATH, "dist", "bspatch") + binExt,
80
- EXTRACTOR: join(ELECTROBUN_DEP_PATH, "dist", "extractor") + binExt,
81
- BSDIFF: join(ELECTROBUN_DEP_PATH, "dist", "bsdiff") + binExt,
82
- CEF_FRAMEWORK_MACOS: join(
83
- ELECTROBUN_DEP_PATH,
84
- "dist",
85
- "cef",
86
- "Chromium Embedded Framework.framework"
87
- ),
88
- CEF_HELPER_MACOS: join(ELECTROBUN_DEP_PATH, "dist", "cef", "process_helper"),
89
- CEF_HELPER_WIN: join(ELECTROBUN_DEP_PATH, "dist", "cef", "process_helper.exe"),
90
- CEF_HELPER_LINUX: join(ELECTROBUN_DEP_PATH, "dist", "cef", "process_helper"),
91
- CEF_DIR: join(ELECTROBUN_DEP_PATH, "dist", "cef"),
92
- };
93
41
 
94
- async function ensureCoreDependencies() {
95
- // Check if core dependencies already exist
96
- if (existsSync(PATHS.BUN_BINARY) && existsSync(PATHS.LAUNCHER_RELEASE)) {
42
+ // Function to get platform-specific paths
43
+ function getPlatformPaths(targetOS: 'macos' | 'win' | 'linux', targetArch: 'arm64' | 'x64') {
44
+ const binExt = targetOS === 'win' ? '.exe' : '';
45
+ const platformDistDir = join(ELECTROBUN_DEP_PATH, `dist-${targetOS}-${targetArch}`);
46
+ const sharedDistDir = join(ELECTROBUN_DEP_PATH, "dist");
47
+
48
+ return {
49
+ // Platform-specific binaries (from dist-OS-ARCH/)
50
+ BUN_BINARY: join(platformDistDir, "bun") + binExt,
51
+ LAUNCHER_DEV: join(platformDistDir, "electrobun") + binExt,
52
+ LAUNCHER_RELEASE: join(platformDistDir, "launcher") + binExt,
53
+ NATIVE_WRAPPER_MACOS: join(platformDistDir, "libNativeWrapper.dylib"),
54
+ NATIVE_WRAPPER_WIN: join(platformDistDir, "libNativeWrapper.dll"),
55
+ NATIVE_WRAPPER_LINUX: join(platformDistDir, "libNativeWrapper.so"),
56
+ NATIVE_WRAPPER_LINUX_CEF: join(platformDistDir, "libNativeWrapper_cef.so"),
57
+ WEBVIEW2LOADER_WIN: join(platformDistDir, "WebView2Loader.dll"),
58
+ BSPATCH: join(platformDistDir, "bspatch") + binExt,
59
+ EXTRACTOR: join(platformDistDir, "extractor") + binExt,
60
+ BSDIFF: join(platformDistDir, "bsdiff") + binExt,
61
+ CEF_FRAMEWORK_MACOS: join(platformDistDir, "cef", "Chromium Embedded Framework.framework"),
62
+ CEF_HELPER_MACOS: join(platformDistDir, "cef", "process_helper"),
63
+ CEF_HELPER_WIN: join(platformDistDir, "cef", "process_helper.exe"),
64
+ CEF_HELPER_LINUX: join(platformDistDir, "cef", "process_helper"),
65
+ CEF_DIR: join(platformDistDir, "cef"),
66
+
67
+ // Shared platform-independent files (from dist/)
68
+ // These work with existing package.json and development workflow
69
+ MAIN_JS: join(sharedDistDir, "main.js"),
70
+ API_DIR: join(sharedDistDir, "api"),
71
+ };
72
+ }
73
+
74
+ // Default PATHS for host platform (backward compatibility)
75
+ const PATHS = getPlatformPaths(OS, ARCH);
76
+
77
+ async function ensureCoreDependencies(targetOS?: 'macos' | 'win' | 'linux', targetArch?: 'arm64' | 'x64') {
78
+ // Use provided target platform or default to host platform
79
+ const platformOS = targetOS || OS;
80
+ const platformArch = targetArch || ARCH;
81
+
82
+ // Get platform-specific paths
83
+ const platformPaths = getPlatformPaths(platformOS, platformArch);
84
+
85
+ // Check platform-specific binaries
86
+ const requiredBinaries = [
87
+ platformPaths.BUN_BINARY,
88
+ platformPaths.LAUNCHER_RELEASE,
89
+ // Platform-specific native wrapper
90
+ platformOS === 'macos' ? platformPaths.NATIVE_WRAPPER_MACOS :
91
+ platformOS === 'win' ? platformPaths.NATIVE_WRAPPER_WIN :
92
+ platformPaths.NATIVE_WRAPPER_LINUX
93
+ ];
94
+
95
+ // Check shared files (main.js should be in shared dist/)
96
+ const requiredSharedFiles = [
97
+ platformPaths.MAIN_JS
98
+ ];
99
+
100
+ const missingBinaries = requiredBinaries.filter(file => !existsSync(file));
101
+ const missingSharedFiles = requiredSharedFiles.filter(file => !existsSync(file));
102
+
103
+ // If only shared files are missing, that's expected in production (they come via npm)
104
+ if (missingBinaries.length === 0 && missingSharedFiles.length > 0) {
105
+ console.log(`Shared files missing (expected in production): ${missingSharedFiles.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', ')}`);
106
+ }
107
+
108
+ // Only download if platform-specific binaries are missing
109
+ if (missingBinaries.length === 0) {
97
110
  return;
98
111
  }
99
112
 
100
- console.log('Core dependencies not found, downloading...');
113
+ // Show which binaries are missing
114
+ console.log(`Core dependencies not found for ${platformOS}-${platformArch}. Missing files:`, missingBinaries.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', '));
115
+ console.log(`Downloading core binaries for ${platformOS}-${platformArch}...`);
101
116
 
102
117
  // Get the current Electrobun version from package.json
103
118
  const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
@@ -112,61 +127,153 @@ async function ensureCoreDependencies() {
112
127
  }
113
128
  }
114
129
 
115
- const platformName = OS === 'macos' ? 'darwin' : OS === 'win' ? 'win32' : 'linux';
116
- const archName = ARCH;
117
- const mainTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-${platformName}-${archName}.tar.gz`;
130
+ const platformName = platformOS === 'macos' ? 'darwin' : platformOS === 'win' ? 'win' : 'linux';
131
+ const archName = platformArch;
132
+ const coreTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-core-${platformName}-${archName}.tar.gz`;
118
133
 
119
- console.log(`Downloading core binaries from: ${mainTarballUrl}`);
134
+ console.log(`Downloading core binaries from: ${coreTarballUrl}`);
120
135
 
121
136
  try {
122
- // Download main tarball
123
- const response = await fetch(mainTarballUrl);
137
+ // Download core binaries tarball
138
+ const response = await fetch(coreTarballUrl);
124
139
  if (!response.ok) {
125
140
  throw new Error(`Failed to download binaries: ${response.status} ${response.statusText}`);
126
141
  }
127
142
 
128
143
  // Create temp file
129
- const tempFile = join(ELECTROBUN_DEP_PATH, 'main-temp.tar.gz');
144
+ const tempFile = join(ELECTROBUN_DEP_PATH, `core-${platformOS}-${platformArch}-temp.tar.gz`);
130
145
  const fileStream = createWriteStream(tempFile);
131
146
 
132
147
  // Write response to file
133
148
  if (response.body) {
134
149
  const reader = response.body.getReader();
150
+ let totalBytes = 0;
135
151
  while (true) {
136
152
  const { done, value } = await reader.read();
137
153
  if (done) break;
138
- fileStream.write(Buffer.from(value));
154
+ const buffer = Buffer.from(value);
155
+ fileStream.write(buffer);
156
+ totalBytes += buffer.length;
139
157
  }
158
+ console.log(`Downloaded ${totalBytes} bytes for ${platformOS}-${platformArch}`);
140
159
  }
141
- fileStream.end();
142
160
 
143
- // Extract to dist directory
144
- console.log('Extracting core dependencies...');
145
- await tar.x({
146
- file: tempFile,
147
- cwd: join(ELECTROBUN_DEP_PATH, 'dist'),
161
+ // Ensure file is properly closed before proceeding
162
+ await new Promise((resolve, reject) => {
163
+ fileStream.end((err) => {
164
+ if (err) reject(err);
165
+ else resolve(null);
166
+ });
148
167
  });
149
168
 
169
+ // Verify the downloaded file exists and has content
170
+ if (!existsSync(tempFile)) {
171
+ throw new Error(`Downloaded file not found: ${tempFile}`);
172
+ }
173
+
174
+ const fileSize = require('fs').statSync(tempFile).size;
175
+ if (fileSize === 0) {
176
+ throw new Error(`Downloaded file is empty: ${tempFile}`);
177
+ }
178
+
179
+ console.log(`Verified download: ${tempFile} (${fileSize} bytes)`);
180
+
181
+ // Extract to platform-specific dist directory
182
+ console.log(`Extracting core dependencies for ${platformOS}-${platformArch}...`);
183
+ const platformDistPath = join(ELECTROBUN_DEP_PATH, `dist-${platformOS}-${platformArch}`);
184
+ mkdirSync(platformDistPath, { recursive: true });
185
+
186
+ // Use Windows native tar.exe on Windows due to npm tar library issues
187
+ if (OS === 'win') {
188
+ console.log('Using Windows native tar.exe for reliable extraction...');
189
+ execSync(`tar -xf "${tempFile}" -C "${platformDistPath}"`, {
190
+ stdio: 'inherit',
191
+ cwd: platformDistPath
192
+ });
193
+ } else {
194
+ await tar.x({
195
+ file: tempFile,
196
+ cwd: platformDistPath,
197
+ preservePaths: false,
198
+ strip: 0,
199
+ });
200
+ }
201
+
202
+ // NOTE: We no longer copy main.js from platform-specific downloads
203
+ // Platform-specific downloads should only contain native binaries
204
+ // main.js and api/ should be shipped via npm in the shared dist/ folder
205
+
150
206
  // Clean up temp file
151
207
  unlinkSync(tempFile);
152
208
 
153
- console.log('Core dependencies downloaded and cached successfully');
209
+ // Debug: List what was actually extracted
210
+ try {
211
+ const extractedFiles = readdirSync(platformDistPath);
212
+ console.log(`Extracted files to ${platformDistPath}:`, extractedFiles);
213
+
214
+ // Check if files are in subdirectories
215
+ for (const file of extractedFiles) {
216
+ const filePath = join(platformDistPath, file);
217
+ const stat = require('fs').statSync(filePath);
218
+ if (stat.isDirectory()) {
219
+ const subFiles = readdirSync(filePath);
220
+ console.log(` ${file}/: ${subFiles.join(', ')}`);
221
+ }
222
+ }
223
+ } catch (e) {
224
+ console.error('Could not list extracted files:', e);
225
+ }
226
+
227
+ // Verify extraction completed successfully - check platform-specific binaries only
228
+ const requiredBinaries = [
229
+ platformPaths.BUN_BINARY,
230
+ platformPaths.LAUNCHER_RELEASE,
231
+ platformOS === 'macos' ? platformPaths.NATIVE_WRAPPER_MACOS :
232
+ platformOS === 'win' ? platformPaths.NATIVE_WRAPPER_WIN :
233
+ platformPaths.NATIVE_WRAPPER_LINUX
234
+ ];
235
+
236
+ const missingBinaries = requiredBinaries.filter(file => !existsSync(file));
237
+ if (missingBinaries.length > 0) {
238
+ console.error(`Missing binaries after extraction: ${missingBinaries.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', ')}`);
239
+ console.error('This suggests the tarball structure is different than expected');
240
+ }
241
+
242
+ // For development: if main.js doesn't exist in shared dist/, copy from platform-specific download as fallback
243
+ const sharedDistPath = join(ELECTROBUN_DEP_PATH, 'dist');
244
+ const extractedMainJs = join(platformDistPath, 'main.js');
245
+ const sharedMainJs = join(sharedDistPath, 'main.js');
246
+
247
+ if (existsSync(extractedMainJs) && !existsSync(sharedMainJs)) {
248
+ console.log('Development fallback: copying main.js from platform-specific download to shared dist/');
249
+ mkdirSync(sharedDistPath, { recursive: true });
250
+ cpSync(extractedMainJs, sharedMainJs);
251
+ }
252
+
253
+ console.log(`Core dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
154
254
 
155
- } catch (error) {
156
- console.error('Failed to download core dependencies:', error.message);
255
+ } catch (error: any) {
256
+ console.error(`Failed to download core dependencies for ${platformOS}-${platformArch}:`, error.message);
157
257
  console.error('Please ensure you have an internet connection and the release exists.');
158
258
  process.exit(1);
159
259
  }
160
260
  }
161
261
 
162
- async function ensureCEFDependencies() {
262
+ async function ensureCEFDependencies(targetOS?: 'macos' | 'win' | 'linux', targetArch?: 'arm64' | 'x64') {
263
+ // Use provided target platform or default to host platform
264
+ const platformOS = targetOS || OS;
265
+ const platformArch = targetArch || ARCH;
266
+
267
+ // Get platform-specific paths
268
+ const platformPaths = getPlatformPaths(platformOS, platformArch);
269
+
163
270
  // Check if CEF dependencies already exist
164
- if (existsSync(PATHS.CEF_DIR)) {
165
- console.log('CEF dependencies found, using cached version');
271
+ if (existsSync(platformPaths.CEF_DIR)) {
272
+ console.log(`CEF dependencies found for ${platformOS}-${platformArch}, using cached version`);
166
273
  return;
167
274
  }
168
275
 
169
- console.log('CEF dependencies not found, downloading...');
276
+ console.log(`CEF dependencies not found for ${platformOS}-${platformArch}, downloading...`);
170
277
 
171
278
  // Get the current Electrobun version from package.json
172
279
  const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
@@ -181,8 +288,8 @@ async function ensureCEFDependencies() {
181
288
  }
182
289
  }
183
290
 
184
- const platformName = OS === 'macos' ? 'darwin' : OS === 'win' ? 'win32' : 'linux';
185
- const archName = ARCH;
291
+ const platformName = platformOS === 'macos' ? 'darwin' : platformOS === 'win' ? 'win' : 'linux';
292
+ const archName = platformArch;
186
293
  const cefTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-cef-${platformName}-${archName}.tar.gz`;
187
294
 
188
295
  console.log(`Downloading CEF from: ${cefTarballUrl}`);
@@ -195,7 +302,7 @@ async function ensureCEFDependencies() {
195
302
  }
196
303
 
197
304
  // Create temp file
198
- const tempFile = join(ELECTROBUN_DEP_PATH, 'cef-temp.tar.gz');
305
+ const tempFile = join(ELECTROBUN_DEP_PATH, `cef-${platformOS}-${platformArch}-temp.tar.gz`);
199
306
  const fileStream = createWriteStream(tempFile);
200
307
 
201
308
  // Write response to file
@@ -209,20 +316,49 @@ async function ensureCEFDependencies() {
209
316
  }
210
317
  fileStream.end();
211
318
 
212
- // Extract to dist directory
213
- console.log('Extracting CEF dependencies...');
214
- await tar.x({
215
- file: tempFile,
216
- cwd: join(ELECTROBUN_DEP_PATH, 'dist'),
217
- });
319
+ // Extract to platform-specific dist directory
320
+ console.log(`Extracting CEF dependencies for ${platformOS}-${platformArch}...`);
321
+ const platformDistPath = join(ELECTROBUN_DEP_PATH, `dist-${platformOS}-${platformArch}`);
322
+ mkdirSync(platformDistPath, { recursive: true });
323
+
324
+ // Use Windows native tar.exe on Windows due to npm tar library issues
325
+ if (OS === 'win') {
326
+ console.log('Using Windows native tar.exe for reliable extraction...');
327
+ execSync(`tar -xf "${tempFile}" -C "${platformDistPath}"`, {
328
+ stdio: 'inherit',
329
+ cwd: platformDistPath
330
+ });
331
+ } else {
332
+ await tar.x({
333
+ file: tempFile,
334
+ cwd: platformDistPath,
335
+ preservePaths: false,
336
+ strip: 0,
337
+ });
338
+ }
218
339
 
219
340
  // Clean up temp file
220
341
  unlinkSync(tempFile);
221
342
 
222
- console.log('CEF dependencies downloaded and cached successfully');
343
+ // Debug: List what was actually extracted for CEF
344
+ try {
345
+ const extractedFiles = readdirSync(platformDistPath);
346
+ console.log(`CEF extracted files to ${platformDistPath}:`, extractedFiles);
347
+
348
+ // Check if CEF directory was created
349
+ const cefDir = join(platformDistPath, 'cef');
350
+ if (existsSync(cefDir)) {
351
+ const cefFiles = readdirSync(cefDir);
352
+ console.log(`CEF directory contents: ${cefFiles.slice(0, 10).join(', ')}${cefFiles.length > 10 ? '...' : ''}`);
353
+ }
354
+ } catch (e) {
355
+ console.error('Could not list CEF extracted files:', e);
356
+ }
357
+
358
+ console.log(`CEF dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
223
359
 
224
- } catch (error) {
225
- console.error('Failed to download CEF dependencies:', error.message);
360
+ } catch (error: any) {
361
+ console.error(`Failed to download CEF dependencies for ${platformOS}-${platformArch}:`, error.message);
226
362
  console.error('Please ensure you have an internet connection and the release exists.');
227
363
  process.exit(1);
228
364
  }
@@ -241,10 +377,6 @@ const commandDefaults = {
241
377
  projectRoot,
242
378
  config: "electrobun.config",
243
379
  },
244
- launcher: {
245
- projectRoot,
246
- config: "electrobun.config",
247
- },
248
380
  };
249
381
 
250
382
  // todo (yoav): add types for config
@@ -257,6 +389,7 @@ const defaultConfig = {
257
389
  build: {
258
390
  buildFolder: "build",
259
391
  artifactFolder: "artifacts",
392
+ targets: undefined, // Will default to current platform if not specified
260
393
  mac: {
261
394
  codesign: false,
262
395
  notarize: false,
@@ -267,6 +400,10 @@ const defaultConfig = {
267
400
  },
268
401
  icons: "icon.iconset",
269
402
  },
403
+ bun: {
404
+ entrypoint: "src/bun/index.ts",
405
+ external: [],
406
+ },
270
407
  },
271
408
  scripts: {
272
409
  postBuild: "",
@@ -286,7 +423,10 @@ if (!command) {
286
423
  const config = getConfig();
287
424
 
288
425
  const envArg =
289
- process.argv.find((arg) => arg.startsWith("env="))?.split("=")[1] || "";
426
+ process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
427
+
428
+ const targetsArg =
429
+ process.argv.find((arg) => arg.startsWith("--targets="))?.split("=")[1] || "";
290
430
 
291
431
  const validEnvironments = ["dev", "canary", "stable"];
292
432
 
@@ -294,8 +434,175 @@ const validEnvironments = ["dev", "canary", "stable"];
294
434
  const buildEnvironment: "dev" | "canary" | "stable" =
295
435
  validEnvironments.includes(envArg) ? envArg : "dev";
296
436
 
437
+ // Determine build targets
438
+ type BuildTarget = { os: 'macos' | 'win' | 'linux', arch: 'arm64' | 'x64' };
439
+
440
+ function parseBuildTargets(): BuildTarget[] {
441
+ // If explicit targets provided via CLI
442
+ if (targetsArg) {
443
+ if (targetsArg === 'current') {
444
+ return [{ os: OS, arch: ARCH }];
445
+ } else if (targetsArg === 'all') {
446
+ return parseConfigTargets();
447
+ } else {
448
+ // Parse comma-separated targets like "macos-arm64,win-x64"
449
+ return targetsArg.split(',').map(target => {
450
+ const [os, arch] = target.trim().split('-') as [string, string];
451
+ if (!['macos', 'win', 'linux'].includes(os) || !['arm64', 'x64'].includes(arch)) {
452
+ console.error(`Invalid target: ${target}. Format should be: os-arch (e.g., macos-arm64)`);
453
+ process.exit(1);
454
+ }
455
+ return { os, arch } as BuildTarget;
456
+ });
457
+ }
458
+ }
459
+
460
+ // Default behavior: always build for current platform only
461
+ // This ensures predictable, fast builds unless explicitly requesting multi-platform
462
+ return [{ os: OS, arch: ARCH }];
463
+ }
464
+
465
+ function parseConfigTargets(): BuildTarget[] {
466
+ // If config has targets, use them
467
+ if (config.build.targets && config.build.targets.length > 0) {
468
+ return config.build.targets.map(target => {
469
+ if (target === 'current') {
470
+ return { os: OS, arch: ARCH };
471
+ }
472
+ const [os, arch] = target.split('-') as [string, string];
473
+ if (!['macos', 'win', 'linux'].includes(os) || !['arm64', 'x64'].includes(arch)) {
474
+ console.error(`Invalid target in config: ${target}. Format should be: os-arch (e.g., macos-arm64)`);
475
+ process.exit(1);
476
+ }
477
+ return { os, arch } as BuildTarget;
478
+ });
479
+ }
480
+
481
+ // If no config targets and --targets=all, use all available platforms
482
+ if (targetsArg === 'all') {
483
+ console.log('No targets specified in config, using all available platforms');
484
+ return [
485
+ { os: 'macos', arch: 'arm64' },
486
+ { os: 'macos', arch: 'x64' },
487
+ { os: 'win', arch: 'x64' },
488
+ { os: 'linux', arch: 'x64' },
489
+ { os: 'linux', arch: 'arm64' }
490
+ ];
491
+ }
492
+
493
+ // Default to current platform
494
+ return [{ os: OS, arch: ARCH }];
495
+ }
496
+
497
+ const buildTargets = parseBuildTargets();
498
+
499
+ // Show build targets to user
500
+ if (buildTargets.length === 1) {
501
+ console.log(`Building for ${buildTargets[0].os}-${buildTargets[0].arch} (${buildEnvironment})`);
502
+ } else {
503
+ const targetList = buildTargets.map(t => `${t.os}-${t.arch}`).join(', ');
504
+ console.log(`Building for multiple targets: ${targetList} (${buildEnvironment})`);
505
+ console.log(`Running ${buildTargets.length} parallel builds...`);
506
+
507
+ // Spawn parallel build processes
508
+ const buildPromises = buildTargets.map(async (target) => {
509
+ const targetString = `${target.os}-${target.arch}`;
510
+ const prefix = `[${targetString}]`;
511
+
512
+ try {
513
+ // Try to find the electrobun binary in node_modules/.bin or use bunx
514
+ const electrobunBin = join(projectRoot, 'node_modules', '.bin', 'electrobun');
515
+ let command: string[];
516
+
517
+ if (existsSync(electrobunBin)) {
518
+ command = [electrobunBin, 'build', `--env=${buildEnvironment}`, `--targets=${targetString}`];
519
+ } else {
520
+ // Fallback to bunx which should resolve node_modules binaries
521
+ command = ['bunx', 'electrobun', 'build', `--env=${buildEnvironment}`, `--targets=${targetString}`];
522
+ }
523
+
524
+ console.log(`${prefix} Running:`, command.join(' '));
525
+
526
+ const result = await Bun.spawn(command, {
527
+ stdio: ['inherit', 'pipe', 'pipe'],
528
+ env: process.env,
529
+ cwd: projectRoot // Ensure we're in the right directory
530
+ });
531
+
532
+ // Pipe output with prefix
533
+ if (result.stdout) {
534
+ const reader = result.stdout.getReader();
535
+ while (true) {
536
+ const { done, value } = await reader.read();
537
+ if (done) break;
538
+ const text = new TextDecoder().decode(value);
539
+ // Add prefix to each line
540
+ const prefixedText = text.split('\n').map(line =>
541
+ line ? `${prefix} ${line}` : line
542
+ ).join('\n');
543
+ process.stdout.write(prefixedText);
544
+ }
545
+ }
546
+
547
+ if (result.stderr) {
548
+ const reader = result.stderr.getReader();
549
+ while (true) {
550
+ const { done, value } = await reader.read();
551
+ if (done) break;
552
+ const text = new TextDecoder().decode(value);
553
+ const prefixedText = text.split('\n').map(line =>
554
+ line ? `${prefix} ${line}` : line
555
+ ).join('\n');
556
+ process.stderr.write(prefixedText);
557
+ }
558
+ }
559
+
560
+ const exitCode = await result.exited;
561
+ return { target, exitCode, success: exitCode === 0 };
562
+
563
+ } catch (error) {
564
+ console.error(`${prefix} Failed to start build:`, error);
565
+ return { target, exitCode: 1, success: false, error };
566
+ }
567
+ });
568
+
569
+ // Wait for all builds to complete
570
+ const results = await Promise.allSettled(buildPromises);
571
+
572
+ // Report final results
573
+ console.log('\n=== Build Results ===');
574
+ let allSucceeded = true;
575
+
576
+ for (const result of results) {
577
+ if (result.status === 'fulfilled') {
578
+ const { target, success, exitCode } = result.value;
579
+ const status = success ? '✅ SUCCESS' : '❌ FAILED';
580
+ console.log(`${target.os}-${target.arch}: ${status} (exit code: ${exitCode})`);
581
+ if (!success) allSucceeded = false;
582
+ } else {
583
+ console.log(`Build rejected: ${result.reason}`);
584
+ allSucceeded = false;
585
+ }
586
+ }
587
+
588
+ if (!allSucceeded) {
589
+ console.log('\nSome builds failed. Check the output above for details.');
590
+ process.exit(1);
591
+ } else {
592
+ console.log('\nAll builds completed successfully! 🎉');
593
+ }
594
+
595
+ process.exit(0);
596
+ }
597
+
297
598
  // todo (yoav): dev builds should include the branch name, and/or allow configuration via external config
298
- const buildSubFolder = `${buildEnvironment}`;
599
+ // For now, assume single target build (we'll refactor for multi-target later)
600
+ const currentTarget = buildTargets[0];
601
+ const buildSubFolder = `${buildEnvironment}-${currentTarget.os}-${currentTarget.arch}`;
602
+
603
+ // Use target OS/ARCH for build logic (instead of current machine's OS/ARCH)
604
+ const targetOS = currentTarget.os;
605
+ const targetARCH = currentTarget.arch;
299
606
 
300
607
  const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
301
608
 
@@ -370,16 +677,71 @@ const appFileName = (
370
677
  )
371
678
  .replace(/\s/g, "")
372
679
  .replace(/\./g, "-");
373
- const bundleFileName = OS === 'macos' ? `${appFileName}.app` : appFileName;
680
+ const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName;
374
681
 
375
682
  // const logPath = `/Library/Logs/Electrobun/ExampleApp/dev/out.log`;
376
683
 
377
684
  let proc = null;
378
685
 
379
686
  if (commandArg === "init") {
380
- // todo (yoav): init a repo folder structure
381
- console.log("initializing electrobun project");
687
+ const projectName = process.argv[indexOfElectrobun + 2] || "my-electrobun-app";
688
+ const templateName = process.argv.find(arg => arg.startsWith("--template="))?.split("=")[1] || "hello-world";
689
+
690
+ console.log(`🚀 Initializing Electrobun project: ${projectName}`);
691
+
692
+ // Validate template name
693
+ const availableTemplates = getTemplateNames();
694
+ if (!availableTemplates.includes(templateName)) {
695
+ console.error(`❌ Template "${templateName}" not found.`);
696
+ console.log(`Available templates: ${availableTemplates.join(", ")}`);
697
+ process.exit(1);
698
+ }
699
+
700
+ const template = getTemplate(templateName);
701
+ if (!template) {
702
+ console.error(`❌ Could not load template "${templateName}"`);
703
+ process.exit(1);
704
+ }
705
+
706
+ // Create project directory
707
+ const projectPath = join(process.cwd(), projectName);
708
+ if (existsSync(projectPath)) {
709
+ console.error(`❌ Directory "${projectName}" already exists.`);
710
+ process.exit(1);
711
+ }
712
+
713
+ mkdirSync(projectPath, { recursive: true });
714
+
715
+ // Extract template files
716
+ let fileCount = 0;
717
+ for (const [relativePath, content] of Object.entries(template.files)) {
718
+ const fullPath = join(projectPath, relativePath);
719
+ const dir = dirname(fullPath);
720
+
721
+ // Create directory if it doesn't exist
722
+ mkdirSync(dir, { recursive: true });
723
+
724
+ // Write file
725
+ writeFileSync(fullPath, content, 'utf-8');
726
+ fileCount++;
727
+ }
728
+
729
+ console.log(`✅ Created ${fileCount} files from "${templateName}" template`);
730
+ console.log(`📁 Project created at: ${projectPath}`);
731
+ console.log("");
732
+ console.log("📦 Next steps:");
733
+ console.log(` cd ${projectName}`);
734
+ console.log(" bun install");
735
+ console.log(" bunx electrobun dev");
736
+ console.log("");
737
+ console.log("🎉 Happy building with Electrobun!");
382
738
  } else if (commandArg === "build") {
739
+ // Ensure core binaries are available for the target platform before starting build
740
+ await ensureCoreDependencies(currentTarget.os, currentTarget.arch);
741
+
742
+ // Get platform-specific paths for the current target
743
+ const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);
744
+
383
745
  // refresh build folder
384
746
  if (existsSync(buildFolder)) {
385
747
  rmdirSync(buildFolder, { recursive: true });
@@ -403,7 +765,7 @@ if (commandArg === "init") {
403
765
  appBundleMacOSPath,
404
766
  appBundleFolderResourcesPath,
405
767
  appBundleFolderFrameworksPath,
406
- } = createAppBundle(appFileName, buildFolder);
768
+ } = createAppBundle(appFileName, buildFolder, targetOS);
407
769
 
408
770
  const appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
409
771
 
@@ -473,29 +835,27 @@ if (commandArg === "init") {
473
835
  // mkdirSync(destLauncherFolder, {recursive: true});
474
836
  // }
475
837
  // cpSync(zigLauncherBinarySource, zigLauncherDestination, {recursive: true, dereference: true});
476
- const bunCliLauncherBinarySource =
477
- buildEnvironment === "dev"
478
- ? // Note: in dev use the cli as the launcher
479
- PATHS.LAUNCHER_DEV
480
- : // Note: for release use the zig launcher optimized for smol size
481
- PATHS.LAUNCHER_RELEASE;
482
- const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + binExt;
483
- const destLauncherFolder = dirname(bunCliLauncherDestination);
484
- if (!existsSync(destLauncherFolder)) {
485
- // console.info('creating folder: ', destFolder);
486
- mkdirSync(destLauncherFolder, { recursive: true });
487
- }
838
+ // Only copy launcher for non-dev builds
839
+ if (buildEnvironment !== "dev") {
840
+ const bunCliLauncherBinarySource = targetPaths.LAUNCHER_RELEASE;
841
+ const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + binExt;
842
+ const destLauncherFolder = dirname(bunCliLauncherDestination);
843
+ if (!existsSync(destLauncherFolder)) {
844
+ // console.info('creating folder: ', destFolder);
845
+ mkdirSync(destLauncherFolder, { recursive: true });
846
+ }
488
847
 
489
- cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
490
- recursive: true,
491
- dereference: true,
492
- });
848
+ cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
849
+ recursive: true,
850
+ dereference: true,
851
+ });
852
+ }
493
853
 
494
- cpSync(PATHS.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
854
+ cpSync(targetPaths.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
495
855
 
496
856
  // Bun runtime binary
497
857
  // todo (yoav): this only works for the current architecture
498
- const bunBinarySourcePath = PATHS.BUN_BINARY;
858
+ const bunBinarySourcePath = targetPaths.BUN_BINARY;
499
859
  // Note: .bin/bun binary in node_modules is a symlink to the versioned one in another place
500
860
  // in node_modules, so we have to dereference here to get the actual binary in the bundle.
501
861
  const bunBinaryDestInBundlePath = join(appBundleMacOSPath, "bun") + binExt;
@@ -507,8 +867,8 @@ if (commandArg === "init") {
507
867
  cpSync(bunBinarySourcePath, bunBinaryDestInBundlePath, { dereference: true });
508
868
 
509
869
  // copy native wrapper dynamic library
510
- if (OS === 'macos') {
511
- const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_MACOS;
870
+ if (targetOS === 'macos') {
871
+ const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_MACOS;
512
872
  const nativeWrapperMacosDestination = join(
513
873
  appBundleMacOSPath,
514
874
  "libNativeWrapper.dylib"
@@ -516,8 +876,8 @@ if (commandArg === "init") {
516
876
  cpSync(nativeWrapperMacosSource, nativeWrapperMacosDestination, {
517
877
  dereference: true,
518
878
  });
519
- } else if (OS === 'win') {
520
- const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_WIN;
879
+ } else if (targetOS === 'win') {
880
+ const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_WIN;
521
881
  const nativeWrapperMacosDestination = join(
522
882
  appBundleMacOSPath,
523
883
  "libNativeWrapper.dll"
@@ -526,7 +886,7 @@ if (commandArg === "init") {
526
886
  dereference: true,
527
887
  });
528
888
 
529
- const webview2LibSource = PATHS.WEBVIEW2LOADER_WIN;
889
+ const webview2LibSource = targetPaths.WEBVIEW2LOADER_WIN;
530
890
  const webview2LibDestination = join(
531
891
  appBundleMacOSPath,
532
892
  "WebView2Loader.dll"
@@ -534,30 +894,34 @@ if (commandArg === "init") {
534
894
  // copy webview2 system webview library
535
895
  cpSync(webview2LibSource, webview2LibDestination);
536
896
 
537
- } else if (OS === 'linux') {
538
- const nativeWrapperLinuxSource = PATHS.NATIVE_WRAPPER_LINUX;
897
+ } else if (targetOS === 'linux') {
898
+ // Choose the appropriate native wrapper based on bundleCEF setting
899
+ const useCEF = config.build.linux?.bundleCEF;
900
+ const nativeWrapperLinuxSource = useCEF ? targetPaths.NATIVE_WRAPPER_LINUX_CEF : targetPaths.NATIVE_WRAPPER_LINUX;
539
901
  const nativeWrapperLinuxDestination = join(
540
902
  appBundleMacOSPath,
541
903
  "libNativeWrapper.so"
542
904
  );
905
+
543
906
  if (existsSync(nativeWrapperLinuxSource)) {
544
907
  cpSync(nativeWrapperLinuxSource, nativeWrapperLinuxDestination, {
545
908
  dereference: true,
546
909
  });
910
+ console.log(`Using ${useCEF ? 'CEF' : 'GTK'} native wrapper for Linux`);
911
+ } else {
912
+ throw new Error(`Native wrapper not found: ${nativeWrapperLinuxSource}`);
547
913
  }
548
914
  }
549
915
 
550
- // Ensure core binaries are available
551
- await ensureCoreDependencies();
552
916
 
553
917
  // Download CEF binaries if needed when bundleCEF is enabled
554
- if ((OS === 'macos' && config.build.mac?.bundleCEF) ||
555
- (OS === 'win' && config.build.win?.bundleCEF) ||
556
- (OS === 'linux' && config.build.linux?.bundleCEF)) {
918
+ if ((targetOS === 'macos' && config.build.mac?.bundleCEF) ||
919
+ (targetOS === 'win' && config.build.win?.bundleCEF) ||
920
+ (targetOS === 'linux' && config.build.linux?.bundleCEF)) {
557
921
 
558
- await ensureCEFDependencies();
559
- if (OS === 'macos') {
560
- const cefFrameworkSource = PATHS.CEF_FRAMEWORK_MACOS;
922
+ await ensureCEFDependencies(currentTarget.os, currentTarget.arch);
923
+ if (targetOS === 'macos') {
924
+ const cefFrameworkSource = targetPaths.CEF_FRAMEWORK_MACOS;
561
925
  const cefFrameworkDestination = join(
562
926
  appBundleFolderFrameworksPath,
563
927
  "Chromium Embedded Framework.framework"
@@ -578,7 +942,7 @@ if (commandArg === "init") {
578
942
  "bun Helper (Renderer)",
579
943
  ];
580
944
 
581
- const helperSourcePath = PATHS.CEF_HELPER_MACOS;
945
+ const helperSourcePath = targetPaths.CEF_HELPER_MACOS;
582
946
  cefHelperNames.forEach((helperName) => {
583
947
  const destinationPath = join(
584
948
  appBundleFolderFrameworksPath,
@@ -598,10 +962,9 @@ if (commandArg === "init") {
598
962
  dereference: true,
599
963
  });
600
964
  });
601
- } else if (OS === 'win') {
602
- // Copy CEF DLLs from dist/cef/ to the main executable directory
603
- const electrobunDistPath = join(ELECTROBUN_DEP_PATH, "dist");
604
- const cefSourcePath = join(electrobunDistPath, "cef");
965
+ } else if (targetOS === 'win') {
966
+ // Copy CEF DLLs from platform-specific dist/cef/ to the main executable directory
967
+ const cefSourcePath = targetPaths.CEF_DIR;
605
968
  const cefDllFiles = [
606
969
  'libcef.dll',
607
970
  'chrome_elf.dll',
@@ -641,7 +1004,7 @@ if (commandArg === "init") {
641
1004
  });
642
1005
 
643
1006
  // Copy CEF resources to MacOS/cef/ subdirectory for other resources like locales
644
- const cefResourcesSource = join(electrobunDistPath, 'cef');
1007
+ const cefResourcesSource = targetPaths.CEF_DIR;
645
1008
  const cefResourcesDestination = join(appBundleMacOSPath, 'cef');
646
1009
 
647
1010
  if (existsSync(cefResourcesSource)) {
@@ -660,7 +1023,7 @@ if (commandArg === "init") {
660
1023
  "bun Helper (Renderer)",
661
1024
  ];
662
1025
 
663
- const helperSourcePath = PATHS.CEF_HELPER_WIN;
1026
+ const helperSourcePath = targetPaths.CEF_HELPER_WIN;
664
1027
  if (existsSync(helperSourcePath)) {
665
1028
  cefHelperNames.forEach((helperName) => {
666
1029
  const destinationPath = join(appBundleMacOSPath, `${helperName}.exe`);
@@ -670,10 +1033,9 @@ if (commandArg === "init") {
670
1033
  } else {
671
1034
  console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
672
1035
  }
673
- } else if (OS === 'linux') {
674
- // Copy CEF shared libraries from dist/cef/ to the main executable directory
675
- const electrobunDistPath = join(ELECTROBUN_DEP_PATH, "dist");
676
- const cefSourcePath = join(electrobunDistPath, "cef");
1036
+ } else if (targetOS === 'linux') {
1037
+ // Copy CEF shared libraries from platform-specific dist/cef/ to the main executable directory
1038
+ const cefSourcePath = targetPaths.CEF_DIR;
677
1039
 
678
1040
  if (existsSync(cefSourcePath)) {
679
1041
  const cefSoFiles = [
@@ -759,12 +1121,12 @@ if (commandArg === "init") {
759
1121
  "bun Helper (Renderer)",
760
1122
  ];
761
1123
 
762
- const helperSourcePath = PATHS.CEF_HELPER_LINUX;
1124
+ const helperSourcePath = targetPaths.CEF_HELPER_LINUX;
763
1125
  if (existsSync(helperSourcePath)) {
764
1126
  cefHelperNames.forEach((helperName) => {
765
1127
  const destinationPath = join(appBundleMacOSPath, helperName);
766
1128
  cpSync(helperSourcePath, destinationPath);
767
- console.log(`Copied CEF helper: ${helperName}`);
1129
+ // console.log(`Copied CEF helper: ${helperName}`);
768
1130
  });
769
1131
  } else {
770
1132
  console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
@@ -775,7 +1137,7 @@ if (commandArg === "init") {
775
1137
 
776
1138
 
777
1139
  // copy native bindings
778
- const bsPatchSource = PATHS.BSPATCH;
1140
+ const bsPatchSource = targetPaths.BSPATCH;
779
1141
  const bsPatchDestination = join(appBundleMacOSPath, "bspatch") + binExt;
780
1142
  const bsPatchDestFolder = dirname(bsPatchDestination);
781
1143
  if (!existsSync(bsPatchDestFolder)) {
@@ -871,12 +1233,21 @@ if (commandArg === "init") {
871
1233
 
872
1234
  // Run postBuild script
873
1235
  if (config.scripts.postBuild) {
874
-
875
- Bun.spawnSync([bunBinarySourcePath, config.scripts.postBuild], {
1236
+ // Use host platform's bun binary for running scripts, not target platform's
1237
+ const hostPaths = getPlatformPaths(OS, ARCH);
1238
+
1239
+ Bun.spawnSync([hostPaths.BUN_BINARY, config.scripts.postBuild], {
876
1240
  stdio: ["ignore", "inherit", "inherit"],
877
1241
  env: {
878
1242
  ...process.env,
879
1243
  ELECTROBUN_BUILD_ENV: buildEnvironment,
1244
+ ELECTROBUN_OS: targetOS, // Use target OS for environment variables
1245
+ ELECTROBUN_ARCH: targetARCH, // Use target ARCH for environment variables
1246
+ ELECTROBUN_BUILD_DIR: buildFolder,
1247
+ ELECTROBUN_APP_NAME: appFileName,
1248
+ ELECTROBUN_APP_VERSION: config.app.version,
1249
+ ELECTROBUN_APP_IDENTIFIER: config.app.identifier,
1250
+ ELECTROBUN_ARTIFACT_DIR: artifactFolder,
880
1251
  },
881
1252
  });
882
1253
  }
@@ -920,8 +1291,9 @@ if (commandArg === "init") {
920
1291
  );
921
1292
 
922
1293
  // todo (yoav): add these to config
1294
+ // Only codesign/notarize when building macOS targets on macOS host
923
1295
  const shouldCodesign =
924
- buildEnvironment !== "dev" && config.build.mac.codesign;
1296
+ buildEnvironment !== "dev" && targetOS === 'macos' && OS === 'macos' && config.build.mac.codesign;
925
1297
  const shouldNotarize = shouldCodesign && config.build.mac.notarize;
926
1298
 
927
1299
  if (shouldCodesign) {
@@ -956,6 +1328,8 @@ if (commandArg === "init") {
956
1328
  // 6.5. code sign and notarize the dmg
957
1329
  // 7. copy artifacts to directory [self-extractor dmg, zstd app bundle, bsdiff patch, update.json]
958
1330
 
1331
+ // Platform suffix is only used for folder names, not file names
1332
+ const platformSuffix = `-${targetOS}-${targetARCH}`;
959
1333
  const tarPath = `${appBundleFolderPath}.tar`;
960
1334
 
961
1335
  // tar the signed and notarized app bundle
@@ -988,19 +1362,22 @@ if (commandArg === "init") {
988
1362
  await ZstdInit().then(async ({ ZstdSimple, ZstdStream }) => {
989
1363
  // Note: Simple is much faster than stream, but stream is better for large files
990
1364
  // todo (yoav): consider a file size cutoff to switch to stream instead of simple.
1365
+ const useStream = tarball.size > 100 * 1024 * 1024;
1366
+
991
1367
  if (tarball.size > 0) {
992
- // Uint8 array filestream of the tar file
1368
+ // Uint8 array filestream of the tar file
993
1369
 
994
1370
  const data = new Uint8Array(tarBuffer);
1371
+
995
1372
  const compressionLevel = 22;
996
1373
  const compressedData = ZstdSimple.compress(data, compressionLevel);
997
1374
 
998
1375
  console.log(
999
1376
  "compressed",
1000
- compressedData.length,
1377
+ data.length,
1001
1378
  "bytes",
1002
1379
  "from",
1003
- data.length,
1380
+ tarBuffer.byteLength,
1004
1381
  "bytes"
1005
1382
  );
1006
1383
 
@@ -1012,7 +1389,7 @@ if (commandArg === "init") {
1012
1389
  // now and it needs the same name as the original app bundle.
1013
1390
  rmdirSync(appBundleFolderPath, { recursive: true });
1014
1391
 
1015
- const selfExtractingBundle = createAppBundle(appFileName, buildFolder);
1392
+ const selfExtractingBundle = createAppBundle(appFileName, buildFolder, targetOS);
1016
1393
  const compressedTarballInExtractingBundlePath = join(
1017
1394
  selfExtractingBundle.appBundleFolderResourcesPath,
1018
1395
  `${hash}.tar.zst`
@@ -1021,7 +1398,7 @@ if (commandArg === "init") {
1021
1398
  // copy the zstd tarball to the self-extracting app bundle
1022
1399
  cpSync(compressedTarPath, compressedTarballInExtractingBundlePath);
1023
1400
 
1024
- const selfExtractorBinSourcePath = PATHS.EXTRACTOR;
1401
+ const selfExtractorBinSourcePath = targetPaths.EXTRACTOR;
1025
1402
  const selfExtractorBinDestinationPath = join(
1026
1403
  selfExtractingBundle.appBundleMacOSPath,
1027
1404
  "launcher"
@@ -1053,28 +1430,45 @@ if (commandArg === "init") {
1053
1430
  console.log("skipping notarization");
1054
1431
  }
1055
1432
 
1056
- console.log("creating dmg...");
1057
- // make a dmg
1058
- const dmgPath = join(buildFolder, `${appFileName}.dmg`);
1059
- artifactsToUpload.push(dmgPath);
1060
- // hdiutil create -volname "YourAppName" -srcfolder /path/to/YourApp.app -ov -format UDZO YourAppName.dmg
1061
- // Note: use UDBZ for better compression vs. UDZO
1062
- execSync(
1063
- `hdiutil create -volname "${appFileName}" -srcfolder ${escapePathForTerminal(
1064
- appBundleFolderPath
1065
- )} -ov -format UDBZ ${escapePathForTerminal(dmgPath)}`
1066
- );
1433
+ // DMG creation for macOS only
1434
+ if (targetOS === 'macos') {
1435
+ console.log("creating dmg...");
1436
+ // make a dmg
1437
+ const dmgPath = join(buildFolder, `${appFileName}.dmg`);
1438
+ artifactsToUpload.push(dmgPath);
1439
+ // hdiutil create -volname "YourAppName" -srcfolder /path/to/YourApp.app -ov -format UDZO YourAppName.dmg
1440
+ // Note: use ULFO (lzfse) for better compatibility with large CEF frameworks and modern macOS
1441
+ execSync(
1442
+ `hdiutil create -volname "${appFileName}" -srcfolder ${escapePathForTerminal(
1443
+ appBundleFolderPath
1444
+ )} -ov -format ULFO ${escapePathForTerminal(dmgPath)}`
1445
+ );
1067
1446
 
1068
- if (shouldCodesign) {
1069
- codesignAppBundle(dmgPath);
1070
- } else {
1071
- console.log("skipping codesign");
1072
- }
1447
+ if (shouldCodesign) {
1448
+ codesignAppBundle(dmgPath);
1449
+ } else {
1450
+ console.log("skipping codesign");
1451
+ }
1073
1452
 
1074
- if (shouldNotarize) {
1075
- notarizeAndStaple(dmgPath);
1453
+ if (shouldNotarize) {
1454
+ notarizeAndStaple(dmgPath);
1455
+ } else {
1456
+ console.log("skipping notarization");
1457
+ }
1076
1458
  } else {
1077
- console.log("skipping notarization");
1459
+ // For Windows and Linux, add the self-extracting bundle directly
1460
+ const platformBundlePath = join(buildFolder, `${appFileName}${platformSuffix}${targetOS === 'win' ? '.exe' : ''}`);
1461
+ // Copy the self-extracting bundle to platform-specific filename
1462
+ if (targetOS === 'win') {
1463
+ // On Windows, create a self-extracting exe
1464
+ // For now, just copy the bundle folder
1465
+ artifactsToUpload.push(compressedTarPath);
1466
+ } else if (targetOS === 'linux') {
1467
+ // On Linux, create a tar.gz of the bundle
1468
+ const linuxTarPath = join(buildFolder, `${appFileName}.tar.gz`);
1469
+ execSync(`tar -czf ${escapePathForTerminal(linuxTarPath)} -C ${escapePathForTerminal(buildFolder)} ${escapePathForTerminal(basename(appBundleFolderPath))}`);
1470
+ artifactsToUpload.push(linuxTarPath);
1471
+ }
1078
1472
  }
1079
1473
 
1080
1474
  // refresh artifacts folder
@@ -1093,39 +1487,48 @@ if (commandArg === "init") {
1093
1487
  // the download button or display on your marketing site or in the app.
1094
1488
  version: config.app.version,
1095
1489
  hash: hash.toString(),
1490
+ platform: OS,
1491
+ arch: ARCH,
1096
1492
  // channel: buildEnvironment,
1097
1493
  // bucketUrl: config.release.bucketUrl
1098
1494
  });
1099
1495
 
1100
- await Bun.write(join(artifactFolder, "update.json"), updateJsonContent);
1496
+ // update.json (no platform suffix in filename, platform is in folder name)
1497
+ await Bun.write(join(artifactFolder, 'update.json'), updateJsonContent);
1101
1498
 
1102
1499
  // generate bsdiff
1103
1500
  // https://storage.googleapis.com/eggbun-static/electrobun-playground/canary/ElectrobunPlayground-canary.app.tar.zst
1104
1501
  console.log("bucketUrl: ", config.release.bucketUrl);
1105
1502
 
1106
1503
  console.log("generating a patch from the previous version...");
1107
- const urlToPrevUpdateJson = join(
1108
- config.release.bucketUrl,
1109
- buildEnvironment,
1110
- `update.json`
1111
- );
1112
- const cacheBuster = Math.random().toString(36).substring(7);
1113
- const updateJsonResponse = await fetch(
1114
- urlToPrevUpdateJson + `?${cacheBuster}`
1115
- ).catch((err) => {
1116
- console.log("bucketURL not found: ", err);
1117
- });
1504
+
1505
+ // Skip patch generation if bucketUrl is not configured
1506
+ if (!config.release.bucketUrl || config.release.bucketUrl.trim() === '') {
1507
+ console.log("No bucketUrl configured, skipping patch generation");
1508
+ console.log("To enable patch generation, configure bucketUrl in your electrobun.config");
1509
+ } else {
1510
+ const urlToPrevUpdateJson = join(
1511
+ config.release.bucketUrl,
1512
+ buildSubFolder,
1513
+ 'update.json'
1514
+ );
1515
+ const cacheBuster = Math.random().toString(36).substring(7);
1516
+ const updateJsonResponse = await fetch(
1517
+ urlToPrevUpdateJson + `?${cacheBuster}`
1518
+ ).catch((err) => {
1519
+ console.log("bucketURL not found: ", err);
1520
+ });
1118
1521
 
1119
1522
  const urlToLatestTarball = join(
1120
1523
  config.release.bucketUrl,
1121
- buildEnvironment,
1524
+ buildSubFolder,
1122
1525
  `${appFileName}.app.tar.zst`
1123
1526
  );
1124
1527
 
1125
1528
 
1126
1529
  // attempt to get the previous version to create a patch file
1127
- if (updateJsonResponse.ok) {
1128
- const prevUpdateJson = await updateJsonResponse.json();
1530
+ if (updateJsonResponse && updateJsonResponse.ok) {
1531
+ const prevUpdateJson = await updateJsonResponse!.json();
1129
1532
 
1130
1533
  const prevHash = prevUpdateJson.hash;
1131
1534
  console.log("PREVIOUS HASH", prevHash);
@@ -1164,7 +1567,7 @@ if (commandArg === "init") {
1164
1567
  console.log("diff previous and new tarballs...");
1165
1568
  // Run it as a separate process to leverage multi-threadedness
1166
1569
  // especially for creating multiple diffs in parallel
1167
- const bsdiffpath = PATHS.BSDIFF;
1570
+ const bsdiffpath = targetPaths.BSDIFF;
1168
1571
  const patchFilePath = join(buildFolder, `${prevHash}.patch`);
1169
1572
  artifactsToUpload.push(patchFilePath);
1170
1573
  const result = Bun.spawnSync(
@@ -1181,6 +1584,7 @@ if (commandArg === "init") {
1181
1584
  console.log("prevoius version not found at: ", urlToLatestTarball);
1182
1585
  console.log("skipping diff generation");
1183
1586
  }
1587
+ } // End of bucketUrl validation block
1184
1588
 
1185
1589
  // compress all the upload files
1186
1590
  console.log("copying artifacts...");
@@ -1209,10 +1613,7 @@ if (commandArg === "init") {
1209
1613
  // todo (yoav): rename to start
1210
1614
 
1211
1615
  // run the project in dev mode
1212
- // this runs the cli in debug mode, on macos executes the app bundle,
1213
- // there is another copy of the cli in the app bundle that will execute the app
1214
- // the two cli processes communicate via named pipes and together manage the dev
1215
- // lifecycle and debug functionality
1616
+ // this runs the bundled bun binary with main.js directly
1216
1617
 
1217
1618
  // Note: this cli will be a bun single-file-executable
1218
1619
  // Note: we want to use the version of bun that's packaged with electrobun
@@ -1290,7 +1691,12 @@ function getConfig() {
1290
1691
  if (existsSync(configPath)) {
1291
1692
  const configFileContents = readFileSync(configPath, "utf8");
1292
1693
  // Note: we want this to hard fail if there's a syntax error
1293
- loadedConfig = JSON.parse(configFileContents);
1694
+ try {
1695
+ loadedConfig = JSON.parse(configFileContents);
1696
+ } catch (error) {
1697
+ console.error("Failed to parse config file:", error);
1698
+ console.error("using default config instead");
1699
+ }
1294
1700
  }
1295
1701
 
1296
1702
  // todo (yoav): write a deep clone fn
@@ -1312,6 +1718,18 @@ function getConfig() {
1312
1718
  ...(loadedConfig?.build?.mac?.entitlements || {}),
1313
1719
  },
1314
1720
  },
1721
+ win: {
1722
+ ...defaultConfig.build.win,
1723
+ ...(loadedConfig?.build?.win || {}),
1724
+ },
1725
+ linux: {
1726
+ ...defaultConfig.build.linux,
1727
+ ...(loadedConfig?.build?.linux || {}),
1728
+ },
1729
+ bun: {
1730
+ ...defaultConfig.build.bun,
1731
+ ...(loadedConfig?.build?.bun || {}),
1732
+ }
1315
1733
  },
1316
1734
  scripts: {
1317
1735
  ...defaultConfig.scripts,
@@ -1473,8 +1891,8 @@ function notarizeAndStaple(appOrDmgPath: string) {
1473
1891
  // have the same name but different subfolders in our build directory. or I guess delete the first one after tar/compression and then create the other one.
1474
1892
  // either way you can pass in the parent folder here for that flexibility.
1475
1893
  // for intel/arm builds on mac we'll probably have separate subfolders as well and build them in parallel.
1476
- function createAppBundle(bundleName: string, parentFolder: string) {
1477
- if (OS === 'macos') {
1894
+ function createAppBundle(bundleName: string, parentFolder: string, targetOS: 'macos' | 'win' | 'linux') {
1895
+ if (targetOS === 'macos') {
1478
1896
  // macOS bundle structure
1479
1897
  const bundleFileName = `${bundleName}.app`;
1480
1898
  const appBundleFolderPath = join(parentFolder, bundleFileName);
@@ -1501,7 +1919,7 @@ function createAppBundle(bundleName: string, parentFolder: string) {
1501
1919
  appBundleFolderResourcesPath,
1502
1920
  appBundleFolderFrameworksPath,
1503
1921
  };
1504
- } else if (OS === 'linux' || OS === 'win') {
1922
+ } else if (targetOS === 'linux' || targetOS === 'win') {
1505
1923
  // Linux/Windows simpler structure
1506
1924
  const appBundleFolderPath = join(parentFolder, bundleName);
1507
1925
  const appBundleFolderContentsPath = appBundleFolderPath; // No Contents folder needed
@@ -1522,6 +1940,6 @@ function createAppBundle(bundleName: string, parentFolder: string) {
1522
1940
  appBundleFolderFrameworksPath,
1523
1941
  };
1524
1942
  } else {
1525
- throw new Error(`Unsupported OS: ${OS}`);
1943
+ throw new Error(`Unsupported OS: ${targetOS}`);
1526
1944
  }
1527
1945
  }