electrobun 0.13.0-beta.0 → 0.13.0-beta.12

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.
@@ -5,6 +5,7 @@ import { execSync } from "child_process";
5
5
  import tar from "tar";
6
6
  import { ZstdInit } from "@oneidentity/zstd-js/wasm";
7
7
  import { OS as currentOS, ARCH as currentArch } from '../../shared/platform';
8
+ import { getPlatformFolder, getTarballFileName } from '../../shared/naming';
8
9
  import { native } from '../proc/native';
9
10
 
10
11
  // setTimeout(async () => {
@@ -142,7 +143,7 @@ const Updater = {
142
143
 
143
144
  const channelBucketUrl = await Updater.channelBucketUrl();
144
145
  const cacheBuster = Math.random().toString(36).substring(7);
145
- const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
146
+ const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
146
147
  const updateInfoUrl = join(localInfo.bucketUrl, platformFolder, `update.json?${cacheBuster}`);
147
148
 
148
149
  try {
@@ -210,7 +211,7 @@ const Updater = {
210
211
  }
211
212
 
212
213
  // check if there's a patch file for it
213
- const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
214
+ const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
214
215
  const patchResponse = await fetch(
215
216
  join(localInfo.bucketUrl, platformFolder, `${currentHash}.patch`)
216
217
  );
@@ -329,17 +330,8 @@ const Updater = {
329
330
  // then just download it and unpack it
330
331
  if (currentHash !== latestHash) {
331
332
  const cacheBuster = Math.random().toString(36).substring(7);
332
- const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
333
- // Platform-specific tarball naming
334
- let tarballName: string;
335
- if (currentOS === 'macos') {
336
- tarballName = `${appFileName}.app.tar.zst`;
337
- } else if (currentOS === 'win') {
338
- tarballName = `${appFileName}.tar.zst`;
339
- } else {
340
- tarballName = `${appFileName}.tar.zst`;
341
- }
342
-
333
+ const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
334
+ const tarballName = getTarballFileName(appFileName, currentOS);
343
335
  const urlToLatestTarball = join(
344
336
  localInfo.bucketUrl,
345
337
  platformFolder,
@@ -694,7 +686,7 @@ start "" launcher.exe
694
686
 
695
687
  channelBucketUrl: async () => {
696
688
  await Updater.getLocallocalInfo();
697
- const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
689
+ const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
698
690
  return join(localInfo.bucketUrl, platformFolder);
699
691
  },
700
692
 
@@ -0,0 +1,285 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import {
3
+ sanitizeAppName,
4
+ getAppFileName,
5
+ getBundleFileName,
6
+ getPlatformFolder,
7
+ getTarballFileName,
8
+ getWindowsSetupFileName,
9
+ getLinuxSetupFileName,
10
+ getLinuxAppImageBaseName,
11
+ getLinuxAppImageFileName,
12
+ sanitizeVolumeNameForHdiutil,
13
+ getDmgVolumeName,
14
+ getUpdateInfoUrl,
15
+ getPatchFileUrl,
16
+ getTarballUrl,
17
+ } from './naming';
18
+
19
+ describe('sanitizeAppName', () => {
20
+ it('removes spaces from app name', () => {
21
+ expect(sanitizeAppName('My App')).toBe('MyApp');
22
+ expect(sanitizeAppName('My Multi Spaced App')).toBe('MyMultiSpacedApp');
23
+ });
24
+
25
+ it('preserves names without spaces', () => {
26
+ expect(sanitizeAppName('MyApp')).toBe('MyApp');
27
+ });
28
+
29
+ it('handles empty string', () => {
30
+ expect(sanitizeAppName('')).toBe('');
31
+ });
32
+ });
33
+
34
+ describe('getAppFileName', () => {
35
+ it('returns sanitized name without suffix for stable builds', () => {
36
+ expect(getAppFileName('My App', 'stable')).toBe('MyApp');
37
+ });
38
+
39
+ it('appends channel suffix for canary builds', () => {
40
+ expect(getAppFileName('My App', 'canary')).toBe('MyApp-canary');
41
+ });
42
+
43
+ it('appends channel suffix for dev builds', () => {
44
+ expect(getAppFileName('My App', 'dev')).toBe('MyApp-dev');
45
+ });
46
+
47
+ it('handles custom channels', () => {
48
+ expect(getAppFileName('My App', 'beta')).toBe('MyApp-beta');
49
+ expect(getAppFileName('My App', 'nightly')).toBe('MyApp-nightly');
50
+ });
51
+ });
52
+
53
+ describe('getBundleFileName', () => {
54
+ describe('macOS', () => {
55
+ it('adds .app extension for stable builds', () => {
56
+ expect(getBundleFileName('My App', 'stable', 'macos')).toBe('MyApp.app');
57
+ });
58
+
59
+ it('adds .app extension for canary builds', () => {
60
+ expect(getBundleFileName('My App', 'canary', 'macos')).toBe('MyApp-canary.app');
61
+ });
62
+ });
63
+
64
+ describe('Windows', () => {
65
+ it('returns plain name for stable builds', () => {
66
+ expect(getBundleFileName('My App', 'stable', 'win')).toBe('MyApp');
67
+ });
68
+
69
+ it('returns plain name with suffix for canary builds', () => {
70
+ expect(getBundleFileName('My App', 'canary', 'win')).toBe('MyApp-canary');
71
+ });
72
+ });
73
+
74
+ describe('Linux', () => {
75
+ it('returns plain name for stable builds', () => {
76
+ expect(getBundleFileName('My App', 'stable', 'linux')).toBe('MyApp');
77
+ });
78
+
79
+ it('returns plain name with suffix for canary builds', () => {
80
+ expect(getBundleFileName('My App', 'canary', 'linux')).toBe('MyApp-canary');
81
+ });
82
+ });
83
+ });
84
+
85
+ describe('getPlatformFolder', () => {
86
+ it('constructs correct folder format for all platform combinations', () => {
87
+ expect(getPlatformFolder('stable', 'macos', 'arm64')).toBe('stable-macos-arm64');
88
+ expect(getPlatformFolder('stable', 'macos', 'x64')).toBe('stable-macos-x64');
89
+ expect(getPlatformFolder('canary', 'win', 'x64')).toBe('canary-win-x64');
90
+ expect(getPlatformFolder('dev', 'linux', 'arm64')).toBe('dev-linux-arm64');
91
+ expect(getPlatformFolder('dev', 'linux', 'x64')).toBe('dev-linux-x64');
92
+ });
93
+ });
94
+
95
+ describe('getTarballFileName', () => {
96
+ describe('macOS', () => {
97
+ it('uses .app.tar.zst extension', () => {
98
+ expect(getTarballFileName('MyApp', 'macos')).toBe('MyApp.app.tar.zst');
99
+ });
100
+
101
+ it('preserves channel suffix in filename', () => {
102
+ expect(getTarballFileName('MyApp-canary', 'macos')).toBe('MyApp-canary.app.tar.zst');
103
+ });
104
+ });
105
+
106
+ describe('Windows', () => {
107
+ it('uses .tar.zst extension', () => {
108
+ expect(getTarballFileName('MyApp', 'win')).toBe('MyApp.tar.zst');
109
+ });
110
+
111
+ it('preserves channel suffix in filename', () => {
112
+ expect(getTarballFileName('MyApp-canary', 'win')).toBe('MyApp-canary.tar.zst');
113
+ });
114
+ });
115
+
116
+ describe('Linux', () => {
117
+ it('uses .tar.zst extension', () => {
118
+ expect(getTarballFileName('MyApp', 'linux')).toBe('MyApp.tar.zst');
119
+ });
120
+
121
+ it('preserves channel suffix in filename', () => {
122
+ expect(getTarballFileName('MyApp-canary', 'linux')).toBe('MyApp-canary.tar.zst');
123
+ });
124
+ });
125
+ });
126
+
127
+ describe('getWindowsSetupFileName', () => {
128
+ it('returns AppName-Setup.exe for stable builds', () => {
129
+ expect(getWindowsSetupFileName('MyApp', 'stable')).toBe('MyApp-Setup.exe');
130
+ });
131
+
132
+ it('includes channel suffix for canary builds', () => {
133
+ expect(getWindowsSetupFileName('MyApp', 'canary')).toBe('MyApp-Setup-canary.exe');
134
+ });
135
+
136
+ it('includes channel suffix for dev builds', () => {
137
+ expect(getWindowsSetupFileName('MyApp', 'dev')).toBe('MyApp-Setup-dev.exe');
138
+ });
139
+ });
140
+
141
+ describe('getLinuxSetupFileName', () => {
142
+ it('returns AppName-Setup.run for stable builds', () => {
143
+ expect(getLinuxSetupFileName('MyApp', 'stable')).toBe('MyApp-Setup.run');
144
+ });
145
+
146
+ it('includes channel suffix for canary builds', () => {
147
+ expect(getLinuxSetupFileName('MyApp', 'canary')).toBe('MyApp-Setup-canary.run');
148
+ });
149
+
150
+ it('includes channel suffix for dev builds', () => {
151
+ expect(getLinuxSetupFileName('MyApp', 'dev')).toBe('MyApp-Setup-dev.run');
152
+ });
153
+ });
154
+
155
+ describe('getLinuxAppImageBaseName', () => {
156
+ it('returns AppName-Setup for stable builds', () => {
157
+ expect(getLinuxAppImageBaseName('MyApp', 'stable')).toBe('MyApp-Setup');
158
+ });
159
+
160
+ it('includes channel suffix for canary builds', () => {
161
+ expect(getLinuxAppImageBaseName('MyApp', 'canary')).toBe('MyApp-Setup-canary');
162
+ });
163
+ });
164
+
165
+ describe('getLinuxAppImageFileName', () => {
166
+ it('returns full filename with .AppImage extension for stable', () => {
167
+ expect(getLinuxAppImageFileName('MyApp', 'stable')).toBe('MyApp-Setup.AppImage');
168
+ });
169
+
170
+ it('returns full filename with .AppImage extension for canary', () => {
171
+ expect(getLinuxAppImageFileName('MyApp', 'canary')).toBe('MyApp-Setup-canary.AppImage');
172
+ });
173
+ });
174
+
175
+ describe('sanitizeVolumeNameForHdiutil', () => {
176
+ it('removes special characters', () => {
177
+ expect(sanitizeVolumeNameForHdiutil('My-App_v1.0')).toBe('MyAppv10');
178
+ });
179
+
180
+ it('removes parentheses and other punctuation', () => {
181
+ expect(sanitizeVolumeNameForHdiutil('My App (Beta)')).toBe('My App Beta');
182
+ });
183
+
184
+ it('preserves spaces and alphanumerics', () => {
185
+ expect(sanitizeVolumeNameForHdiutil('My App 2024')).toBe('My App 2024');
186
+ });
187
+
188
+ it('trims leading and trailing whitespace', () => {
189
+ expect(sanitizeVolumeNameForHdiutil(' My App ')).toBe('My App');
190
+ });
191
+
192
+ it('handles names with only special characters', () => {
193
+ expect(sanitizeVolumeNameForHdiutil('---')).toBe('');
194
+ });
195
+ });
196
+
197
+ describe('getDmgVolumeName', () => {
198
+ it('adds -stable suffix for stable builds (to avoid CI volume conflicts)', () => {
199
+ expect(getDmgVolumeName('MyApp', 'stable')).toBe('MyApp-stable');
200
+ });
201
+
202
+ it('returns sanitized name for canary builds (already has suffix)', () => {
203
+ expect(getDmgVolumeName('MyApp-canary', 'canary')).toBe('MyAppcanary');
204
+ });
205
+
206
+ it('sanitizes special characters', () => {
207
+ expect(getDmgVolumeName('My-App', 'stable')).toBe('MyApp-stable');
208
+ });
209
+ });
210
+
211
+ describe('URL construction functions', () => {
212
+ const bucketUrl = 'https://storage.example.com/releases';
213
+ const platformFolder = 'canary-macos-arm64';
214
+
215
+ describe('getUpdateInfoUrl', () => {
216
+ it('constructs correct URL', () => {
217
+ expect(getUpdateInfoUrl(bucketUrl, platformFolder))
218
+ .toBe('https://storage.example.com/releases/canary-macos-arm64/update.json');
219
+ });
220
+
221
+ it('handles bucket URLs with trailing content', () => {
222
+ expect(getUpdateInfoUrl('https://example.com/bucket', 'stable-win-x64'))
223
+ .toBe('https://example.com/bucket/stable-win-x64/update.json');
224
+ });
225
+ });
226
+
227
+ describe('getPatchFileUrl', () => {
228
+ it('constructs correct URL with hash', () => {
229
+ expect(getPatchFileUrl(bucketUrl, platformFolder, 'abc123def456'))
230
+ .toBe('https://storage.example.com/releases/canary-macos-arm64/abc123def456.patch');
231
+ });
232
+ });
233
+
234
+ describe('getTarballUrl', () => {
235
+ it('constructs correct URL for macOS tarball', () => {
236
+ expect(getTarballUrl(bucketUrl, platformFolder, 'MyApp.app.tar.zst'))
237
+ .toBe('https://storage.example.com/releases/canary-macos-arm64/MyApp.app.tar.zst');
238
+ });
239
+
240
+ it('constructs correct URL for Windows tarball', () => {
241
+ expect(getTarballUrl(bucketUrl, 'stable-win-x64', 'MyApp.tar.zst'))
242
+ .toBe('https://storage.example.com/releases/stable-win-x64/MyApp.tar.zst');
243
+ });
244
+ });
245
+ });
246
+
247
+ // Integration tests that verify CLI and Updater would produce matching values
248
+ describe('CLI and Updater consistency', () => {
249
+ it('produces matching platform folders', () => {
250
+ // CLI uses: `${buildEnvironment}-${currentTarget.os}-${currentTarget.arch}`
251
+ // Updater uses: `${localInfo.channel}-${currentOS}-${currentArch}`
252
+ // Both should use getPlatformFolder()
253
+ const cliResult = getPlatformFolder('canary', 'macos', 'arm64');
254
+ const updaterResult = getPlatformFolder('canary', 'macos', 'arm64');
255
+ expect(cliResult).toBe(updaterResult);
256
+ expect(cliResult).toBe('canary-macos-arm64');
257
+ });
258
+
259
+ it('produces matching tarball names for macOS', () => {
260
+ const appFileName = getAppFileName('My App', 'canary');
261
+ const tarballName = getTarballFileName(appFileName, 'macos');
262
+ expect(tarballName).toBe('MyApp-canary.app.tar.zst');
263
+ });
264
+
265
+ it('produces matching tarball names for Windows', () => {
266
+ const appFileName = getAppFileName('My App', 'stable');
267
+ const tarballName = getTarballFileName(appFileName, 'win');
268
+ expect(tarballName).toBe('MyApp.tar.zst');
269
+ });
270
+
271
+ it('produces matching tarball names for Linux', () => {
272
+ const appFileName = getAppFileName('My App', 'dev');
273
+ const tarballName = getTarballFileName(appFileName, 'linux');
274
+ expect(tarballName).toBe('MyApp-dev.tar.zst');
275
+ });
276
+
277
+ it('stable builds have no channel suffix in artifact names', () => {
278
+ // This is the regression test for the -stable bug
279
+ expect(getAppFileName('MyApp', 'stable')).toBe('MyApp');
280
+ expect(getAppFileName('MyApp', 'stable')).not.toContain('-stable');
281
+ expect(getTarballFileName(getAppFileName('MyApp', 'stable'), 'macos')).toBe('MyApp.app.tar.zst');
282
+ expect(getWindowsSetupFileName('MyApp', 'stable')).toBe('MyApp-Setup.exe');
283
+ expect(getLinuxSetupFileName('MyApp', 'stable')).toBe('MyApp-Setup.run');
284
+ });
285
+ });
@@ -0,0 +1,131 @@
1
+ import type { SupportedOS, SupportedArch } from './platform';
2
+
3
+ /**
4
+ * Build environment/channel types.
5
+ * "stable" is special - it produces artifacts without a channel suffix.
6
+ */
7
+ export type BuildEnvironment = 'stable' | 'canary' | 'dev' | (string & {});
8
+
9
+ /**
10
+ * Sanitizes an app name by removing spaces.
11
+ * Used as the base for all artifact naming.
12
+ */
13
+ export function sanitizeAppName(appName: string): string {
14
+ return appName.replace(/ /g, '');
15
+ }
16
+
17
+ /**
18
+ * Generates the app file name based on build environment.
19
+ * Format: "AppName" (stable) or "AppName-channel" (non-stable)
20
+ *
21
+ * @example
22
+ * getAppFileName("My App", "stable") // "MyApp"
23
+ * getAppFileName("My App", "canary") // "MyApp-canary"
24
+ */
25
+ export function getAppFileName(appName: string, buildEnvironment: BuildEnvironment): string {
26
+ const sanitized = sanitizeAppName(appName);
27
+ return buildEnvironment === 'stable' ? sanitized : `${sanitized}-${buildEnvironment}`;
28
+ }
29
+
30
+ /**
31
+ * Generates the bundle file name (with platform-specific extension).
32
+ * macOS: "AppName.app" or "AppName-channel.app"
33
+ * Others: "AppName" or "AppName-channel"
34
+ */
35
+ export function getBundleFileName(appName: string, buildEnvironment: BuildEnvironment, os: SupportedOS): string {
36
+ const appFileName = getAppFileName(appName, buildEnvironment);
37
+ return os === 'macos' ? `${appFileName}.app` : appFileName;
38
+ }
39
+
40
+ /**
41
+ * Generates the platform folder name for artifacts.
42
+ * Format: "channel-os-arch" (e.g., "stable-macos-arm64", "canary-win-x64")
43
+ */
44
+ export function getPlatformFolder(buildEnvironment: BuildEnvironment, os: SupportedOS, arch: SupportedArch): string {
45
+ return `${buildEnvironment}-${os}-${arch}`;
46
+ }
47
+
48
+ /**
49
+ * Generates the tarball file name for update distribution.
50
+ * macOS: "AppFileName.app.tar.zst"
51
+ * Others: "AppFileName.tar.zst"
52
+ */
53
+ export function getTarballFileName(appFileName: string, os: SupportedOS): string {
54
+ return os === 'macos' ? `${appFileName}.app.tar.zst` : `${appFileName}.tar.zst`;
55
+ }
56
+
57
+ /**
58
+ * Generates the Windows installer setup file name.
59
+ * Format: "AppName-Setup.exe" (stable) or "AppName-Setup-channel.exe" (non-stable)
60
+ */
61
+ export function getWindowsSetupFileName(appName: string, buildEnvironment: BuildEnvironment): string {
62
+ return buildEnvironment === 'stable'
63
+ ? `${appName}-Setup.exe`
64
+ : `${appName}-Setup-${buildEnvironment}.exe`;
65
+ }
66
+
67
+ /**
68
+ * Generates the Linux self-extracting binary file name.
69
+ * Format: "AppName-Setup.run" (stable) or "AppName-Setup-channel.run" (non-stable)
70
+ */
71
+ export function getLinuxSetupFileName(appName: string, buildEnvironment: BuildEnvironment): string {
72
+ return buildEnvironment === 'stable'
73
+ ? `${appName}-Setup.run`
74
+ : `${appName}-Setup-${buildEnvironment}.run`;
75
+ }
76
+
77
+ /**
78
+ * Generates the Linux AppImage wrapper name (without extension).
79
+ * Format: "AppName-Setup" (stable) or "AppName-Setup-channel" (non-stable)
80
+ */
81
+ export function getLinuxAppImageBaseName(appName: string, buildEnvironment: BuildEnvironment): string {
82
+ return buildEnvironment === 'stable'
83
+ ? `${appName}-Setup`
84
+ : `${appName}-Setup-${buildEnvironment}`;
85
+ }
86
+
87
+ /**
88
+ * Generates the full Linux AppImage file name.
89
+ */
90
+ export function getLinuxAppImageFileName(appName: string, buildEnvironment: BuildEnvironment): string {
91
+ return `${getLinuxAppImageBaseName(appName, buildEnvironment)}.AppImage`;
92
+ }
93
+
94
+ /**
95
+ * Sanitizes a volume name for hdiutil (macOS DMG creation).
96
+ * Removes all non-alphanumeric characters except spaces.
97
+ */
98
+ export function sanitizeVolumeNameForHdiutil(name: string): string {
99
+ return name.replace(/[^a-zA-Z0-9 ]/g, '').trim();
100
+ }
101
+
102
+ /**
103
+ * Generates the DMG volume name for macOS during creation.
104
+ * Uses sanitized name with "-stable" suffix for stable builds to avoid
105
+ * CI volume mounting conflicts. The DMG is renamed after creation.
106
+ */
107
+ export function getDmgVolumeName(appFileName: string, buildEnvironment: BuildEnvironment): string {
108
+ const baseName = sanitizeVolumeNameForHdiutil(appFileName);
109
+ return buildEnvironment === 'stable' ? `${baseName}-stable` : baseName;
110
+ }
111
+
112
+ /**
113
+ * Constructs the full URL for the update.json file.
114
+ */
115
+ export function getUpdateInfoUrl(bucketUrl: string, platformFolder: string): string {
116
+ return `${bucketUrl}/${platformFolder}/update.json`;
117
+ }
118
+
119
+ /**
120
+ * Constructs the full URL for a patch file.
121
+ */
122
+ export function getPatchFileUrl(bucketUrl: string, platformFolder: string, hash: string): string {
123
+ return `${bucketUrl}/${platformFolder}/${hash}.patch`;
124
+ }
125
+
126
+ /**
127
+ * Constructs the full URL for a tarball.
128
+ */
129
+ export function getTarballUrl(bucketUrl: string, platformFolder: string, tarballFileName: string): string {
130
+ return `${bucketUrl}/${platformFolder}/${tarballFileName}`;
131
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrobun",
3
- "version": "0.13.0-beta.0",
3
+ "version": "0.13.0-beta.12",
4
4
  "description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
5
5
  "license": "MIT",
6
6
  "author": "Blackboard Technologies Inc.",
@@ -35,6 +35,7 @@
35
35
  "dev:clean": "cd ../kitchen && rm -rf node_modules && rm -rf vendors/cef && cd ../package && bun dev",
36
36
  "dev:rerun": "cd ../kitchen && bun start",
37
37
  "dev:canary": "bun install && bun build:release && bun build:cli && cd ../kitchen && bun install && bun build:canary",
38
+ "dev:stable": "bun install && bun build:release && bun build:cli && cd ../kitchen && bun install && bun build:stable",
38
39
  "run:template": "bun install && bun build:dev && bun build:cli && cd ../templates/interactive-playground && bun install && bun build:dev && bun start",
39
40
  "dev:docs": "cd ../documentation && bun start",
40
41
  "build:docs:release": "cd ../documentation && bun run build",
@@ -46,7 +47,8 @@
46
47
  "push:major": "bun scripts/push-version.js major",
47
48
  "push:stable": "bun scripts/push-version.js stable",
48
49
  "build:push:artifacts": "bun scripts/build-and-upload-artifacts.js",
49
- "test": "bun install && bun build:dev && bun build:cli && cd ../tests && bun install && bun build:dev && bun start"
50
+ "test": "bun install && bun build:dev && bun build:cli && cd ../tests && bun install && bun build:dev && bun start",
51
+ "test:unit": "bun test src/shared"
50
52
  },
51
53
  "devDependencies": {
52
54
  "@types/archiver": "^6.0.3",
package/src/cli/index.ts CHANGED
@@ -22,6 +22,17 @@ import tar from "tar";
22
22
  import archiver from "archiver";
23
23
  import { ZstdInit } from "@oneidentity/zstd-js/wasm";
24
24
  import { OS, ARCH } from '../shared/platform';
25
+ import {
26
+ getAppFileName,
27
+ getBundleFileName,
28
+ getPlatformFolder,
29
+ getTarballFileName,
30
+ getWindowsSetupFileName,
31
+ getLinuxSetupFileName,
32
+ getLinuxAppImageBaseName,
33
+ sanitizeVolumeNameForHdiutil,
34
+ getDmgVolumeName,
35
+ } from '../shared/naming';
25
36
  import { getTemplate, getTemplateNames } from './templates/embedded';
26
37
  // import { loadBsdiff, loadBspatch } from 'bsdiff-wasm';
27
38
  // MacOS named pipes hang at around 4KB
@@ -1014,6 +1025,12 @@ if (commandArg === "init") {
1014
1025
  console.log(" bun start");
1015
1026
  console.log("");
1016
1027
  console.log("🎉 Happy building with Electrobun!");
1028
+ console.log("");
1029
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
1030
+ console.log("NOTE FOR AI ASSISTANTS: Electrobun is NOT Electron.");
1031
+ console.log("Different architecture, different APIs. Do not use Electron patterns.");
1032
+ console.log("Docs: https://blackboard.sh/electrobun/llms.txt");
1033
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
1017
1034
  })();
1018
1035
  } else if (commandArg === "build") {
1019
1036
  // Get config
@@ -1030,10 +1047,10 @@ if (commandArg === "init") {
1030
1047
  const targetOS = currentTarget.os;
1031
1048
  const targetARCH = currentTarget.arch;
1032
1049
  const targetBinExt = targetOS === 'win' ? '.exe' : '';
1033
- const appFileName = `${config.app.name.replace(/ /g, "")}-${buildEnvironment}`;
1034
- const buildSubFolder = `${buildEnvironment}-${currentTarget.os}-${currentTarget.arch}`;
1050
+ const appFileName = getAppFileName(config.app.name, buildEnvironment);
1051
+ const buildSubFolder = getPlatformFolder(buildEnvironment, currentTarget.os, currentTarget.arch);
1035
1052
  const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
1036
- const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName;
1053
+ const bundleFileName = getBundleFileName(config.app.name, buildEnvironment, targetOS);
1037
1054
  const artifactFolder = join(projectRoot, config.build.artifactFolder, buildSubFolder);
1038
1055
 
1039
1056
  // Ensure core binaries are available for the target platform before starting build
@@ -1041,11 +1058,6 @@ if (commandArg === "init") {
1041
1058
 
1042
1059
  // Get platform-specific paths for the current target
1043
1060
  const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);
1044
-
1045
- // Helper functions
1046
- const sanitizeVolumeNameForHdiutil = (name: string) => {
1047
- return name.replace(/[^a-zA-Z0-9 ]/g, '').trim();
1048
- };
1049
1061
 
1050
1062
  // Helper to run lifecycle hook scripts
1051
1063
  const runHook = (hookName: keyof typeof config.scripts, extraEnv: Record<string, string> = {}) => {
@@ -1087,11 +1099,28 @@ if (commandArg === "init") {
1087
1099
 
1088
1100
  const buildIcons = (appBundleFolderResourcesPath: string) => {
1089
1101
  // Platform-specific icon handling
1090
- if (targetOS === 'macos' && config.build.mac?.icon) {
1091
- const iconPath = join(projectRoot, config.build.mac.icon);
1092
- if (existsSync(iconPath)) {
1093
- const targetIconPath = join(appBundleFolderResourcesPath, "AppIcon.icns");
1094
- cpSync(iconPath, targetIconPath, { dereference: true });
1102
+ if (targetOS === 'macos' && config.build.mac?.icons) {
1103
+ // macOS uses .iconset folders that get converted to .icns using iconutil
1104
+ // This only works when building on macOS since iconutil is a macOS-only tool
1105
+ const iconSourceFolder = join(projectRoot, config.build.mac.icons);
1106
+ const iconDestPath = join(appBundleFolderResourcesPath, "AppIcon.icns");
1107
+ if (existsSync(iconSourceFolder)) {
1108
+ if (OS === 'macos') {
1109
+ // Use iconutil to convert .iconset folder to .icns
1110
+ Bun.spawnSync(
1111
+ ["iconutil", "-c", "icns", "-o", iconDestPath, iconSourceFolder],
1112
+ {
1113
+ cwd: appBundleFolderResourcesPath,
1114
+ stdio: ["ignore", "inherit", "inherit"],
1115
+ env: {
1116
+ ...process.env,
1117
+ ELECTROBUN_BUILD_ENV: buildEnvironment,
1118
+ },
1119
+ }
1120
+ );
1121
+ } else {
1122
+ console.log(`WARNING: Cannot build macOS icons on ${OS} - iconutil is only available on macOS`);
1123
+ }
1095
1124
  }
1096
1125
  } else if (targetOS === 'linux' && config.build.linux?.icon) {
1097
1126
  const iconSourcePath = join(projectRoot, config.build.linux.icon);
@@ -1965,7 +1994,8 @@ if (commandArg === "init") {
1965
1994
  if (shouldCodesign) {
1966
1995
  codesignAppBundle(
1967
1996
  appBundleFolderPath,
1968
- join(buildFolder, "entitlements.plist")
1997
+ join(buildFolder, "entitlements.plist"),
1998
+ config
1969
1999
  );
1970
2000
  } else {
1971
2001
  console.log("skipping codesign");
@@ -1975,11 +2005,11 @@ if (commandArg === "init") {
1975
2005
  // NOTE: Codesigning fails in dev mode (when using a single-file-executable bun cli as the launcher)
1976
2006
  // see https://github.com/oven-sh/bun/issues/7208
1977
2007
  if (shouldNotarize) {
1978
- notarizeAndStaple(appBundleFolderPath);
2008
+ notarizeAndStaple(appBundleFolderPath, config);
1979
2009
  } else {
1980
2010
  console.log("skipping notarization");
1981
2011
  }
1982
-
2012
+
1983
2013
  const artifactsToUpload = [];
1984
2014
 
1985
2015
  console.log(`DEBUG: Checking for Linux AppImage creation - targetOS: ${targetOS}, buildEnvironment: ${buildEnvironment}`);
@@ -2203,7 +2233,8 @@ if (commandArg === "init") {
2203
2233
  if (shouldCodesign) {
2204
2234
  codesignAppBundle(
2205
2235
  selfExtractingBundle.appBundleFolderPath,
2206
- join(buildFolder, "entitlements.plist")
2236
+ join(buildFolder, "entitlements.plist"),
2237
+ config
2207
2238
  );
2208
2239
  } else {
2209
2240
  console.log("skipping codesign");
@@ -2211,7 +2242,7 @@ if (commandArg === "init") {
2211
2242
 
2212
2243
  // Note: we need to notarize the original app bundle, the self-extracting app bundle, and the dmg
2213
2244
  if (shouldNotarize) {
2214
- notarizeAndStaple(selfExtractingBundle.appBundleFolderPath);
2245
+ notarizeAndStaple(selfExtractingBundle.appBundleFolderPath, config);
2215
2246
  } else {
2216
2247
  console.log("skipping notarization");
2217
2248
  }
@@ -2229,11 +2260,7 @@ if (commandArg === "init") {
2229
2260
  buildEnvironment === "stable"
2230
2261
  ? join(buildFolder, `${appFileName}-stable.dmg`)
2231
2262
  : finalDmgPath;
2232
- const baseVolumeName = sanitizeVolumeNameForHdiutil(appFileName);
2233
- const dmgVolumeName =
2234
- buildEnvironment === "stable"
2235
- ? `${baseVolumeName}-stable`
2236
- : baseVolumeName;
2263
+ const dmgVolumeName = getDmgVolumeName(appFileName, buildEnvironment);
2237
2264
 
2238
2265
  // Create a staging directory for DMG contents (app + Applications shortcut)
2239
2266
  const dmgStagingDir = join(buildFolder, '.dmg-staging');
@@ -2266,13 +2293,13 @@ if (commandArg === "init") {
2266
2293
  artifactsToUpload.push(finalDmgPath);
2267
2294
 
2268
2295
  if (shouldCodesign) {
2269
- codesignAppBundle(finalDmgPath);
2296
+ codesignAppBundle(finalDmgPath, undefined, config);
2270
2297
  } else {
2271
2298
  console.log("skipping codesign");
2272
2299
  }
2273
2300
 
2274
2301
  if (shouldNotarize) {
2275
- notarizeAndStaple(finalDmgPath);
2302
+ notarizeAndStaple(finalDmgPath, config);
2276
2303
  } else {
2277
2304
  console.log("skipping notarization");
2278
2305
  }
@@ -2702,11 +2729,7 @@ async function createWindowsSelfExtractingExe(
2702
2729
  ): Promise<string> {
2703
2730
  console.log("Creating Windows installer with separate archive...");
2704
2731
 
2705
- // Format: MyApp-Setup.exe (stable) or MyApp-Setup-canary.exe (non-stable)
2706
- const setupFileName = buildEnvironment === "stable"
2707
- ? `${config.app.name}-Setup.exe`
2708
- : `${config.app.name}-Setup-${buildEnvironment}.exe`;
2709
-
2732
+ const setupFileName = getWindowsSetupFileName(config.app.name, buildEnvironment);
2710
2733
  const outputExePath = join(buildFolder, setupFileName);
2711
2734
 
2712
2735
  // Copy the extractor exe
@@ -2793,14 +2816,12 @@ async function createLinuxSelfExtractingBinary(
2793
2816
  compressedTarPath: string,
2794
2817
  appFileName: string,
2795
2818
  targetPaths: any,
2796
- buildEnvironment: string
2819
+ buildEnvironment: string,
2820
+ config: Awaited<ReturnType<typeof getConfig>>
2797
2821
  ): Promise<string> {
2798
2822
  console.log("Creating self-extracting Linux binary...");
2799
-
2800
- // Format: MyApp-Setup.run (stable) or MyApp-Setup-canary.run (non-stable)
2801
- const setupFileName = buildEnvironment === "stable"
2802
- ? `${config.app.name}-Setup.run`
2803
- : `${config.app.name}-Setup-${buildEnvironment}.run`;
2823
+
2824
+ const setupFileName = getLinuxSetupFileName(config.app.name, buildEnvironment);
2804
2825
 
2805
2826
  const outputPath = join(buildFolder, setupFileName);
2806
2827
 
@@ -2967,10 +2988,7 @@ async function createLinuxSelfExtractingAppImage(
2967
2988
  console.log('Creating Linux AppImage wrapper...');
2968
2989
 
2969
2990
  // Create wrapper AppImage filename
2970
- const wrapperName = buildEnvironment === 'stable'
2971
- ? `${config.app.name}-Setup`
2972
- : `${config.app.name}-Setup-${buildEnvironment}`;
2973
-
2991
+ const wrapperName = getLinuxAppImageBaseName(config.app.name, buildEnvironment);
2974
2992
  const wrapperAppImagePath = join(buildFolder, `${wrapperName}.AppImage`);
2975
2993
  const wrapperAppDirPath = join(buildFolder, `${wrapperName}.AppDir`);
2976
2994
 
@@ -3111,7 +3129,8 @@ Categories=Utility;
3111
3129
 
3112
3130
  function codesignAppBundle(
3113
3131
  appBundleOrDmgPath: string,
3114
- entitlementsFilePath?: string
3132
+ entitlementsFilePath: string | undefined,
3133
+ config: Awaited<ReturnType<typeof getConfig>>
3115
3134
  ) {
3116
3135
  console.log("code signing...");
3117
3136
  if (OS !== 'macos' || !config.build.mac.codesign) {
@@ -3310,7 +3329,7 @@ function codesignAppBundle(
3310
3329
  );
3311
3330
  }
3312
3331
 
3313
- function notarizeAndStaple(appOrDmgPath: string) {
3332
+ function notarizeAndStaple(appOrDmgPath: string, config: Awaited<ReturnType<typeof getConfig>>) {
3314
3333
  if (OS !== 'macos' || !config.build.mac.notarize) {
3315
3334
  return;
3316
3335
  }