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.
- package/README.md +342 -146
- package/bin/seabox-rebuild.mjs +95 -0
- package/bin/seabox.mjs +135 -0
- package/lib/{blob.js → blob.mjs} +12 -18
- package/lib/bootstrap.cjs +753 -0
- package/lib/build-cache.mjs +199 -0
- package/lib/build.mjs +75 -0
- package/lib/config.mjs +234 -0
- package/lib/{crypto-assets.js → crypto-assets.mjs} +12 -47
- package/lib/entry-bundler.mjs +64 -0
- package/lib/{fetch-node.js → fetch-node.mjs} +10 -16
- package/lib/index.mjs +26 -0
- package/lib/{inject.js → inject.mjs} +24 -27
- package/lib/{manifest.js → manifest.mjs} +5 -11
- package/lib/multi-target-builder.mjs +620 -0
- package/lib/native-scanner.mjs +210 -0
- package/lib/{obfuscate.js → obfuscate.mjs} +9 -31
- package/lib/require-shim.mjs +111 -0
- package/lib/rolldown-bundler.mjs +415 -0
- package/lib/{unsign.js → unsign.cjs} +7 -32
- package/package.json +9 -5
- package/bin/seabox-rebuild.js +0 -395
- package/bin/seabox.js +0 -81
- package/lib/bindings.js +0 -31
- package/lib/bootstrap.js +0 -697
- package/lib/build.js +0 -283
- package/lib/bytenode-hack.js +0 -56
- package/lib/config.js +0 -119
- package/lib/index.js +0 -27
- package/lib/scanner.js +0 -153
|
@@ -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,75 @@
|
|
|
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 fs from 'fs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Main build function for v2
|
|
12
|
+
* @param {Object} options - Build options
|
|
13
|
+
* @param {string} [options.configPath] - Path to config file
|
|
14
|
+
* @param {boolean} [options.verbose] - Enable verbose logging
|
|
15
|
+
* @param {boolean} [options.debug] - Keep temporary build files
|
|
16
|
+
* @param {string} [options.projectRoot] - Project root directory
|
|
17
|
+
*/
|
|
18
|
+
export async function build(options = {}) {
|
|
19
|
+
// Support both options object and legacy process.argv parsing
|
|
20
|
+
let configPath = options.configPath;
|
|
21
|
+
let verbose = options.verbose;
|
|
22
|
+
let debug = options.debug;
|
|
23
|
+
let projectRoot = options.projectRoot || process.cwd();
|
|
24
|
+
|
|
25
|
+
// Fallback to process.argv if called without options
|
|
26
|
+
if (!options.configPath && !options.verbose && !options.debug) {
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
configPath = args.includes('--config') ? args[args.indexOf('--config') + 1] : null;
|
|
29
|
+
verbose = args.includes('--verbose');
|
|
30
|
+
debug = args.includes('--debug');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Load configuration
|
|
35
|
+
const config = loadConfig(configPath, projectRoot);
|
|
36
|
+
|
|
37
|
+
// Config should exist if we got here (CLI checks first)
|
|
38
|
+
if (!config) {
|
|
39
|
+
throw new Error('No configuration found. Run: npx seabox init');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Override verbose from CLI if specified
|
|
43
|
+
if (verbose) {
|
|
44
|
+
config.verbose = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Create and run multi-target builder
|
|
48
|
+
const builder = new MultiTargetBuilder(config, projectRoot);
|
|
49
|
+
const results = await builder.buildAll();
|
|
50
|
+
|
|
51
|
+
// Display results
|
|
52
|
+
console.log('📦 Output files:');
|
|
53
|
+
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)`);
|
|
57
|
+
}
|
|
58
|
+
console.log('');
|
|
59
|
+
|
|
60
|
+
return results;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('\n❌ Build failed:', error.message);
|
|
63
|
+
if (verbose || process.argv.includes('--verbose')) {
|
|
64
|
+
console.error(error.stack);
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Run if called directly
|
|
71
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
72
|
+
build().catch(error => {
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
|
75
|
+
}
|
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
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 {boolean} [verbose] - Enable verbose logging
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load SEA configuration from seabox.config.json or package.json
|
|
42
|
+
* @param {string} [configPath] - Optional path to config file
|
|
43
|
+
* @param {string} [projectRoot] - Project root directory
|
|
44
|
+
* @returns {SeaboxConfig|null} Config object or null if not found
|
|
45
|
+
*/
|
|
46
|
+
export function loadConfig(configPath, projectRoot = process.cwd()) {
|
|
47
|
+
let config;
|
|
48
|
+
|
|
49
|
+
// Priority: CLI arg > seabox.config.json > package.json "seabox" field
|
|
50
|
+
if (configPath) {
|
|
51
|
+
// If explicit config path provided, it must exist
|
|
52
|
+
const fullPath = path.resolve(projectRoot, configPath);
|
|
53
|
+
if (!fs.existsSync(fullPath)) {
|
|
54
|
+
throw new Error(`Config file not found: ${fullPath}`);
|
|
55
|
+
}
|
|
56
|
+
config = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
57
|
+
} else if (fs.existsSync(path.join(projectRoot, 'seabox.config.json'))) {
|
|
58
|
+
// Check for seabox.config.json
|
|
59
|
+
const configFile = path.join(projectRoot, 'seabox.config.json');
|
|
60
|
+
config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
61
|
+
} else {
|
|
62
|
+
// Check for "seabox" field in package.json
|
|
63
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
64
|
+
|
|
65
|
+
if (fs.existsSync(pkgPath)) {
|
|
66
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
67
|
+
|
|
68
|
+
if (pkg.seabox) {
|
|
69
|
+
config = normalizeConfig(pkg.seabox, pkg);
|
|
70
|
+
} else {
|
|
71
|
+
// No config found anywhere - return null
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// No package.json - return null
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
validateConfig(config);
|
|
81
|
+
|
|
82
|
+
return config;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Normalize config from package.json to standard format
|
|
87
|
+
* @param {Object} pkgConfig - Config from package.json "seabox" field
|
|
88
|
+
* @param {Object} pkg - package.json contents
|
|
89
|
+
* @returns {SeaboxConfig}
|
|
90
|
+
*/
|
|
91
|
+
export function normalizeConfig(pkgConfig, pkg) {
|
|
92
|
+
// If already in outputs format, return as-is
|
|
93
|
+
if (pkgConfig.outputs) {
|
|
94
|
+
return {
|
|
95
|
+
...pkgConfig,
|
|
96
|
+
assets: pkgConfig.assets || [],
|
|
97
|
+
bundler: pkgConfig.bundler || { external: [] },
|
|
98
|
+
_packageName: pkg.name,
|
|
99
|
+
_packageVersion: pkg.version
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Convert old targets format to outputs format
|
|
104
|
+
const outputs = (pkgConfig.targets || []).map(target => ({
|
|
105
|
+
path: pkgConfig.outputPath || 'dist',
|
|
106
|
+
target: target,
|
|
107
|
+
output: pkgConfig.output || 'app.exe',
|
|
108
|
+
libraries: pkgConfig.binaries || pkgConfig.libraries,
|
|
109
|
+
rcedit: pkgConfig.rcedit
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
entry: pkgConfig.entry,
|
|
114
|
+
outputs: outputs,
|
|
115
|
+
assets: pkgConfig.assets || [],
|
|
116
|
+
bundler: {
|
|
117
|
+
external: pkgConfig.external || []
|
|
118
|
+
},
|
|
119
|
+
encryptAssets: pkgConfig.encryptAssets || false,
|
|
120
|
+
encryptExclude: pkgConfig.encryptExclude || [],
|
|
121
|
+
useSnapshot: pkgConfig.useSnapshot || false,
|
|
122
|
+
useCodeCache: pkgConfig.useCodeCache || false,
|
|
123
|
+
cacheLocation: pkgConfig.cacheLocation,
|
|
124
|
+
verbose: pkgConfig.verbose || false,
|
|
125
|
+
_packageName: pkg.name,
|
|
126
|
+
_packageVersion: pkg.version
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate configuration
|
|
132
|
+
* @param {SeaboxConfig} config
|
|
133
|
+
*/
|
|
134
|
+
export function validateConfig(config) {
|
|
135
|
+
// Required fields
|
|
136
|
+
if (!config.entry) {
|
|
137
|
+
throw new Error('Missing required field: entry');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!config.outputs || !Array.isArray(config.outputs) || config.outputs.length === 0) {
|
|
141
|
+
throw new Error('Missing required field: outputs (must be non-empty array)');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Validate each output target
|
|
145
|
+
for (const output of config.outputs) {
|
|
146
|
+
if (!output.path) {
|
|
147
|
+
throw new Error('Output target missing required field: path');
|
|
148
|
+
}
|
|
149
|
+
if (!output.target) {
|
|
150
|
+
throw new Error('Output target missing required field: target');
|
|
151
|
+
}
|
|
152
|
+
if (!output.output) {
|
|
153
|
+
throw new Error('Output target missing required field: output');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Validate target format
|
|
157
|
+
const targetPattern = /^node\d+\.\d+\.\d+-\w+-\w+$/;
|
|
158
|
+
if (!targetPattern.test(output.target)) {
|
|
159
|
+
throw new Error(`Invalid target format: "${output.target}". Expected: nodeX.Y.Z-platform-arch`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse a target string into components
|
|
166
|
+
* @param {string} target - e.g., "node24.11.0-win32-x64"
|
|
167
|
+
* @returns {{nodeVersion: string, platform: string, arch: string}}
|
|
168
|
+
*/
|
|
169
|
+
export function parseTarget(target) {
|
|
170
|
+
const match = target.match(/^node(\d+\.\d+\.\d+)-(\w+)-(\w+)$/);
|
|
171
|
+
if (!match) {
|
|
172
|
+
throw new Error(`Cannot parse target: ${target}`);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
nodeVersion: match[1],
|
|
176
|
+
platform: match[2],
|
|
177
|
+
arch: match[3]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get default library patterns for a platform
|
|
183
|
+
* @param {string} platform - Platform name (win32, linux, darwin)
|
|
184
|
+
* @returns {string[]}
|
|
185
|
+
*/
|
|
186
|
+
export function getDefaultLibraryPatterns(platform) {
|
|
187
|
+
switch (platform) {
|
|
188
|
+
case 'win32':
|
|
189
|
+
return ['**/*.dll'];
|
|
190
|
+
case 'linux':
|
|
191
|
+
return ['**/*.so', '**/*.so.*'];
|
|
192
|
+
case 'darwin':
|
|
193
|
+
return ['**/*.dylib'];
|
|
194
|
+
default:
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Generate default configuration
|
|
201
|
+
* @param {Object} options - Options for config generation
|
|
202
|
+
* @returns {SeaboxConfig}
|
|
203
|
+
*/
|
|
204
|
+
export function generateDefaultConfig(options = {}) {
|
|
205
|
+
return {
|
|
206
|
+
entry: options.entry || './src/index.js',
|
|
207
|
+
outputs: [
|
|
208
|
+
{
|
|
209
|
+
path: './dist/win',
|
|
210
|
+
target: 'node24.11.0-win32-x64',
|
|
211
|
+
output: 'app.exe',
|
|
212
|
+
libraries: ['**/*.dll']
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
path: './dist/linux',
|
|
216
|
+
target: 'node24.11.0-linux-x64',
|
|
217
|
+
output: 'app',
|
|
218
|
+
libraries: ['**/*.so', '**/*.so.*']
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
path: './dist/macos',
|
|
222
|
+
target: 'node24.11.0-darwin-arm64',
|
|
223
|
+
output: 'app',
|
|
224
|
+
libraries: ['**/*.dylib']
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
assets: [],
|
|
228
|
+
bundler: {
|
|
229
|
+
external: []
|
|
230
|
+
},
|
|
231
|
+
encryptAssets: false,
|
|
232
|
+
useSnapshot: true
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* crypto-assets.
|
|
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
|
-
|
|
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 {
|
|
70
|
+
* @param {Array} assets - Assets to encrypt
|
|
72
71
|
* @param {Buffer} key - Encryption key
|
|
73
|
-
* @param {string[]} [excludePatterns] - Patterns to exclude from encryption
|
|
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
|
|
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 ||
|
|
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
|
-
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entry-bundler.mjs
|
|
3
|
+
* Bundles the entry file with bootstrap and require shim
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Module from 'module';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const require = Module.createRequire(import.meta.url);
|
|
11
|
+
|
|
12
|
+
const { generateRequireShim, replaceRequireCalls } = require('./require-shim.mjs');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Bundle the entry file for SEA
|
|
16
|
+
* @param {string} entryContent - The entry file content
|
|
17
|
+
* @param {string} augmentedBootstrap - The bootstrap code
|
|
18
|
+
* @param {boolean} useSnapshot - Whether to use snapshot mode
|
|
19
|
+
* @param {boolean} verbose - Enable verbose logging
|
|
20
|
+
* @returns {string} - The bundled entry content
|
|
21
|
+
*/
|
|
22
|
+
export function bundleEntry(entryContent, augmentedBootstrap, useSnapshot, verbose) {
|
|
23
|
+
// Generate the require shim and replace require() calls
|
|
24
|
+
const requireSeaboxShim = generateRequireShim();
|
|
25
|
+
const transformed = replaceRequireCalls(entryContent, verbose);
|
|
26
|
+
const transformedContent = transformed.code;
|
|
27
|
+
|
|
28
|
+
let bundledEntry;
|
|
29
|
+
|
|
30
|
+
if (useSnapshot) {
|
|
31
|
+
// Snapshot mode
|
|
32
|
+
const parts = [
|
|
33
|
+
augmentedBootstrap,
|
|
34
|
+
'\n\n',
|
|
35
|
+
requireSeaboxShim,
|
|
36
|
+
'\n\n',
|
|
37
|
+
'// Application entry - will be wrapped by bootstrap\'s setDeserializeMainFunction interceptor\n',
|
|
38
|
+
'(function() {\n',
|
|
39
|
+
' const v8 = __originalRequire(\'v8\');\n',
|
|
40
|
+
' if (v8.startupSnapshot && v8.startupSnapshot.isBuildingSnapshot()) {\n',
|
|
41
|
+
' v8.startupSnapshot.setDeserializeMainFunction(() => {\n',
|
|
42
|
+
' if (typeof exports === \'undefined\') {\n',
|
|
43
|
+
' var exports = {};\n',
|
|
44
|
+
' var module = { exports: exports };\n',
|
|
45
|
+
' }\n',
|
|
46
|
+
' // Run the application code\n',
|
|
47
|
+
transformedContent,
|
|
48
|
+
'\n',
|
|
49
|
+
' });\n',
|
|
50
|
+
' } else {\n',
|
|
51
|
+
' // Not building snapshot, run normally\n',
|
|
52
|
+
transformedContent,
|
|
53
|
+
'\n',
|
|
54
|
+
' }\n',
|
|
55
|
+
'})();\n'
|
|
56
|
+
];
|
|
57
|
+
bundledEntry = parts.join('');
|
|
58
|
+
} else {
|
|
59
|
+
// Non-snapshot mode
|
|
60
|
+
bundledEntry = augmentedBootstrap + '\n\n' + requireSeaboxShim + '\n' + transformedContent;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return bundledEntry;
|
|
64
|
+
}
|