seabox 0.1.0-beta.4 → 0.1.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/README.md CHANGED
@@ -57,12 +57,12 @@ Create a `seabox.config.json` file in your project root:
57
57
  | `outputs[].path` | `string` | Yes | Output directory for this target |
58
58
  | `outputs[].target` | `string` | Yes | Build target (format: `nodeX.Y.Z-platform-arch`) |
59
59
  | `outputs[].output` | `string` | Yes | Output filename |
60
- | `outputs[].libraries` | `array` | No | Glob patterns for shared libraries (DLLs/SOs) requiring filesystem extraction (defaults: `**/*.dll` for Windows, `**/*.so*` for Linux, `**/*.dylib` for macOS) |
60
+ | `outputs[].libraries` | `array` | No | Explicit glob patterns for shared libraries (DLLs/SOs) requiring filesystem extraction. Libraries referenced in code via `, ...)` are automatically detected. |
61
61
  | `outputs[].rcedit` | `object` | No | Windows executable metadata (icon, version info) |
62
62
  | `assets` | `array` | No | Glob patterns for assets to embed (merged with auto-detected assets) |
63
- | `bundler` | `object` | No | Bundler options |
63
+ | `bundler` | `object` | No | Rolldown Bundler options |
64
64
  | `bundler.external` | `array` | No | Modules to exclude from bundling |
65
- | `bundler.plugins` | `array` | No | Additional Rollup plugins |
65
+ | `bundler.plugins` | `array` | No | Additional Rolldown plugins |
66
66
  | `bundler.minify` | `boolean` | No | Minify bundled code |
67
67
  | `bundler.sourcemap` | `boolean` | No | Generate source maps |
68
68
  | `encryptAssets` | `boolean` | No | Enable asset encryption (default: false) |
@@ -70,6 +70,7 @@ Create a `seabox.config.json` file in your project root:
70
70
  | `useSnapshot` | `boolean` | No | Enable V8 startup snapshots (default: true) |
71
71
  | `useCodeCache` | `boolean` | No | Enable V8 code cache (default: false) |
72
72
  | `cacheLocation` | `string` | No | Path for code cache storage |
73
+ | `sign` | `string` | No | Path to custom signing script (.mjs/.cjs) |
73
74
  | `verbose` | `boolean` | No | Enable verbose logging (default: false) |
74
75
 
75
76
  ## Usage
