seabox 0.1.0-beta.3 → 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.
@@ -0,0 +1,199 @@
1
+ /**
2
+ * build-cache.mjs
3
+ * Build caching system to speed up incremental builds.
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import crypto from 'crypto';
9
+
10
+ /**
11
+ * @typedef {Object} CacheEntry
12
+ * @property {string} hash - Content hash of input
13
+ * @property {number} timestamp - When cached
14
+ * @property {any} data - Cached data
15
+ */
16
+
17
+ export class BuildCache {
18
+ /**
19
+ * @param {string} cacheDir - Cache directory path
20
+ */
21
+ constructor(cacheDir = '.seabox-cache') {
22
+ this.cacheDir = cacheDir;
23
+ this.ensureCacheDir();
24
+ }
25
+
26
+ /**
27
+ * Ensure cache directory exists
28
+ */
29
+ ensureCacheDir() {
30
+ if (!fs.existsSync(this.cacheDir)) {
31
+ fs.mkdirSync(this.cacheDir, { recursive: true });
32
+ }
33
+
34
+ // Create subdirectories
35
+ const subdirs = ['bundles', 'natives', 'blobs'];
36
+ for (const subdir of subdirs) {
37
+ const dirPath = path.join(this.cacheDir, subdir);
38
+ if (!fs.existsSync(dirPath)) {
39
+ fs.mkdirSync(dirPath, { recursive: true });
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Compute cache key from entry path and config
46
+ * @param {string} entryPath - Entry file path
47
+ * @param {Object} config - Build config
48
+ * @returns {string}
49
+ */
50
+ computeCacheKey(entryPath, config) {
51
+ const configStr = JSON.stringify({
52
+ bundler: config.bundler,
53
+ entry: entryPath
54
+ });
55
+
56
+ return crypto
57
+ .createHash('sha256')
58
+ .update(configStr)
59
+ .digest('hex')
60
+ .substring(0, 16);
61
+ }
62
+
63
+ /**
64
+ * Get cached bundle if valid
65
+ * @param {string} entryPath - Entry file path
66
+ * @param {Object} config - Build config
67
+ * @returns {Object|null}
68
+ */
69
+ async getCachedBundle(entryPath, config) {
70
+ const cacheKey = this.computeCacheKey(entryPath, config);
71
+ const cachePath = path.join(this.cacheDir, 'bundles', `${cacheKey}.json`);
72
+
73
+ if (!fs.existsSync(cachePath)) {
74
+ return null;
75
+ }
76
+
77
+ try {
78
+ const cached = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
79
+
80
+ // Validate that source files haven't changed
81
+ if (await this.isValid(cached, entryPath)) {
82
+ return cached;
83
+ }
84
+ } catch (err) {
85
+ // Invalid cache entry
86
+ }
87
+
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Cache a bundle result
93
+ * @param {string} entryPath - Entry file path
94
+ * @param {Object} config - Build config
95
+ * @param {Object} bundleResult - Bundle result to cache
96
+ */
97
+ async cacheBundle(entryPath, config, bundleResult) {
98
+ const cacheKey = this.computeCacheKey(entryPath, config);
99
+ const cachePath = path.join(this.cacheDir, 'bundles', `${cacheKey}.json`);
100
+
101
+ const cacheEntry = {
102
+ hash: await this.hashFile(entryPath),
103
+ timestamp: Date.now(),
104
+ data: bundleResult
105
+ };
106
+
107
+ fs.writeFileSync(cachePath, JSON.stringify(cacheEntry, null, 2));
108
+ }
109
+
110
+ /**
111
+ * Get cached native module build
112
+ * @param {string} moduleRoot - Module root path
113
+ * @param {string} target - Build target
114
+ * @returns {string|null} - Path to cached .node file
115
+ */
116
+ getCachedNativeBuild(moduleRoot, target) {
117
+ const cacheKey = crypto
118
+ .createHash('sha256')
119
+ .update(moduleRoot + target)
120
+ .digest('hex')
121
+ .substring(0, 16);
122
+
123
+ const cachePath = path.join(this.cacheDir, 'natives', `${cacheKey}.node`);
124
+
125
+ if (fs.existsSync(cachePath)) {
126
+ // Check if source has changed
127
+ const bindingGypPath = path.join(moduleRoot, 'binding.gyp');
128
+ if (fs.existsSync(bindingGypPath)) {
129
+ const cacheStats = fs.statSync(cachePath);
130
+ const sourceStats = fs.statSync(bindingGypPath);
131
+
132
+ if (cacheStats.mtime > sourceStats.mtime) {
133
+ return cachePath;
134
+ }
135
+ }
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * Cache a native module build
143
+ * @param {string} moduleRoot - Module root path
144
+ * @param {string} target - Build target
145
+ * @param {string} builtBinaryPath - Path to built .node file
146
+ */
147
+ cacheNativeBuild(moduleRoot, target, builtBinaryPath) {
148
+ const cacheKey = crypto
149
+ .createHash('sha256')
150
+ .update(moduleRoot + target)
151
+ .digest('hex')
152
+ .substring(0, 16);
153
+
154
+ const cachePath = path.join(this.cacheDir, 'natives', `${cacheKey}.node`);
155
+
156
+ fs.copyFileSync(builtBinaryPath, cachePath);
157
+ }
158
+
159
+ /**
160
+ * Check if cache entry is valid
161
+ * @param {CacheEntry} cached - Cached entry
162
+ * @param {string} filePath - Source file path
163
+ * @returns {Promise<boolean>}
164
+ */
165
+ async isValid(cached, filePath) {
166
+ if (!fs.existsSync(filePath)) {
167
+ return false;
168
+ }
169
+
170
+ const currentHash = await this.hashFile(filePath);
171
+ return currentHash === cached.hash;
172
+ }
173
+
174
+ /**
175
+ * Compute hash of a file
176
+ * @param {string} filePath - File path
177
+ * @returns {Promise<string>}
178
+ */
179
+ hashFile(filePath) {
180
+ return new Promise((resolve, reject) => {
181
+ const hash = crypto.createHash('sha256');
182
+ const stream = fs.createReadStream(filePath);
183
+
184
+ stream.on('data', chunk => hash.update(chunk));
185
+ stream.on('end', () => resolve(hash.digest('hex')));
186
+ stream.on('error', reject);
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Clear cache
192
+ */
193
+ clear() {
194
+ if (fs.existsSync(this.cacheDir)) {
195
+ fs.rmSync(this.cacheDir, { recursive: true, force: true });
196
+ }
197
+ this.ensureCacheDir();
198
+ }
199
+ }
package/lib/build.mjs ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * build.mjs (v2)
3
+ * Main build orchestrator using v2 architecture with multi-target support.
4
+ */
5
+
6
+ import { loadConfig } from './config.mjs';
7
+ import { MultiTargetBuilder } from './multi-target-builder.mjs';
8
+ import * as diag from './diagnostics.mjs';
9
+ import fs from 'fs';
10
+
11
+ /**
12
+ * Main build function for v2
13
+ * @param {Object} options - Build options
14
+ * @param {string} [options.configPath] - Path to config file
15
+ * @param {boolean} [options.verbose] - Enable verbose logging
16
+ * @param {boolean} [options.debug] - Keep temporary build files
17
+ * @param {string} [options.projectRoot] - Project root directory
18
+ */
19
+ export async function build(options = {}) {
20
+ // Support both options object and legacy process.argv parsing
21
+ let configPath = options.configPath;
22
+ let verbose = options.verbose;
23
+ let debug = options.debug;
24
+ let projectRoot = options.projectRoot || process.cwd();
25
+
26
+ // Fallback to process.argv if called without options
27
+ if (!options.configPath && !options.verbose && !options.debug) {
28
+ const args = process.argv.slice(2);
29
+ configPath = args.includes('--config') ? args[args.indexOf('--config') + 1] : null;
30
+ verbose = args.includes('--verbose');
31
+ debug = args.includes('--debug');
32
+ }
33
+
34
+ try {
35
+ // Load configuration
36
+ const config = loadConfig(configPath, projectRoot);
37
+
38
+ // Config should exist if we got here (CLI checks first)
39
+ if (!config) {
40
+ throw new Error('No configuration found. Run: npx seabox init');
41
+ }
42
+
43
+ // Override verbose from CLI if specified
44
+ if (verbose) {
45
+ config.verbose = true;
46
+ }
47
+
48
+ // Create and run multi-target builder
49
+ const builder = new MultiTargetBuilder(config, projectRoot);
50
+ const results = await builder.buildAll();
51
+
52
+ // Display results
53
+ diag.separator();
54
+ diag.info('Output files:');
55
+ for (const result of results) {
56
+ const size = diag.formatSize(fs.statSync(result.path).size);
57
+ diag.info(` ${result.target}: ${result.path} (${size})`);
58
+ }
59
+ diag.separator();
60
+
61
+ return results;
62
+ } catch (error) {
63
+ diag.separator();
64
+ diag.error(`Build failed: ${error.message}`);
65
+ if (verbose || process.argv.includes('--verbose')) {
66
+ console.error(error.stack);
67
+ }
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ // Run if called directly
73
+ if (import.meta.url === `file://${process.argv[1]}`) {
74
+ build().catch(error => {
75
+ process.exit(1);
76
+ });
77
+ }
package/lib/config.mjs ADDED
@@ -0,0 +1,243 @@
1
+ /**
2
+ * config.mjs
3
+ * Load and validate SEA configuration from seabox.config.json or package.json.
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+
9
+ /**
10
+ * @typedef {Object} OutputTarget
11
+ * @property {string} path - Output directory
12
+ * @property {string} target - Node version and platform (e.g., node24.11.0-win32-x64)
13
+ * @property {string} output - Executable filename
14
+ * @property {string[]} [libraries] - Glob patterns for shared libraries (DLLs/SOs) requiring filesystem extraction
15
+ * @property {Object} [rcedit] - Windows executable customization options
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} BundlerConfig
20
+ * @property {string[]} [external] - Modules to externalize
21
+ * @property {Array} [plugins] - Rollup plugins
22
+ * @property {boolean} [minify] - Minify output
23
+ * @property {boolean} [sourcemap] - Generate sourcemaps
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} SeaboxConfig
28
+ * @property {string} entry - Entry point source file
29
+ * @property {OutputTarget[]} outputs - Multi-target output configurations
30
+ * @property {string[]} [assets] - Glob patterns for assets to embed (auto-detected assets are merged)
31
+ * @property {BundlerConfig} [bundler] - Bundler configuration
32
+ * @property {boolean} [encryptAssets] - Enable asset encryption
33
+ * @property {string[]} [encryptExclude] - Assets to exclude from encryption
34
+ * @property {boolean} [useSnapshot] - Enable V8 snapshot
35
+ * @property {boolean} [useCodeCache] - Enable V8 code cache
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>
38
+ * @property {boolean} [verbose] - Enable verbose logging
39
+ */
40
+
41
+ /**
42
+ * Load SEA configuration from seabox.config.json or package.json
43
+ * @param {string} [configPath] - Optional path to config file
44
+ * @param {string} [projectRoot] - Project root directory
45
+ * @returns {SeaboxConfig|null} Config object or null if not found
46
+ */
47
+ export function loadConfig(configPath, projectRoot = process.cwd()) {
48
+ let config;
49
+
50
+ // Priority: CLI arg > seabox.config.json > package.json "seabox" field
51
+ if (configPath) {
52
+ // If explicit config path provided, it must exist
53
+ const fullPath = path.resolve(projectRoot, configPath);
54
+ if (!fs.existsSync(fullPath)) {
55
+ throw new Error(`Config file not found: ${fullPath}`);
56
+ }
57
+ config = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
58
+ } else if (fs.existsSync(path.join(projectRoot, 'seabox.config.json'))) {
59
+ // Check for seabox.config.json
60
+ const configFile = path.join(projectRoot, 'seabox.config.json');
61
+ config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
62
+ } else {
63
+ // Check for "seabox" field in package.json
64
+ const pkgPath = path.join(projectRoot, 'package.json');
65
+
66
+ if (fs.existsSync(pkgPath)) {
67
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
68
+
69
+ if (pkg.seabox) {
70
+ config = normalizeConfig(pkg.seabox, pkg);
71
+ } else {
72
+ // No config found anywhere - return null
73
+ return null;
74
+ }
75
+ } else {
76
+ // No package.json - return null
77
+ return null;
78
+ }
79
+ }
80
+
81
+ validateConfig(config);
82
+
83
+ return config;
84
+ }
85
+
86
+ /**
87
+ * Normalize config from package.json to standard format
88
+ * @param {Object} pkgConfig - Config from package.json "seabox" field
89
+ * @param {Object} pkg - package.json contents
90
+ * @returns {SeaboxConfig}
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
+
101
+ // If already in outputs format, return as-is
102
+ if (pkgConfig.outputs) {
103
+ return {
104
+ ...pkgConfig,
105
+ assets: normalizeAssets(pkgConfig.assets),
106
+ bundler: pkgConfig.bundler || { external: [] },
107
+ _packageName: pkg.name,
108
+ _packageVersion: pkg.version
109
+ };
110
+ }
111
+
112
+ // Convert old targets format to outputs format
113
+ const outputs = (pkgConfig.targets || []).map(target => ({
114
+ path: pkgConfig.outputPath || 'dist',
115
+ target: target,
116
+ output: pkgConfig.output || 'app.exe',
117
+ libraries: pkgConfig.binaries || pkgConfig.libraries,
118
+ rcedit: pkgConfig.rcedit
119
+ }));
120
+
121
+ return {
122
+ entry: pkgConfig.entry,
123
+ outputs: outputs,
124
+ assets: normalizeAssets(pkgConfig.assets),
125
+ bundler: {
126
+ external: pkgConfig.external || []
127
+ },
128
+ encryptAssets: pkgConfig.encryptAssets || false,
129
+ encryptExclude: pkgConfig.encryptExclude || [],
130
+ useSnapshot: pkgConfig.useSnapshot || false,
131
+ useCodeCache: pkgConfig.useCodeCache || false,
132
+ cacheLocation: pkgConfig.cacheLocation,
133
+ verbose: pkgConfig.verbose || false,
134
+ _packageName: pkg.name,
135
+ _packageVersion: pkg.version
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Validate configuration
141
+ * @param {SeaboxConfig} config
142
+ */
143
+ export function validateConfig(config) {
144
+ // Required fields
145
+ if (!config.entry) {
146
+ throw new Error('Missing required field: entry');
147
+ }
148
+
149
+ if (!config.outputs || !Array.isArray(config.outputs) || config.outputs.length === 0) {
150
+ throw new Error('Missing required field: outputs (must be non-empty array)');
151
+ }
152
+
153
+ // Validate each output target
154
+ for (const output of config.outputs) {
155
+ if (!output.path) {
156
+ throw new Error('Output target missing required field: path');
157
+ }
158
+ if (!output.target) {
159
+ throw new Error('Output target missing required field: target');
160
+ }
161
+ if (!output.output) {
162
+ throw new Error('Output target missing required field: output');
163
+ }
164
+
165
+ // Validate target format
166
+ const targetPattern = /^node\d+\.\d+\.\d+-\w+-\w+$/;
167
+ if (!targetPattern.test(output.target)) {
168
+ throw new Error(`Invalid target format: "${output.target}". Expected: nodeX.Y.Z-platform-arch`);
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Parse a target string into components
175
+ * @param {string} target - e.g., "node24.11.0-win32-x64"
176
+ * @returns {{nodeVersion: string, platform: string, arch: string}}
177
+ */
178
+ export function parseTarget(target) {
179
+ const match = target.match(/^node(\d+\.\d+\.\d+)-(\w+)-(\w+)$/);
180
+ if (!match) {
181
+ throw new Error(`Cannot parse target: ${target}`);
182
+ }
183
+ return {
184
+ nodeVersion: match[1],
185
+ platform: match[2],
186
+ arch: match[3]
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Get default library patterns for a platform
192
+ * @param {string} platform - Platform name (win32, linux, darwin)
193
+ * @returns {string[]}
194
+ */
195
+ export function getDefaultLibraryPatterns(platform) {
196
+ switch (platform) {
197
+ case 'win32':
198
+ return ['**/*.dll'];
199
+ case 'linux':
200
+ return ['**/*.so', '**/*.so.*'];
201
+ case 'darwin':
202
+ return ['**/*.dylib'];
203
+ default:
204
+ return [];
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Generate default configuration
210
+ * @param {Object} options - Options for config generation
211
+ * @returns {SeaboxConfig}
212
+ */
213
+ export function generateDefaultConfig(options = {}) {
214
+ return {
215
+ entry: options.entry || './src/index.js',
216
+ outputs: [
217
+ {
218
+ path: './dist/win',
219
+ target: 'node24.11.0-win32-x64',
220
+ output: 'app.exe',
221
+ libraries: ['**/*.dll']
222
+ },
223
+ {
224
+ path: './dist/linux',
225
+ target: 'node24.11.0-linux-x64',
226
+ output: 'app',
227
+ libraries: ['**/*.so', '**/*.so.*']
228
+ },
229
+ {
230
+ path: './dist/macos',
231
+ target: 'node24.11.0-darwin-arm64',
232
+ output: 'app',
233
+ libraries: ['**/*.dylib']
234
+ }
235
+ ],
236
+ assets: [],
237
+ bundler: {
238
+ external: []
239
+ },
240
+ encryptAssets: false,
241
+ useSnapshot: true
242
+ };
243
+ }
@@ -1,17 +1,16 @@
1
1
  /**
2
- * crypto-assets.js
2
+ * crypto-assets.mjs
3
3
  * Asset encryption/decryption utilities for SEA applications.
4
- * The encryption key is embedded in the bootstrap code and, when using snapshots,
5
- * becomes part of the V8 bytecode, providing obfuscation.
6
4
  */
7
5
 
8
- const crypto = require('crypto');
6
+ import crypto from 'crypto';
7
+ import fs from 'fs';
9
8
 
10
9
  /**
11
10
  * Generate a random encryption key.
12
11
  * @returns {Buffer} - 32-byte key for AES-256
13
12
  */
14
- function generateEncryptionKey() {
13
+ export function generateEncryptionKey() {
15
14
  return crypto.randomBytes(32);
16
15
  }
17
16
 
@@ -21,7 +20,7 @@ function generateEncryptionKey() {
21
20
  * @param {Buffer} key - 32-byte encryption key
22
21
  * @returns {Buffer} - Encrypted data with IV and auth tag prepended
23
22
  */
24
- function encryptAsset(data, key) {
23
+ export function encryptAsset(data, key) {
25
24
  // Generate a random IV for this encryption
26
25
  const iv = crypto.randomBytes(16);
27
26
 
@@ -47,7 +46,7 @@ function encryptAsset(data, key) {
47
46
  * @param {Buffer} key - 32-byte encryption key
48
47
  * @returns {Buffer} - Decrypted data
49
48
  */
50
- function decryptAsset(encryptedData, key) {
49
+ export function decryptAsset(encryptedData, key) {
51
50
  // Extract IV, auth tag, and encrypted content
52
51
  const iv = encryptedData.slice(0, 16);
53
52
  const authTag = encryptedData.slice(16, 32);
@@ -68,16 +67,16 @@ function decryptAsset(encryptedData, key) {
68
67
 
69
68
  /**
70
69
  * Encrypt multiple assets.
71
- * @param {import('./scanner').AssetEntry[]} assets - Assets to encrypt
70
+ * @param {Array} assets - Assets to encrypt
72
71
  * @param {Buffer} key - Encryption key
73
- * @param {string[]} [excludePatterns] - Patterns to exclude from encryption (e.g., ['*.txt'])
72
+ * @param {string[]} [excludePatterns] - Patterns to exclude from encryption
74
73
  * @returns {Map<string, Buffer>} - Map of asset key to encrypted content
75
74
  */
76
- function encryptAssets(assets, key, excludePatterns = []) {
75
+ export function encryptAssets(assets, key, excludePatterns = []) {
77
76
  const encrypted = new Map();
78
77
 
79
78
  for (const asset of assets) {
80
- // Skip binaries - they need to be extracted as-is
79
+ // Skip binaries
81
80
  if (asset.isBinary) {
82
81
  continue;
83
82
  }
@@ -96,7 +95,7 @@ function encryptAssets(assets, key, excludePatterns = []) {
96
95
  }
97
96
 
98
97
  // Get asset content
99
- const content = asset.content || require('fs').readFileSync(asset.sourcePath);
98
+ const content = asset.content || fs.readFileSync(asset.sourcePath);
100
99
 
101
100
  // Encrypt it
102
101
  const encryptedContent = encryptAsset(content, key);
@@ -106,37 +105,12 @@ function encryptAssets(assets, key, excludePatterns = []) {
106
105
  return encrypted;
107
106
  }
108
107
 
109
- /**
110
- * Generate the encryption key as a code string for embedding in bootstrap.
111
- * Returns a string that evaluates to a Buffer containing the key.
112
- * @param {Buffer} key - Encryption key
113
- * @returns {string} - JavaScript code that creates the key Buffer
114
- */
115
- function keyToCode(key) {
116
- // Convert key to hex string for embedding
117
- const hexKey = key.toString('hex');
118
-
119
- // Return code that reconstructs the Buffer
120
- // Using multiple obfuscation techniques:
121
- // 1. Split the hex string
122
- // 2. Use Array.from with character codes
123
- // 3. XOR with a simple constant (adds one more layer)
124
-
125
- const parts = [];
126
- for (let i = 0; i < hexKey.length; i += 8) {
127
- parts.push(hexKey.slice(i, i + 8));
128
- }
129
-
130
- return `Buffer.from('${hexKey}', 'hex')`;
131
- }
132
-
133
108
  /**
134
109
  * Generate a more obfuscated version of the key embedding code.
135
- * This version splits the key and uses character code manipulation.
136
110
  * @param {Buffer} key - Encryption key
137
111
  * @returns {string} - Obfuscated JavaScript code
138
112
  */
139
- function keyToObfuscatedCode(key) {
113
+ export function keyToObfuscatedCode(key) {
140
114
  // Convert to array of byte values
141
115
  const bytes = Array.from(key);
142
116
 
@@ -149,12 +123,3 @@ function keyToObfuscatedCode(key) {
149
123
  // Return code that reconstructs the key
150
124
  return `Buffer.from([${masked.join(',')}].map(b => b ^ 0x${xorMask.toString(16)}))`;
151
125
  }
152
-
153
- module.exports = {
154
- generateEncryptionKey,
155
- encryptAsset,
156
- decryptAsset,
157
- encryptAssets,
158
- keyToCode,
159
- keyToObfuscatedCode
160
- };