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,203 @@
1
+ /**
2
+ * diagnostics.mjs
3
+ * Centralized console output formatting for CLI tooling.
4
+ * Provides consistent formatting, indentation, and styling.
5
+ */
6
+
7
+ /**
8
+ * Output levels
9
+ */
10
+ export const Level = {
11
+ ERROR: 'error',
12
+ WARN: 'warn',
13
+ INFO: 'info',
14
+ SUCCESS: 'success',
15
+ VERBOSE: 'verbose'
16
+ };
17
+
18
+ /**
19
+ * Formatting utilities
20
+ */
21
+ const colors = {
22
+ reset: '\x1b[0m',
23
+ red: '\x1b[31m',
24
+ green: '\x1b[32m',
25
+ yellow: '\x1b[33m',
26
+ blue: '\x1b[34m',
27
+ gray: '\x1b[90m'
28
+ };
29
+
30
+ function withColor(text, color) {
31
+ return `${color}${text}${colors.reset}`;
32
+ }
33
+
34
+ /**
35
+ * Global verbose flag
36
+ */
37
+ let verboseEnabled = false;
38
+
39
+ /**
40
+ * Enable or disable verbose output
41
+ * @param {boolean} enabled
42
+ */
43
+ export function setVerbose(enabled) {
44
+ verboseEnabled = enabled;
45
+ }
46
+
47
+ /**
48
+ * Core logging function
49
+ * @param {string} message
50
+ * @param {string} prefix
51
+ * @param {number} indent
52
+ */
53
+ function log(message, prefix = '', indent = 0) {
54
+ const indentation = ' '.repeat(indent);
55
+ console.log(`${indentation}${prefix}${message}`);
56
+ }
57
+
58
+ /**
59
+ * Print section header
60
+ * @param {string} title
61
+ */
62
+ export function header(title) {
63
+ console.log(`\n${title}`);
64
+ console.log('='.repeat(40));
65
+ }
66
+
67
+ /**
68
+ * Print sub-header
69
+ * @param {string} title
70
+ */
71
+ export function subheader(title) {
72
+ console.log(`\n${title}`);
73
+ console.log('-'.repeat(40));
74
+ }
75
+
76
+ /**
77
+ * Print step with number
78
+ * @param {number} step
79
+ * @param {number} total
80
+ * @param {string} message
81
+ */
82
+ export function step(step, total, message) {
83
+ log(`[${step}/${total}] ${message}`);
84
+ }
85
+
86
+ /**
87
+ * Print build step
88
+ * @param {number} buildNum
89
+ * @param {number} stepNum
90
+ * @param {string} message
91
+ */
92
+ export function buildStep(buildNum, stepNum, message) {
93
+ log(`[${buildNum}.${stepNum}] ${message}`, '', 1);
94
+ }
95
+
96
+ /**
97
+ * Print success message
98
+ * @param {string} message
99
+ * @param {number} indent
100
+ */
101
+ export function success(message, indent = 1) {
102
+ log(withColor(`[✓] ${message}`, colors.green), '', indent);
103
+ }
104
+
105
+ /**
106
+ * Print info message
107
+ * @param {string} message
108
+ * @param {number} indent
109
+ */
110
+ export function info(message, indent = 0) {
111
+ log(message, '', indent);
112
+ }
113
+
114
+ /**
115
+ * Print warning
116
+ * @param {string} message
117
+ * @param {number} indent
118
+ */
119
+ export function warn(message, indent = 1) {
120
+ log(withColor(`[!] ${message}`, colors.yellow), '', indent);
121
+ }
122
+
123
+ /**
124
+ * Print error
125
+ * @param {string} message
126
+ * @param {number} indent
127
+ */
128
+ export function error(message, indent = 0) {
129
+ console.error(`${' '.repeat(indent)}${withColor(`[X] ${message}`, colors.red)}`);
130
+ }
131
+
132
+ /**
133
+ * Print verbose message (only if verbose enabled)
134
+ * @param {string} message
135
+ * @param {number} indent
136
+ */
137
+ export function verbose(message, indent = 2) {
138
+ if (verboseEnabled) {
139
+ log(withColor(message, colors.gray), '', indent);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Print list item
145
+ * @param {string} message
146
+ * @param {number} indent
147
+ */
148
+ export function listItem(message, indent = 1) {
149
+ log(`- ${message}`, '', indent);
150
+ }
151
+
152
+ /**
153
+ * Print numbered list item
154
+ * @param {number} num
155
+ * @param {string} message
156
+ * @param {number} indent
157
+ */
158
+ export function numberedItem(num, message, indent = 1) {
159
+ log(`${num}. ${message}`, '', indent);
160
+ }
161
+
162
+ /**
163
+ * Print section separator
164
+ */
165
+ export function separator() {
166
+ console.log('');
167
+ }
168
+
169
+ /**
170
+ * Print final summary
171
+ * @param {string} message
172
+ */
173
+ export function summary(message) {
174
+ console.log('');
175
+ console.log('='.repeat(40));
176
+ log(withColor(message, colors.green));
177
+ console.log('='.repeat(40));
178
+ console.log('');
179
+ }
180
+
181
+ /**
182
+ * Format file size in human-readable format
183
+ * @param {number} bytes
184
+ * @returns {string}
185
+ */
186
+ export function formatSize(bytes) {
187
+ const mb = bytes / 1024 / 1024;
188
+ if (mb >= 1) {
189
+ return `${mb.toFixed(2)} MB`;
190
+ }
191
+ const kb = bytes / 1024;
192
+ return `${kb.toFixed(2)} KB`;
193
+ }
194
+
195
+ /**
196
+ * Format target string
197
+ * @param {string} platform
198
+ * @param {string} arch
199
+ * @returns {string}
200
+ */
201
+ export function formatTarget(platform, arch) {
202
+ return `${platform}-${arch}`;
203
+ }
@@ -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
+ }
@@ -1,13 +1,16 @@
1
1
  /**
2
- * fetch-node.js
2
+ * fetch-node.mjs
3
3
  * Download target Node.js binary for SEA injection.
4
4
  */
5
5
 
6
- const fs = require('fs');
7
- const path = require('path');
8
- const https = require('https');
9
- const { pipeline } = require('stream');
10
- const { promisify } = require('util');
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import https from 'https';
9
+ import { pipeline } from 'stream';
10
+ import { promisify } from 'util';
11
+ import AdmZip from 'adm-zip';
12
+ import tar from 'tar';
13
+ import * as diag from './diagnostics.mjs';
11
14
 
12
15
  const pipelineAsync = promisify(pipeline);
13
16
 
@@ -78,9 +81,6 @@ async function downloadFile(url, outputPath) {
78
81
  * @returns {Promise<string>} - Path to extracted node binary
79
82
  */
80
83
  async function extractNodeBinary(archivePath, outputDir, platform) {
81
- const AdmZip = require('adm-zip');
82
- const tar = require('tar');
83
-
84
84
  if (platform === 'win32') {
85
85
  // Extract from ZIP
86
86
  const zip = new AdmZip(archivePath);
@@ -135,7 +135,7 @@ async function extractNodeBinary(archivePath, outputDir, platform) {
135
135
  * @param {string} cacheDir - Directory to cache downloads
136
136
  * @returns {Promise<string>} - Path to the node binary
137
137
  */
138
- async function fetchNodeBinary(nodeVersion, platform, arch, cacheDir) {
138
+ export async function fetchNodeBinary(nodeVersion, platform, arch, cacheDir) {
139
139
  if (!fs.existsSync(cacheDir)) {
140
140
  fs.mkdirSync(cacheDir, { recursive: true });
141
141
  }
@@ -145,23 +145,23 @@ async function fetchNodeBinary(nodeVersion, platform, arch, cacheDir) {
145
145
 
146
146
  // Check cache
147
147
  if (fs.existsSync(cachedBinary)) {
148
- console.log(`✓ Using cached Node binary: ${cachedBinary}`);
148
+ diag.verbose(`Using cached Node binary: ${cachedBinary}`, 1);
149
149
  return cachedBinary;
150
150
  }
151
151
 
152
- console.log(`Downloading Node.js v${nodeVersion} for ${platform}-${arch}...`);
152
+ diag.verbose(`Downloading Node.js v${nodeVersion} for ${platform}-${arch}...`, 1);
153
153
  const url = getNodeDownloadUrl(nodeVersion, platform, arch);
154
154
  const archiveName = path.basename(url);
155
155
  const archivePath = path.join(cacheDir, archiveName);
156
156
 
157
157
  await downloadFile(url, archivePath);
158
- console.log(`✓ Downloaded: ${archivePath}`);
158
+ diag.verbose(`Downloaded: ${archivePath}`, 1);
159
159
 
160
160
  const extractDir = path.join(cacheDir, `${nodeVersion}-${platform}-${arch}`);
161
161
  fs.mkdirSync(extractDir, { recursive: true });
162
162
 
163
163
  const binaryPath = await extractNodeBinary(archivePath, extractDir, platform);
164
- console.log(`✓ Extracted Node binary: ${binaryPath}`);
164
+ diag.verbose(`Extracted Node binary: ${binaryPath}`, 1);
165
165
 
166
166
  // Clean up archive
167
167
  fs.unlinkSync(archivePath);
@@ -169,9 +169,4 @@ async function fetchNodeBinary(nodeVersion, platform, arch, cacheDir) {
169
169
  return binaryPath;
170
170
  }
171
171
 
172
- module.exports = {
173
- getNodeDownloadUrl,
174
- downloadFile,
175
- extractNodeBinary,
176
- fetchNodeBinary
177
- };
172
+ export { getNodeDownloadUrl, downloadFile, extractNodeBinary };
package/lib/index.mjs ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * index.mjs
3
+ * Main entry point for Seabox v2 library.
4
+ * Exports all public APIs.
5
+ */
6
+
7
+ export { build } from './build.mjs';
8
+ export {
9
+ loadConfig,
10
+ validateConfig,
11
+ parseTarget,
12
+ generateDefaultConfig,
13
+ normalizeConfig,
14
+ getDefaultLibraryPatterns
15
+ } from './config.mjs';
16
+ export { MultiTargetBuilder } from './multi-target-builder.mjs';
17
+ export { bundleWithRollup, NativeModuleDetectorPlugin } from './rolldown-bundler.mjs';
18
+ export { scanDependenciesForNativeModules, findNativeModuleBuildPath } from './native-scanner.mjs';
19
+ export { BuildCache } from './build-cache.mjs';
20
+ export { generateManifest, serializeManifest } from './manifest.mjs';
21
+ export { createSeaConfig, writeSeaConfigJson, generateBlob } from './blob.mjs';
22
+ export { fetchNodeBinary } from './fetch-node.mjs';
23
+ export { injectBlob } from './inject.mjs';
24
+ export { generateEncryptionKey, encryptAsset, decryptAsset, encryptAssets, keyToObfuscatedCode } from './crypto-assets.mjs';
25
+ export { obfuscateBootstrap } from './obfuscate.mjs';
26
+ export { bundleEntry } from './entry-bundler.mjs';
package/lib/inject.mjs ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * inject.mjs
3
+ * Inject SEA blob into Node binary using postject.
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import Module from 'module';
9
+ import { fileURLToPath } from 'url';
10
+ import * as diag from './diagnostics.mjs';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ // Import unsign using require since it's CommonJS
16
+ const require = Module.createRequire(import.meta.url);
17
+ const { removeSignature, setVerbose: setUnsignVerbose } = require('./unsign.cjs');
18
+
19
+ /**
20
+ * Inject a SEA blob into a Node.js binary using postject.
21
+ * @param {string} nodeBinaryPath - Path to the source Node binary
22
+ * @param {string} blobPath - Path to the SEA blob file
23
+ * @param {string} outputPath - Path for the output executable
24
+ * @param {string} platform - Target platform (win32, linux, darwin)
25
+ * @param {boolean} verbose - Enable verbose logging
26
+ * @param {Object} [rceditOptions] - Optional rcedit configuration for Windows executables
27
+ * @returns {Promise<void>}
28
+ */
29
+ export async function injectBlob(nodeBinaryPath, blobPath, outputPath, platform, verbose, rceditOptions) {
30
+ // Ensure output directory exists
31
+ const outputDir = path.dirname(outputPath);
32
+ if (!fs.existsSync(outputDir)) {
33
+ fs.mkdirSync(outputDir, { recursive: true });
34
+ }
35
+
36
+ // Copy node binary to output location
37
+ fs.copyFileSync(nodeBinaryPath, outputPath);
38
+
39
+ // Remove existing signature before postject injection
40
+ setUnsignVerbose(verbose);
41
+ await removeSignature(outputPath, platform);
42
+
43
+ // Apply rcedit changes (Windows only, before postject)
44
+ if (platform === 'win32' && rceditOptions && typeof rceditOptions === 'object') {
45
+ await applyRcedit(outputPath, rceditOptions, verbose);
46
+ }
47
+
48
+ // Prepare postject command
49
+ const sentinel = 'NODE_SEA_BLOB';
50
+ const sentinelFuse = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2';
51
+
52
+ diag.verbose(`Injecting SEA blob into: ${outputPath}`, 1);
53
+
54
+ // Use postject programmatically
55
+ const postject = (await import('postject')).default;
56
+
57
+ // Read blob data as buffer
58
+ const blobData = fs.readFileSync(blobPath);
59
+
60
+ const injectOptions = {
61
+ sentinelFuse,
62
+ machoSegmentName: platform === 'darwin' ? 'NODE_SEA' : undefined
63
+ };
64
+
65
+ try {
66
+ await postject.inject(outputPath, sentinel, blobData, injectOptions);
67
+ diag.verbose('SEA blob injected successfully', 2);
68
+ } catch (error) {
69
+ throw new Error(`Postject injection failed: ${error.message}`);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Apply rcedit to modify Windows executable resources.
75
+ * @param {string} exePath - Path to the executable
76
+ * @param {Object} options - rcedit options (icon, version-string, file-version, product-version, etc.)
77
+ * @param {boolean} verbose - Enable verbose logging
78
+ * @returns {Promise<void>}
79
+ */
80
+ async function applyRcedit(exePath, options, verbose) {
81
+ diag.verbose('Applying rcedit to modify executable resources...', 2);
82
+ diag.verbose(`Options: ${JSON.stringify(options, null, 2)}`, 2);
83
+
84
+ // Dynamic import for rcedit (it's CommonJS)
85
+ const rcedit = (await import('rcedit')).default;
86
+
87
+ try {
88
+ await rcedit(exePath, options);
89
+ diag.verbose('rcedit applied successfully', 2);
90
+ } catch (error) {
91
+ throw new Error(`rcedit failed: ${error.message}`);
92
+ }
93
+ }
94
+
95
+ export { applyRcedit };
@@ -1,9 +1,9 @@
1
1
  /**
2
- * manifest.js
2
+ * manifest.mjs
3
3
  * Generate runtime manifest with asset metadata and extraction rules.
4
4
  */
5
5
 
6
- const path = require('path');
6
+ import path from 'path';
7
7
 
8
8
  /**
9
9
  * @typedef {Object} BinaryManifestEntry
@@ -28,13 +28,13 @@ const path = require('path');
28
28
 
29
29
  /**
30
30
  * Generate a runtime manifest from scanned assets.
31
- * @param {import('./scanner').AssetEntry[]} assets - All scanned assets
31
+ * @param {Array} assets - All scanned assets
32
32
  * @param {Object} config - SEA configuration
33
33
  * @param {string} targetPlatform - Target platform (win32, linux, darwin)
34
34
  * @param {string} targetArch - Target architecture (x64, arm64)
35
35
  * @returns {RuntimeManifest}
36
36
  */
37
- function generateManifest(assets, config, targetPlatform, targetArch) {
37
+ export function generateManifest(assets, config, targetPlatform, targetArch) {
38
38
  const binaries = assets
39
39
  .filter(a => a.isBinary)
40
40
  .map((asset, index) => {
@@ -95,12 +95,6 @@ function inferExtractionOrder(fileName, fallbackIndex) {
95
95
  * @param {RuntimeManifest} manifest
96
96
  * @returns {string}
97
97
  */
98
- function serializeManifest(manifest) {
98
+ export function serializeManifest(manifest) {
99
99
  return JSON.stringify(manifest, null, 2);
100
100
  }
101
-
102
- module.exports = {
103
- generateManifest,
104
- inferExtractionOrder,
105
- serializeManifest
106
- };