seabox 0.1.0-beta.3 → 0.1.0-beta.4

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.
@@ -0,0 +1,620 @@
1
+ /**
2
+ * multi-target-builder.mjs
3
+ * Orchestrate parallel builds for multiple target platforms.
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { execSync } from 'child_process';
9
+ import crypto from 'crypto';
10
+ import { fileURLToPath } from 'url';
11
+ import Module from 'module';
12
+
13
+ import { bundleWithRollup } from './rolldown-bundler.mjs';
14
+ import { scanDependenciesForNativeModules } from './native-scanner.mjs';
15
+ import { BuildCache } from './build-cache.mjs';
16
+ import { parseTarget } from './config.mjs';
17
+ import { generateManifest, serializeManifest } from './manifest.mjs';
18
+ import { createSeaConfig, writeSeaConfigJson, generateBlob } from './blob.mjs';
19
+ import { fetchNodeBinary } from './fetch-node.mjs';
20
+ import { injectBlob } from './inject.mjs';
21
+ import { generateEncryptionKey, encryptAssets, keyToObfuscatedCode } from './crypto-assets.mjs';
22
+ import { obfuscateBootstrap } from './obfuscate.mjs';
23
+ import { bundleEntry } from './entry-bundler.mjs';
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = path.dirname(__filename);
27
+ const require = Module.createRequire(import.meta.url);
28
+
29
+ /**
30
+ * Multi-target build orchestrator
31
+ */
32
+ export class MultiTargetBuilder {
33
+ /**
34
+ * @param {import('./config.mjs').SeaboxConfigV2} config - Build configuration
35
+ * @param {string} projectRoot - Project root directory
36
+ */
37
+ constructor(config, projectRoot = process.cwd()) {
38
+ this.config = config;
39
+ this.projectRoot = projectRoot;
40
+ this.cache = new BuildCache(path.join(projectRoot, '.seabox-cache'));
41
+ this.verbose = config.verbose || false;
42
+ }
43
+
44
+ /**
45
+ * Build all configured targets
46
+ * @returns {Promise<Array<{target: string, path: string}>>}
47
+ */
48
+ async buildAll() {
49
+ console.log('🚀 Seabox Multi-Target Build');
50
+ console.log('═══════════════════════════════════════\n');
51
+
52
+ // Step 1: Bundle entry once (platform-agnostic JavaScript)
53
+ const { bundledPath, nativeModules, detectedAssets } = await this.bundleEntry();
54
+
55
+ if (this.verbose && detectedAssets.size > 0) {
56
+ console.log('\n🔍 Auto-detected assets:');
57
+ for (const assetPath of detectedAssets) {
58
+ console.log(` - ${assetPath}`);
59
+ }
60
+ }
61
+
62
+ // Step 2: Scan for additional native modules in node_modules
63
+ const scannedNatives = await this.scanNativeModules();
64
+
65
+ // Merge detected native modules
66
+ const allNativeModules = this.mergeNativeModules(nativeModules, scannedNatives);
67
+
68
+ if (this.verbose && allNativeModules.size > 0) {
69
+ console.log('\n📦 Native modules detected:');
70
+ for (const [name, info] of allNativeModules) {
71
+ console.log(` - ${name}: ${info.packageRoot}`);
72
+ }
73
+ }
74
+
75
+ // Step 3: Build all targets (can be parallelized)
76
+ console.log(`\n🎯 Building ${this.config.outputs.length} target(s)...\n`);
77
+
78
+ const buildPromises = this.config.outputs.map((output, index) =>
79
+ this.buildTarget(output, bundledPath, allNativeModules, detectedAssets, index + 1)
80
+ );
81
+
82
+ const results = await Promise.all(buildPromises);
83
+
84
+ console.log('\n✅ All builds completed successfully!');
85
+ console.log('═══════════════════════════════════════\n');
86
+
87
+ return results;
88
+ }
89
+
90
+ /**
91
+ * Bundle the entry file with Rollup
92
+ * @returns {Promise<{bundledPath: string, nativeModules: Map}>}
93
+ */
94
+ async bundleEntry() {
95
+ console.log('[1/6] 📝 Bundling application with Rollup...');
96
+
97
+ const entryPath = path.resolve(this.projectRoot, this.config.entry);
98
+
99
+ if (!fs.existsSync(entryPath)) {
100
+ throw new Error(`Entry file not found: ${entryPath}`);
101
+ }
102
+
103
+ const bundledPath = path.join(this.projectRoot, 'out', '_sea-entry.js');
104
+
105
+ // Create output directory
106
+ fs.mkdirSync(path.dirname(bundledPath), { recursive: true });
107
+
108
+ const result = await bundleWithRollup(
109
+ entryPath,
110
+ bundledPath,
111
+ { ...this.config, _projectRoot: this.projectRoot },
112
+ process.platform, // Use current platform for bundling (JavaScript is platform-agnostic)
113
+ process.arch,
114
+ this.verbose
115
+ );
116
+
117
+ console.log(` ✓ Bundle created: ${bundledPath}`);
118
+
119
+ return result;
120
+ }
121
+
122
+ /**
123
+ * Scan node_modules for native modules
124
+ * @returns {Promise<Array>}
125
+ */
126
+ async scanNativeModules() {
127
+ if (this.verbose) {
128
+ console.log('\n[2/6] 🔍 Scanning node_modules for native modules...');
129
+ }
130
+
131
+ const nativeModules = await scanDependenciesForNativeModules(this.projectRoot, this.verbose);
132
+
133
+ return nativeModules;
134
+ }
135
+
136
+ /**
137
+ * Merge detected native modules from bundler and scanner
138
+ */
139
+ mergeNativeModules(bundlerModules, scannedModules) {
140
+ const merged = new Map(bundlerModules);
141
+
142
+ // Add scanned modules that weren't detected during bundling
143
+ for (const scanned of scannedModules) {
144
+ if (!merged.has(scanned.name)) {
145
+ merged.set(scanned.name, {
146
+ packageRoot: scanned.path,
147
+ moduleName: scanned.name,
148
+ buildPath: path.join(scanned.path, 'build/Release'),
149
+ binaryFiles: scanned.binaryFiles
150
+ });
151
+ }
152
+ }
153
+
154
+ return merged;
155
+ }
156
+
157
+ /**
158
+ * Build a single target
159
+ * @param {import('./config.mjs').OutputTarget} outputConfig - Target configuration
160
+ * @param {string} bundledEntryPath - Path to bundled entry
161
+ * @param {Map} nativeModules - Detected native modules
162
+ * @param {Set<string>} detectedAssets - Auto-detected assets from bundler
163
+ * @param {number} buildNumber - Build number for display
164
+ * @returns {Promise<{target: string, path: string}>}
165
+ */
166
+ async buildTarget(outputConfig, bundledEntryPath, nativeModules, detectedAssets, buildNumber) {
167
+ const { target, path: outputPath, output: executableName } = outputConfig;
168
+ const { nodeVersion, platform, arch } = parseTarget(target);
169
+
170
+ console.log(`\n[Build ${buildNumber}] Target: ${target}`);
171
+ console.log('─────────────────────────────────────────');
172
+
173
+ // Step 1: Rebuild native modules for this target
174
+ const rebuiltModules = await this.rebuildNativeModulesForTarget(
175
+ nativeModules,
176
+ target,
177
+ buildNumber
178
+ );
179
+
180
+ // Step 2: Collect config assets (manual globs)
181
+ const configAssets = await this.collectConfigAssets(
182
+ this.config.assets || [],
183
+ buildNumber
184
+ );
185
+
186
+ // Step 3: Collect auto-detected assets (from path.join(__dirname, ...))
187
+ const autoAssets = await this.collectDetectedAssets(
188
+ detectedAssets,
189
+ buildNumber
190
+ );
191
+
192
+ // Step 4: Collect platform-specific libraries (DLLs/SOs)
193
+ const platformLibraries = await this.collectPlatformLibraries(
194
+ outputConfig.libraries,
195
+ platform,
196
+ arch,
197
+ buildNumber
198
+ );
199
+
200
+ // Step 5: Prepare bundled entry with bootstrap
201
+ const finalEntryPath = await this.prepareFinalEntry(
202
+ bundledEntryPath,
203
+ buildNumber
204
+ );
205
+
206
+ // Step 6: Combine all assets (dedupe by assetKey)
207
+ const assetMap = new Map();
208
+
209
+ // Add in order of priority (later overwrites earlier)
210
+ for (const asset of [...rebuiltModules, ...configAssets, ...autoAssets, ...platformLibraries]) {
211
+ assetMap.set(asset.assetKey, asset);
212
+ }
213
+
214
+ const allAssets = Array.from(assetMap.values());
215
+
216
+ // Step 7: Generate SEA
217
+ await this.generateSEAForTarget({
218
+ assets: allAssets,
219
+ entryPath: finalEntryPath,
220
+ target,
221
+ outputPath,
222
+ executableName,
223
+ platform,
224
+ arch,
225
+ nodeVersion,
226
+ rcedit: outputConfig.rcedit,
227
+ buildNumber
228
+ });
229
+
230
+ const finalPath = path.join(outputPath, executableName);
231
+ console.log(` ✅ Build complete: ${finalPath}`);
232
+
233
+ return {
234
+ target,
235
+ path: finalPath
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Rebuild native modules for specific target
241
+ */
242
+ async rebuildNativeModulesForTarget(nativeModules, target, buildNumber) {
243
+ if (nativeModules.size === 0) {
244
+ return [];
245
+ }
246
+
247
+ console.log(` [${buildNumber}.1] 🔨 Rebuilding ${nativeModules.size} native module(s)...`);
248
+
249
+ const rebuiltAssets = [];
250
+ const { platform, arch } = parseTarget(target);
251
+
252
+ for (const [moduleName, moduleInfo] of nativeModules) {
253
+ try {
254
+ // Check cache first
255
+ const cachedBuild = this.cache.getCachedNativeBuild(moduleInfo.packageRoot, target);
256
+
257
+ if (cachedBuild) {
258
+ if (this.verbose) {
259
+ console.log(` ✓ Using cached build: ${moduleName}`);
260
+ }
261
+
262
+ rebuiltAssets.push({
263
+ sourcePath: cachedBuild,
264
+ assetKey: `native/${moduleName}.node`,
265
+ isBinary: true,
266
+ hash: await this.computeHash(cachedBuild)
267
+ });
268
+ continue;
269
+ }
270
+
271
+ // Rebuild the module
272
+ if (this.verbose) {
273
+ console.log(` 🔧 Rebuilding: ${moduleName}`);
274
+ }
275
+
276
+ await this.rebuildNativeModule(moduleInfo.packageRoot, target);
277
+
278
+ // Find the built binary
279
+ const builtPath = await this.findBuiltBinary(moduleInfo, target);
280
+
281
+ if (builtPath) {
282
+ // Cache the build
283
+ this.cache.cacheNativeBuild(moduleInfo.packageRoot, target, builtPath);
284
+
285
+ rebuiltAssets.push({
286
+ sourcePath: builtPath,
287
+ assetKey: `native/${moduleName}.node`,
288
+ isBinary: true,
289
+ hash: await this.computeHash(builtPath)
290
+ });
291
+
292
+ if (this.verbose) {
293
+ console.log(` ✓ Built: ${moduleName} -> ${builtPath}`);
294
+ }
295
+ }
296
+ } catch (err) {
297
+ console.warn(` ⚠️ Failed to rebuild ${moduleName}:`, err.message);
298
+ }
299
+ }
300
+
301
+ console.log(` ✓ Native modules processed`);
302
+ return rebuiltAssets;
303
+ }
304
+
305
+ /**
306
+ * Rebuild a single native module
307
+ */
308
+ async rebuildNativeModule(packageRoot, target) {
309
+ const rebuildScript = path.join(__dirname, '..', 'bin', 'seabox-rebuild.mjs');
310
+
311
+ if (!fs.existsSync(rebuildScript)) {
312
+ throw new Error('seabox-rebuild.mjs not found');
313
+ }
314
+
315
+ try {
316
+ execSync(`node "${rebuildScript}" --target ${target} "${packageRoot}"`, {
317
+ stdio: this.verbose ? 'inherit' : 'pipe',
318
+ cwd: this.projectRoot
319
+ });
320
+ } catch (err) {
321
+ throw new Error(`Rebuild failed: ${err.message}`);
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Find the built .node binary
327
+ */
328
+ async findBuiltBinary(moduleInfo, target) {
329
+ // Try common build locations
330
+ const searchPaths = [
331
+ path.join(moduleInfo.packageRoot, 'build/Release'),
332
+ path.join(moduleInfo.packageRoot, 'build/Debug'),
333
+ moduleInfo.buildPath
334
+ ];
335
+
336
+ for (const searchPath of searchPaths) {
337
+ if (fs.existsSync(searchPath)) {
338
+ const files = fs.readdirSync(searchPath);
339
+ const nodeFile = files.find(f => f.endsWith('.node'));
340
+
341
+ if (nodeFile) {
342
+ return path.join(searchPath, nodeFile);
343
+ }
344
+ }
345
+ }
346
+
347
+ return null;
348
+ }
349
+
350
+ /**
351
+ * Collect platform-specific libraries (DLLs, SOs, DYLIBs) that need filesystem extraction
352
+ * @param {string[]} libraryPatterns - Glob patterns for libraries
353
+ * @param {string} platform - Target platform
354
+ * @param {string} arch - Target architecture
355
+ * @param {number} buildNumber - Build number for display
356
+ */
357
+ async collectPlatformLibraries(libraryPatterns, platform, arch, buildNumber) {
358
+ // Use provided patterns or default to platform-specific patterns
359
+ const { getDefaultLibraryPatterns } = await import('./config.mjs');
360
+ const patterns = libraryPatterns && libraryPatterns.length > 0
361
+ ? libraryPatterns
362
+ : getDefaultLibraryPatterns(platform);
363
+
364
+ if (!patterns || patterns.length === 0) {
365
+ return [];
366
+ }
367
+
368
+ console.log(` [${buildNumber}.4] 📚 Collecting platform libraries (${platform})...`);
369
+
370
+ const { glob } = await import('glob');
371
+ const libraries = [];
372
+
373
+ for (const pattern of patterns) {
374
+ const matches = await glob(pattern, {
375
+ cwd: this.projectRoot,
376
+ nodir: true,
377
+ absolute: false,
378
+ ignore: ['**/node_modules/**', '**/.git/**']
379
+ });
380
+
381
+ for (const match of matches) {
382
+ const sourcePath = path.resolve(this.projectRoot, match);
383
+
384
+ libraries.push({
385
+ sourcePath,
386
+ assetKey: match.replace(/\\/g, '/'),
387
+ isBinary: true,
388
+ hash: await this.computeHash(sourcePath)
389
+ });
390
+ }
391
+ }
392
+
393
+ if (libraries.length > 0) {
394
+ console.log(` ✓ Collected ${libraries.length} library file(s)`);
395
+ }
396
+
397
+ return libraries;
398
+ }
399
+
400
+ /**
401
+ * Collect assets from config globs
402
+ * @param {string[]} assetPatterns - Glob patterns for assets
403
+ * @param {number} buildNumber - Build number for display
404
+ */
405
+ async collectConfigAssets(assetPatterns, buildNumber) {
406
+ if (!assetPatterns || assetPatterns.length === 0) {
407
+ return [];
408
+ }
409
+
410
+ console.log(` [${buildNumber}.2] 📦 Collecting config assets...`);
411
+
412
+ const { glob } = await import('glob');
413
+ const assets = [];
414
+
415
+ for (const pattern of assetPatterns) {
416
+ const matches = await glob(pattern, {
417
+ cwd: this.projectRoot,
418
+ nodir: true,
419
+ absolute: false,
420
+ ignore: ['**/node_modules/**', '**/.git/**']
421
+ });
422
+
423
+ for (const match of matches) {
424
+ const sourcePath = path.resolve(this.projectRoot, match);
425
+
426
+ assets.push({
427
+ sourcePath,
428
+ assetKey: match.replace(/\\/g, '/'),
429
+ isBinary: this.isBinaryFile(sourcePath),
430
+ hash: await this.computeHash(sourcePath)
431
+ });
432
+ }
433
+ }
434
+
435
+ if (assets.length > 0) {
436
+ console.log(` ✓ Collected ${assets.length} config asset(s)`);
437
+ }
438
+
439
+ return assets;
440
+ }
441
+
442
+ /**
443
+ * Collect auto-detected assets from bundler (path.join(__dirname, ...))
444
+ * @param {Set<string>} detectedAssets - Asset paths detected during bundling
445
+ * @param {number} buildNumber - Build number for display
446
+ */
447
+ async collectDetectedAssets(detectedAssets, buildNumber) {
448
+ if (!detectedAssets || detectedAssets.size === 0) {
449
+ return [];
450
+ }
451
+
452
+ console.log(` [${buildNumber}.3] 🔍 Processing auto-detected assets...`);
453
+
454
+ const assets = [];
455
+
456
+ for (const assetKey of detectedAssets) {
457
+ const sourcePath = path.resolve(this.projectRoot, assetKey);
458
+
459
+ // Verify file still exists
460
+ if (fs.existsSync(sourcePath)) {
461
+ assets.push({
462
+ sourcePath,
463
+ assetKey: assetKey,
464
+ isBinary: this.isBinaryFile(sourcePath),
465
+ hash: await this.computeHash(sourcePath)
466
+ });
467
+ }
468
+ }
469
+
470
+ if (assets.length > 0) {
471
+ console.log(` ✓ Collected ${assets.length} auto-detected asset(s)`);
472
+ }
473
+
474
+ return assets;
475
+ }
476
+
477
+ /**
478
+ * Check if a file is binary based on extension
479
+ */
480
+ isBinaryFile(filePath) {
481
+ const binaryExtensions = ['.node', '.dll', '.so', '.dylib', '.exe', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot'];
482
+ const ext = path.extname(filePath).toLowerCase();
483
+ return binaryExtensions.includes(ext);
484
+ }
485
+
486
+ /**
487
+ * Prepare final entry with bootstrap
488
+ */
489
+ async prepareFinalEntry(bundledEntryPath, buildNumber) {
490
+ console.log(` [${buildNumber}.3] 🎁 Preparing bootstrap...`);
491
+
492
+ const bootstrapPath = path.join(__dirname, 'bootstrap.cjs');
493
+ const bootstrapContent = fs.readFileSync(bootstrapPath, 'utf8');
494
+
495
+ let augmentedBootstrap = bootstrapContent;
496
+
497
+ // Handle encryption if enabled
498
+ if (this.config.encryptAssets) {
499
+ const encryptionKey = generateEncryptionKey();
500
+ const keyCode = keyToObfuscatedCode(encryptionKey);
501
+
502
+ const encryptionSetup = `
503
+ const SEA_ENCRYPTION_KEY = ${keyCode};
504
+ const SEA_ENCRYPTED_ASSETS = new Set([]);
505
+ `;
506
+
507
+ augmentedBootstrap = bootstrapContent.replace(
508
+ " 'use strict';",
509
+ " 'use strict';\n" + encryptionSetup
510
+ );
511
+
512
+ if (this.verbose) {
513
+ console.log(' ✓ Encryption enabled and bootstrap obfuscated');
514
+ }
515
+
516
+ augmentedBootstrap = obfuscateBootstrap(augmentedBootstrap);
517
+ }
518
+
519
+ // Read bundled entry
520
+ const entryContent = fs.readFileSync(bundledEntryPath, 'utf8');
521
+
522
+ // Bundle with bootstrap
523
+ const finalEntry = bundleEntry(entryContent, augmentedBootstrap, this.config.useSnapshot, this.verbose);
524
+
525
+ // Write final entry
526
+ const finalPath = bundledEntryPath.replace('.js', '-final.js');
527
+ fs.writeFileSync(finalPath, finalEntry, 'utf8');
528
+
529
+ console.log(` ✓ Bootstrap prepared`);
530
+ return finalPath;
531
+ }
532
+
533
+ /**
534
+ * Generate SEA for a specific target
535
+ */
536
+ async generateSEAForTarget(options) {
537
+ const {
538
+ assets,
539
+ entryPath,
540
+ target,
541
+ outputPath: outputDir,
542
+ executableName,
543
+ platform,
544
+ arch,
545
+ nodeVersion,
546
+ rcedit,
547
+ buildNumber
548
+ } = options;
549
+
550
+ console.log(` [${buildNumber}.4] 🔧 Generating SEA blob...`);
551
+
552
+ // Generate manifest
553
+ const manifest = generateManifest(
554
+ assets,
555
+ {
556
+ _packageName: this.config._packageName || 'app',
557
+ _packageVersion: this.config._packageVersion || '1.0.0',
558
+ cacheLocation: this.config.cacheLocation
559
+ },
560
+ platform,
561
+ arch
562
+ );
563
+
564
+ const manifestJson = serializeManifest(manifest);
565
+ const manifestAsset = {
566
+ sourcePath: null,
567
+ assetKey: 'sea-manifest.json',
568
+ isBinary: false,
569
+ content: Buffer.from(manifestJson, 'utf8')
570
+ };
571
+
572
+ const allAssets = [...assets, manifestAsset];
573
+
574
+ // Create SEA config
575
+ const tempDir = path.join(this.projectRoot, 'out', '.sea-temp', target);
576
+ fs.mkdirSync(tempDir, { recursive: true });
577
+
578
+ const blobOutputPath = path.join(tempDir, 'sea-blob.blob');
579
+ const seaConfig = createSeaConfig(entryPath, blobOutputPath, allAssets, this.config);
580
+
581
+ const seaConfigPath = path.join(tempDir, 'sea-config.json');
582
+ writeSeaConfigJson(seaConfig, seaConfigPath, allAssets, tempDir);
583
+
584
+ // Generate blob
585
+ await generateBlob(seaConfigPath, process.execPath);
586
+ console.log(` ✓ SEA blob generated`);
587
+
588
+ // Fetch Node binary
589
+ console.log(` [${buildNumber}.5] 📥 Fetching Node.js binary...`);
590
+ const cacheDir = path.join(this.projectRoot, 'node_modules', '.cache', 'sea-node-binaries');
591
+ const nodeBinary = await fetchNodeBinary(nodeVersion, platform, arch, cacheDir);
592
+ console.log(` ✓ Node binary ready`);
593
+
594
+ // Inject blob
595
+ console.log(` [${buildNumber}.6] 💉 Injecting blob into executable...`);
596
+ const outputExe = path.join(outputDir, executableName);
597
+ await injectBlob(nodeBinary, blobOutputPath, outputExe, platform, this.verbose, rcedit);
598
+
599
+ // Cleanup
600
+ if (!this.verbose) {
601
+ fs.rmSync(tempDir, { recursive: true, force: true });
602
+ }
603
+
604
+ const sizeMB = (fs.statSync(outputExe).size / 1024 / 1024).toFixed(2);
605
+ console.log(` ✓ Injected (${sizeMB} MB)`);
606
+ }
607
+
608
+ /**
609
+ * Compute SHA-256 hash of a file
610
+ */
611
+ async computeHash(filePath) {
612
+ return new Promise((resolve, reject) => {
613
+ const hash = crypto.createHash('sha256');
614
+ const stream = fs.createReadStream(filePath);
615
+ stream.on('data', chunk => hash.update(chunk));
616
+ stream.on('end', () => resolve(hash.digest('hex')));
617
+ stream.on('error', reject);
618
+ });
619
+ }
620
+ }