seabox 0.2.0 → 0.4.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
@@ -148,17 +148,19 @@ seabox automates the entire SEA build process:
148
148
  - **Config globs**: Patterns specified in `assets` array
149
149
  - **Libraries**: Platform-specific shared libraries (DLLs/SOs)
150
150
 
151
- 3. **Native Module Rebuilding** - Rebuilds native modules for target platform
151
+ 3. **Binary Preparation** - Downloads target Node.js binary
152
152
 
153
- 4. **Bootstrap Injection** - Adds runtime code for asset loading and native module extraction
153
+ 4. **Native Module Rebuilding** - Rebuilds native modules using the downloaded Node.js binary, ensuring ABI compatibility
154
154
 
155
- 5. **SEA Blob Creation** - Packages everything using Node.js SEA tooling
155
+ 5. **Bootstrap Injection** - Adds runtime code for asset loading and native module extraction
156
156
 
157
- 6. **Binary Preparation** - Downloads target Node.js binary and removes code signature
157
+ 6. **SEA Blob Creation** - Packages everything using the target Node.js binary
158
158
 
159
- 7. **Injection** - Uses `postject` to inject the blob into the Node.js binary
159
+ 7. **Signature Removal** - Removes code signature from the binary before injection
160
160
 
161
- 8. **Output** - Produces standalone executable(s) ready for distribution
161
+ 8. **Injection** - Uses `postject` to inject the blob into the Node.js binary
162
+
163
+ 9. **Output** - Produces standalone executable(s) ready for distribution
162
164
 
163
165
  ### Automatic Asset Detection
164
166
 
@@ -16,15 +16,16 @@ const __dirname = path.dirname(__filename);
16
16
  /**
17
17
  * Rebuild a native module for a specific target
18
18
  * @param {string} modulePath - Path to the native module
19
+ * @param {string} nodeVersion - Target Node.js version (e.g., "24.13.0")
19
20
  * @param {string} platform - Target platform (win32, linux, darwin)
20
21
  * @param {string} arch - Target architecture (x64, arm64)
21
22
  * @param {boolean} verbose - Enable verbose logging
22
23
  */
