electrobun 1.13.1 → 1.14.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -7
- package/src/cli/index.ts +548 -230
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "electrobun",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0-beta.0",
|
|
4
4
|
"description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Blackboard Technologies Inc.",
|
|
@@ -26,17 +26,14 @@
|
|
|
26
26
|
"url": "git+https://github.com/blackboardsh/electrobun.git"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build:cli": "mkdir -p bin && vendors/bun/bun build src/cli/index.ts --compile --outfile bin/electrobun",
|
|
30
29
|
"start": "bun src/bun/index.ts",
|
|
31
30
|
"check-zig-version": "vendors/zig/zig version",
|
|
32
31
|
"build:dev": "bun build.ts",
|
|
33
32
|
"build:release": "bun build.ts --release",
|
|
34
|
-
"dev": "bun install && bun build:dev &&
|
|
33
|
+
"dev": "bun install && bun build:dev && cd ../kitchen && bun install && bun dev",
|
|
35
34
|
"dev:clean": "cd ../kitchen && rm -rf node_modules && rm -rf vendors/cef && cd ../package && bun dev",
|
|
36
|
-
"dev:
|
|
37
|
-
"dev:
|
|
38
|
-
"dev:stable": "bun install && bun build:release && bun build:cli && cd ../kitchen && bun install && bun build:stable",
|
|
39
|
-
"dev:docs": "cd ../documentation && bun start",
|
|
35
|
+
"dev:canary": "bun install && bun build:release && cd ../kitchen && bun install && bun build:canary",
|
|
36
|
+
"dev:stable": "bun install && bun build:release && cd ../kitchen && bun install && bun build:stable",
|
|
40
37
|
"build:docs:release": "cd ../documentation && bun run build",
|
|
41
38
|
"npm:publish": "bun build:release && npm publish",
|
|
42
39
|
"npm:publish:beta": "bun build:release && npm publish --tag beta",
|
package/src/cli/index.ts
CHANGED
|
@@ -367,7 +367,13 @@ function getEffectiveCEFDir(
|
|
|
367
367
|
cefVersion?: string,
|
|
368
368
|
): string {
|
|
369
369
|
if (cefVersion) {
|
|
370
|
-
return join(
|
|
370
|
+
return join(
|
|
371
|
+
projectRoot,
|
|
372
|
+
"node_modules",
|
|
373
|
+
".electrobun-cache",
|
|
374
|
+
"cef-override",
|
|
375
|
+
`${platformOS}-${platformArch}`,
|
|
376
|
+
);
|
|
371
377
|
}
|
|
372
378
|
return getPlatformPaths(platformOS, platformArch).CEF_DIR;
|
|
373
379
|
}
|
|
@@ -387,7 +393,13 @@ async function ensureBunBinary(
|
|
|
387
393
|
}
|
|
388
394
|
|
|
389
395
|
const binExt = targetOS === "win" ? ".exe" : "";
|
|
390
|
-
const overrideDir = join(
|
|
396
|
+
const overrideDir = join(
|
|
397
|
+
projectRoot,
|
|
398
|
+
"node_modules",
|
|
399
|
+
".electrobun-cache",
|
|
400
|
+
"bun-override",
|
|
401
|
+
`${targetOS}-${targetArch}`,
|
|
402
|
+
);
|
|
391
403
|
const overrideBinary = join(overrideDir, `bun${binExt}`);
|
|
392
404
|
const versionFile = join(overrideDir, ".bun-version");
|
|
393
405
|
|
|
@@ -431,17 +443,29 @@ async function downloadCustomBun(
|
|
|
431
443
|
bunUrlSegment = "bun-windows-x64-baseline.zip";
|
|
432
444
|
bunDirName = "bun-windows-x64-baseline";
|
|
433
445
|
} else if (platformOS === "macos") {
|
|
434
|
-
bunUrlSegment =
|
|
435
|
-
|
|
446
|
+
bunUrlSegment =
|
|
447
|
+
platformArch === "arm64"
|
|
448
|
+
? "bun-darwin-aarch64.zip"
|
|
449
|
+
: "bun-darwin-x64.zip";
|
|
450
|
+
bunDirName =
|
|
451
|
+
platformArch === "arm64" ? "bun-darwin-aarch64" : "bun-darwin-x64";
|
|
436
452
|
} else if (platformOS === "linux") {
|
|
437
|
-
bunUrlSegment =
|
|
438
|
-
|
|
453
|
+
bunUrlSegment =
|
|
454
|
+
platformArch === "arm64" ? "bun-linux-aarch64.zip" : "bun-linux-x64.zip";
|
|
455
|
+
bunDirName =
|
|
456
|
+
platformArch === "arm64" ? "bun-linux-aarch64" : "bun-linux-x64";
|
|
439
457
|
} else {
|
|
440
458
|
throw new Error(`Unsupported platform for custom Bun: ${platformOS}`);
|
|
441
459
|
}
|
|
442
460
|
|
|
443
461
|
const binExt = platformOS === "win" ? ".exe" : "";
|
|
444
|
-
const overrideDir = join(
|
|
462
|
+
const overrideDir = join(
|
|
463
|
+
projectRoot,
|
|
464
|
+
"node_modules",
|
|
465
|
+
".electrobun-cache",
|
|
466
|
+
"bun-override",
|
|
467
|
+
`${platformOS}-${platformArch}`,
|
|
468
|
+
);
|
|
445
469
|
const overrideBinary = join(overrideDir, `bun${binExt}`);
|
|
446
470
|
const bunUrl = `https://github.com/oven-sh/bun/releases/download/bun-v${bunVersion}/${bunUrlSegment}`;
|
|
447
471
|
|
|
@@ -516,7 +540,9 @@ async function downloadCustomBun(
|
|
|
516
540
|
if (existsSync(extractedBinary)) {
|
|
517
541
|
renameSync(extractedBinary, overrideBinary);
|
|
518
542
|
} else {
|
|
519
|
-
throw new Error(
|
|
543
|
+
throw new Error(
|
|
544
|
+
`Bun binary not found after extraction at ${extractedBinary}`,
|
|
545
|
+
);
|
|
520
546
|
}
|
|
521
547
|
|
|
522
548
|
// Set execute permissions on non-Windows
|
|
@@ -530,7 +556,8 @@ async function downloadCustomBun(
|
|
|
530
556
|
// Clean up
|
|
531
557
|
if (existsSync(tempZipPath)) unlinkSync(tempZipPath);
|
|
532
558
|
const extractedDir = join(overrideDir, bunDirName);
|
|
533
|
-
if (existsSync(extractedDir))
|
|
559
|
+
if (existsSync(extractedDir))
|
|
560
|
+
rmSync(extractedDir, { recursive: true, force: true });
|
|
534
561
|
|
|
535
562
|
console.log(
|
|
536
563
|
`Custom Bun ${bunVersion} for ${platformOS}-${platformArch} set up successfully`,
|
|
@@ -569,7 +596,11 @@ async function ensureCEFDependencies(
|
|
|
569
596
|
// If custom CEF version specified, download from Spotify CDN
|
|
570
597
|
// Custom CEF is stored in vendors/cef-override/ to survive dist rebuilds
|
|
571
598
|
if (cefVersion) {
|
|
572
|
-
const overrideDir = getEffectiveCEFDir(
|
|
599
|
+
const overrideDir = getEffectiveCEFDir(
|
|
600
|
+
platformOS,
|
|
601
|
+
platformArch,
|
|
602
|
+
cefVersion,
|
|
603
|
+
);
|
|
573
604
|
// Check if already downloaded with matching version
|
|
574
605
|
const cefVersionFile = join(overrideDir, ".cef-version");
|
|
575
606
|
if (existsSync(overrideDir) && existsSync(cefVersionFile)) {
|
|
@@ -978,10 +1009,9 @@ async function downloadAndExtractCustomCEF(
|
|
|
978
1009
|
);
|
|
979
1010
|
|
|
980
1011
|
// Extract tar.bz2 using system tar (bz2 requires it)
|
|
981
|
-
execSync(
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
);
|
|
1012
|
+
execSync(`tar -xjf "${tempFile}" --strip-components=1 -C "${cefDir}"`, {
|
|
1013
|
+
stdio: "inherit",
|
|
1014
|
+
});
|
|
985
1015
|
|
|
986
1016
|
// The Spotify distribution layout has runtime files in Release/ and Resources/
|
|
987
1017
|
// subdirectories, but the CLI expects them at the cef/ root. Copy them up.
|
|
@@ -1127,6 +1157,8 @@ const defaultConfig = {
|
|
|
1127
1157
|
| Record<string, { entrypoint: string; [key: string]: unknown }>
|
|
1128
1158
|
| undefined,
|
|
1129
1159
|
copy: undefined as Record<string, string> | undefined,
|
|
1160
|
+
watch: undefined as string[] | undefined,
|
|
1161
|
+
watchIgnore: undefined as string[] | undefined,
|
|
1130
1162
|
},
|
|
1131
1163
|
runtime: {} as Record<string, unknown>,
|
|
1132
1164
|
scripts: {
|
|
@@ -1191,73 +1223,73 @@ function escapePathForTerminal(path: string): string {
|
|
|
1191
1223
|
* Creates a Linux installer tar.gz containing:
|
|
1192
1224
|
* - Self-extracting installer executable (with embedded app archive)
|
|
1193
1225
|
* - README.txt with instructions
|
|
1194
|
-
*
|
|
1226
|
+
*
|
|
1195
1227
|
* This replaces the AppImage-based installer to avoid libfuse2 dependency.
|
|
1196
1228
|
* The installer executable has the compressed app archive embedded within it
|
|
1197
1229
|
* using magic markers, similar to how Windows installers work.
|
|
1198
1230
|
*/
|
|
1199
1231
|
async function createLinuxInstallerArchive(
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1232
|
+
buildFolder: string,
|
|
1233
|
+
compressedTarPath: string,
|
|
1234
|
+
appFileName: string,
|
|
1235
|
+
config: any,
|
|
1236
|
+
buildEnvironment: string,
|
|
1237
|
+
hash: string,
|
|
1238
|
+
targetPaths: ReturnType<typeof getPlatformPaths>,
|
|
1207
1239
|
): Promise<string> {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1240
|
+
console.log("Creating Linux installer archive...");
|
|
1241
|
+
|
|
1242
|
+
// Create installer name using sanitized app file name (no spaces, URL-safe)
|
|
1243
|
+
// Note: appFileName already includes the channel suffix for non-stable builds
|
|
1244
|
+
const installerName = `${appFileName}-Setup`;
|
|
1245
|
+
|
|
1246
|
+
// Create temp directory for staging
|
|
1247
|
+
const stagingDir = join(buildFolder, `${installerName}-staging`);
|
|
1248
|
+
if (existsSync(stagingDir)) {
|
|
1249
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
1250
|
+
}
|
|
1251
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
1252
|
+
|
|
1253
|
+
try {
|
|
1254
|
+
// 1. Create self-extracting installer binary
|
|
1255
|
+
// Read the extractor binary
|
|
1256
|
+
const extractorBinary = readFileSync(targetPaths.EXTRACTOR);
|
|
1257
|
+
|
|
1258
|
+
// Read the compressed archive
|
|
1259
|
+
const compressedArchive = readFileSync(compressedTarPath);
|
|
1260
|
+
|
|
1261
|
+
// Create metadata JSON
|
|
1262
|
+
const metadata = {
|
|
1263
|
+
identifier: config.app.identifier,
|
|
1264
|
+
name: config.app.name,
|
|
1265
|
+
channel: buildEnvironment,
|
|
1266
|
+
hash: hash,
|
|
1267
|
+
};
|
|
1268
|
+
const metadataJson = JSON.stringify(metadata);
|
|
1269
|
+
const metadataBuffer = Buffer.from(metadataJson, "utf8");
|
|
1270
|
+
|
|
1271
|
+
// Create marker buffers
|
|
1272
|
+
const metadataMarker = Buffer.from("ELECTROBUN_METADATA_V1", "utf8");
|
|
1273
|
+
const archiveMarker = Buffer.from("ELECTROBUN_ARCHIVE_V1", "utf8");
|
|
1274
|
+
|
|
1275
|
+
// Combine extractor + metadata marker + metadata + archive marker + archive
|
|
1276
|
+
const combinedBuffer = Buffer.concat([
|
|
1277
|
+
new Uint8Array(extractorBinary),
|
|
1278
|
+
new Uint8Array(metadataMarker),
|
|
1279
|
+
new Uint8Array(metadataBuffer),
|
|
1280
|
+
new Uint8Array(archiveMarker),
|
|
1281
|
+
new Uint8Array(compressedArchive),
|
|
1282
|
+
]);
|
|
1283
|
+
|
|
1284
|
+
// Write the self-extracting installer
|
|
1285
|
+
const installerPath = join(stagingDir, "installer");
|
|
1286
|
+
writeFileSync(installerPath, new Uint8Array(combinedBuffer), {
|
|
1287
|
+
mode: 0o755,
|
|
1288
|
+
});
|
|
1289
|
+
execSync(`chmod +x ${escapePathForTerminal(installerPath)}`);
|
|
1290
|
+
|
|
1291
|
+
// 2. Create README for clarity
|
|
1292
|
+
const readmeContent = `${config.app.name} Installer
|
|
1261
1293
|
========================
|
|
1262
1294
|
|
|
1263
1295
|
To install ${config.app.name}:
|
|
@@ -1269,46 +1301,44 @@ The installer will:
|
|
|
1269
1301
|
- Extract the application to ~/.local/share/
|
|
1270
1302
|
- Create a desktop shortcut with the app's icon
|
|
1271
1303
|
|
|
1272
|
-
For more information, visit: ${config.app.homepage ||
|
|
1304
|
+
For more information, visit: ${config.app.homepage || "https://electrobun.dev"}
|
|
1273
1305
|
`;
|
|
1274
1306
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
// 3. Create the tar.gz archive (extract contents directly, no nested folder)
|
|
1278
|
-
const archiveName = `${installerName}.tar.gz`;
|
|
1279
|
-
const archivePath = join(buildFolder, archiveName);
|
|
1280
|
-
|
|
1281
|
-
console.log(`Creating installer archive: ${archivePath}`);
|
|
1282
|
-
|
|
1283
|
-
// Use tar to create the archive, preserving executable permissions
|
|
1284
|
-
// The -C changes to the staging dir, then . archives its contents directly
|
|
1285
|
-
execSync(
|
|
1286
|
-
`tar -czf ${escapePathForTerminal(archivePath)} -C ${escapePathForTerminal(stagingDir)} .`,
|
|
1287
|
-
{ stdio: 'inherit', env: { ...process.env, COPYFILE_DISABLE: "1" } }
|
|
1288
|
-
);
|
|
1289
|
-
|
|
1290
|
-
// Verify the archive was created
|
|
1291
|
-
if (!existsSync(archivePath)) {
|
|
1292
|
-
throw new Error(`Installer archive was not created at expected path: ${archivePath}`);
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
const stats = statSync(archivePath);
|
|
1296
|
-
console.log(
|
|
1297
|
-
`✓ Linux installer archive created: ${archivePath} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`
|
|
1298
|
-
);
|
|
1299
|
-
|
|
1300
|
-
return archivePath;
|
|
1301
|
-
} finally {
|
|
1302
|
-
// Clean up staging directory
|
|
1303
|
-
if (existsSync(stagingDir)) {
|
|
1304
|
-
rmSync(stagingDir, { recursive: true, force: true });
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1307
|
+
writeFileSync(join(stagingDir, "README.txt"), readmeContent);
|
|
1308
1308
|
|
|
1309
|
+
// 3. Create the tar.gz archive (extract contents directly, no nested folder)
|
|
1310
|
+
const archiveName = `${installerName}.tar.gz`;
|
|
1311
|
+
const archivePath = join(buildFolder, archiveName);
|
|
1309
1312
|
|
|
1313
|
+
console.log(`Creating installer archive: ${archivePath}`);
|
|
1310
1314
|
|
|
1315
|
+
// Use tar to create the archive, preserving executable permissions
|
|
1316
|
+
// The -C changes to the staging dir, then . archives its contents directly
|
|
1317
|
+
execSync(
|
|
1318
|
+
`tar -czf ${escapePathForTerminal(archivePath)} -C ${escapePathForTerminal(stagingDir)} .`,
|
|
1319
|
+
{ stdio: "inherit", env: { ...process.env, COPYFILE_DISABLE: "1" } },
|
|
1320
|
+
);
|
|
1311
1321
|
|
|
1322
|
+
// Verify the archive was created
|
|
1323
|
+
if (!existsSync(archivePath)) {
|
|
1324
|
+
throw new Error(
|
|
1325
|
+
`Installer archive was not created at expected path: ${archivePath}`,
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const stats = statSync(archivePath);
|
|
1330
|
+
console.log(
|
|
1331
|
+
`✓ Linux installer archive created: ${archivePath} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`,
|
|
1332
|
+
);
|
|
1333
|
+
|
|
1334
|
+
return archivePath;
|
|
1335
|
+
} finally {
|
|
1336
|
+
// Clean up staging directory
|
|
1337
|
+
if (existsSync(stagingDir)) {
|
|
1338
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1312
1342
|
|
|
1313
1343
|
// Helper function to generate usage description entries for Info.plist
|
|
1314
1344
|
function generateUsageDescriptions(
|
|
@@ -1508,6 +1538,40 @@ ${schemesXml}
|
|
|
1508
1538
|
? envArg
|
|
1509
1539
|
: "dev";
|
|
1510
1540
|
|
|
1541
|
+
try {
|
|
1542
|
+
await runBuild(config, buildEnvironment);
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
if (error instanceof Error) {
|
|
1545
|
+
console.error(error.message);
|
|
1546
|
+
}
|
|
1547
|
+
process.exit(1);
|
|
1548
|
+
}
|
|
1549
|
+
} else if (commandArg === "run") {
|
|
1550
|
+
const config = await getConfig();
|
|
1551
|
+
await runAppWithSignalHandling(config);
|
|
1552
|
+
} else if (commandArg === "dev") {
|
|
1553
|
+
const config = await getConfig();
|
|
1554
|
+
const watchMode = process.argv.includes("--watch");
|
|
1555
|
+
|
|
1556
|
+
if (watchMode) {
|
|
1557
|
+
await runDevWatch(config);
|
|
1558
|
+
} else {
|
|
1559
|
+
try {
|
|
1560
|
+
await runBuild(config, "dev");
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
if (error instanceof Error) {
|
|
1563
|
+
console.error(error.message);
|
|
1564
|
+
}
|
|
1565
|
+
process.exit(1);
|
|
1566
|
+
}
|
|
1567
|
+
await runAppWithSignalHandling(config);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
async function runBuild(
|
|
1572
|
+
config: Awaited<ReturnType<typeof getConfig>>,
|
|
1573
|
+
buildEnvironment: string,
|
|
1574
|
+
) {
|
|
1511
1575
|
// Determine current platform as default target
|
|
1512
1576
|
const currentTarget = { os: OS, arch: ARCH };
|
|
1513
1577
|
|
|
@@ -1588,11 +1652,14 @@ ${schemesXml}
|
|
|
1588
1652
|
console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);
|
|
1589
1653
|
console.error("Script path:", hookScript);
|
|
1590
1654
|
console.error("Working directory:", projectRoot);
|
|
1591
|
-
|
|
1655
|
+
throw new Error("Build failed: hook script failed");
|
|
1592
1656
|
}
|
|
1593
1657
|
};
|
|
1594
1658
|
|
|
1595
|
-
const buildIcons = (
|
|
1659
|
+
const buildIcons = (
|
|
1660
|
+
appBundleFolderResourcesPath: string,
|
|
1661
|
+
appBundleFolderPath: string,
|
|
1662
|
+
) => {
|
|
1596
1663
|
// Platform-specific icon handling
|
|
1597
1664
|
if (targetOS === "macos" && config.build.mac?.icons) {
|
|
1598
1665
|
// macOS uses .iconset folders that get converted to .icns using iconutil
|
|
@@ -1666,10 +1733,12 @@ StartupWMClass=${config.app.name}
|
|
|
1666
1733
|
Categories=Utility;Application;
|
|
1667
1734
|
`;
|
|
1668
1735
|
|
|
1669
|
-
const desktopFilePath = join(
|
|
1736
|
+
const desktopFilePath = join(
|
|
1737
|
+
appBundleFolderPath,
|
|
1738
|
+
`${config.app.name}.desktop`,
|
|
1739
|
+
);
|
|
1670
1740
|
writeFileSync(desktopFilePath, desktopContent);
|
|
1671
1741
|
console.log(`Created Linux desktop file: ${desktopFilePath}`);
|
|
1672
|
-
|
|
1673
1742
|
} else if (targetOS === "win" && config.build.win?.icon) {
|
|
1674
1743
|
const iconPath = join(projectRoot, config.build.win.icon);
|
|
1675
1744
|
if (existsSync(iconPath)) {
|
|
@@ -1692,10 +1761,9 @@ Categories=Utility;Application;
|
|
|
1692
1761
|
const bunSource = join(projectRoot, bunConfig.entrypoint);
|
|
1693
1762
|
|
|
1694
1763
|
if (!existsSync(bunSource)) {
|
|
1695
|
-
|
|
1764
|
+
throw new Error(
|
|
1696
1765
|
`failed to bundle ${bunSource} because it doesn't exist.\n You need a config.build.bun.entrypoint source file to build.`,
|
|
1697
1766
|
);
|
|
1698
|
-
process.exit(1);
|
|
1699
1767
|
}
|
|
1700
1768
|
|
|
1701
1769
|
// build macos bundle
|
|
@@ -1872,7 +1940,11 @@ Categories=Utility;Application;
|
|
|
1872
1940
|
|
|
1873
1941
|
// Bun runtime binary
|
|
1874
1942
|
// todo (yoav): this only works for the current architecture
|
|
1875
|
-
const bunBinarySourcePath = await ensureBunBinary(
|
|
1943
|
+
const bunBinarySourcePath = await ensureBunBinary(
|
|
1944
|
+
currentTarget.os,
|
|
1945
|
+
currentTarget.arch,
|
|
1946
|
+
config.build.bunVersion,
|
|
1947
|
+
);
|
|
1876
1948
|
// Note: .bin/bun binary in node_modules is a symlink to the versioned one in another place
|
|
1877
1949
|
// in node_modules, so we have to dereference here to get the actual binary in the bundle.
|
|
1878
1950
|
const bunBinaryDestInBundlePath =
|
|
@@ -1970,7 +2042,9 @@ Categories=Utility;Application;
|
|
|
1970
2042
|
cpSync(nativeWrapperLinuxSource, nativeWrapperLinuxDestination, {
|
|
1971
2043
|
dereference: true,
|
|
1972
2044
|
});
|
|
1973
|
-
console.log(
|
|
2045
|
+
console.log(
|
|
2046
|
+
`Using ${useCEF ? "CEF (with weak linking)" : "GTK-only"} native wrapper for Linux`,
|
|
2047
|
+
);
|
|
1974
2048
|
} else {
|
|
1975
2049
|
throw new Error(
|
|
1976
2050
|
`Native wrapper not found: ${nativeWrapperLinuxSource}`,
|
|
@@ -2020,9 +2094,16 @@ Categories=Utility;Application;
|
|
|
2020
2094
|
(targetOS === "win" && config.build.win?.bundleCEF) ||
|
|
2021
2095
|
(targetOS === "linux" && config.build.linux?.bundleCEF)
|
|
2022
2096
|
) {
|
|
2023
|
-
const effectiveCEFDir = await ensureCEFDependencies(
|
|
2097
|
+
const effectiveCEFDir = await ensureCEFDependencies(
|
|
2098
|
+
currentTarget.os,
|
|
2099
|
+
currentTarget.arch,
|
|
2100
|
+
config.build.cefVersion,
|
|
2101
|
+
);
|
|
2024
2102
|
if (targetOS === "macos") {
|
|
2025
|
-
const cefFrameworkSource = join(
|
|
2103
|
+
const cefFrameworkSource = join(
|
|
2104
|
+
effectiveCEFDir,
|
|
2105
|
+
"Chromium Embedded Framework.framework",
|
|
2106
|
+
);
|
|
2026
2107
|
const cefFrameworkDestination = join(
|
|
2027
2108
|
appBundleFolderFrameworksPath,
|
|
2028
2109
|
"Chromium Embedded Framework.framework",
|
|
@@ -2387,7 +2468,7 @@ Categories=Utility;Application;
|
|
|
2387
2468
|
|
|
2388
2469
|
if (!buildResult.success) {
|
|
2389
2470
|
console.error("failed to build", bunSource, buildResult.logs);
|
|
2390
|
-
|
|
2471
|
+
throw new Error("Build failed: bun build failed");
|
|
2391
2472
|
}
|
|
2392
2473
|
|
|
2393
2474
|
// transpile developer's view code
|
|
@@ -2532,7 +2613,7 @@ Categories=Utility;Application;
|
|
|
2532
2613
|
if (!existsSync(zigAsarCli)) {
|
|
2533
2614
|
console.error(`zig-asar CLI not found at: ${zigAsarCli}`);
|
|
2534
2615
|
console.error("Make sure to run setup/vendoring first");
|
|
2535
|
-
|
|
2616
|
+
throw new Error("Build failed: zig-asar CLI not found");
|
|
2536
2617
|
}
|
|
2537
2618
|
|
|
2538
2619
|
// Build zig-asar command arguments
|
|
@@ -2601,13 +2682,14 @@ Categories=Utility;Application;
|
|
|
2601
2682
|
);
|
|
2602
2683
|
}
|
|
2603
2684
|
console.error("Command:", zigAsarCli, ...asarArgs);
|
|
2604
|
-
|
|
2685
|
+
throw new Error("Build failed: ASAR packing failed");
|
|
2605
2686
|
}
|
|
2606
2687
|
|
|
2607
2688
|
// Verify ASAR was created
|
|
2608
2689
|
if (!existsSync(asarPath)) {
|
|
2609
|
-
|
|
2610
|
-
|
|
2690
|
+
throw new Error(
|
|
2691
|
+
"Build failed: ASAR file was not created: " + asarPath,
|
|
2692
|
+
);
|
|
2611
2693
|
}
|
|
2612
2694
|
|
|
2613
2695
|
console.log("✓ Created app.asar");
|
|
@@ -2642,7 +2724,7 @@ Categories=Utility;Application;
|
|
|
2642
2724
|
}
|
|
2643
2725
|
}
|
|
2644
2726
|
// Check if Bun.Archive is available (Bun 1.3.0+)
|
|
2645
|
-
if (typeof Bun.Archive !==
|
|
2727
|
+
if (typeof Bun.Archive !== "undefined") {
|
|
2646
2728
|
const archiveBytes = await new Bun.Archive(bundleFiles).bytes();
|
|
2647
2729
|
// Note: wyhash is the default in Bun.hash but that may change in the future
|
|
2648
2730
|
// so we're being explicit here.
|
|
@@ -2650,7 +2732,7 @@ Categories=Utility;Application;
|
|
|
2650
2732
|
} else {
|
|
2651
2733
|
// Fallback for older Bun versions - use a simple hash of file paths
|
|
2652
2734
|
console.warn("Bun.Archive not available, using fallback hash method");
|
|
2653
|
-
const fileList = Object.keys(bundleFiles).sort().join(
|
|
2735
|
+
const fileList = Object.keys(bundleFiles).sort().join("\n");
|
|
2654
2736
|
hash = Bun.hash.wyhash(fileList).toString(36);
|
|
2655
2737
|
}
|
|
2656
2738
|
console.timeEnd("Generate Bundle hash");
|
|
@@ -2690,7 +2772,9 @@ Categories=Utility;Application;
|
|
|
2690
2772
|
defaultRenderer: platformConfig?.defaultRenderer ?? "native",
|
|
2691
2773
|
availableRenderers: bundlesCEF ? ["native", "cef"] : ["native"],
|
|
2692
2774
|
runtime: config.runtime ?? {},
|
|
2693
|
-
...(bundlesCEF
|
|
2775
|
+
...(bundlesCEF
|
|
2776
|
+
? { cefVersion: config.build?.cefVersion ?? DEFAULT_CEF_VERSION_STRING }
|
|
2777
|
+
: {}),
|
|
2694
2778
|
bunVersion: config.build?.bunVersion ?? BUN_VERSION,
|
|
2695
2779
|
};
|
|
2696
2780
|
|
|
@@ -2767,7 +2851,7 @@ Categories=Utility;Application;
|
|
|
2767
2851
|
|
|
2768
2852
|
// Tar the app bundle for all platforms
|
|
2769
2853
|
createTar(tarPath, buildFolder, [basename(appBundleFolderPath)]);
|
|
2770
|
-
|
|
2854
|
+
|
|
2771
2855
|
// Remove the app bundle folder after tarring (except on Linux where it might be needed for dev)
|
|
2772
2856
|
if (targetOS !== "linux" || buildEnvironment !== "dev") {
|
|
2773
2857
|
rmdirSync(appBundleFolderPath, { recursive: true });
|
|
@@ -3025,7 +3109,10 @@ Categories=Utility;Application;
|
|
|
3025
3109
|
dereference: true,
|
|
3026
3110
|
});
|
|
3027
3111
|
|
|
3028
|
-
buildIcons(
|
|
3112
|
+
buildIcons(
|
|
3113
|
+
selfExtractingBundle.appBundleFolderResourcesPath,
|
|
3114
|
+
selfExtractingBundle.appBundleFolderPath,
|
|
3115
|
+
);
|
|
3029
3116
|
await Bun.write(
|
|
3030
3117
|
join(selfExtractingBundle.appBundleFolderContentsPath, "Info.plist"),
|
|
3031
3118
|
InfoPlistContents,
|
|
@@ -3255,49 +3342,88 @@ Categories=Utility;Application;
|
|
|
3255
3342
|
// for a dmg.
|
|
3256
3343
|
// can also use stapler validate -v to validate the dmg and look for teamId, signingId, and the response signedTicket
|
|
3257
3344
|
// stapler validate -v <app path>
|
|
3258
|
-
}
|
|
3259
|
-
// todo (yoav): rename to start
|
|
3345
|
+
}
|
|
3260
3346
|
|
|
3261
|
-
|
|
3262
|
-
|
|
3347
|
+
// Take over as the terminal's foreground process group (macOS/Linux).
|
|
3348
|
+
// This prevents the parent bun script runner from receiving SIGINT
|
|
3349
|
+
// when Ctrl+C is pressed, keeping the terminal busy until the app
|
|
3350
|
+
// finishes shutting down gracefully.
|
|
3351
|
+
// Call once per CLI session — returns a restore function.
|
|
3352
|
+
async function takeoverForeground(): Promise<() => void> {
|
|
3353
|
+
let restoreFn = () => {};
|
|
3354
|
+
if (OS === "win") return restoreFn;
|
|
3355
|
+
try {
|
|
3356
|
+
const { dlopen, ptr } = await import("bun:ffi");
|
|
3357
|
+
const libName = OS === "macos" ? "libSystem.B.dylib" : "libc.so.6";
|
|
3358
|
+
const libc = dlopen(libName, {
|
|
3359
|
+
open: { args: ["ptr", "i32"], returns: "i32" },
|
|
3360
|
+
close: { args: ["i32"], returns: "i32" },
|
|
3361
|
+
getpid: { args: [], returns: "i32" },
|
|
3362
|
+
setpgid: { args: ["i32", "i32"], returns: "i32" },
|
|
3363
|
+
tcgetpgrp: { args: ["i32"], returns: "i32" },
|
|
3364
|
+
tcsetpgrp: { args: ["i32", "i32"], returns: "i32" },
|
|
3365
|
+
signal: { args: ["i32", "ptr"], returns: "ptr" },
|
|
3366
|
+
});
|
|
3263
3367
|
|
|
3264
|
-
|
|
3265
|
-
|
|
3368
|
+
const ttyPathBuf = new Uint8Array(Buffer.from("/dev/tty\0"));
|
|
3369
|
+
const ttyFd = libc.symbols.open(ptr(ttyPathBuf), 2); // O_RDWR
|
|
3370
|
+
|
|
3371
|
+
if (ttyFd >= 0) {
|
|
3372
|
+
const originalPgid = libc.symbols.tcgetpgrp(ttyFd);
|
|
3373
|
+
if (originalPgid >= 0) {
|
|
3374
|
+
// Ignore SIGTTOU at C level so tcsetpgrp works from background group.
|
|
3375
|
+
// bun's process.on("SIGTTOU") doesn't set the C-level disposition.
|
|
3376
|
+
// SIG_IGN = (void(*)(int))1, SIGTTOU = 22 on macOS/Linux
|
|
3377
|
+
libc.symbols.signal(22, 1);
|
|
3378
|
+
|
|
3379
|
+
if (libc.symbols.setpgid(0, 0) === 0) {
|
|
3380
|
+
const myPid = libc.symbols.getpid();
|
|
3381
|
+
if (libc.symbols.tcsetpgrp(ttyFd, myPid) === 0) {
|
|
3382
|
+
restoreFn = () => {
|
|
3383
|
+
try {
|
|
3384
|
+
libc.symbols.signal(22, 1);
|
|
3385
|
+
libc.symbols.tcsetpgrp(ttyFd, originalPgid);
|
|
3386
|
+
libc.symbols.close(ttyFd);
|
|
3387
|
+
} catch {}
|
|
3388
|
+
};
|
|
3389
|
+
} else {
|
|
3390
|
+
libc.symbols.setpgid(0, originalPgid);
|
|
3391
|
+
libc.symbols.close(ttyFd);
|
|
3392
|
+
}
|
|
3393
|
+
} else {
|
|
3394
|
+
libc.symbols.close(ttyFd);
|
|
3395
|
+
}
|
|
3396
|
+
} else {
|
|
3397
|
+
libc.symbols.close(ttyFd);
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
} catch {
|
|
3401
|
+
// Fall back to default behavior (prompt may return early on Ctrl+C)
|
|
3402
|
+
}
|
|
3403
|
+
return restoreFn;
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
async function runApp(
|
|
3407
|
+
config: Awaited<ReturnType<typeof getConfig>>,
|
|
3408
|
+
options?: { onExit?: () => void },
|
|
3409
|
+
): Promise<{ kill: () => void; exited: Promise<number> }> {
|
|
3410
|
+
// Launch the already-built dev bundle
|
|
3266
3411
|
|
|
3267
|
-
// Set up dev build variables (similar to build mode)
|
|
3268
3412
|
const buildEnvironment = "dev";
|
|
3269
|
-
const currentTarget = { os: OS, arch: ARCH };
|
|
3270
3413
|
const appFileName = getAppFileName(config.app.name, buildEnvironment);
|
|
3271
|
-
// macOS uses display name with spaces for the actual .app folder
|
|
3272
3414
|
const macOSBundleDisplayName = getMacOSBundleDisplayName(
|
|
3273
3415
|
config.app.name,
|
|
3274
3416
|
buildEnvironment,
|
|
3275
3417
|
);
|
|
3276
|
-
const buildSubFolder = `${buildEnvironment}-${
|
|
3418
|
+
const buildSubFolder = `${buildEnvironment}-${OS}-${ARCH}`;
|
|
3277
3419
|
const buildFolder = join(
|
|
3278
3420
|
projectRoot,
|
|
3279
3421
|
config.build.buildFolder,
|
|
3280
3422
|
buildSubFolder,
|
|
3281
3423
|
);
|
|
3282
|
-
// Use display name for macOS bundles (with spaces), sanitized name for other platforms
|
|
3283
3424
|
const bundleFileName =
|
|
3284
3425
|
OS === "macos" ? `${macOSBundleDisplayName}.app` : appFileName;
|
|
3285
3426
|
|
|
3286
|
-
// Note: this cli will be a bun single-file-executable
|
|
3287
|
-
// Note: we want to use the version of bun that's packaged with electrobun
|
|
3288
|
-
// const bunPath = join(projectRoot, 'node_modules', '.bin', 'bun');
|
|
3289
|
-
// const mainPath = join(buildFolder, 'bun', 'index.js');
|
|
3290
|
-
// const mainPath = join(buildFolder, bundleFileName);
|
|
3291
|
-
// console.log('running ', bunPath, mainPath);
|
|
3292
|
-
|
|
3293
|
-
// Note: open will open the app bundle as a completely different process
|
|
3294
|
-
// This is critical to fully test the app (including plist configuration, etc.)
|
|
3295
|
-
// but also to get proper cmd+tab and dock behaviour and not run the windowed app
|
|
3296
|
-
// as a child of the terminal process which steels keyboard focus from any descendant nswindows.
|
|
3297
|
-
// Bun.spawn(["open", mainPath], {
|
|
3298
|
-
// env: {},
|
|
3299
|
-
// });
|
|
3300
|
-
|
|
3301
3427
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3302
3428
|
let mainProc: any;
|
|
3303
3429
|
let bundleExecPath: string;
|
|
@@ -3312,7 +3438,6 @@ Categories=Utility;Application;
|
|
|
3312
3438
|
"Resources",
|
|
3313
3439
|
);
|
|
3314
3440
|
} else if (OS === "linux") {
|
|
3315
|
-
// Directory bundle mode
|
|
3316
3441
|
bundleExecPath = join(buildFolder, bundleFileName, "bin");
|
|
3317
3442
|
_bundleResourcesPath = join(buildFolder, bundleFileName, "Resources");
|
|
3318
3443
|
} else if (OS === "win") {
|
|
@@ -3322,62 +3447,6 @@ Categories=Utility;Application;
|
|
|
3322
3447
|
throw new Error(`Unsupported OS: ${OS}`);
|
|
3323
3448
|
}
|
|
3324
3449
|
|
|
3325
|
-
// Take over as the terminal's foreground process group (macOS/Linux).
|
|
3326
|
-
// This prevents the parent bun script runner from receiving SIGINT
|
|
3327
|
-
// when Ctrl+C is pressed, keeping the terminal busy until the app
|
|
3328
|
-
// finishes shutting down gracefully.
|
|
3329
|
-
let restoreForeground = () => {};
|
|
3330
|
-
if (OS !== "win") {
|
|
3331
|
-
try {
|
|
3332
|
-
const { dlopen, ptr } = await import("bun:ffi");
|
|
3333
|
-
const libName = OS === "macos" ? "libSystem.B.dylib" : "libc.so.6";
|
|
3334
|
-
const libc = dlopen(libName, {
|
|
3335
|
-
open: { args: ["ptr", "i32"], returns: "i32" },
|
|
3336
|
-
close: { args: ["i32"], returns: "i32" },
|
|
3337
|
-
getpid: { args: [], returns: "i32" },
|
|
3338
|
-
setpgid: { args: ["i32", "i32"], returns: "i32" },
|
|
3339
|
-
tcgetpgrp: { args: ["i32"], returns: "i32" },
|
|
3340
|
-
tcsetpgrp: { args: ["i32", "i32"], returns: "i32" },
|
|
3341
|
-
signal: { args: ["i32", "ptr"], returns: "ptr" },
|
|
3342
|
-
});
|
|
3343
|
-
|
|
3344
|
-
const ttyPathBuf = new Uint8Array(Buffer.from("/dev/tty\0"));
|
|
3345
|
-
const ttyFd = libc.symbols.open(ptr(ttyPathBuf), 2); // O_RDWR
|
|
3346
|
-
|
|
3347
|
-
if (ttyFd >= 0) {
|
|
3348
|
-
const originalPgid = libc.symbols.tcgetpgrp(ttyFd);
|
|
3349
|
-
if (originalPgid >= 0) {
|
|
3350
|
-
// Ignore SIGTTOU at C level so tcsetpgrp works from background group.
|
|
3351
|
-
// bun's process.on("SIGTTOU") doesn't set the C-level disposition.
|
|
3352
|
-
// SIG_IGN = (void(*)(int))1, SIGTTOU = 22 on macOS/Linux
|
|
3353
|
-
libc.symbols.signal(22, 1);
|
|
3354
|
-
|
|
3355
|
-
if (libc.symbols.setpgid(0, 0) === 0) {
|
|
3356
|
-
const myPid = libc.symbols.getpid();
|
|
3357
|
-
if (libc.symbols.tcsetpgrp(ttyFd, myPid) === 0) {
|
|
3358
|
-
restoreForeground = () => {
|
|
3359
|
-
try {
|
|
3360
|
-
libc.symbols.signal(22, 1);
|
|
3361
|
-
libc.symbols.tcsetpgrp(ttyFd, originalPgid);
|
|
3362
|
-
libc.symbols.close(ttyFd);
|
|
3363
|
-
} catch {}
|
|
3364
|
-
};
|
|
3365
|
-
} else {
|
|
3366
|
-
libc.symbols.setpgid(0, originalPgid);
|
|
3367
|
-
libc.symbols.close(ttyFd);
|
|
3368
|
-
}
|
|
3369
|
-
} else {
|
|
3370
|
-
libc.symbols.close(ttyFd);
|
|
3371
|
-
}
|
|
3372
|
-
} else {
|
|
3373
|
-
libc.symbols.close(ttyFd);
|
|
3374
|
-
}
|
|
3375
|
-
}
|
|
3376
|
-
} catch {
|
|
3377
|
-
// Fall back to default behavior (prompt may return early on Ctrl+C)
|
|
3378
|
-
}
|
|
3379
|
-
}
|
|
3380
|
-
|
|
3381
3450
|
if (OS === "macos" || OS === "linux") {
|
|
3382
3451
|
// For Linux dev mode, update libNativeWrapper.so based on bundleCEF setting
|
|
3383
3452
|
if (OS === "linux") {
|
|
@@ -3399,7 +3468,6 @@ Categories=Utility;Application;
|
|
|
3399
3468
|
}
|
|
3400
3469
|
}
|
|
3401
3470
|
|
|
3402
|
-
// Use the zig launcher for macOS and Linux
|
|
3403
3471
|
mainProc = Bun.spawn([join(bundleExecPath, "launcher")], {
|
|
3404
3472
|
stdio: ["inherit", "inherit", "inherit"],
|
|
3405
3473
|
cwd: bundleExecPath,
|
|
@@ -3411,36 +3479,288 @@ Categories=Utility;Application;
|
|
|
3411
3479
|
});
|
|
3412
3480
|
}
|
|
3413
3481
|
|
|
3414
|
-
|
|
3482
|
+
if (!mainProc) {
|
|
3483
|
+
throw new Error("Failed to spawn app process");
|
|
3484
|
+
}
|
|
3415
3485
|
|
|
3486
|
+
const exitedPromise = mainProc.exited.then((code: number) => {
|
|
3487
|
+
options?.onExit?.();
|
|
3488
|
+
return code ?? 0;
|
|
3489
|
+
});
|
|
3490
|
+
|
|
3491
|
+
return {
|
|
3492
|
+
kill: () => {
|
|
3493
|
+
try {
|
|
3494
|
+
mainProc.kill();
|
|
3495
|
+
} catch {}
|
|
3496
|
+
},
|
|
3497
|
+
exited: exitedPromise,
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
async function runAppWithSignalHandling(
|
|
3502
|
+
config: Awaited<ReturnType<typeof getConfig>>,
|
|
3503
|
+
) {
|
|
3504
|
+
const restoreForeground = await takeoverForeground();
|
|
3505
|
+
const handle = await runApp(config);
|
|
3506
|
+
|
|
3507
|
+
let sigintCount = 0;
|
|
3416
3508
|
process.on("SIGINT", () => {
|
|
3417
3509
|
sigintCount++;
|
|
3418
|
-
|
|
3419
3510
|
if (sigintCount === 1) {
|
|
3420
|
-
// First Ctrl+C: The app already received SIGINT from the process group.
|
|
3421
|
-
// Its SIGINT handler calls quit() which fires beforeQuit and shuts down
|
|
3422
|
-
// gracefully. Don't send another signal - just wait.
|
|
3423
3511
|
console.log(
|
|
3424
3512
|
"\n[electrobun dev] Shutting down gracefully... (press Ctrl+C again to force quit)",
|
|
3425
3513
|
);
|
|
3426
3514
|
} else {
|
|
3427
|
-
|
|
3515
|
+
console.log("\n[electrobun dev] Force quitting...");
|
|
3516
|
+
try {
|
|
3517
|
+
process.kill(0, "SIGKILL");
|
|
3518
|
+
} catch {}
|
|
3519
|
+
process.exit(0);
|
|
3520
|
+
}
|
|
3521
|
+
});
|
|
3522
|
+
|
|
3523
|
+
const code = await handle.exited;
|
|
3524
|
+
restoreForeground();
|
|
3525
|
+
process.exit(code);
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
async function runDevWatch(config: Awaited<ReturnType<typeof getConfig>>) {
|
|
3529
|
+
const { watch } = await import("fs");
|
|
3530
|
+
|
|
3531
|
+
// Collect watch directories from config entrypoints
|
|
3532
|
+
const watchDirs = new Set<string>();
|
|
3533
|
+
|
|
3534
|
+
// Bun entrypoint directory
|
|
3535
|
+
if (config.build.bun?.entrypoint) {
|
|
3536
|
+
watchDirs.add(join(projectRoot, dirname(config.build.bun.entrypoint)));
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
// View entrypoint directories
|
|
3540
|
+
if (config.build.views) {
|
|
3541
|
+
for (const viewConfig of Object.values(config.build.views)) {
|
|
3542
|
+
if (viewConfig.entrypoint) {
|
|
3543
|
+
watchDirs.add(join(projectRoot, dirname(viewConfig.entrypoint)));
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
// Copy source directories
|
|
3549
|
+
if (config.build.copy) {
|
|
3550
|
+
for (const src of Object.keys(config.build.copy)) {
|
|
3551
|
+
const srcPath = join(projectRoot, src);
|
|
3552
|
+
try {
|
|
3553
|
+
const stat = statSync(srcPath);
|
|
3554
|
+
watchDirs.add(stat.isDirectory() ? srcPath : dirname(srcPath));
|
|
3555
|
+
} catch {
|
|
3556
|
+
watchDirs.add(dirname(srcPath));
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
// User-specified additional watch paths
|
|
3562
|
+
if (config.build.watch) {
|
|
3563
|
+
for (const entry of config.build.watch) {
|
|
3564
|
+
const entryPath = join(projectRoot, entry);
|
|
3565
|
+
try {
|
|
3566
|
+
const stat = statSync(entryPath);
|
|
3567
|
+
watchDirs.add(stat.isDirectory() ? entryPath : dirname(entryPath));
|
|
3568
|
+
} catch {
|
|
3569
|
+
// Path doesn't exist yet — watch its parent directory
|
|
3570
|
+
watchDirs.add(dirname(entryPath));
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
// Deduplicate overlapping directories (remove children if parent is watched)
|
|
3576
|
+
const sortedDirs = [...watchDirs].sort();
|
|
3577
|
+
const dedupedDirs = sortedDirs.filter((dir, i) => {
|
|
3578
|
+
return !sortedDirs.some(
|
|
3579
|
+
(other, j) => j < i && dir.startsWith(other + "/"),
|
|
3580
|
+
);
|
|
3581
|
+
});
|
|
3582
|
+
|
|
3583
|
+
if (dedupedDirs.length === 0) {
|
|
3584
|
+
console.error(
|
|
3585
|
+
"[electrobun dev --watch] No directories to watch. Check your config entrypoints.",
|
|
3586
|
+
);
|
|
3587
|
+
process.exit(1);
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
console.log(`
|
|
3591
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
3592
|
+
║ ELECTROBUN DEV --watch ║
|
|
3593
|
+
║ Watching ${String(dedupedDirs.length).padEnd(2)} director${dedupedDirs.length === 1 ? "y " : "ies"} ║
|
|
3594
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
3595
|
+
`);
|
|
3596
|
+
for (const dir of dedupedDirs) {
|
|
3597
|
+
console.log(` ${dir}`);
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
// Set up terminal foreground takeover once for the whole session
|
|
3601
|
+
const restoreForeground = await takeoverForeground();
|
|
3602
|
+
|
|
3603
|
+
// Paths to ignore in file watcher (build output, node_modules, artifacts)
|
|
3604
|
+
const buildDir = join(projectRoot, config.build.buildFolder);
|
|
3605
|
+
const artifactDir = join(projectRoot, config.build.artifactFolder);
|
|
3606
|
+
const ignoreDirs = [
|
|
3607
|
+
buildDir,
|
|
3608
|
+
artifactDir,
|
|
3609
|
+
join(projectRoot, "node_modules"),
|
|
3610
|
+
];
|
|
3611
|
+
|
|
3612
|
+
// Compile watchIgnore glob patterns
|
|
3613
|
+
const ignoreGlobs = (config.build.watchIgnore || []).map(
|
|
3614
|
+
(pattern) => new Bun.Glob(pattern),
|
|
3615
|
+
);
|
|
3616
|
+
|
|
3617
|
+
function shouldIgnore(fullPath: string): boolean {
|
|
3618
|
+
// Check built-in ignore dirs
|
|
3619
|
+
if (
|
|
3620
|
+
ignoreDirs.some(
|
|
3621
|
+
(ignored) =>
|
|
3622
|
+
fullPath.startsWith(ignored + "/") || fullPath === ignored,
|
|
3623
|
+
)
|
|
3624
|
+
) {
|
|
3625
|
+
return true;
|
|
3626
|
+
}
|
|
3627
|
+
// Check user-configured watchIgnore globs (match against project-relative path)
|
|
3628
|
+
const relativePath = fullPath.replace(projectRoot + "/", "");
|
|
3629
|
+
if (ignoreGlobs.some((glob) => glob.match(relativePath))) {
|
|
3630
|
+
return true;
|
|
3631
|
+
}
|
|
3632
|
+
return false;
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
let appHandle: { kill: () => void; exited: Promise<number> } | null = null;
|
|
3636
|
+
let lastChangedFile = "";
|
|
3637
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
3638
|
+
let shuttingDown = false;
|
|
3639
|
+
let watchers: ReturnType<typeof watch>[] = [];
|
|
3640
|
+
|
|
3641
|
+
function startWatchers() {
|
|
3642
|
+
for (const dir of dedupedDirs) {
|
|
3643
|
+
const watcher = watch(dir, { recursive: true }, (_event, filename) => {
|
|
3644
|
+
if (shuttingDown) return;
|
|
3645
|
+
|
|
3646
|
+
if (filename) {
|
|
3647
|
+
const fullPath = join(dir, filename);
|
|
3648
|
+
if (shouldIgnore(fullPath)) {
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
lastChangedFile = fullPath;
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3655
|
+
debounceTimer = setTimeout(() => {
|
|
3656
|
+
triggerRebuild();
|
|
3657
|
+
}, 300);
|
|
3658
|
+
});
|
|
3659
|
+
watchers.push(watcher);
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
function stopWatchers() {
|
|
3664
|
+
for (const watcher of watchers) {
|
|
3665
|
+
try { watcher.close(); } catch {}
|
|
3666
|
+
}
|
|
3667
|
+
watchers = [];
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
async function triggerRebuild() {
|
|
3671
|
+
if (shuttingDown) return;
|
|
3672
|
+
|
|
3673
|
+
// Stop watching during build so build output doesn't trigger more events
|
|
3674
|
+
stopWatchers();
|
|
3675
|
+
|
|
3676
|
+
const changedDisplay = lastChangedFile
|
|
3677
|
+
? lastChangedFile.replace(projectRoot + "/", "")
|
|
3678
|
+
: "unknown";
|
|
3679
|
+
console.log(`
|
|
3680
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
3681
|
+
║ FILE CHANGED: ${changedDisplay.padEnd(44)}║
|
|
3682
|
+
║ Rebuilding... ║
|
|
3683
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
3684
|
+
`);
|
|
3685
|
+
|
|
3686
|
+
// Kill running app if any
|
|
3687
|
+
if (appHandle) {
|
|
3688
|
+
appHandle.kill();
|
|
3689
|
+
try {
|
|
3690
|
+
await appHandle.exited;
|
|
3691
|
+
} catch {}
|
|
3692
|
+
appHandle = null;
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
try {
|
|
3696
|
+
await runBuild(config, "dev");
|
|
3428
3697
|
console.log(
|
|
3429
|
-
"
|
|
3698
|
+
"[electrobun dev --watch] Build succeeded, launching app...",
|
|
3430
3699
|
);
|
|
3431
|
-
|
|
3700
|
+
|
|
3701
|
+
appHandle = await runApp(config, {
|
|
3702
|
+
onExit: () => {
|
|
3703
|
+
appHandle = null;
|
|
3704
|
+
},
|
|
3705
|
+
});
|
|
3706
|
+
} catch (error) {
|
|
3707
|
+
console.error("[electrobun dev --watch] Build failed:", error);
|
|
3708
|
+
console.log("[electrobun dev --watch] Waiting for file changes...");
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
// Resume watching after build + hooks are done
|
|
3712
|
+
if (!shuttingDown) {
|
|
3713
|
+
startWatchers();
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
function cleanup() {
|
|
3718
|
+
shuttingDown = true;
|
|
3719
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3720
|
+
stopWatchers();
|
|
3721
|
+
if (appHandle) {
|
|
3722
|
+
appHandle.kill();
|
|
3723
|
+
}
|
|
3724
|
+
restoreForeground();
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
// Ctrl+C handling for watch mode
|
|
3728
|
+
let sigintCount = 0;
|
|
3729
|
+
process.on("SIGINT", () => {
|
|
3730
|
+
sigintCount++;
|
|
3731
|
+
if (sigintCount === 1) {
|
|
3732
|
+
console.log(
|
|
3733
|
+
"\n[electrobun dev --watch] Shutting down... (press Ctrl+C again to force quit)",
|
|
3734
|
+
);
|
|
3735
|
+
cleanup();
|
|
3736
|
+
// Wait briefly for app to exit, then exit
|
|
3737
|
+
setTimeout(() => process.exit(0), 2000);
|
|
3738
|
+
} else {
|
|
3739
|
+
try {
|
|
3740
|
+
process.kill(0, "SIGKILL");
|
|
3741
|
+
} catch {}
|
|
3432
3742
|
process.exit(0);
|
|
3433
3743
|
}
|
|
3434
3744
|
});
|
|
3435
3745
|
|
|
3436
|
-
//
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3746
|
+
// Initial build + launch (watchers start after build completes)
|
|
3747
|
+
try {
|
|
3748
|
+
await runBuild(config, "dev");
|
|
3749
|
+
appHandle = await runApp(config, {
|
|
3750
|
+
onExit: () => {
|
|
3751
|
+
appHandle = null;
|
|
3752
|
+
},
|
|
3753
|
+
});
|
|
3754
|
+
} catch (error) {
|
|
3755
|
+
console.error("[electrobun dev --watch] Initial build failed:", error);
|
|
3756
|
+
console.log("[electrobun dev --watch] Waiting for file changes...");
|
|
3443
3757
|
}
|
|
3758
|
+
|
|
3759
|
+
// Start watching only after initial build + all hooks are done
|
|
3760
|
+
startWatchers();
|
|
3761
|
+
|
|
3762
|
+
// Keep the process alive
|
|
3763
|
+
await new Promise(() => {});
|
|
3444
3764
|
}
|
|
3445
3765
|
|
|
3446
3766
|
// Helper functions
|
|
@@ -3709,8 +4029,6 @@ Categories=Utility;Application;
|
|
|
3709
4029
|
});
|
|
3710
4030
|
}
|
|
3711
4031
|
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
4032
|
function codesignAppBundle(
|
|
3715
4033
|
appBundleOrDmgPath: string,
|
|
3716
4034
|
entitlementsFilePath: string | undefined,
|