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