electrobun 0.13.0-beta.2 → 0.13.0-beta.20

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,
@@ -563,9 +555,18 @@ const Updater = {
563
555
 
564
556
  // Move current running app to backup
565
557
  renameSync(runningAppBundlePath, backupPath);
566
-
558
+
567
559
  // Move new app to running location
568
560
  renameSync(newAppBundlePath, runningAppBundlePath);
561
+
562
+ // Remove quarantine extended attributes to prevent "damaged" error
563
+ // The inner bundle is already signed/notarized, but macOS applies
564
+ // quarantine attributes when extracting from a downloaded archive
565
+ try {
566
+ execSync(`xattr -r -d com.apple.quarantine "${runningAppBundlePath}"`, { stdio: 'ignore' });
567
+ } catch (e) {
568
+ // Ignore errors - attribute may not exist
569
+ }
569
570
  } else if (currentOS === 'linux') {
570
571
  // On Linux, backup and replace AppImage file
571
572
  // Remove existing backup if it exists
@@ -669,16 +670,22 @@ start "" launcher.exe
669
670
  // On Windows, launch the run.bat file which handles versioning
670
671
  const parentDir = dirname(runningAppBundlePath);
671
672
  const runBatPath = join(parentDir, "run.bat");
672
-
673
-
674
- await Bun.spawn(["cmd", "/c", runBatPath], { detached: true });
673
+
674
+ // Use start command to launch detached process that survives parent termination
675
+ await Bun.spawn(["cmd", "/c", "start", "", "/d", parentDir, "run.bat"], { detached: true });
675
676
  break;
676
677
  case 'linux':
677
678
  // On Linux, launch the AppImage directly
678
679
  Bun.spawn(["sh", "-c", `"${runningAppBundlePath}" &`], { detached: true});
679
680
  break;
680
681
  }
681
- // Use native killApp to properly clean up all resources
682
+
683
+ // Small delay on Windows to ensure new process starts before we terminate
684
+ if (currentOS === 'win') {
685
+ await new Promise(resolve => setTimeout(resolve, 500));
686
+ }
687
+
688
+ // Use native killApp to properly clean up all resources
682
689
  try {
683
690
  native.symbols.killApp();
684
691
  // Still call process.exit as a fallback
@@ -687,14 +694,14 @@ start "" launcher.exe
687
694
  // Fallback if native binding fails
688
695
  console.error('Failed to call native killApp:', e);
689
696
  process.exit(0);
690
- }
697
+ }
691
698
  }
692
699
  }
693
700
  },
694
701
 
695
702
  channelBucketUrl: async () => {
696
703
  await Updater.getLocallocalInfo();
697
- const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
704
+ const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
698
705
  return join(localInfo.bucketUrl, platformFolder);
699
706
  },
700
707
 
