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.
@@ -0,0 +1,415 @@
1
+ /**
2
+ * rolldown-bundler.mjs
3
+ * Automatic bundling with Rollup and native module detection.
4
+ * Replaces manual bundling step with integrated solution.
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { rolldown as rolldown } from 'rolldown';
10
+ import commonjs from '@rollup/plugin-commonjs';
11
+ import { nodeResolve } from '@rollup/plugin-node-resolve';
12
+ import json from '@rollup/plugin-json';
13
+ import Module from 'module';
14
+ import { fileURLToPath } from 'url';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+
19
+ /**
20
+ * @typedef {Object} NativeModuleInfo
21
+ * @property {string} binaryPath - Path to the .node file
22
+ * @property {string} packageRoot - Root directory of the native module package
23
+ * @property {string} moduleName - Name of the module
24
+ * @property {string} buildPath - Path to build directory (e.g., build/Release)
25
+ */
26
+
27
+ /**
28
+ * Rollup plugin to detect and transform native module patterns.
29
+ * Based on rollup-plugin-natives approach - transforms at build time.
30
+ * Also detects path.join(__dirname, ...) asset references for auto-embedding.
31
+ */
32
+ class NativeModuleDetectorPlugin {
33
+ constructor(options = {}) {
34
+ /** @type {Map<string, NativeModuleInfo>} */
35
+ this.nativeModules = new Map();
36
+ /** @type {Set<string>} */
37
+ this.detectedAssets = new Set();
38
+ this.targetPlatform = options.targetPlatform || process.platform;
39
+ this.targetArch = options.targetArch || process.arch;
40
+ this.projectRoot = options.projectRoot || process.cwd();
41
+ }
42
+
43
+ name = 'native-module-detector';
44
+
45
+ /**
46
+ * Get module root (package.json directory) for a given file
47
+ */
48
+ getModuleRoot(id) {
49
+ let moduleRoot = path.dirname(id);
50
+ let prev = null;
51
+
52
+ while (true) {
53
+ if (moduleRoot === '.') {
54
+ moduleRoot = process.cwd();
55
+ }
56
+
57
+ if (fs.existsSync(path.join(moduleRoot, 'package.json')) ||
58
+ fs.existsSync(path.join(moduleRoot, 'node_modules'))) {
59
+ break;
60
+ }
61
+
62
+ if (prev === moduleRoot) break;
63
+
64
+ prev = moduleRoot;
65
+ moduleRoot = path.resolve(moduleRoot, '..');
66
+ }
67
+
68
+ return moduleRoot;
69
+ }
70
+
71
+ /**
72
+ * Register a native module
73
+ */
74
+ registerNativeModule(nativePath) {
75
+ if (this.nativeModules.has(nativePath)) {
76
+ return this.nativeModules.get(nativePath);
77
+ }
78
+
79
+ const packageRoot = this.findPackageRoot(nativePath);
80
+ const moduleName = packageRoot ? path.basename(packageRoot) : path.basename(nativePath, '.node');
81
+ const fileName = path.basename(nativePath);
82
+
83
+ const info = {
84
+ binaryPath: nativePath,
85
+ packageRoot: packageRoot || path.dirname(nativePath),
86
+ moduleName: moduleName,
87
+ buildPath: path.dirname(nativePath),
88
+ assetKey: `native/${moduleName}/${fileName}`
89
+ };
90
+
91
+ this.nativeModules.set(nativePath, info);
92
+ return info;
93
+ }
94
+
95
+ /**
96
+ * Transform hook - detect and replace native module patterns
97
+ */
98
+ transform = (code, id) => {
99
+ let hasChanges = false;
100
+ let transformedCode = code;
101
+
102
+ // Detect path.join(__dirname, 'relative/path') patterns for auto-embedding assets
103
+ this.detectDirnameAssets(code, id);
104
+
105
+ // Pattern 1: require('bindings')('module_name')
106
+ const bindingsPattern = /require\(['"]bindings['"]\)\(((['"])(.+?)\2)?\)/g;
107
+ const moduleRoot = this.getModuleRoot(id);
108
+ const self = this;
109
+
110
+ transformedCode = transformedCode.replace(bindingsPattern, function(match, args, quote, name) {
111
+ const nativeAlias = name || 'bindings.node';
112
+ const nodeName = nativeAlias.endsWith('.node') ? nativeAlias : `${nativeAlias}.node`;
113
+
114
+ // Try standard build locations
115
+ const possibilities = [
116
+ path.join(moduleRoot, 'build', nodeName),
117
+ path.join(moduleRoot, 'build', 'Debug', nodeName),
118
+ path.join(moduleRoot, 'build', 'Release', nodeName),
119
+ ];
120
+
121
+ const chosenPath = possibilities.find(p => fs.existsSync(p));
122
+ if (chosenPath) {
123
+ const info = self.registerNativeModule(chosenPath);
124
+ hasChanges = true;
125
+ return `__requireSeabox('${info.assetKey}')`;
126
+ }
127
+
128
+ return match;
129
+ });
130
+
131
+ // Pattern 2: Direct require('./path.node') or require('./path')
132
+ const directRequirePattern = /require\(['"]([^'"]+)['"]\)/g;
133
+ transformedCode = transformedCode.replace(directRequirePattern, function(match, modulePath) {
134
+ // Only process potential native module paths
135
+ if (!modulePath.includes('.node') && !modulePath.includes('/build/')) {
136
+ return match;
137
+ }
138
+
139
+ let testPath = modulePath;
140
+ if (!testPath.endsWith('.node')) {
141
+ testPath += '.node';
142
+ }
143
+
144
+ // Resolve relative to the current file
145
+ if (modulePath.startsWith('.')) {
146
+ testPath = path.resolve(path.dirname(id), testPath);
147
+ } else {
148
+ testPath = path.join(moduleRoot, testPath);
149
+ }
150
+
151
+ if (fs.existsSync(testPath)) {
152
+ const info = self.registerNativeModule(testPath);
153
+ hasChanges = true;
154
+ return `__requireSeabox('${info.assetKey}')`;
155
+ }
156
+
157
+ return match;
158
+ });
159
+
160
+ // Pattern 3: node-gyp-build pattern - needs runtime platform detection
161
+ if (code.includes('node-gyp-build')) {
162
+ const nodeGypBuildPattern = /require\(['"]node-gyp-build['"]\)\(__dirname\)/g;
163
+ transformedCode = transformedCode.replace(nodeGypBuildPattern, function(match) {
164
+ const prebuildsDir = path.join(moduleRoot, 'prebuilds');
165
+
166
+ // For node-gyp-build, we need to handle it differently since it's platform-specific
167
+ // Check if we're building for a specific platform or current platform
168
+ if (fs.existsSync(prebuildsDir)) {
169
+ const platformArchDir = path.join(prebuildsDir, `${self.targetPlatform}-${self.targetArch}`);
170
+
171
+ if (fs.existsSync(platformArchDir)) {
172
+ const files = fs.readdirSync(platformArchDir);
173
+ const nodeFiles = files.filter(f => f.endsWith('.node'));
174
+
175
+ if (nodeFiles.length > 0) {
176
+ // Pick best match (prefer napi builds)
177
+ const napiFile = nodeFiles.find(f => f.includes('napi')) || nodeFiles[0];
178
+ const nativePath = path.join(platformArchDir, napiFile);
179
+ const info = self.registerNativeModule(nativePath);
180
+ hasChanges = true;
181
+ return `__requireSeabox('${info.assetKey}')`;
182
+ }
183
+ }
184
+ }
185
+
186
+ // If prebuilds don't exist, fall back to build/Release for gyp builds
187
+ const buildRelease = path.join(moduleRoot, 'build', 'Release');
188
+ if (fs.existsSync(buildRelease)) {
189
+ const files = fs.readdirSync(buildRelease);
190
+ const nodeFile = files.find(f => f.endsWith('.node'));
191
+ if (nodeFile) {
192
+ const nativePath = path.join(buildRelease, nodeFile);
193
+ const info = self.registerNativeModule(nativePath);
194
+ hasChanges = true;
195
+ return `__requireSeabox('${info.assetKey}')`;
196
+ }
197
+ }
198
+
199
+ return match;
200
+ });
201
+ }
202
+
203
+ // Pattern 4: node-pre-gyp pattern (strip it out and use direct path)
204
+ if (code.includes('node-pre-gyp') || code.includes('@mapbox/node-pre-gyp')) {
205
+ const preGypPattern = /(?:var|let|const)\s+(\w+)\s+=\s+require\(['"](?:@mapbox\/)?node-pre-gyp['"]\)/g;
206
+ const varMatch = preGypPattern.exec(code);
207
+
208
+ if (varMatch) {
209
+ const varName = varMatch[1];
210
+ const binaryPattern = new RegExp(
211
+ `(?:var|let|const)\\s+(\\w+)\\s+=\\s+${varName}\\.find\\(path\\.resolve\\(path\\.join\\(__dirname,\\s*(['"])(.+?)\\2\\)\\)\\);?\\s*(?:var|let|const)\\s+(\\w+)\\s+=\\s+require\\(\\1\\)`,
212
+ 'g'
213
+ );
214
+
215
+ transformedCode = transformedCode.replace(binaryPattern, function(match, pathVar, quote, relPath) {
216
+ // Try to find the actual binary using standard node-pre-gyp structure
217
+ const possibilities = [
218
+ path.join(moduleRoot, 'lib', 'binding'),
219
+ path.join(moduleRoot, 'build', 'Release')
220
+ ];
221
+
222
+ for (const dir of possibilities) {
223
+ if (fs.existsSync(dir)) {
224
+ const files = fs.readdirSync(dir);
225
+ const nodeFile = files.find(f => f.endsWith('.node'));
226
+ if (nodeFile) {
227
+ const nativePath = path.join(dir, nodeFile);
228
+ const info = self.registerNativeModule(nativePath);
229
+ hasChanges = true;
230
+ // Remove the pre-gyp require entirely
231
+ transformedCode = transformedCode.replace(varMatch[0], '');
232
+ const requireVarMatch = match.match(/const\s+(\w+)\s+=\s+require/);
233
+ const requireVar = requireVarMatch ? requireVarMatch[1] : 'binding';
234
+ return `const ${pathVar} = '${info.assetKey}'; const ${requireVar} = __requireSeabox('${info.assetKey}')`;
235
+ }
236
+ }
237
+ }
238
+
239
+ return match;
240
+ });
241
+ }
242
+ }
243
+
244
+ return hasChanges ? { code: transformedCode, map: null } : null;
245
+ }
246
+
247
+ /**
248
+ * Detect path.join(__dirname, ...) patterns and register them as assets
249
+ */
250
+ detectDirnameAssets(code, id) {
251
+ // Match: path.join(__dirname, 'relative/path') or path.join(__dirname, '..', 'path')
252
+ // Also: path.resolve(__dirname, ...) patterns
253
+ const pathJoinPattern = /path\.(?:join|resolve)\s*\(\s*__dirname\s*,\s*(.+?)\)/g;
254
+
255
+ let match;
256
+ while ((match = pathJoinPattern.exec(code)) !== null) {
257
+ try {
258
+ // Extract the path arguments - they could be literals or variables
259
+ const args = match[1];
260
+
261
+ // Try to extract string literals (simple case)
262
+ const literalPattern = /['"]([^'"]+)['"]/g;
263
+ const literals = [];
264
+ let literalMatch;
265
+ while ((literalMatch = literalPattern.exec(args)) !== null) {
266
+ literals.push(literalMatch[1]);
267
+ }
268
+
269
+ if (literals.length > 0) {
270
+ // Construct the relative path
271
+ const relativePath = path.join(...literals);
272
+
273
+ // Resolve from the file's directory
274
+ const fileDir = path.dirname(id);
275
+ const absolutePath = path.resolve(fileDir, relativePath);
276
+
277
+ // Make it relative to project root for the asset key
278
+ const relativeToProject = path.relative(this.projectRoot, absolutePath);
279
+
280
+ // Only register if the file exists
281
+ if (fs.existsSync(absolutePath) && !fs.statSync(absolutePath).isDirectory()) {
282
+ // Normalize path separators for asset key
283
+ const assetKey = relativeToProject.replace(/\\/g, '/');
284
+ this.detectedAssets.add(assetKey);
285
+ }
286
+ }
287
+ } catch (err) {
288
+ // Ignore parse errors - some patterns might be too complex
289
+ }
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Find the package root containing binding.gyp
295
+ */
296
+ findPackageRoot(filePath) {
297
+ let current = path.dirname(filePath);
298
+ const root = path.parse(current).root;
299
+
300
+ while (current !== root) {
301
+ const pkgPath = path.join(current, 'package.json');
302
+
303
+ if (fs.existsSync(pkgPath)) {
304
+ // Check if this package has native bindings
305
+ const hasBindingGyp = fs.existsSync(path.join(current, 'binding.gyp'));
306
+
307
+ try {
308
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
309
+ if (hasBindingGyp || pkg.gypfile === true) {
310
+ return current;
311
+ }
312
+ } catch (err) {
313
+ // Ignore JSON parse errors
314
+ }
315
+ }
316
+
317
+ current = path.dirname(current);
318
+ }
319
+
320
+ return null;
321
+ }
322
+
323
+ /**
324
+ * Get all detected native modules
325
+ */
326
+ getNativeModules() {
327
+ return this.nativeModules;
328
+ }
329
+
330
+ /**
331
+ * Get all detected assets from path.join(__dirname, ...) patterns
332
+ */
333
+ getDetectedAssets() {
334
+ return this.detectedAssets;
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Bundle application with Rollup and detect native modules
340
+ * @param {string} entryPath - Absolute path to entry file
341
+ * @param {string} outputPath - Path for bundled output
342
+ * @param {Object} config - Build configuration
343
+ * @param {string} targetPlatform - Target platform
344
+ * @param {string} targetArch - Target architecture
345
+ * @param {boolean} verbose - Enable verbose logging
346
+ * @returns {Promise<{bundledPath: string, nativeModules: Map<string, NativeModuleInfo>, detectedAssets: Set<string>}>}
347
+ */
348
+ export async function bundleWithRollup(entryPath, outputPath, config = {}, targetPlatform = process.platform, targetArch = process.arch, verbose = false) {
349
+ const projectRoot = config._projectRoot || process.cwd();
350
+
351
+ const nativeDetector = new NativeModuleDetectorPlugin({
352
+ targetPlatform,
353
+ targetArch,
354
+ projectRoot
355
+ });
356
+
357
+ if (verbose) {
358
+ console.log('[Bundler] Bundling entry:', entryPath);
359
+ console.log('[Bundler] Target:', `${targetPlatform}-${targetArch}`);
360
+ }
361
+
362
+ // Get Node.js built-in modules to mark as external
363
+ const builtinModules = Module.builtinModules || [];
364
+
365
+ const bundle = await rolldown({
366
+ input: entryPath,
367
+ platform: "node",
368
+ plugins: [
369
+ nativeDetector,
370
+ json(),
371
+ ...(config.bundler?.plugins || [])
372
+ ],
373
+ external: [
374
+ // Node built-ins are always external
375
+ ...builtinModules,
376
+ // User-specified externals (filter out functions for Rolldown compatibility)
377
+ ...(config.bundler?.external || []).filter(e => typeof e !== 'function'),
378
+ // Match .node files with regex instead of function
379
+ /\.node$/
380
+ ],
381
+ onwarn: (warning, warn) => {
382
+ // Suppress certain warnings
383
+ if (warning.code === 'CIRCULAR_DEPENDENCY') return;
384
+ if (warning.code === 'EVAL') return;
385
+
386
+ if (verbose) {
387
+ warn(warning);
388
+ }
389
+ }
390
+ });
391
+
392
+ await bundle.write({
393
+ file: outputPath,
394
+ format: 'cjs',
395
+ exports: 'auto',
396
+ banner: '/* Bundled by Seabox */\n',
397
+ sourcemap: config.bundler?.sourcemap || false
398
+ });
399
+
400
+ await bundle.close();
401
+
402
+ if (verbose) {
403
+ console.log('[Bundler] Bundle complete:', outputPath);
404
+ console.log('[Bundler] Native modules detected:', nativeDetector.nativeModules.size);
405
+ console.log('[Bundler] Assets detected:', nativeDetector.detectedAssets.size);
406
+ }
407
+
408
+ return {
409
+ bundledPath: outputPath,
410
+ nativeModules: nativeDetector.getNativeModules(),
411
+ detectedAssets: nativeDetector.getDetectedAssets()
412
+ };
413
+ }
414
+
415
+ export { NativeModuleDetectorPlugin };
@@ -18,7 +18,6 @@ async function checkSignToolAvailability(platform) {
18
18
  const tools = {
19
19
  win32: 'signtool.exe',
20
20
  darwin: 'codesign',
21
- linux: 'osslsigncode'
22
21
  };
23
22
 
24
23
  const tool = tools[platform];
@@ -32,9 +31,7 @@ async function checkSignToolAvailability(platform) {
32
31
  await execFileAsync('where', ['signtool.exe']);
33
32
  } else if (platform === 'darwin') {
34
33
  await execFileAsync('which', ['codesign']);
35
- } else if (platform === 'linux') {
36
- await execFileAsync('which', ['osslsigncode']);
37
- }
34
+ }
38
35
  return { available: true, tool };
39
36
  } catch (error) {
40
37
  return { available: false, tool };
@@ -98,29 +95,6 @@ async function removeMacSignature(exePath) {
98
95
  }
99
96
  }
100
97
 
101
- /**
102
- * Remove signature from a Linux executable.
103
- * Note: Linux binaries are rarely signed, but osslsigncode can handle Authenticode signatures.
104
- * @param {string} exePath - Path to the executable
105
- * @returns {Promise<{success: boolean, message: string}>}
106
- */
107
- async function removeLinuxSignature(exePath) {
108
- try {
109
- // osslsigncode can remove Authenticode signatures (used for cross-platform PE files)
110
- const { stdout, stderr } = await execFileAsync('osslsigncode', ['remove-signature', exePath]);
111
-
112
- return { success: true, message: 'Signature removed successfully' };
113
- } catch (error) {
114
- const errorMsg = error.message || error.stderr || '';
115
-
116
- // Not an error if there was no signature
117
- if (errorMsg.includes('not signed') || errorMsg.includes('no signature')) {
118
- return { success: true, message: 'Binary was not signed (no signature to remove)' };
119
- }
120
-
121
- return { success: false, message: `Failed to remove signature: ${errorMsg}` };
122
- }
123
- }
124
98
 
125
99
  /**
126
100
  * Remove code signature from an executable before injection.
@@ -130,6 +104,12 @@ async function removeLinuxSignature(exePath) {
130
104
  * @returns {Promise<void>}
131
105
  */
132
106
  async function removeSignature(exePath, platform) {
107
+ // Linux binaries are typically not signed, so skip signature removal
108
+ if (platform === 'linux') {
109
+ console.log('Skipping signature removal (Linux binaries are typically unsigned)');
110
+ return;
111
+ }
112
+
133
113
  //console.log('Checking for code signature removal tools...');
134
114
 
135
115
  const { available, tool } = await checkSignToolAvailability(platform);
@@ -145,8 +125,6 @@ async function removeSignature(exePath, platform) {
145
125
  } else if (platform === 'darwin') {
146
126
  console.warn(` Install Xcode Command Line Tools to get codesign`);
147
127
  console.warn(` Run: xcode-select --install`);
148
- } else if (platform === 'linux') {
149
- console.warn(` Install osslsigncode: apt-get install osslsigncode (Debian/Ubuntu)`);
150
128
  }
151
129
 
152
130
  console.warn('');
@@ -160,8 +138,6 @@ async function removeSignature(exePath, platform) {
160
138
  result = await removeWindowsSignature(exePath);
161
139
  } else if (platform === 'darwin') {
162
140
  result = await removeMacSignature(exePath);
163
- } else if (platform === 'linux') {
164
- result = await removeLinuxSignature(exePath);
165
141
  } else {
166
142
  console.warn(`⚠️ Warning: Unsupported platform for signature removal: ${platform}`);
167
143
  return;
@@ -180,5 +156,4 @@ module.exports = {
180
156
  checkSignToolAvailability,
181
157
  removeWindowsSignature,
182
158
  removeMacSignature,
183
- removeLinuxSignature
184
159
  };
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "seabox",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "Node.js Single Executable Application (SEA) builder tool with native and library extraction",
5
- "main": "lib/index.js",
6
- "types": "dist/index.d.ts",
5
+ "main": "lib/index.mjs",
6
+ "type": "module",
7
7
  "bin": {
8
- "seabox": "bin/seabox.js",
9
- "seabox-rebuild": "bin/seabox-rebuild.js"
8
+ "seabox": "bin/seabox.mjs"
10
9
  },
11
10
  "scripts": {
12
11
  "clean": "rimraf dist",
@@ -34,10 +33,15 @@
34
33
  },
35
34
  "homepage": "https://github.com/MeirionHughes/seabox",
36
35
  "dependencies": {
36
+ "@rollup/plugin-commonjs": "^28.0.2",
37
+ "@rollup/plugin-json": "^6.1.0",
38
+ "@rollup/plugin-node-resolve": "^15.3.0",
37
39
  "adm-zip": "^0.5.16",
38
40
  "glob": "^10.0.0",
39
41
  "postject": "^1.0.0-alpha.6",
40
42
  "rcedit": "^4.0.1",
43
+ "rolldown": "^1.0.0-beta.50",
44
+ "rollup": "^4.28.1",
41
45
  "tar": "^6.2.1"
42
46
  },
43
47
  "devDependencies": {