seabox 0.1.0-beta.2 → 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 +353 -110
- 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.mjs +111 -0
- 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 +10 -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 -118
- package/lib/index.js +0 -27
- package/lib/inject.js +0 -81
- package/lib/scanner.js +0 -153
package/lib/bootstrap.js
DELETED
|
@@ -1,697 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bootstrap.js
|
|
3
|
-
* Runtime template for SEA applications.
|
|
4
|
-
* Handles binary extraction and module resolution override.
|
|
5
|
-
*
|
|
6
|
-
* This file is prepended to the application entry bundle.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
(function() {
|
|
10
|
-
'use strict';
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const crypto = require('crypto');
|
|
15
|
-
const Module = require('module');
|
|
16
|
-
|
|
17
|
-
// PHASE 1: Override fs methods IMMEDIATELY (before app code captures them)
|
|
18
|
-
// We'll populate the redirect maps in Phase 2 after extraction
|
|
19
|
-
const assetPathMapGlobal = {};
|
|
20
|
-
let cacheDirGlobal = '';
|
|
21
|
-
|
|
22
|
-
const originalExistsSync = fs.existsSync;
|
|
23
|
-
const originalReadFileSync = fs.readFileSync;
|
|
24
|
-
const originalStatSync = fs.statSync;
|
|
25
|
-
const originalLstatSync = fs.lstatSync;
|
|
26
|
-
const originalRealpathSync = fs.realpathSync;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Check if a requested path should be redirected to cache.
|
|
30
|
-
* @param {string} requestedPath
|
|
31
|
-
* @returns {string|null} - Redirected path or null
|
|
32
|
-
*/
|
|
33
|
-
function getRedirectedPath(requestedPath) {
|
|
34
|
-
if (!requestedPath) return null;
|
|
35
|
-
|
|
36
|
-
// Normalize the requested path
|
|
37
|
-
const normalized = path.normalize(requestedPath);
|
|
38
|
-
|
|
39
|
-
// Check direct match against asset keys
|
|
40
|
-
for (const [assetKey, extractedPath] of Object.entries(assetPathMapGlobal)) {
|
|
41
|
-
// Try exact match
|
|
42
|
-
if (normalized.endsWith(assetKey)) {
|
|
43
|
-
return extractedPath;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Try basename match
|
|
47
|
-
const assetBasename = path.basename(assetKey);
|
|
48
|
-
if (path.basename(normalized) === assetBasename) {
|
|
49
|
-
return extractedPath;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Try matching the path components
|
|
53
|
-
if (normalized.includes(assetKey.replace(/\//g, path.sep))) {
|
|
54
|
-
return extractedPath;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Override existsSync immediately
|
|
62
|
-
fs.existsSync = function(filePath) {
|
|
63
|
-
if (!filePath) return originalExistsSync(filePath);
|
|
64
|
-
|
|
65
|
-
// If this path should be redirected, check if the REDIRECTED path exists
|
|
66
|
-
const redirected = getRedirectedPath(filePath);
|
|
67
|
-
if (redirected) {
|
|
68
|
-
return originalExistsSync(redirected);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// For .node files, ONLY allow cache paths for our managed binaries
|
|
72
|
-
if (filePath.endsWith('.node') && cacheDirGlobal) {
|
|
73
|
-
const basename = path.basename(filePath);
|
|
74
|
-
|
|
75
|
-
// Check if this is one of our managed binaries
|
|
76
|
-
for (const assetKey of Object.keys(assetPathMapGlobal)) {
|
|
77
|
-
if (path.basename(assetKey) === basename) {
|
|
78
|
-
// This is one of our binaries
|
|
79
|
-
// Only return true if the path is in the cache directory
|
|
80
|
-
if (filePath.startsWith(cacheDirGlobal)) {
|
|
81
|
-
return originalExistsSync(filePath);
|
|
82
|
-
} else {
|
|
83
|
-
// Block all non-cache paths for our binaries
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return originalExistsSync(filePath);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// Override readFileSync immediately
|
|
94
|
-
fs.readFileSync = function(filePath, options) {
|
|
95
|
-
const redirected = getRedirectedPath(filePath);
|
|
96
|
-
if (redirected) {
|
|
97
|
-
return originalReadFileSync(redirected, options);
|
|
98
|
-
}
|
|
99
|
-
return originalReadFileSync(filePath, options);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// Override statSync immediately
|
|
103
|
-
fs.statSync = function(filePath, options) {
|
|
104
|
-
const redirected = getRedirectedPath(filePath);
|
|
105
|
-
if (redirected) {
|
|
106
|
-
return originalStatSync(redirected, options);
|
|
107
|
-
}
|
|
108
|
-
return originalStatSync(filePath, options);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
// Override lstatSync immediately
|
|
112
|
-
fs.lstatSync = function(filePath, options) {
|
|
113
|
-
const redirected = getRedirectedPath(filePath);
|
|
114
|
-
if (redirected) {
|
|
115
|
-
return originalLstatSync(redirected, options);
|
|
116
|
-
}
|
|
117
|
-
return originalLstatSync(filePath, options);
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// Override realpathSync immediately
|
|
121
|
-
fs.realpathSync = function(filePath, options) {
|
|
122
|
-
const redirected = getRedirectedPath(filePath);
|
|
123
|
-
if (redirected) {
|
|
124
|
-
return redirected; // Return the redirected path as the "real" path
|
|
125
|
-
}
|
|
126
|
-
return originalRealpathSync(filePath, options);
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// SEA API - check if we're in an SEA by trying to load the sea module
|
|
130
|
-
let sea = null;
|
|
131
|
-
let isSEA = false;
|
|
132
|
-
try {
|
|
133
|
-
sea = require('node:sea');
|
|
134
|
-
isSEA = true;
|
|
135
|
-
console.log('[SEA Bootstrap] Running in SEA context');
|
|
136
|
-
} catch (err) {
|
|
137
|
-
// Not in SEA context - this is fine for development
|
|
138
|
-
console.log('[SEA Bootstrap] Not in SEA context (normal mode)');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Decrypt an encrypted asset.
|
|
143
|
-
* @param {Buffer} encryptedData - Encrypted data with IV and auth tag prepended
|
|
144
|
-
* @param {Buffer} key - 32-byte encryption key
|
|
145
|
-
* @returns {Buffer} - Decrypted data
|
|
146
|
-
*/
|
|
147
|
-
function decryptAsset(encryptedData, key) {
|
|
148
|
-
// Extract IV, auth tag, and encrypted content
|
|
149
|
-
const iv = encryptedData.slice(0, 16);
|
|
150
|
-
const authTag = encryptedData.slice(16, 32);
|
|
151
|
-
const encrypted = encryptedData.slice(32);
|
|
152
|
-
|
|
153
|
-
// Create decipher
|
|
154
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
155
|
-
decipher.setAuthTag(authTag);
|
|
156
|
-
|
|
157
|
-
// Decrypt the data
|
|
158
|
-
const decrypted = Buffer.concat([
|
|
159
|
-
decipher.update(encrypted),
|
|
160
|
-
decipher.final()
|
|
161
|
-
]);
|
|
162
|
-
|
|
163
|
-
return decrypted;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Get an asset from the SEA, decrypting if necessary.
|
|
168
|
-
* @param {string} assetKey - Asset key
|
|
169
|
-
* @param {string} [encoding] - Optional encoding (e.g., 'utf8')
|
|
170
|
-
* @returns {Buffer|string} - Asset content
|
|
171
|
-
*/
|
|
172
|
-
function getAsset(assetKey, encoding) {
|
|
173
|
-
if (!sea) {
|
|
174
|
-
throw new Error('Cannot get asset: not in SEA context');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Check if this asset is encrypted
|
|
178
|
-
const isEncrypted = typeof SEA_ENCRYPTED_ASSETS !== 'undefined' && SEA_ENCRYPTED_ASSETS.has(assetKey);
|
|
179
|
-
|
|
180
|
-
let content;
|
|
181
|
-
if (isEncrypted && typeof SEA_ENCRYPTION_KEY !== 'undefined') {
|
|
182
|
-
// Get raw encrypted data
|
|
183
|
-
const encryptedData = sea.getRawAsset(assetKey);
|
|
184
|
-
const buffer = Buffer.isBuffer(encryptedData) ? encryptedData : Buffer.from(encryptedData);
|
|
185
|
-
|
|
186
|
-
// Decrypt it
|
|
187
|
-
content = decryptAsset(buffer, SEA_ENCRYPTION_KEY);
|
|
188
|
-
} else {
|
|
189
|
-
// Get unencrypted asset
|
|
190
|
-
content = sea.getAsset(assetKey, encoding);
|
|
191
|
-
if (!encoding && !Buffer.isBuffer(content)) {
|
|
192
|
-
content = Buffer.from(content);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Apply encoding if requested
|
|
197
|
-
if (encoding && Buffer.isBuffer(content)) {
|
|
198
|
-
return content.toString(encoding);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return content;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Resolve environment variables in a path string.
|
|
207
|
-
* Supports %VAR% on Windows and $VAR or ${VAR} on Unix-like systems.
|
|
208
|
-
* @param {string} pathStr - Path string that may contain environment variables
|
|
209
|
-
* @returns {string} - Path with environment variables expanded
|
|
210
|
-
*/
|
|
211
|
-
function resolveEnvVars(pathStr) {
|
|
212
|
-
if (!pathStr) return pathStr;
|
|
213
|
-
|
|
214
|
-
// Replace Windows-style %VAR%
|
|
215
|
-
let resolved = pathStr.replace(/%([^%]+)%/g, (match, varName) => {
|
|
216
|
-
return process.env[varName] || match;
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Replace Unix-style $VAR and ${VAR}
|
|
220
|
-
resolved = resolved.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
|
221
|
-
return process.env[varName] || match;
|
|
222
|
-
});
|
|
223
|
-
resolved = resolved.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, varName) => {
|
|
224
|
-
return process.env[varName] || match;
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
return resolved;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Get the extraction cache directory for this application.
|
|
232
|
-
* @param {string} appName
|
|
233
|
-
* @param {string} appVersion
|
|
234
|
-
* @param {string} platform
|
|
235
|
-
* @param {string} arch
|
|
236
|
-
* @param {string} [configuredCacheLocation] - Optional configured cache location from manifest
|
|
237
|
-
* @returns {string}
|
|
238
|
-
*/
|
|
239
|
-
function getExtractionCacheDir(appName, appVersion, platform, arch, configuredCacheLocation) {
|
|
240
|
-
// Priority: SEACACHE env var > configured location > default
|
|
241
|
-
if(process.env.SEACACHE) return process.env.SEACACHE;
|
|
242
|
-
|
|
243
|
-
if (configuredCacheLocation) {
|
|
244
|
-
// Resolve environment variables in the configured path
|
|
245
|
-
const resolvedBase = resolveEnvVars(configuredCacheLocation);
|
|
246
|
-
// Make relative paths absolute (relative to cwd)
|
|
247
|
-
const absoluteBase = path.isAbsolute(resolvedBase) ? resolvedBase : path.resolve(process.cwd(), resolvedBase);
|
|
248
|
-
return path.join(absoluteBase, appName, `${appVersion}-${platform}-${arch}`);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Default behavior
|
|
252
|
-
const defaultBase = './.sea-cache';
|
|
253
|
-
const absoluteBase = path.resolve(process.cwd(), defaultBase);
|
|
254
|
-
return path.join(absoluteBase, appName, `${appVersion}-${platform}-${arch}`);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Extract a binary asset to the cache directory.
|
|
259
|
-
* @param {string} assetKey
|
|
260
|
-
* @param {string} targetPath
|
|
261
|
-
* @param {string} expectedHash
|
|
262
|
-
* @returns {boolean} - True if extracted or already valid
|
|
263
|
-
*/
|
|
264
|
-
function extractBinary(assetKey, targetPath, expectedHash) {
|
|
265
|
-
// Check if already extracted and valid
|
|
266
|
-
if (fs.existsSync(targetPath)) {
|
|
267
|
-
const existingHash = hashFile(targetPath);
|
|
268
|
-
if (existingHash === expectedHash) {
|
|
269
|
-
return true; // Already valid
|
|
270
|
-
}
|
|
271
|
-
// Hash mismatch, re-extract
|
|
272
|
-
fs.unlinkSync(targetPath);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Extract from SEA blob (binaries are never encrypted)
|
|
276
|
-
const assetBuffer = sea.getRawAsset(assetKey);
|
|
277
|
-
if (!assetBuffer) {
|
|
278
|
-
throw new Error(`Asset not found in SEA blob: ${assetKey}`);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Convert ArrayBuffer to Buffer if needed
|
|
282
|
-
const buffer = Buffer.isBuffer(assetBuffer)
|
|
283
|
-
? assetBuffer
|
|
284
|
-
: Buffer.from(assetBuffer);
|
|
285
|
-
|
|
286
|
-
// Verify hash before writing
|
|
287
|
-
const actualHash = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
288
|
-
if (actualHash !== expectedHash) {
|
|
289
|
-
throw new Error(`Integrity check failed for ${assetKey}: expected ${expectedHash}, got ${actualHash}`);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Write to cache
|
|
293
|
-
const dir = path.dirname(targetPath);
|
|
294
|
-
if (!fs.existsSync(dir)) {
|
|
295
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
fs.writeFileSync(targetPath, buffer);
|
|
299
|
-
|
|
300
|
-
// Set executable permissions on Unix-like systems
|
|
301
|
-
if (process.platform !== 'win32') {
|
|
302
|
-
fs.chmodSync(targetPath, 0o755);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return true;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Compute SHA-256 hash of a file.
|
|
310
|
-
* @param {string} filePath
|
|
311
|
-
* @returns {string}
|
|
312
|
-
*/
|
|
313
|
-
function hashFile(filePath) {
|
|
314
|
-
const content = fs.readFileSync(filePath);
|
|
315
|
-
return crypto.createHash('sha256').update(content).digest('hex');
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Override fs module methods to intercept file access for extracted binaries.
|
|
320
|
-
* This provides a generic solution that works with any native module loading pattern
|
|
321
|
-
* (bindings package, direct require, etc.)
|
|
322
|
-
* @param {string} cacheDir - Cache directory where binaries are extracted
|
|
323
|
-
* @param {Object.<string, string>} assetPathMap - Map of asset key -> extracted path
|
|
324
|
-
*/
|
|
325
|
-
/**
|
|
326
|
-
* Override Module._resolveFilename and Module._load to redirect native module loads to extracted cache.
|
|
327
|
-
* @param {Object.<string, string>} nativeModuleMap - Map of module name -> extracted path
|
|
328
|
-
*/
|
|
329
|
-
function overrideModuleResolution(nativeModuleMap) {
|
|
330
|
-
const originalResolveFilename = Module._resolveFilename;
|
|
331
|
-
const originalLoad = Module._load;
|
|
332
|
-
|
|
333
|
-
// Override _resolveFilename for normal resolution
|
|
334
|
-
Module._resolveFilename = function (request, parent, isMain, options) {
|
|
335
|
-
// Normalize the request path
|
|
336
|
-
const normalized = path.normalize(request);
|
|
337
|
-
|
|
338
|
-
// Check direct match
|
|
339
|
-
if (nativeModuleMap[request]) {
|
|
340
|
-
return nativeModuleMap[request];
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Check for basename match
|
|
344
|
-
const basename = path.basename(request);
|
|
345
|
-
if (nativeModuleMap[basename]) {
|
|
346
|
-
return nativeModuleMap[basename];
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Check if the request includes any of our native modules
|
|
350
|
-
for (const [moduleName, extractedPath] of Object.entries(nativeModuleMap)) {
|
|
351
|
-
if (request.endsWith(moduleName) || request.includes(moduleName) ||
|
|
352
|
-
normalized.includes(moduleName.replace(/\//g, path.sep))) {
|
|
353
|
-
return extractedPath;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Fall back to original resolution
|
|
358
|
-
return originalResolveFilename.call(this, request, parent, isMain, options);
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
// Override _load to intercept at load time (catches SEA embedderRequire)
|
|
362
|
-
Module._load = function (request, parent, isMain) {
|
|
363
|
-
// Normalize the request path
|
|
364
|
-
const normalized = path.normalize(request);
|
|
365
|
-
|
|
366
|
-
// Check direct match
|
|
367
|
-
if (nativeModuleMap[request]) {
|
|
368
|
-
return originalLoad.call(this, nativeModuleMap[request], parent, isMain);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Check for basename match
|
|
372
|
-
const basename = path.basename(request);
|
|
373
|
-
if (nativeModuleMap[basename]) {
|
|
374
|
-
return originalLoad.call(this, nativeModuleMap[basename], parent, isMain);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Check if the request includes any of our native modules
|
|
378
|
-
for (const [moduleName, extractedPath] of Object.entries(nativeModuleMap)) {
|
|
379
|
-
if (request.endsWith(moduleName) || request.includes(moduleName) ||
|
|
380
|
-
normalized.includes(moduleName.replace(/\//g, path.sep))) {
|
|
381
|
-
return originalLoad.call(this, extractedPath, parent, isMain);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Fall back to original load
|
|
386
|
-
return originalLoad.call(this, request, parent, isMain);
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Bootstrap the SEA application.
|
|
392
|
-
* Extracts binaries and sets up module resolution.
|
|
393
|
-
*/
|
|
394
|
-
function bootstrap() {
|
|
395
|
-
if (!sea) {
|
|
396
|
-
throw new Error('This script must run in a SEA (Single Executable Application) context');
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Patch sea.getAsset to handle decryption transparently
|
|
400
|
-
if (typeof SEA_ENCRYPTED_ASSETS !== 'undefined' && typeof SEA_ENCRYPTION_KEY !== 'undefined') {
|
|
401
|
-
const originalGetAsset = sea.getAsset.bind(sea);
|
|
402
|
-
const originalGetRawAsset = sea.getRawAsset.bind(sea);
|
|
403
|
-
|
|
404
|
-
sea.getAsset = function(assetKey, encoding) {
|
|
405
|
-
const isEncrypted = SEA_ENCRYPTED_ASSETS.has(assetKey);
|
|
406
|
-
|
|
407
|
-
if (isEncrypted) {
|
|
408
|
-
// Get raw encrypted data
|
|
409
|
-
const encryptedData = originalGetRawAsset(assetKey);
|
|
410
|
-
const buffer = Buffer.isBuffer(encryptedData) ? encryptedData : Buffer.from(encryptedData);
|
|
411
|
-
|
|
412
|
-
// Decrypt it
|
|
413
|
-
const decrypted = decryptAsset(buffer, SEA_ENCRYPTION_KEY);
|
|
414
|
-
|
|
415
|
-
// Apply encoding if requested
|
|
416
|
-
if (encoding) {
|
|
417
|
-
return decrypted.toString(encoding);
|
|
418
|
-
}
|
|
419
|
-
return decrypted;
|
|
420
|
-
} else {
|
|
421
|
-
// Not encrypted, use original method
|
|
422
|
-
return originalGetAsset(assetKey, encoding);
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Load the manifest from SEA assets (manifest is never encrypted)
|
|
428
|
-
const manifestJson = sea.getAsset('sea-manifest.json', 'utf8');
|
|
429
|
-
if (!manifestJson) {
|
|
430
|
-
throw new Error('SEA manifest not found in blob');
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const manifest = JSON.parse(manifestJson);
|
|
434
|
-
const verbose = process.env.SEA_VERBOSE === 'true';
|
|
435
|
-
|
|
436
|
-
if (verbose) {
|
|
437
|
-
console.log('SEA Bootstrap:');
|
|
438
|
-
console.log(` App: ${manifest.appName} v${manifest.appVersion}`);
|
|
439
|
-
console.log(` Platform: ${manifest.platform}-${manifest.arch}`);
|
|
440
|
-
console.log(` Binaries to extract: ${manifest.binaries.length}`);
|
|
441
|
-
if (manifest.cacheLocation) {
|
|
442
|
-
console.log(` Configured cache location: ${manifest.cacheLocation}`);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Filter binaries for current platform
|
|
447
|
-
const platformBinaries = manifest.binaries.filter(b =>
|
|
448
|
-
(b.platform === '*' || b.platform === process.platform) &&
|
|
449
|
-
(b.arch === '*' || b.arch === process.arch)
|
|
450
|
-
);
|
|
451
|
-
|
|
452
|
-
// Sort by extraction order
|
|
453
|
-
platformBinaries.sort((a, b) => a.order - b.order);
|
|
454
|
-
|
|
455
|
-
const cacheDir = getExtractionCacheDir(
|
|
456
|
-
manifest.appName,
|
|
457
|
-
manifest.appVersion,
|
|
458
|
-
manifest.platform,
|
|
459
|
-
manifest.arch,
|
|
460
|
-
manifest.cacheLocation
|
|
461
|
-
);
|
|
462
|
-
|
|
463
|
-
const nativeModuleMap = {};
|
|
464
|
-
|
|
465
|
-
// Extract binaries
|
|
466
|
-
for (const binary of platformBinaries) {
|
|
467
|
-
const targetPath = path.join(cacheDir, binary.fileName);
|
|
468
|
-
|
|
469
|
-
if (verbose) {
|
|
470
|
-
console.log(` Extracting: ${binary.assetKey} -> ${targetPath}`);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
extractBinary(binary.assetKey, targetPath, binary.hash);
|
|
474
|
-
|
|
475
|
-
// Map the module name to extracted path
|
|
476
|
-
nativeModuleMap[binary.fileName] = targetPath;
|
|
477
|
-
|
|
478
|
-
// Also map without extension for easier resolution
|
|
479
|
-
const nameWithoutExt = path.basename(binary.fileName, path.extname(binary.fileName));
|
|
480
|
-
nativeModuleMap[nameWithoutExt] = targetPath;
|
|
481
|
-
|
|
482
|
-
// PHASE 2: Populate global asset map for fs overrides
|
|
483
|
-
assetPathMapGlobal[binary.assetKey] = targetPath;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// PHASE 2: Set global cache directory
|
|
487
|
-
cacheDirGlobal = cacheDir;
|
|
488
|
-
|
|
489
|
-
if (verbose) {
|
|
490
|
-
console.log('✓ Binary extraction complete');
|
|
491
|
-
console.log(` Cache directory: ${cacheDir}`);
|
|
492
|
-
console.log(` Asset mappings: ${Object.keys(assetPathMapGlobal).length}`);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// On Windows, add the cache directory to DLL search path
|
|
496
|
-
if (process.platform === 'win32') {
|
|
497
|
-
// Add to PATH so that native addons can find dependent DLLs
|
|
498
|
-
process.env.PATH = `${cacheDir};${process.env.PATH}`;
|
|
499
|
-
|
|
500
|
-
if (verbose) {
|
|
501
|
-
console.log(` Added to PATH: ${cacheDir}`);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Also try to add DLL directory using SetDllDirectory (if available)
|
|
505
|
-
try {
|
|
506
|
-
// Windows-specific: preload all DLLs to ensure they're in the process
|
|
507
|
-
platformBinaries
|
|
508
|
-
.filter(b => b.fileName.endsWith('.dll'))
|
|
509
|
-
.forEach(b => {
|
|
510
|
-
const dllPath = path.join(cacheDir, b.fileName);
|
|
511
|
-
if (verbose) {
|
|
512
|
-
console.log(` Preloading DLL: ${dllPath}`);
|
|
513
|
-
}
|
|
514
|
-
// Use process.dlopen to preload the DLL
|
|
515
|
-
try {
|
|
516
|
-
process.dlopen({ exports: {} }, dllPath);
|
|
517
|
-
} catch (err) {
|
|
518
|
-
// DLL might not be a valid node addon, which is fine
|
|
519
|
-
if (verbose) {
|
|
520
|
-
console.log(` (Non-addon DLL, will be loaded by OS loader)`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
});
|
|
524
|
-
} catch (err) {
|
|
525
|
-
console.warn('Warning: Could not preload DLLs:', err.message);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Set environment variable for cache directory (used by applications)
|
|
530
|
-
process.env.SEA_CACHE_DIR = cacheDir;
|
|
531
|
-
|
|
532
|
-
// Export native module map and cache dir to global for require override in app code
|
|
533
|
-
global.__seaNativeModuleMap = nativeModuleMap;
|
|
534
|
-
global.__seaCacheDir = cacheDir;
|
|
535
|
-
|
|
536
|
-
// Add cache directory to module paths so bindings can find extracted .node files
|
|
537
|
-
if (!Module.globalPaths.includes(cacheDir)) {
|
|
538
|
-
Module.globalPaths.unshift(cacheDir);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Override module resolution
|
|
542
|
-
overrideModuleResolution(nativeModuleMap);
|
|
543
|
-
|
|
544
|
-
// Provide a 'bindings' module shim for SEA compatibility
|
|
545
|
-
// This gets injected into the module cache so require('bindings') works
|
|
546
|
-
const bindingsShim = function(name) {
|
|
547
|
-
// Ensure .node extension
|
|
548
|
-
if (!name.endsWith('.node')) {
|
|
549
|
-
name += '.node';
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
if (verbose) {
|
|
553
|
-
console.log(`[bindings shim] Loading native module: ${name}`);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Try to load from native module map
|
|
557
|
-
if (nativeModuleMap[name]) {
|
|
558
|
-
const exports = {};
|
|
559
|
-
process.dlopen({ exports }, nativeModuleMap[name]);
|
|
560
|
-
return exports;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Try basename without .node
|
|
564
|
-
const baseName = path.basename(name, '.node');
|
|
565
|
-
if (nativeModuleMap[baseName]) {
|
|
566
|
-
const exports = {};
|
|
567
|
-
process.dlopen({ exports }, nativeModuleMap[baseName]);
|
|
568
|
-
return exports;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
throw new Error(`Could not load native module "${name}" - not found in SEA cache`);
|
|
572
|
-
};
|
|
573
|
-
|
|
574
|
-
// Inject bindings into module cache
|
|
575
|
-
Module._cache['bindings'] = {
|
|
576
|
-
id: 'bindings',
|
|
577
|
-
exports: bindingsShim,
|
|
578
|
-
loaded: true
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
if (verbose) {
|
|
582
|
-
console.log('[SEA] Injected bindings shim into module cache');
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// CRITICAL: Override the GLOBAL require function (not just Module.prototype.require)
|
|
586
|
-
// In SEA context, the bundled app code uses the global require which we CAN override
|
|
587
|
-
const originalGlobalRequire = global.require;
|
|
588
|
-
|
|
589
|
-
if (verbose) {
|
|
590
|
-
console.log('[SEA] About to override global.require');
|
|
591
|
-
console.log('[SEA] Original global.require type:', typeof originalGlobalRequire);
|
|
592
|
-
console.log('[SEA] Original global.require name:', originalGlobalRequire?.name);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
global.require = function seaRequire(id) {
|
|
596
|
-
// Check if this is a .node file
|
|
597
|
-
if (typeof id === 'string' && id.endsWith('.node')) {
|
|
598
|
-
if (verbose) {
|
|
599
|
-
console.log(`[SEA require] Intercepted .node require: ${id}`);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Get the basename
|
|
603
|
-
const basename = path.basename(id);
|
|
604
|
-
|
|
605
|
-
// Check if we have this in our native module map
|
|
606
|
-
if (nativeModuleMap[basename]) {
|
|
607
|
-
if (verbose) {
|
|
608
|
-
console.log(`[SEA require] Loading from cache via dlopen: ${nativeModuleMap[basename]}`);
|
|
609
|
-
}
|
|
610
|
-
const exports = {};
|
|
611
|
-
process.dlopen({ exports }, nativeModuleMap[basename]);
|
|
612
|
-
return exports;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Check if absolute path is in cache
|
|
616
|
-
if (path.isAbsolute(id) && id.startsWith(cacheDir)) {
|
|
617
|
-
if (verbose) {
|
|
618
|
-
console.log(`[SEA require] Loading cache path via dlopen: ${id}`);
|
|
619
|
-
}
|
|
620
|
-
const exports = {};
|
|
621
|
-
process.dlopen({ exports }, id);
|
|
622
|
-
return exports;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Check asset map for path matching
|
|
626
|
-
for (const [assetKey, extractedPath] of Object.entries(assetPathMapGlobal)) {
|
|
627
|
-
if (id.includes(assetKey) || basename === path.basename(assetKey)) {
|
|
628
|
-
if (verbose) {
|
|
629
|
-
console.log(`[SEA require] Loading from asset map via dlopen: ${extractedPath}`);
|
|
630
|
-
}
|
|
631
|
-
const exports = {};
|
|
632
|
-
process.dlopen({ exports }, extractedPath);
|
|
633
|
-
return exports;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// For all other requires, use the original
|
|
639
|
-
return originalGlobalRequire.call(this, id);
|
|
640
|
-
};
|
|
641
|
-
|
|
642
|
-
if (verbose) {
|
|
643
|
-
console.log('[SEA] global.require has been overridden');
|
|
644
|
-
console.log('[SEA] New global.require type:', typeof global.require);
|
|
645
|
-
console.log('[SEA] New global.require name:', global.require.name);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// Also override Module.prototype.require for completeness
|
|
649
|
-
const originalModuleRequire = Module.prototype.require;
|
|
650
|
-
Module.prototype.require = function(id) {
|
|
651
|
-
// Delegate to our global require override
|
|
652
|
-
return global.require(id);
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Run bootstrap if in SEA context
|
|
657
|
-
if (isSEA && sea) {
|
|
658
|
-
// Check if we're building a snapshot
|
|
659
|
-
let isBuildingSnapshot = false;
|
|
660
|
-
try {
|
|
661
|
-
const v8 = require('v8');
|
|
662
|
-
isBuildingSnapshot = v8.startupSnapshot && v8.startupSnapshot.isBuildingSnapshot && v8.startupSnapshot.isBuildingSnapshot();
|
|
663
|
-
} catch (err) {
|
|
664
|
-
// v8.startupSnapshot not available
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (isBuildingSnapshot) {
|
|
668
|
-
console.log('[SEA Bootstrap] Snapshot build mode - setting up deserialization callback');
|
|
669
|
-
// During snapshot build: set up callback to run bootstrap at runtime
|
|
670
|
-
const v8 = require('v8');
|
|
671
|
-
const originalCallback = v8.startupSnapshot.setDeserializeMainFunction;
|
|
672
|
-
|
|
673
|
-
// Intercept setDeserializeMainFunction to add bootstrap before app code
|
|
674
|
-
v8.startupSnapshot.setDeserializeMainFunction = function(callback) {
|
|
675
|
-
originalCallback.call(this, () => {
|
|
676
|
-
console.log('[SEA Bootstrap] Running at deserialization');
|
|
677
|
-
bootstrap();
|
|
678
|
-
// Then run the application callback
|
|
679
|
-
callback();
|
|
680
|
-
});
|
|
681
|
-
};
|
|
682
|
-
} else {
|
|
683
|
-
// Normal runtime: run bootstrap immediately
|
|
684
|
-
bootstrap();
|
|
685
|
-
}
|
|
686
|
-
} else {
|
|
687
|
-
console.log('[SEA Bootstrap] Skipping extraction (not in SEA)');
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
})(); // End bootstrap IIFE
|
|
691
|
-
|
|
692
|
-
// Export for testing (only accessible if loaded as module)
|
|
693
|
-
if (typeof module !== 'undefined' && module.exports) {
|
|
694
|
-
module.exports = {
|
|
695
|
-
// Empty exports - bootstrap runs automatically
|
|
696
|
-
};
|
|
697
|
-
}
|