@@ -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,134 @@
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
+ const sanitized = sanitizeAppName(appName);
63
+ return buildEnvironment === 'stable'
64
+ ? `${sanitized}-Setup.exe`
65
+ : `${sanitized}-Setup-${buildEnvironment}.exe`;
66
+ }
67
+
68
+ /**
69
+ * Generates the Linux self-extracting binary file name.
70
+ * Format: "AppName-Setup.run" (stable) or "AppName-Setup-channel.run" (non-stable)
71
+ */
72
+ export function getLinuxSetupFileName(appName: string, buildEnvironment: BuildEnvironment): string {
73
+ const sanitized = sanitizeAppName(appName);
74
+ return buildEnvironment === 'stable'
75
+ ? `${sanitized}-Setup.run`
76
+ : `${sanitized}-Setup-${buildEnvironment}.run`;
77
+ }
78
+
79
+ /**
80
+ * Generates the Linux AppImage wrapper name (without extension).
81
+ * Format: "AppName-Setup" (stable) or "AppName-Setup-channel" (non-stable)
82
+ */
83
+ export function getLinuxAppImageBaseName(appName: string, buildEnvironment: BuildEnvironment): string {
84
+ const sanitized = sanitizeAppName(appName);
85
+ return buildEnvironment === 'stable'
86
+ ? `${sanitized}-Setup`
87
+ : `${sanitized}-Setup-${buildEnvironment}`;
88
+ }
89
+
90
+ /**
91
+ * Generates the full Linux AppImage file name.
92
+ */
93
+ export function getLinuxAppImageFileName(appName: string, buildEnvironment: BuildEnvironment): string {
94
+ return `${getLinuxAppImageBaseName(appName, buildEnvironment)}.AppImage`;
95
+ }
96
+
97
+ /**
98
+ * Sanitizes a volume name for hdiutil (macOS DMG creation).
99
+ * Removes all non-alphanumeric characters except spaces.
100
+ */
101
+ export function sanitizeVolumeNameForHdiutil(name: string): string {
102
+ return name.replace(/[^a-zA-Z0-9 ]/g, '').trim();
103
+ }
104
+
105
+ /**
106
+ * Generates the DMG volume name for macOS during creation.
107
+ * Uses sanitized name with "-stable" suffix for stable builds to avoid
108
+ * CI volume mounting conflicts. The DMG is renamed after creation.
109
+ */
110
+ export function getDmgVolumeName(appFileName: string, buildEnvironment: BuildEnvironment): string {
111
+ const baseName = sanitizeVolumeNameForHdiutil(appFileName);
112
+ return buildEnvironment === 'stable' ? `${baseName}-stable` : baseName;
113
+ }
114
+
115
+ /**
116
+ * Constructs the full URL for the update.json file.
117
+ */
118
+ export function getUpdateInfoUrl(bucketUrl: string, platformFolder: string): string {
119
+ return `${bucketUrl}/${platformFolder}/update.json`;
120
+ }
121
+
122
+ /**
123
+ * Constructs the full URL for a patch file.
124
+ */
125
+ export function getPatchFileUrl(bucketUrl: string, platformFolder: string, hash: string): string {
126
+ return `${bucketUrl}/${platformFolder}/${hash}.patch`;
127
+ }
128
+
129
+ /**
130
+ * Constructs the full URL for a tarball.
131
+ */
132
+ export function getTarballUrl(bucketUrl: string, platformFolder: string, tarballFileName: string): string {
133
+ return `${bucketUrl}/${platformFolder}/${tarballFileName}`;
134
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrobun",
3
- "version": "0.13.0-beta.2",
3
+ "version": "0.13.0-beta.20",
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
@@ -1036,10 +1047,10 @@ if (commandArg === "init") {
1036
1047
  const targetOS = currentTarget.os;
1037
1048
  const targetARCH = currentTarget.arch;
1038
1049
  const targetBinExt = targetOS === 'win' ? '.exe' : '';
1039
- const appFileName = `${config.app.name.replace(/ /g, "")}-${buildEnvironment}`;
1040
- const buildSubFolder = `${buildEnvironment}-${currentTarget.os}-${currentTarget.arch}`;
1050
+ const appFileName = getAppFileName(config.app.name, buildEnvironment);
1051
+ const buildSubFolder = getPlatformFolder(buildEnvironment, currentTarget.os, currentTarget.arch);
1041
1052
  const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
1042
- const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName;
1053
+ const bundleFileName = getBundleFileName(config.app.name, buildEnvironment, targetOS);
1043
1054
  const artifactFolder = join(projectRoot, config.build.artifactFolder, buildSubFolder);
1044
1055
 
1045
1056
  // Ensure core binaries are available for the target platform before starting build
@@ -1047,11 +1058,6 @@ if (commandArg === "init") {
1047
1058
 
1048
1059
  // Get platform-specific paths for the current target
1049
1060
  const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);
1050
-
1051
- // Helper functions
1052
- const sanitizeVolumeNameForHdiutil = (name: string) => {
1053
- return name.replace(/[^a-zA-Z0-9 ]/g, '').trim();
1054
- };
1055
1061
 
1056
1062
  // Helper to run lifecycle hook scripts
1057
1063
  const runHook = (hookName: keyof typeof config.scripts, extraEnv: Record<string, string> = {}) => {
@@ -2254,11 +2260,7 @@ if (commandArg === "init") {
2254
2260
  buildEnvironment === "stable"
2255
2261
  ? join(buildFolder, `${appFileName}-stable.dmg`)
2256
2262
  : finalDmgPath;
2257
- const baseVolumeName = sanitizeVolumeNameForHdiutil(appFileName);
2258
- const dmgVolumeName =
2259
- buildEnvironment === "stable"
2260
- ? `${baseVolumeName}-stable`
2261
- : baseVolumeName;
2263
+ const dmgVolumeName = getDmgVolumeName(appFileName, buildEnvironment);
2262
2264
 
2263
2265
  // Create a staging directory for DMG contents (app + Applications shortcut)
2264
2266
  const dmgStagingDir = join(buildFolder, '.dmg-staging');
@@ -2727,11 +2729,7 @@ async function createWindowsSelfExtractingExe(
2727
2729
  ): Promise<string> {
2728
2730
  console.log("Creating Windows installer with separate archive...");
2729
2731
 
2730
- // Format: MyApp-Setup.exe (stable) or MyApp-Setup-canary.exe (non-stable)
2731
- const setupFileName = buildEnvironment === "stable"
2732
- ? `${config.app.name}-Setup.exe`
2733
- : `${config.app.name}-Setup-${buildEnvironment}.exe`;
2734
-
2732
+ const setupFileName = getWindowsSetupFileName(config.app.name, buildEnvironment);
2735
2733
  const outputExePath = join(buildFolder, setupFileName);
2736
2734
 
2737
2735
  // Copy the extractor exe
@@ -2822,11 +2820,8 @@ async function createLinuxSelfExtractingBinary(
2822
2820
  config: Awaited<ReturnType<typeof getConfig>>
2823
2821
  ): Promise<string> {
2824
2822
  console.log("Creating self-extracting Linux binary...");
2825
-
2826
- // Format: MyApp-Setup.run (stable) or MyApp-Setup-canary.run (non-stable)
2827
- const setupFileName = buildEnvironment === "stable"
2828
- ? `${config.app.name}-Setup.run`
2829
- : `${config.app.name}-Setup-${buildEnvironment}.run`;
2823
+
2824
+ const setupFileName = getLinuxSetupFileName(config.app.name, buildEnvironment);
2830
2825
 
2831
2826
  const outputPath = join(buildFolder, setupFileName);
2832
2827
 
@@ -2993,10 +2988,7 @@ async function createLinuxSelfExtractingAppImage(
2993
2988
  console.log('Creating Linux AppImage wrapper...');
2994
2989
 
2995
2990
  // Create wrapper AppImage filename
2996
- const wrapperName = buildEnvironment === 'stable'
2997
- ? `${config.app.name}-Setup`
2998
- : `${config.app.name}-Setup-${buildEnvironment}`;
2999
-
2991
+ const wrapperName = getLinuxAppImageBaseName(config.app.name, buildEnvironment);
3000
2992
  const wrapperAppImagePath = join(buildFolder, `${wrapperName}.AppImage`);
3001
2993
  const wrapperAppDirPath = join(buildFolder, `${wrapperName}.AppDir`);
3002
2994