@@ -127,9 +128,10 @@ await build({
127
128
 
128
129
  seabox automates the entire SEA build process:
129
130
 
130
- 1. **Bundling** - Automatically bundles your app with Rollup, detecting:
131
+ 1. **Bundling** - Automatically bundles your app with Rolldown, detecting:
131
132
  - Native module patterns (`bindings`, `node-gyp-build`, direct `.node` requires)
132
133
  - Asset references via `path.join(__dirname, 'relative/path')`
134
+ - Additional
133
135
 
134
136
  2. **Asset Collection** - Gathers assets from three sources:
135
137
  - **Auto-detected**: Files referenced via `path.join(__dirname, ...)` patterns
@@ -163,11 +165,6 @@ const configPath = path.join(__dirname, '../config/app.json');
163
165
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
164
166
  ```
165
167
 
166
- **Detection works with:**
167
- - `path.join(__dirname, 'relative/path')`
168
- - `path.resolve(__dirname, 'relative/path')`
169
- - Multiple path segments: `path.join(__dirname, '..', 'assets', 'file.txt')`
170
-
171
168
  **Asset sources (merged and deduplicated):**
172
169
  1. **Auto-detected** from code analysis during bundling
173
170
  2. **Config globs** from `assets: ["./data/**/*", "./public/**/*"]`
@@ -187,28 +184,35 @@ seabox automatically handles native modules without any configuration:
187
184
  - Native modules are extracted to a cache directory on first run
188
185
  - Modules are integrity-checked with SHA-256 hashes
189
186
  - Custom `require()` shim loads modules from cache
190
- - Works transparently with packages like `better-sqlite3`, `sharp`, `canvas`, etc.
191
-
192
187
 
193
188
  ### Platform-Specific Libraries
194
189
 
195
- Libraries that require filesystem access (like DLLs that are loaded via `dlopen`) can be specified with glob patterns:
190
+ Libraries that require filesystem access (like DLLs loaded via `dlopen`) can be included in two ways:
191
+
192
+ **1. Automatic Detection (Recommended)**
193
+
194
+ If your code references a DLL using `path.join(__dirname, ...)`, it will be automatically detected and included:
195
+
196
+ ```javascript
197
+ // This will be automatically detected during bundling
198
+ const dllPath = path.join(__dirname, './lib/RGDevice.dll');
199
+ ```
200
+
201
+ **2. Explicit Glob Patterns**
202
+
203
+ You can also explicitly specify library patterns in your config:
196
204
 
197
205
  ```json
198
206
  {
199
207
  "outputs": [
200
208
  {
201
209
  "target": "node24.11.0-win32-x64",
202
- "libraries": ["**/*.dll"] // Auto-extracted at runtime
210
+ "libraries": ["lib/*.dll"] // Manually specify DLLs to include
203
211
  }
204
212
  ]
205
213
  }
206
214
  ```
207
215
 
208
- **Defaults by platform:**
209
- - **Windows**: `**/*.dll`
210
- - **Linux**: `**/*.so`, `**/*.so.*`
211
- - **macOS**: `**/*.dylib`
212
216
 
213
217
  These files are extracted on first run (like `.node` files) since they need to be loaded from the filesystem.
214
218
 
@@ -219,6 +223,43 @@ Required before SEA injection. Platform-specific tools needed:
219
223
  - **macOS**: `codesign` (included with Xcode)
220
224
  - **Linux**: Not required
221
225
 
226
+ ### Custom Signing
227
+
228
+ You can apply code signing after the build completes by specifying a custom signing script:
229
+
230
+ ```json
231
+ {
232
+ "sign": "./scripts/sign.mjs"
233
+ }
234
+ ```
235
+
236
+ The signing script must export a default function that receives a config object:
237
+
238
+ ```javascript
239
+ // scripts/sign.mjs
240
+ export default async function sign(config) {
241
+ const { exePath, target, platform, arch, nodeVersion, projectRoot } = config;
242
+
243
+ // Example: Windows code signing with signtool
244
+ if (platform === 'win32') {
245
+ execSync(`signtool sign /fd SHA256 /a "${exePath}"`);
246
+ }
247
+
248
+ // Example: macOS code signing
249
+ if (platform === 'darwin') {
250
+ execSync(`codesign --force --sign "Developer ID" "${exePath}"`);
251
+ }
252
+ }
253
+ ```
254
+
255
+ **Config parameters:**
256
+ - `exePath` - Absolute path to the built executable
257
+ - `target` - Full target string (e.g., "node24.11.0-win32-x64")
258
+ - `platform` - Platform name (win32, linux, darwin)
259
+ - `arch` - Architecture (x64, arm64)
260
+ - `nodeVersion` - Node.js version
261
+ - `projectRoot` - Absolute path to project root
262
+
222
263
  ## Asset Encryption
223
264
 
224
265
  seabox supports optional AES-256-GCM encryption of embedded assets to protect your application code and data:
@@ -238,231 +279,17 @@ seabox supports optional AES-256-GCM encryption of embedded assets to protect yo
238
279
  1. **Build Time**: A random 256-bit encryption key is generated
239
280
  2. **Asset Encryption**: Non-binary assets are encrypted using AES-256-GCM
240
281
  3. **Key Embedding**: The encryption key is obfuscated and embedded in the bootstrap code
241
- 4. **Key Obfuscation**: the bootstrap and key code are obfuscated, but not removed
282
+ 4. **Key Obfuscation**: the bootstrap and key code are obfuscated
242
283
  5. **Runtime Decryption**: Assets are transparently decrypted when accessed
243
284
 
244
285
  ### Considerations
245
286
 
246
287
  - **Binary files** (`.node`, `.dll`, `.so`, `.dylib`) are **never encrypted** as they must be extracted as-is
247
288
  - The manifest (`sea-manifest.json`) is **not encrypted** to allow bootstrap initialization
248
- - **V8 snapshot includes the original source**, this is currently a limitation of Node's SEA.
289
+ - **V8 snapshot includes the original source**, this is currently a limitation of Node's SEA tooling.
249
290
  - Encryption provides **obfuscation**, not cryptographic security against determined attackers
250
291
  - The bootloader code, that includes the encryption key, is obfuscated in the source embedded by Node's SEA
251
292
 
252
-
253
- ## Platform Support
254
-
255
- - **Windows**: `win32-x64`, `win32-arm64`
256
- - **Linux**: `linux-x64`, `linux-arm64`
257
- - **macOS**: `darwin-x64`, `darwin-arm64`
258
-
259
- ## License
260
-
261
- MIT
262
- Copyright Meirion Hughes 2025
263
- ## Examples
264
-
265
- ### Basic Application
266
-
267
- ```javascript
268
- // src/index.js
269
- console.log('Hello from SEA!');
270
- console.log('Platform:', process.platform);
271
- console.log('Architecture:', process.arch);
272
- ```
273
-
274
- ```json
275
- // seabox.config.json
276
- {
277
- "entry": "./src/index.js",
278
- "outputs": [
279
- {
280
- "path": "./dist",
281
- "target": "node24.11.0-win32-x64",
282
- "output": "hello.exe"
283
- }
284
- ]
285
- }
286
- ```
287
-
288
- ### With Assets (Auto-Detection)
289
-
290
- ```javascript
291
- // src/index.js
292
- import fs from 'fs';
293
- import path from 'path';
294
- import { fileURLToPath } from 'url';
295
-
296
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
297
-
298
- // Assets referenced via path.join(__dirname, ...) are auto-detected
299
- const configPath = path.join(__dirname, '../config/settings.json');
300
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
301
-
302
- console.log('Config loaded:', config);
303
- ```
304
-
305
- No configuration needed - the asset is automatically detected and embedded!
306
-
307
- ### With Config Assets
308
-
309
- ```json
310
- {
311
- "entry": "./src/index.js",
312
- "outputs": [
313
- {
314
- "path": "./dist",
315
- "target": "node24.11.0-win32-x64",
316
- "output": "myapp.exe"
317
- }
318
- ],
319
- "assets": [
320
- "./public/**/*",
321
- "./data/**/*.json",
322
- "!**/*.md"
323
- ]
324
- }
325
- ```
326
-
327
- All files matching the glob patterns will be embedded. Auto-detected assets are merged automatically.
328
-
329
- ### With Native Modules
330
-
331
- ```javascript
332
- // src/index.js
333
- import Database from 'better-sqlite3';
334
-
335
- const db = new Database(':memory:');
336
- db.exec('CREATE TABLE users (name TEXT)');
337
- db.prepare('INSERT INTO users VALUES (?)').run('Alice');
338
-
339
- const users = db.prepare('SELECT * FROM users').all();
340
- console.log('Users:', users);
341
-
342
- db.close();
343
- ```
344
-
345
- No special configuration needed - seabox automatically detects and handles the native module!
346
-
347
- ### Multi-Platform Build
348
-
349
- ```json
350
- {
351
- "entry": "./src/index.js",
352
- "outputs": [
353
- {
354
- "path": "./dist/win",
355
- "target": "node24.11.0-win32-x64",
356
- "output": "myapp.exe"
357
- },
358
- {
359
- "path": "./dist/linux",
360
- "target": "node24.11.0-linux-x64",
361
- "output": "myapp"
362
- },
363
- {
364
- "path": "./dist/macos",
365
- "target": "node24.11.0-darwin-arm64",
366
- "output": "myapp"
367
- }
368
- ],
369
- "bundler": {
370
- "external": []
371
- },
372
- "useSnapshot": true
373
- }
374
- ```
375
-
376
- Run `seabox build` and get executables for all three platforms!
377
-
378
- ## Advanced Features
379
-
380
- ### Asset Encryption
381
-
382
- Protect your source code with AES-256-GCM encryption:
383
-
384
- ```json
385
- {
386
- "entry": "./src/index.js",
387
- "outputs": [
388
- {
389
- "path": "./dist",
390
- "target": "node24.11.0-win32-x64",
391
- "output": "myapp.exe"
392
- }
393
- ],
394
- "encryptAssets": true,
395
- "encryptExclude": ["*.txt"]
396
- }
397
- ```
398
-
399
- ### External Dependencies
400
-
401
- Exclude packages from bundling:
402
-
403
- ```json
404
- {
405
- "entry": "./src/index.js",
406
- "outputs": [
407
- {
408
- "path": "./dist",
409
- "target": "node24.11.0-win32-x64",
410
- "output": "myapp.exe"
411
- }
412
- ],
413
- "bundler": {
414
- "external": ["fsevents", "some-optional-dep"]
415
- }
416
- }
417
- ```json
418
- {
419
- "bundler": {
420
- "external": ["fsevents", "some-optional-dep"]
421
- }
422
- }
423
- ```
424
-
425
- Useful for:
426
- - Platform-specific optional dependencies
427
- - Packages that don't bundle well
428
- - Reducing bundle size
429
-
430
- ## Platform Support
431
-
432
- ### Supported Targets
433
-
434
- | Platform | Architectures | Example |
435
- |----------|--------------|---------|
436
- | Windows | x64, arm64 | `node24.11.0-win32-x64` |
437
- | Linux | x64, arm64 | `node24.11.0-linux-x64` |
438
- | macOS | x64, arm64 | `node24.11.0-darwin-arm64` |
439
-
440
- ### Node.js Versions
441
-
442
- Works with Node.js 18.0.0 and above that support SEA.
443
-
444
- ## Troubleshooting
445
-
446
- ### Native modules not loading
447
-
448
- If you see errors about missing `.node` files:
449
- 1. Check that the module was detected during build (look for "Native modules detected" in output)
450
- 2. Run with `--verbose` to see detailed bundling info
451
- 3. Ensure the module uses standard patterns (`bindings`, `node-gyp-build`, etc.)
452
-
453
- ### Build fails with signature removal error
454
-
455
- Install the required tools:
456
- - **Windows**: Install Windows SDK for `signtool.exe`
457
- - **macOS**: Install Xcode Command Line Tools for `codesign`
458
-
459
- ### Cross-compilation issues
460
-
461
- When building for a different platform than your current OS:
462
- - Native module detection works cross-platform
463
- - The bundled JavaScript is platform-agnostic
464
- - Each target is built independently with the correct Node.js binary
465
-
466
293
  ## Contributing
467
294
 
468
295
  Contributions welcome! Please open an issue or PR on [GitHub](https://github.com/MeirionHughes/seabox).
@@ -8,6 +8,7 @@ import { execSync } from 'child_process';
8
8
  import fs from 'fs';
9
9
  import path from 'path';
10
10
  import { fileURLToPath } from 'url';
11
+ import * as diag from '../lib/diagnostics.mjs';
11
12
 
12
13
  const __filename = fileURLToPath(import.meta.url);
13
14
  const __dirname = path.dirname(__filename);
@@ -20,10 +21,10 @@ const __dirname = path.dirname(__filename);
20
21
  * @param {boolean} verbose - Enable verbose logging
21
22
  */
22
23
  function rebuildNativeModule(modulePath, platform, arch, verbose = false) {
23
- if (verbose) {
24
- console.log(`Rebuilding native module: ${modulePath}`);
25
- console.log(`Target: ${platform}-${arch}`);
26
- }
24
+ diag.setVerbose(verbose);
25
+
26
+ diag.verbose(`Rebuilding native module: ${modulePath}`);
27
+ diag.verbose(`Target: ${platform}-${arch}`);
27
28
 
28
29
  const packageJsonPath = path.join(modulePath, 'package.json');
29
30
  if (!fs.existsSync(packageJsonPath)) {
@@ -36,9 +37,7 @@ function rebuildNativeModule(modulePath, platform, arch, verbose = false) {
36
37
  // Check if module has native bindings
37
38
  const hasBindingGyp = fs.existsSync(path.join(modulePath, 'binding.gyp'));
38
39
  if (!hasBindingGyp && !pkg.gypfile) {
39
- if (verbose) {
40
- console.log(`Module ${moduleName} does not appear to have native bindings, skipping`);
41
- }
40
+ diag.verbose(`Module ${moduleName} does not appear to have native bindings, skipping`);
42
41
  return;
43
42
  }
44
43
 
@@ -46,9 +45,7 @@ function rebuildNativeModule(modulePath, platform, arch, verbose = false) {
46
45
  // Use node-gyp to rebuild for the target platform
47
46
  const cmd = `npx node-gyp rebuild --target_platform=${platform} --target_arch=${arch}`;
48
47
 
49
- if (verbose) {
50
- console.log(`Running: ${cmd}`);
51
- }
48
+ diag.verbose(`Running: ${cmd}`);
52
49
 
53
50
  execSync(cmd, {
54
51
  cwd: modulePath,
@@ -60,13 +57,9 @@ function rebuildNativeModule(modulePath, platform, arch, verbose = false) {
60
57
  }
61
58
  });
62
59
 
63
- if (verbose) {
64
- console.log(`āœ“ Successfully rebuilt ${moduleName}`);
65
- }
60
+ diag.verbose(`Successfully rebuilt ${moduleName}`);
66
61
  } catch (error) {
67
- if (verbose) {
68
- console.error(`Failed to rebuild ${moduleName}:`, error.message);
69
- }
62
+ diag.verbose(`Failed to rebuild ${moduleName}: ${error.message}`);
70
63
  throw error;
71
64
  }
72
65
  }
@@ -76,7 +69,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
76
69
  const args = process.argv.slice(2);
77
70
 
78
71
  if (args.length < 3) {
79
- console.error('Usage: seabox-rebuild <module-path> <platform> <arch> [--verbose]');
72
+ diag.error('Usage: seabox-rebuild <module-path> <platform> <arch> [--verbose]');
80
73
  process.exit(1);
81
74
  }
82
75
 
@@ -87,7 +80,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
87
80
  rebuildNativeModule(modulePath, platform, arch, verbose);
88
81
  process.exit(0);
89
82
  } catch (error) {
90
- console.error('Rebuild failed:', error.message);
83
+ diag.error(`Rebuild failed: ${error.message}`);
91
84
  process.exit(1);
92
85
  }
93
86
  }
package/bin/seabox.mjs CHANGED
@@ -11,6 +11,7 @@ import path from 'path';
11
11
  import fs from 'fs';
12
12
  import { fileURLToPath } from 'url';
13
13
  import Module from 'module';
14
+ import * as diag from '../lib/diagnostics.mjs';
14
15
 
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = path.dirname(__filename);
@@ -26,12 +27,15 @@ const commands = {
26
27
  const config = loadConfig(configPath, projectRoot);
27
28
 
28
29
  if (!config) {
29
- console.log('āŒ No configuration found\n');
30
- console.log('Seabox looks for configuration in this order:');
31
- console.log(' 1. --config <path> (command line argument)');
32
- console.log(' 2. seabox.config.json (in current directory)');
33
- console.log(' 3. "seabox" field in package.json\n');
34
- console.log('To get started, run: npx seabox init\n');
30
+ diag.error('No configuration found');
31
+ diag.separator();
32
+ diag.info('Seabox looks for configuration in this order:');
33
+ diag.numberedItem(1, '--config <path> (command line argument)');
34
+ diag.numberedItem(2, 'seabox.config.json (in current directory)');
35
+ diag.numberedItem(3, '"seabox" field in package.json');
36
+ diag.separator();
37
+ diag.info('To get started, run: npx seabox init');
38
+ diag.separator();
35
39
  commands.help();
36
40
  process.exit(1);
37
41
  }
@@ -61,7 +65,7 @@ const commands = {
61
65
  const configPath = path.join(process.cwd(), 'seabox.config.json');
62
66
 
63
67
  if (fs.existsSync(configPath)) {
64
- console.error('āŒ Error: seabox.config.json already exists');
68
+ diag.error('seabox.config.json already exists');
65
69
  process.exit(1);
66
70
  }
67
71
 
@@ -71,27 +75,34 @@ const commands = {
71
75
 
72
76
  fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf8');
73
77
 
74
- console.log('āœ… Created seabox.config.json');
75
- console.log('\nšŸ“ Next steps:');
76
- console.log(' 1. Edit seabox.config.json to configure your build');
77
- console.log(' 2. Run: npx seabox build\n');
78
+ diag.success('Created seabox.config.json', 0);
79
+ diag.separator();
80
+ diag.info('Next steps:');
81
+ diag.numberedItem(1, 'Edit seabox.config.json to configure your build');
82
+ diag.numberedItem(2, 'Run: npx seabox build');
83
+ diag.separator();
78
84
  },
79
85
 
80
86
  help: () => {
81
- console.log('Seabox v2 - Node.js Single Executable Application Builder\n');
82
- console.log('Usage: seabox [command] [options]\n');
83
- console.log('Commands:');
84
- console.log(' build Build executable(s) for configured targets (default)');
85
- console.log(' init Create a default seabox.config.json\n');
86
- console.log('Build Options:');
87
- console.log(' --config Path to config file (default: seabox.config.json)');
88
- console.log(' --verbose Enable verbose logging');
89
- console.log(' --debug Keep temporary build files\n');
90
- console.log('Examples:');
91
- console.log(' seabox init');
92
- console.log(' seabox build');
93
- console.log(' seabox --verbose # Same as: seabox build --verbose');
94
- console.log(' seabox build --verbose\n');
87
+ diag.info('Seabox v2 - Node.js Single Executable Application Builder');
88
+ diag.separator();
89
+ diag.info('Usage: seabox [command] [options]');
90
+ diag.separator();
91
+ diag.info('Commands:');
92
+ diag.info(' build Build executable(s) for configured targets (default)');
93
+ diag.info(' init Create a default seabox.config.json');
94
+ diag.separator();
95
+ diag.info('Build Options:');
96
+ diag.info(' --config Path to config file (default: seabox.config.json)');
97
+ diag.info(' --verbose Enable verbose logging');
98
+ diag.info(' --debug Keep temporary build files');
99
+ diag.separator();
100
+ diag.info('Examples:');
101
+ diag.info(' seabox init');
102
+ diag.info(' seabox build');
103
+ diag.info(' seabox --verbose # Same as: seabox build --verbose');
104
+ diag.info(' seabox build --verbose');
105
+ diag.separator();
95
106
  }
96
107
  };
97
108
 
@@ -119,14 +130,15 @@ async function main() {
119
130
  try {
120
131
  await commands[command](commandArgs);
121
132
  } catch (error) {
122
- console.error('Error:', error.message);
133
+ diag.error(error.message);
123
134
  if (args.includes('--verbose')) {
124
135
  console.error(error.stack);
125
136
  }
126
137
  process.exit(1);
127
138
  }
128
139
  } else {
129
- console.error(`Unknown command: ${command}\n`);
140
+ diag.error(`Unknown command: ${command}`);
141
+ diag.separator();
130
142
  commands.help();
131
143
  process.exit(1);
132
144
  }
package/lib/blob.mjs CHANGED
@@ -7,6 +7,7 @@ import fs from 'fs';
7
7
  import path from 'path';
8
8
  import { execFile } from 'child_process';
9
9
  import { promisify } from 'util';
10
+ import * as diag from './diagnostics.mjs';
10
11
 
11
12
  const execFileAsync = promisify(execFile);
12
13
 
@@ -96,7 +97,7 @@ export function writeSeaConfigJson(seaConfig, outputPath, assets, tempDir) {
96
97
  export async function generateBlob(seaConfigPath, nodeBinary = process.execPath) {
97
98
  try {
98
99
  await execFileAsync(nodeBinary, ['--experimental-sea-config', seaConfigPath]);
99
- console.log('āœ“ SEA blob generated successfully');
100
+ diag.verbose('SEA blob generated successfully', 2);
100
101
  } catch (error) {
101
102
  throw new Error(`Failed to generate SEA blob: ${error.message}`);
102
103
  }
package/lib/build.mjs CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { loadConfig } from './config.mjs';
7
7
  import { MultiTargetBuilder } from './multi-target-builder.mjs';
8
+ import * as diag from './diagnostics.mjs';
8
9
  import fs from 'fs';
9
10
 
10
11
  /**
@@ -49,17 +50,18 @@ export async function build(options = {}) {
49
50
  const results = await builder.buildAll();
50
51
 
51
52
  // Display results
52
- console.log('šŸ“¦ Output files:');
53
+ diag.separator();
54
+ diag.info('Output files:');
53
55
  for (const result of results) {
54
- const stats = fs.statSync(result.path);
55
- const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
56
- console.log(` ${result.target}: ${result.path} (${sizeMB} MB)`);
56
+ const size = diag.formatSize(fs.statSync(result.path).size);
57
+ diag.info(` ${result.target}: ${result.path} (${size})`);
57
58
  }
58
- console.log('');
59
+ diag.separator();
59
60
 
60
61
  return results;
61
62
  } catch (error) {
62
- console.error('\nāŒ Build failed:', error.message);
63
+ diag.separator();
64
+ diag.error(`Build failed: ${error.message}`);
63
65
  if (verbose || process.argv.includes('--verbose')) {
64
66
  console.error(error.stack);
65
67
  }
package/lib/config.mjs CHANGED
@@ -34,6 +34,7 @@ import path from 'path';
34
34
  * @property {boolean} [useSnapshot] - Enable V8 snapshot
35
35
  * @property {boolean} [useCodeCache] - Enable V8 code cache
36
36
  * @property {string} [cacheLocation] - Cache directory for extracted binaries
37
+ * @property {string} [sign] - Path to signing script (.mjs/.cjs) that exports a function(config) => Promise<void>
37
38
  * @property {boolean} [verbose] - Enable verbose logging
38
39
  */
39
40
 
@@ -89,11 +90,19 @@ export function loadConfig(configPath, projectRoot = process.cwd()) {
89
90
  * @returns {SeaboxConfig}
90
91
  */
91
92
  export function normalizeConfig(pkgConfig, pkg) {
93
+ // Helper to normalize assets to array
94
+ const normalizeAssets = (assets) => {
95
+ if (!assets) return [];
96
+ if (Array.isArray(assets)) return assets;
97
+ if (typeof assets === 'string') return [assets];
98
+ return [];
99
+ };
100
+
92
101
  // If already in outputs format, return as-is
93
102
  if (pkgConfig.outputs) {
94
103
  return {
95
104
  ...pkgConfig,
96
- assets: pkgConfig.assets || [],
105
+ assets: normalizeAssets(pkgConfig.assets),
97
106
  bundler: pkgConfig.bundler || { external: [] },
98
107
  _packageName: pkg.name,
99
108
  _packageVersion: pkg.version
@@ -112,7 +121,7 @@ export function normalizeConfig(pkgConfig, pkg) {
112
121
  return {
113
122
  entry: pkgConfig.entry,
114
123
  outputs: outputs,
115
- assets: pkgConfig.assets || [],
124
+ assets: normalizeAssets(pkgConfig.assets),
116
125
  bundler: {
117
126
  external: pkgConfig.external || []
118
127
  },