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.
- package/dist/api/bun/core/Updater.ts +6 -14
- package/dist/api/shared/naming.test.ts +285 -0
- package/dist/api/shared/naming.ts +131 -0
- package/package.json +4 -2
- package/src/cli/index.ts +61 -42
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
333
|
-
|
|
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 =
|
|
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.
|
|
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 =
|
|
1034
|
-
const buildSubFolder =
|
|
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 =
|
|
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?.
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|