23
- function rebuildNativeModule(modulePath, platform, arch, verbose = false) {
24
+ function rebuildNativeModule(modulePath, nodeVersion, platform, arch, verbose = false) {
24
25
  diag.setVerbose(verbose);
25
26
 
26
27
  diag.verbose(`Rebuilding native module: ${modulePath}`);
27
- diag.verbose(`Target: ${platform}-${arch}`);
28
+ diag.verbose(`Target: Node.js ${nodeVersion} ${platform}-${arch}`);
28
29
 
29
30
  const packageJsonPath = path.join(modulePath, 'package.json');
30
31
  if (!fs.existsSync(packageJsonPath)) {
@@ -42,42 +43,60 @@ function rebuildNativeModule(modulePath, platform, arch, verbose = false) {
42
43
  }
43
44
 
44
45
  try {
45
- // Use node-gyp to rebuild for the target platform
46
- const cmd = `npx node-gyp rebuild --target_platform=${platform} --target_arch=${arch}`;
46
+ // Use node-gyp to rebuild for the target Node.js version, platform, and architecture
47
+ // --target specifies the Node.js version (critical for NODE_MODULE_VERSION)
48
+ // --dist-url ensures node-gyp downloads headers from the correct location
49
+ const cmd = `npx node-gyp rebuild --target=${nodeVersion} --arch=${arch} --dist-url=https://nodejs.org/dist`;
47
50
 
48
- diag.verbose(`Running: ${cmd}`);
51
+ diag.verbose(`Building for Node.js ${nodeVersion} (${platform}-${arch})`, 2);
52
+ diag.verbose(`Command: ${cmd}`, 2);
49
53
 
50
54
  execSync(cmd, {
51
55
  cwd: modulePath,
52
- stdio: verbose ? 'inherit' : 'pipe',
56
+ stdio: 'pipe', // Hide output unless error occurs
53
57
  env: {
54
58
  ...process.env,
55
- npm_config_target_platform: platform,
56
- npm_config_target_arch: arch
59
+ npm_config_target: nodeVersion,
60
+ npm_config_arch: arch,
61
+ npm_config_target_arch: arch,
62
+ npm_config_disturl: 'https://nodejs.org/dist'
57
63
  }
58
64
  });
59
65
 
60
- diag.verbose(`Successfully rebuilt ${moduleName}`);
66
+ diag.verbose(`Successfully built for Node.js ${nodeVersion}`, 2);
61
67
  } catch (error) {
68
+ // Show node-gyp output on error
69
+ if (error.stdout) console.error(error.stdout.toString());
70
+ if (error.stderr) console.error(error.stderr.toString());
62
71
  diag.verbose(`Failed to rebuild ${moduleName}: ${error.message}`);
63
72
  throw error;
64
73
  }
65
74
  }
66
75
 
67
- // CLI entry point
68
- if (import.meta.url === `file://${process.argv[1]}`) {
76
+ // CLI entry point - compare normalized paths for cross-platform compatibility
77
+ const isMainModule = (() => {
78
+ try {
79
+ const scriptPath = fileURLToPath(import.meta.url);
80
+ const argPath = path.resolve(process.argv[1]);
81
+ return scriptPath === argPath;
82
+ } catch {
83
+ return false;
84
+ }
85
+ })();
86
+
87
+ if (isMainModule) {
69
88
  const args = process.argv.slice(2);
70
89
 
71
- if (args.length < 3) {
72
- diag.error('Usage: seabox-rebuild <module-path> <platform> <arch> [--verbose]');
90
+ if (args.length < 4) {
91
+ diag.error('Usage: seabox-rebuild <module-path> <node-version> <platform> <arch> [--verbose]');
73
92
  process.exit(1);
74
93
  }
75
94
 
76
- const [modulePath, platform, arch] = args;
95
+ const [modulePath, nodeVersion, platform, arch] = args;
77
96
  const verbose = args.includes('--verbose') || args.includes('-v');
78
97
 
79
98
  try {
80
- rebuildNativeModule(modulePath, platform, arch, verbose);
99
+ rebuildNativeModule(modulePath, nodeVersion, platform, arch, verbose);
81
100
  process.exit(0);
82
101
  } catch (error) {
83
102
  diag.error(`Rebuild failed: ${error.message}`);
@@ -110,13 +110,15 @@ export class BuildCache {
110
110
  /**
111
111
  * Get cached native module build
112
112
  * @param {string} moduleRoot - Module root path
113
- * @param {string} target - Build target
113
+ * @param {string} target - Build target (e.g., node24.11.0-win32-x64)
114
114
  * @returns {string|null} - Path to cached .node file
115
115
  */
116
116
  getCachedNativeBuild(moduleRoot, target) {
117
+ // Include the FULL target string (with Node.js version) in cache key
118
+ // This ensures different Node.js versions have separate cache entries
117
119
  const cacheKey = crypto
118
120
  .createHash('sha256')
119
- .update(moduleRoot + target)
121
+ .update(`${moduleRoot}||${target}`)
120
122
  .digest('hex')
121
123
  .substring(0, 16);
122
124
 
@@ -132,6 +134,9 @@ export class BuildCache {
132
134
  if (cacheStats.mtime > sourceStats.mtime) {
133
135
  return cachePath;
134
136
  }
137
+ } else {
138
+ // If no binding.gyp, still return cached binary (e.g., prebuild scenario)
139
+ return cachePath;
135
140
  }
136
141
  }
137
142
 
@@ -141,13 +146,15 @@ export class BuildCache {
141
146
  /**
142
147
  * Cache a native module build
143
148
  * @param {string} moduleRoot - Module root path
144
- * @param {string} target - Build target
149
+ * @param {string} target - Build target (e.g., node24.11.0-win32-x64)
145
150
  * @param {string} builtBinaryPath - Path to built .node file
146
151
  */
147
152
  cacheNativeBuild(moduleRoot, target, builtBinaryPath) {
153
+ // Use the FULL target string (with Node.js version) in cache key
154
+ // This ensures different Node.js versions have separate cache entries
148
155
  const cacheKey = crypto
149
156
  .createHash('sha256')
150
- .update(moduleRoot + target)
157
+ .update(`${moduleRoot}||${target}`)
151
158
  .digest('hex')
152
159
  .substring(0, 16);
153
160
 
@@ -9,7 +9,7 @@ import https from 'https';
9
9
  import { pipeline } from 'stream';
10
10
  import { promisify } from 'util';
11
11
  import AdmZip from 'adm-zip';
12
- import tar from 'tar';
12
+ import * as tar from 'tar';
13
13
  import * as diag from './diagnostics.mjs';
14
14
 
15
15
  const pipelineAsync = promisify(pipeline);
@@ -135,26 +135,33 @@ export class MultiTargetBuilder {
135
135
 
136
136
  diag.subheader(`[Build ${buildNumber}] Target: ${target}`);
137
137
 
138
- // Step 1: Rebuild native modules for this target
138
+ // Step 1: Fetch Node binary FIRST - needed for all subsequent operations
139
+ diag.buildStep(buildNumber, 1, 'Fetching Node.js binary...');
140
+ const cacheDir = path.join(this.projectRoot, 'node_modules', '.cache', 'sea-node-binaries');
141
+ const nodeBinary = await fetchNodeBinary(nodeVersion, platform, arch, cacheDir);
142
+ diag.success('Node binary ready');
143
+
144
+ // Step 2: Rebuild native modules using the TARGET Node binary
139
145
  const rebuiltModules = await this.rebuildNativeModulesForTarget(
140
146
  nativeModules,
141
147
  target,
148
+ nodeBinary,
142
149
  buildNumber
143
150
  );
144
151
 
145
- // Step 2: Collect config assets (manual globs)
152
+ // Step 3: Collect config assets (manual globs)
146
153
  const configAssets = await this.collectConfigAssets(
147
154
  this.config.assets || [],
148
155
  buildNumber
149
156
  );
150
157
 
151
- // Step 3: Collect auto-detected assets (from path.join(__dirname, ...))
158
+ // Step 4: Collect auto-detected assets (from path.join(__dirname, ...))
152
159
  const autoAssets = await this.collectDetectedAssets(
153
160
  detectedAssets,
154
161
  buildNumber
155
162
  );
156
163
 
157
- // Step 4: Collect platform-specific libraries (DLLs/SOs)
164
+ // Step 5: Collect platform-specific libraries (DLLs/SOs)
158
165
  const platformLibraries = await this.collectPlatformLibraries(
159
166
  outputConfig.libraries,
160
167
  platform,
@@ -162,13 +169,13 @@ export class MultiTargetBuilder {
162
169
  buildNumber
163
170
  );
164
171
 
165
- // Step 5: Prepare bundled entry with bootstrap
172
+ // Step 6: Prepare bundled entry with bootstrap
166
173
  const finalEntryPath = await this.prepareFinalEntry(
167
174
  bundledEntryPath,
168
175
  buildNumber
169
176
  );
170
177
 
171
- // Step 6: Combine all assets (dedupe by assetKey)
178
+ // Step 7: Combine all assets (dedupe by assetKey)
172
179
  const assetMap = new Map();
173
180
 
174
181
  // Add in order of priority (later overwrites earlier)
@@ -213,11 +220,12 @@ export class MultiTargetBuilder {
213
220
  }
214
221
  }
215
222
 
216
- // Step 7: Generate SEA
223
+ // Step 8: Generate SEA using the target Node binary
217
224
  await this.generateSEAForTarget({
218
225
  assets: allAssets,
219
226
  entryPath: finalEntryPath,
220
227
  target,
228
+ nodeBinary,
221
229
  outputPath,
222
230
  executableName,
223
231
  platform,
@@ -244,12 +252,12 @@ export class MultiTargetBuilder {
244
252
  /**
245
253
  * Rebuild native modules for specific target
246
254
  */
247
- async rebuildNativeModulesForTarget(nativeModules, target, buildNumber) {
255
+ async rebuildNativeModulesForTarget(nativeModules, target, nodeBinary, buildNumber) {
248
256
  if (nativeModules.size === 0) {
249
257
  return [];
250
258
  }
251
259
 
252
- diag.buildStep(buildNumber, 1, `Rebuilding ${nativeModules.size} native module(s)...`);
260
+ diag.buildStep(buildNumber, 2, `Rebuilding ${nativeModules.size} native module(s)...`);
253
261
 
254
262
  const rebuiltAssets = [];
255
263
  const { platform, arch } = parseTarget(target);
@@ -287,9 +295,9 @@ export class MultiTargetBuilder {
287
295
  }
288
296
 
289
297
  // Rebuild the module
290
- diag.verbose(`Rebuilding: ${moduleName}`, 2);
298
+ diag.verbose(`Rebuilding: ${moduleName} for Node.js ${parseTarget(target).nodeVersion}`, 2);
291
299
 
292
- await this.rebuildNativeModule(moduleInfo.packageRoot, target);
300
+ await this.rebuildNativeModule(moduleInfo.packageRoot, target, nodeBinary);
293
301
 
294
302
  // Find the built binary
295
303
  const builtPath = await this.findBuiltBinary(moduleInfo, target);
@@ -317,18 +325,22 @@ export class MultiTargetBuilder {
317
325
  }
318
326
 
319
327
  /**
320
- * Rebuild a single native module
328
+ * Rebuild a single native module using the target Node binary
321
329
  */
322
- async rebuildNativeModule(packageRoot, target) {
330
+ async rebuildNativeModule(packageRoot, target, nodeBinary) {
323
331
  const rebuildScript = path.join(__dirname, '..', 'bin', 'seabox-rebuild.mjs');
324
332
 
325
333
  if (!fs.existsSync(rebuildScript)) {
326
334
  throw new Error('seabox-rebuild.mjs not found');
327
335
  }
328
336
 
337
+ const { nodeVersion, platform, arch } = parseTarget(target);
338
+
329
339
  try {
330
- execSync(`node "${rebuildScript}" --target ${target} "${packageRoot}"`, {
331
- stdio: this.verbose ? 'inherit' : 'pipe',
340
+ // Use the TARGET Node.js binary (not system node) to run the rebuild script
341
+ // This ensures all operations use the exact Node version we'll embed in the SEA
342
+ execSync(`"${nodeBinary}" "${rebuildScript}" "${packageRoot}" ${nodeVersion} ${platform} ${arch}${this.verbose ? ' --verbose' : ''}`, {
343
+ stdio: 'inherit', // Show node-gyp output including header downloads
332
344
  cwd: this.projectRoot
333
345
  });
334
346
  } catch (err) {
@@ -576,7 +588,8 @@ const SEA_ENCRYPTED_ASSETS = new Set([]);
576
588
  arch,
577
589
  nodeVersion,
578
590
  rcedit,
579
- buildNumber
591
+ buildNumber,
592
+ nodeBinary
580
593
  } = options;
581
594
 
582
595
  diag.buildStep(buildNumber, 4, 'Generating SEA blob...');
@@ -613,16 +626,12 @@ const SEA_ENCRYPTED_ASSETS = new Set([]);
613
626
  const seaConfigPath = path.join(tempDir, 'sea-config.json');
614
627
  writeSeaConfigJson(seaConfig, seaConfigPath, allAssets, tempDir);
615
628
 
616
- // Generate blob
617
- await generateBlob(seaConfigPath, process.execPath);
629
+ // Generate blob using the TARGET Node.js binary (already fetched)
630
+ // This is critical for snapshots - they must be built with the target Node version
631
+ diag.buildStep(buildNumber, 5, 'Generating SEA blob...');
632
+ await generateBlob(seaConfigPath, nodeBinary);
618
633
  diag.success('SEA blob generated');
619
634
 
620
- // Fetch Node binary
621
- diag.buildStep(buildNumber, 5, 'Fetching Node.js binary...');
622
- const cacheDir = path.join(this.projectRoot, 'node_modules', '.cache', 'sea-node-binaries');
623
- const nodeBinary = await fetchNodeBinary(nodeVersion, platform, arch, cacheDir);
624
- diag.success('Node binary ready');
625
-
626
635
  // Inject blob
627
636
  diag.buildStep(buildNumber, 6, 'Injecting blob into executable...');
628
637
  const outputExe = path.join(outputDir, executableName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seabox",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Node.js Single Executable Application (SEA) builder tool with native and library extraction",
5
5
  "main": "lib/index.mjs",
6
6
  "type": "module",
@@ -37,11 +37,11 @@
37
37
  "adm-zip": "^0.5.16",
38
38
  "glob": "^10.0.0",
39
39
  "postject": "^1.0.0-alpha.6",
40
- "rolldown": "^1.0.0-beta.50",
41
- "tar": "^6.2.1"
40
+ "rolldown": "^1.0.0-rc.2",
41
+ "tar": "^7.5.7"
42
42
  },
43
43
  "peerDependencies": {
44
- "rcedit": "^4.0.1"
44
+ "rcedit": "^4 || ^5"
45
45
  },
46
46
  "peerDependenciesMeta": {
47
47
  "rcedit": {
@@ -51,7 +51,7 @@
51
51
  "devDependencies": {
52
52
  "chai": "^6.2.1",
53
53
  "javascript-obfuscator": "^4.1.1",
54
- "mocha": "^11.7.5",
54
+ "mocha": "^11.3.0",
55
55
  "node-api-headers": "^1.2.0",
56
56
  "rimraf": "^6.0.1"
57
57
  },