bundlerbus 1.0.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 ADDED
@@ -0,0 +1,351 @@
1
+ # 📦🚍 Bundlerbus
2
+
3
+ **Universal native bindings bundler for Bun's `--compile` flag**
4
+
5
+ Bundlerbus solves the critical problem of compiling Bun projects that use native Node.js modules (like Sharp, Canvas, serialport, etc.) into single-file executables. Bun's built-in `--compile` flag fails when these libraries try to load `.node` bindings from the virtual filesystem.
6
+
7
+ The only exception is better-sqlite3 that still didn't work.
8
+
9
+ ## The Problem
10
+
11
+ ```bash
12
+ # This fails when your project uses Sharp, Canvas, or other native modules
13
+ bun build --compile ./app.js --outfile ./app.exe
14
+
15
+ # Error: Cannot find module '/path/to/$bunfs/node_modules/sharp/...'
16
+ ```
17
+
18
+ Native modules expect to load binaries from a **real filesystem path**, but Bun's `$bunfs` is virtual.
19
+
20
+ ## The Solution
21
+
22
+ Bundlerbus extracts your application and `node_modules` to a real directory at runtime (on first launch), then loads your code from there. Native bindings work perfectly because they see real filesystem paths.
23
+
24
+ ```bash
25
+ # This works!
26
+ bundlerbus ./app.js --outfile ./app.exe
27
+ ```
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install -g bundlerbus
33
+
34
+ # Or use with bunx (no installation)
35
+ bunx bundlerbus ./app.js --outfile ./app.exe
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ Bundlerbus is a **drop-in replacement** for `bun build --compile`:
41
+
42
+ ```bash
43
+ # Instead of:
44
+ bun build --compile ./src/cli.js --target bun-windows-x64 --outfile ./dist/app.exe
45
+
46
+ # Use:
47
+ bundlerbus ./src/cli.js --target bun-windows-x64 --outfile ./dist/app.exe
48
+ ```
49
+
50
+ All flags are forwarded directly to Bun, so it works with:
51
+ - `--target` (platform selection)
52
+ - `--outfile` (output path)
53
+ - `--minify`, `--sourcemap`
54
+ - `--define` (compile-time constants)
55
+ - `--windows-icon`, `--windows-publisher`, etc.
56
+ - Any future Bun flags
57
+
58
+ ## Entry Point Resolution
59
+
60
+ Bundlerbus automatically detects your entry point using this priority:
61
+
62
+ 1. **Explicit argument:** `bundlerbus ./src/cli.js`
63
+ 2. **package.json "bin":** If you have a single binary defined
64
+ 3. **package.json "main":** Fallback to main entry
65
+ 4. **Convention:** Checks `./index.js` or `./src/index.js`
66
+
67
+ ### Multiple Binaries
68
+
69
+ If your `package.json` has multiple bins:
70
+
71
+ ```json
72
+ {
73
+ "bin": {
74
+ "app": "./src/cli.js",
75
+ "server": "./src/server.js"
76
+ }
77
+ }
78
+ ```
79
+
80
+ You must specify which one to build:
81
+
82
+ ```bash
83
+ bundlerbus ./src/cli.js --outfile ./dist/app.exe
84
+ bundlerbus ./src/server.js --outfile ./dist/server.exe
85
+ ```
86
+
87
+ ## What Gets Packed
88
+
89
+ Bundlerbus respects the npm standard `package.json` "files" field:
90
+
91
+ ```json
92
+ {
93
+ "files": [
94
+ "src/",
95
+ "templates/",
96
+ "config/"
97
+ ]
98
+ }
99
+ ```
100
+
101
+ This acts as a whitelist. Only these directories/files are packed into the executable (plus `node_modules` which is always included).
102
+
103
+ ### Standard Files
104
+
105
+ These are always included (following npm conventions):
106
+ - `package.json`
107
+ - `README.md` / `README`
108
+ - `LICENSE` / `LICENCE`
109
+ - `NOTICE`
110
+
111
+ ### Always Excluded
112
+
113
+ These are never packed:
114
+ - `node_modules` (handled separately)
115
+ - `.git`, `.github`
116
+ - `.vscode`, `.idea`
117
+ - `dist/`, `build/`
118
+ - `*.log`
119
+ - `.env` files
120
+
121
+ ### No "files" Field?
122
+
123
+ If your `package.json` doesn't have a "files" field, Bundlerbus will:
124
+ 1. Pack the directory containing your entry point
125
+ 2. Show a warning suggesting you add the "files" field
126
+
127
+ ```bash
128
+ [BUNDLER] Warning: No "files" field in package.json, packing src/
129
+ ```
130
+
131
+ ## How It Works
132
+
133
+ Bundlerbus uses a three-stage approach:
134
+
135
+ ### 1. Build Time (Your Machine)
136
+
137
+ ```bash
138
+ bundlerbus ./src/cli.js --outfile ./app.exe
139
+ ```
140
+
141
+ - Packs your source files + `node_modules` into `payload.tar.gz`
142
+ - Generates a `bootstrap.js` loader with your entry point injected
143
+ - Calls `bun build --compile bootstrap.js` (with all your flags)
144
+ - Cleans up temporary files
145
+
146
+ ### 2. First Run (User's Machine)
147
+
148
+ ```bash
149
+ ./app.exe
150
+ ```
151
+
152
+ - Bootstrap extracts payload to OS-specific cache directory:
153
+ - **Windows:** `%LOCALAPPDATA%\your-app\1.0.0\`
154
+ - **macOS:** `~/Library/Application Support/your-app/1.0.0/`
155
+ - **Linux:** `~/.cache/your-app/1.0.0/`
156
+ - Calculates MD5 hash of payload
157
+ - Injects native library paths into `PATH`/`LD_LIBRARY_PATH`
158
+ - Loads your entry point
159
+
160
+ ### 3. Subsequent Runs
161
+
162
+ - Bootstrap compares payload hash
163
+ - If unchanged, skips extraction (instant startup)
164
+ - If changed (new version), re-extracts
165
+
166
+ ## Cache Management
167
+
168
+ ### Version Organization
169
+
170
+ ```
171
+ AppData/
172
+ your-app/
173
+ 1.0.0/ ← Current version
174
+ .hash ← Payload hash (triggers re-extract)
175
+ node_modules/
176
+ src/
177
+ 0.9.0/ ← Old version (still there)
178
+ ```
179
+
180
+ Old versions accumulate but don't interfere. Users can manually delete old version folders if needed.
181
+
182
+ ### Cache Invalidation
183
+
184
+ Cache is invalidated when:
185
+ - Your code changes
186
+ - Dependencies change
187
+ - Anything in the payload changes
188
+
189
+ This is **hash-based**, not version-based, so even if you forget to bump your version number, changes will trigger re-extraction.
190
+
191
+ ## Examples
192
+
193
+ ### Basic Usage
194
+
195
+ ```bash
196
+ # Simple project
197
+ bundlerbus ./index.js --outfile ./dist/app.exe
198
+
199
+ # With minification
200
+ bundlerbus ./src/main.js --minify --outfile ./build/app
201
+
202
+ # Cross-compilation
203
+ bundlerbus ./cli.js --target bun-windows-x64 --outfile ./dist/app-win.exe
204
+ bundlerbus ./cli.js --target bun-darwin-arm64 --outfile ./dist/app-mac
205
+ bundlerbus ./cli.js --target bun-linux-x64 --outfile ./dist/app-linux
206
+ ```
207
+
208
+ ### Real-World Project (THYPRESS)
209
+
210
+ ```javascript
211
+ // build-exe.js
212
+ import { execSync } from 'child_process';
213
+ import pkg from './package.json' assert { type: 'json' };
214
+
215
+ const configs = {
216
+ win32: {
217
+ flags: [
218
+ '--windows-icon=./icon.ico',
219
+ '--windows-publisher="THYPRESS™"',
220
+ `--windows-version="${pkg.version}"`,
221
+ '--target=bun-windows-x64',
222
+ '--outfile=./dist/THYPRESS-BINDER.exe'
223
+ ]
224
+ },
225
+ darwin: {
226
+ flags: [
227
+ '--target=bun-darwin-arm64',
228
+ '--outfile=./dist/thypress-binder-mac'
229
+ ]
230
+ }
231
+ };
232
+
233
+ const target = process.platform;
234
+ const config = configs[target];
235
+
236
+ const cmd = [
237
+ 'bundlerbus ./src/cli.js',
238
+ `--define globalThis.__VERSION__='"${pkg.version}"'`,
239
+ ...config.flags
240
+ ].join(' ');
241
+
242
+ execSync(cmd, { stdio: 'inherit' });
243
+ ```
244
+
245
+ ## Troubleshooting
246
+
247
+ ### Native Module Not Found
248
+
249
+ ```
250
+ Error: Cannot find module 'sharp'
251
+ ```
252
+
253
+ **Solution:** Ensure the module is in `dependencies` (not `devDependencies`) and run `npm install` before building.
254
+
255
+ ### Extraction Failed
256
+
257
+ ```
258
+ [BOOTSTRAP] Fatal Error: EACCES: permission denied
259
+ ```
260
+
261
+ **Solution:** User needs write access to:
262
+ - Windows: `%LOCALAPPDATA%`
263
+ - macOS: `~/Library/Application Support`
264
+ - Linux: `~/.cache` or `$XDG_CACHE_HOME`
265
+
266
+ ### Wrong Entry Point
267
+
268
+ ```
269
+ [FAILURE] Could not determine entry point
270
+ ```
271
+
272
+ **Solution:** Specify entry point explicitly:
273
+
274
+ ```bash
275
+ bundlerbus ./src/index.js [flags...]
276
+ ```
277
+
278
+ Or add to `package.json`:
279
+
280
+ ```json
281
+ {
282
+ "bin": "./src/cli.js"
283
+ }
284
+ ```
285
+
286
+ ## Comparison to Bun's --compile
287
+
288
+ | Feature | Bun --compile | Bundlerbus |
289
+ |---------|---------------|------------|
290
+ | Pure JS projects | ✅ Works | ✅ Works |
291
+ | Native bindings (Sharp, Canvas) | ❌ Fails | ✅ Works |
292
+ | Startup time | Instant | Slight delay on first run |
293
+ | Distribution size | Smaller | Larger (includes full node_modules) |
294
+ | Cache invalidation | N/A | Hash-based (robust) |
295
+ | Flag compatibility | Native | Transparent forwarding |
296
+
297
+ ## Technical Details
298
+
299
+ ### Why Extraction Works
300
+
301
+ Many native modules use `__dirname` or similar to locate their binaries:
302
+
303
+ ```javascript
304
+ // Inside sharp/lib/constructor.js
305
+ const libvips = require(path.join(__dirname, '../build/Release/sharp.node'));
306
+ ```
307
+
308
+ In Bun's `$bunfs`, `__dirname` points to a fake path like:
309
+ ```
310
+ $bunfs/node_modules/sharp/lib
311
+ ```
312
+
313
+ Native code can't read from `$bunfs`. It needs a real path like:
314
+ ```
315
+ C:\Users\Alice\AppData\Local\myapp\1.0.0\node_modules\sharp\lib
316
+ ```
317
+
318
+ Bundlerbus provides that real path by extracting to disk.
319
+
320
+ ### Path Injection
321
+
322
+ After extraction, Bundlerbus scans `node_modules` for `.dll`, `.so`, `.dylib`, `.node` files and injects their directories into:
323
+ - **Windows:** `PATH`
324
+ - **macOS:** `DYLD_LIBRARY_PATH`
325
+ - **Linux:** `LD_LIBRARY_PATH`
326
+
327
+ This ensures native binaries can find their dependencies.
328
+
329
+ ## Limitations
330
+
331
+ - **First-run extraction:** Adds ~1-2 seconds on first launch
332
+ - **Disk space:** Full `node_modules` is extracted (can be 100MB+)
333
+ - **Write permissions:** Users need write access to cache directory
334
+
335
+ These tradeoffs are acceptable for the **100% reliability** with native bindings.
336
+
337
+ ## Future Enhancements
338
+
339
+ - [x] Glob pattern support in "files" field
340
+ - [ ] Lazy extraction (only extract modules actually `require()`'d)
341
+ - [ ] Delta updates (only re-extract changed files)
342
+ - [ ] `bundlerbus clean` command to purge old caches
343
+ - [ ] Compression options (brotli, zstd)
344
+
345
+ ## License
346
+
347
+ MIT
348
+
349
+ ## Credits
350
+
351
+ Inspired by Electron's ASAR format, but designed specifically for Bun's compilation model and native binding requirements. Cooked by [@phteocos](http://x.com/phteocos)
@@ -0,0 +1,104 @@
1
+ // bootstrap.template.js
2
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, createWriteStream, rmSync, chmodSync } from 'fs';
3
+ import { join, dirname, extname } from 'path';
4
+ import { homedir } from 'os';
5
+ import { createGunzip } from 'zlib';
6
+ import { Readable } from 'stream';
7
+ import tar from 'tar-stream';
8
+ import { createHash } from 'crypto';
9
+
10
+ // The embedded archive
11
+ import payloadArchive from './payload.tar.gz' with { type: 'file' };
12
+
13
+ const APP_NAME = '___BUNDLERBUS_APP_NAME___';
14
+ const APP_VERSION = '___BUNDLERBUS_APP_VERSION___';
15
+
16
+ function getCacheDir() {
17
+ const platform = process.platform;
18
+ if (platform === 'win32') {
19
+ return join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), APP_NAME, APP_VERSION);
20
+ } else if (platform === 'darwin') {
21
+ return join(homedir(), 'Library', 'Application Support', APP_NAME, APP_VERSION);
22
+ } else {
23
+ // Linux / Unix (XDG Spec)
24
+ const base = process.env.XDG_CACHE_HOME || join(homedir(), '.cache');
25
+ return join(base, APP_NAME, APP_VERSION);
26
+ }
27
+ }
28
+
29
+ async function bootstrap() {
30
+ try {
31
+ const cacheDir = getCacheDir();
32
+ const payloadBuffer = readFileSync(payloadArchive);
33
+
34
+ const currentHash = createHash('md5').update(payloadBuffer).digest('hex');
35
+ const hashFile = join(cacheDir, '.hash');
36
+ const existingHash = existsSync(hashFile) ? readFileSync(hashFile, 'utf8') : null;
37
+
38
+ if (currentHash !== existingHash) {
39
+ console.log('[BOOTSTRAP] Unfolding environment...');
40
+ if (existsSync(cacheDir)) rmSync(cacheDir, { recursive: true, force: true });
41
+ mkdirSync(cacheDir, { recursive: true });
42
+
43
+ await new Promise((resolve, reject) => {
44
+ const extract = tar.extract();
45
+ extract.on('entry', (header, stream, next) => {
46
+ const outPath = join(cacheDir, header.name);
47
+ if (header.type === 'directory') {
48
+ mkdirSync(outPath, { recursive: true });
49
+ stream.resume(); next();
50
+ } else {
51
+ mkdirSync(dirname(outPath), { recursive: true });
52
+ const outStream = createWriteStream(outPath);
53
+ stream.pipe(outStream);
54
+ outStream.on('finish', () => {
55
+ // On Unix, ensure binaries are executable
56
+ if (process.platform !== 'win32') {
57
+ const isBin = outPath.endsWith('.node') || outPath.endsWith('.so') ||
58
+ outPath.endsWith('.dylib') || outPath.includes('bin/');
59
+ if (isBin) chmodSync(outPath, 0o755);
60
+ }
61
+ next();
62
+ });
63
+ outStream.on('error', reject);
64
+ }
65
+ });
66
+ extract.on('finish', resolve);
67
+ Readable.from(payloadBuffer).pipe(createGunzip()).pipe(extract);
68
+ });
69
+ writeFileSync(hashFile, currentHash);
70
+ }
71
+
72
+ // --- NATIVE SEARCH PATH INJECTION ---
73
+ const nodeModules = join(cacheDir, 'node_modules');
74
+ const libPaths = new Set();
75
+ const scan = (dir) => {
76
+ if (!existsSync(dir)) return;
77
+ const entries = readdirSync(dir, { withFileTypes: true });
78
+ for (const e of entries) {
79
+ const p = join(dir, e.name);
80
+ if (e.isDirectory()) scan(p);
81
+ else if (['.dll', '.so', '.node', '.dylib'].includes(extname(e.name).toLowerCase())) {
82
+ libPaths.add(dir);
83
+ }
84
+ }
85
+ };
86
+ scan(nodeModules);
87
+
88
+ const platform = process.platform;
89
+ const envVar = platform === 'win32' ? 'PATH' : (platform === 'darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH');
90
+ const sep = platform === 'win32' ? ';' : ':';
91
+
92
+ // Inject native paths into the OS environment
93
+ process.env[envVar] = Array.from(libPaths).join(sep) + sep + (process.env[envVar] || '');
94
+
95
+ const entryPoint = join(cacheDir, '___BUNDLERBUS_ENTRY___');
96
+ await import(entryPoint);
97
+
98
+ } catch (err) {
99
+ console.error('[BOOTSTRAP] Fatal Error:', err);
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ bootstrap();
package/bundler.js ADDED
@@ -0,0 +1,169 @@
1
+ // bundler.js
2
+ import { createWriteStream, existsSync, readdirSync, statSync, lstatSync, readFileSync, rmSync } from 'fs';
3
+ import { createGzip } from 'zlib';
4
+ import tar from 'tar-stream';
5
+ import { join, resolve, relative } from 'path';
6
+ import { minimatch } from 'minimatch';
7
+
8
+ // Standard files npm always includes implicitly
9
+ const NPM_STANDARD_FILES = [
10
+ 'package.json',
11
+ 'README.md',
12
+ 'README',
13
+ 'LICENSE',
14
+ 'LICENCE',
15
+ 'NOTICE'
16
+ ];
17
+
18
+ // Directories/files to always exclude from the scan
19
+ const ALWAYS_EXCLUDE = [
20
+ '.git',
21
+ '.github',
22
+ '.vscode',
23
+ '.idea',
24
+ 'dist',
25
+ 'build',
26
+ '.DS_Store',
27
+ 'Thumbs.db',
28
+ '*.log',
29
+ '.env',
30
+ '.env.local',
31
+ 'payload.tar.gz',
32
+ 'bootstrap.js'
33
+ ];
34
+
35
+ export async function createPayload(packageJson, entryPoint) {
36
+ console.log('[BUNDLER] Creating payload...');
37
+
38
+ const pack = tar.pack();
39
+ const gzip = createGzip();
40
+ const output = createWriteStream('payload.tar.gz');
41
+
42
+ pack.pipe(gzip).pipe(output);
43
+
44
+ const processedPaths = new Set();
45
+ const root = process.cwd();
46
+
47
+ // Prepare Whitelist Patterns
48
+ const hasFilesField = packageJson.files && Array.isArray(packageJson.files);
49
+ const patterns = hasFilesField ? packageJson.files : ['**/*'];
50
+
51
+ /**
52
+ * Checks if a relative path matches the package.json "files" whitelist
53
+ */
54
+ function isWhitelisted(relPath) {
55
+ const normalizedRel = relPath.replace(/\\/g, '/');
56
+
57
+ return patterns.some(pattern => {
58
+ // 1. Glob match (e.g., "src/**/*.js")
59
+ if (minimatch(normalizedRel, pattern)) return true;
60
+
61
+ // 2. Directory prefix match (e.g., pattern "src" matches "src/app.js")
62
+ const dirPrefix = pattern.endsWith('/') ? pattern : `${pattern}/`;
63
+ if (normalizedRel.startsWith(dirPrefix)) return true;
64
+
65
+ return false;
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Global exclusion filter
71
+ */
72
+ function isGlobalExcluded(path) {
73
+ const baseName = path.split(/[\\/]/).pop().toLowerCase();
74
+ for (const pattern of ALWAYS_EXCLUDE) {
75
+ if (pattern.startsWith('*')) {
76
+ const ext = pattern.substring(1);
77
+ if (baseName.endsWith(ext)) return true;
78
+ } else if (baseName === pattern || path.includes(`/${pattern}/`) || path.includes(`\\${pattern}\\`)) {
79
+ return true;
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+
85
+ function addEntry(localPath, archivePath) {
86
+ if (!existsSync(localPath)) return;
87
+
88
+ // 1. Prevent duplicate packing and circular refs
89
+ const normalized = resolve(localPath);
90
+ if (processedPaths.has(normalized)) return;
91
+ processedPaths.add(normalized);
92
+
93
+ // 2. Symlink protection (Fixes infinite loops in 'bun link')
94
+ const lstats = lstatSync(localPath);
95
+ if (lstats.isSymbolicLink()) return;
96
+
97
+ // 3. Skip global junk
98
+ if (isGlobalExcluded(localPath) && !localPath.endsWith('node_modules')) {
99
+ return;
100
+ }
101
+
102
+ const relPath = relative(root, localPath);
103
+ const stats = statSync(localPath);
104
+ const isDirectory = stats.isDirectory();
105
+ const isNodeModules = localPath.includes('node_modules');
106
+
107
+ // 4. Whitelist Enforcement
108
+ // Directories are always entered to find files, but files must be whitelisted
109
+ if (!isNodeModules && !isDirectory && !isWhitelisted(relPath)) {
110
+ const baseName = relPath.split(/[\\/]/).pop();
111
+ const isImplicit = NPM_STANDARD_FILES.includes(baseName) || relPath === 'package.json';
112
+ if (!isImplicit) return;
113
+ }
114
+
115
+ if (isDirectory) {
116
+ try {
117
+ const items = readdirSync(localPath);
118
+ for (const item of items) {
119
+ addEntry(join(localPath, item), join(archivePath, item));
120
+ }
121
+ } catch (err) {
122
+ console.warn(`[BUNDLER] Could not read directory ${localPath}: ${err.message}`);
123
+ }
124
+ } else {
125
+ // 5. File Packing & Optimization
126
+ const ext = localPath.split('.').pop().toLowerCase();
127
+
128
+ // Slim down node_modules by skipping heavy source/map files
129
+ if (isNodeModules) {
130
+ const isJunk = ['map', 'ts', 'tsx', 'h', 'cpp', 'c', 'cc', 'md', 'txt'].includes(ext);
131
+ if (isJunk) return;
132
+ }
133
+
134
+ try {
135
+ const content = readFileSync(localPath);
136
+ // Ensure TAR internal paths always use forward slashes for cross-platform extraction
137
+ const tarPath = archivePath.replace(/\\/g, '/');
138
+ pack.entry({ name: tarPath, mode: stats.mode }, content);
139
+ } catch (err) {
140
+ console.warn(`[BUNDLER] Failed to pack file ${localPath}: ${err.message}`);
141
+ }
142
+ }
143
+ }
144
+
145
+ // PHASE 1: Scan Project Root (Filters via Whitelist)
146
+ const rootEntries = readdirSync('.');
147
+ for (const item of rootEntries) {
148
+ if (item === 'node_modules' || item === 'dist') continue;
149
+ addEntry(item, item);
150
+ }
151
+
152
+ // PHASE 2: Pack node_modules (Implicitly required for native bindings)
153
+ console.log('[BUNDLER] Packing node_modules...');
154
+ addEntry('./node_modules', 'node_modules');
155
+
156
+ pack.finalize();
157
+
158
+ await new Promise((resolve, reject) => {
159
+ output.on('finish', resolve);
160
+ output.on('error', reject);
161
+ });
162
+
163
+ console.log('[BUNDLER] Payload created: payload.tar.gz');
164
+ }
165
+
166
+ export function cleanupPayload() {
167
+ if (existsSync('payload.tar.gz')) rmSync('payload.tar.gz');
168
+ if (existsSync('bootstrap.js')) rmSync('bootstrap.js');
169
+ }
package/cli.js ADDED
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ // cli.js
3
+ import { execSync } from 'child_process';
4
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
5
+ import { resolve, dirname } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import { createPayload, cleanupPayload } from './bundler.js';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+
11
+ function resolveEntryPoint(args) {
12
+ // Try package.json first
13
+ let packageJson = {};
14
+ if (existsSync('./package.json')) {
15
+ try {
16
+ packageJson = JSON.parse(readFileSync('./package.json', 'utf8'));
17
+ } catch (err) {
18
+ console.error('[FAILURE] Could not parse package.json:', err.message);
19
+ process.exit(1);
20
+ }
21
+ }
22
+
23
+ // Check for explicit entry point in args
24
+ const firstArg = args[0];
25
+
26
+ // If first arg looks like a file (ends with .js/.ts/.mjs), use it
27
+ if (firstArg && !firstArg.startsWith('-') && /\.(js|mjs|ts|tsx)$/.test(firstArg)) {
28
+ if (!existsSync(firstArg)) {
29
+ console.error(`[FAILURE] Entry point not found: ${firstArg}`);
30
+ process.exit(1);
31
+ }
32
+ return { entryPoint: firstArg, packageJson, bunFlags: args.slice(1) };
33
+ }
34
+
35
+ // No explicit entry, check package.json
36
+ let entryPoint = null;
37
+
38
+ // Check "bin" field
39
+ if (packageJson.bin) {
40
+ if (typeof packageJson.bin === 'string') {
41
+ entryPoint = packageJson.bin;
42
+ } else if (typeof packageJson.bin === 'object') {
43
+ const binEntries = Object.values(packageJson.bin);
44
+ if (binEntries.length === 1) {
45
+ entryPoint = binEntries[0];
46
+ } else if (binEntries.length > 1) {
47
+ console.error('[FAILURE] Multiple bin entries found in package.json');
48
+ console.error('Please specify entry point explicitly:');
49
+ console.error(' bundlerbus <entry-point> [bun-flags...]');
50
+ process.exit(1);
51
+ }
52
+ }
53
+ }
54
+
55
+ // Check "main" field
56
+ if (!entryPoint && packageJson.main) {
57
+ entryPoint = packageJson.main;
58
+ }
59
+
60
+ // Fallback checks
61
+ if (!entryPoint) {
62
+ const fallbacks = ['./index.js', './src/index.js', './main.js'];
63
+ for (const fallback of fallbacks) {
64
+ if (existsSync(fallback)) {
65
+ entryPoint = fallback;
66
+ break;
67
+ }
68
+ }
69
+ }
70
+
71
+ if (!entryPoint) {
72
+ console.error('[FAILURE] Could not determine entry point');
73
+ console.error('Please specify entry point explicitly:');
74
+ console.error(' bundlerbus <entry-point> [bun-flags...]');
75
+ console.error('Or add "bin" or "main" field to package.json');
76
+ process.exit(1);
77
+ }
78
+
79
+ return { entryPoint, packageJson, bunFlags: args };
80
+ }
81
+
82
+ function generateBootstrap(entryPoint, packageJson) {
83
+ const templatePath = resolve(__dirname, 'bootstrap.template.js');
84
+ const template = readFileSync(templatePath, 'utf8');
85
+
86
+ // Normalize entry point (remove leading ./)
87
+ const normalizedEntry = entryPoint.replace(/^\.\//, '');
88
+
89
+ // Get app metadata
90
+ const appName = packageJson.name || 'bundlerbus-app';
91
+ const appVersion = packageJson.version || '0.0.0';
92
+
93
+ // Replace placeholders
94
+ const bootstrap = template
95
+ .replace('___BUNDLERBUS_ENTRY___', normalizedEntry)
96
+ .replace('___BUNDLERBUS_APP_NAME___', appName)
97
+ .replace('___BUNDLERBUS_APP_VERSION___', appVersion);
98
+
99
+ writeFileSync('./bootstrap.js', bootstrap);
100
+ console.log('[SUCCESS] Generated bootstrap.js');
101
+ }
102
+
103
+ async function build() {
104
+ console.log('='.repeat(50));
105
+ console.log('BUNDLERBUS - Native Bindings Bundler');
106
+ console.log('='.repeat(50));
107
+
108
+ const args = process.argv.slice(2);
109
+
110
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
111
+ console.log(`
112
+ Usage:
113
+ bundlerbus <entry-point> [bun-flags...]
114
+ bundlerbus [bun-flags...]
115
+
116
+ Examples:
117
+ bundlerbus ./src/cli.js --target bun-windows-x64
118
+ bundlerbus ./index.js --outfile ./dist/app.exe --minify
119
+ bundlerbus --target bun-linux-x64 --outfile ./dist/app
120
+
121
+ Entry Point Resolution:
122
+ 1. First positional argument (if it's a .js/.ts file)
123
+ 2. package.json "bin" field
124
+ 3. package.json "main" field
125
+ 4. Fallback to ./index.js or ./src/index.js
126
+
127
+ All flags after the entry point are forwarded to 'bun build --compile'
128
+ `);
129
+ process.exit(0);
130
+ }
131
+
132
+ try {
133
+ // Step 1: Resolve entry point
134
+ const { entryPoint, packageJson, bunFlags } = resolveEntryPoint(args);
135
+ console.log(`[INFO] Entry point: ${entryPoint}`);
136
+ console.log(`[INFO] App: ${packageJson.name}@${packageJson.version}`);
137
+
138
+ // Step 2: Create payload
139
+ await createPayload(packageJson, entryPoint);
140
+
141
+ // Step 3: Generate bootstrap
142
+ generateBootstrap(entryPoint, packageJson);
143
+
144
+ // Step 4: Build with Bun
145
+ console.log('[INFO] Compiling with Bun...');
146
+ const bunCmd = ['bun', 'build', '--compile', './bootstrap.js', ...bunFlags].join(' ');
147
+ console.log(`[INFO] Running: ${bunCmd}`);
148
+
149
+ execSync(bunCmd, { stdio: 'inherit' });
150
+
151
+ console.log('[SUCCESS] Build complete!');
152
+
153
+ } catch (err) {
154
+ console.error('[FAILURE] Build failed:', err.message);
155
+ process.exit(1);
156
+ } finally {
157
+ // Step 5: Cleanup
158
+ console.log('[INFO] Cleaning up temporary files...');
159
+ cleanupPayload();
160
+ }
161
+
162
+ console.log('='.repeat(50));
163
+ }
164
+
165
+ build();
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "bundlerbus",
3
+ "version": "1.0.0",
4
+ "description": "Universal native bindings bundler for Bun's --compile flag",
5
+ "type": "module",
6
+ "bin": {
7
+ "bundlerbus": "./cli.js"
8
+ },
9
+ "main": "./cli.js",
10
+ "files": [
11
+ "cli.js",
12
+ "bundler.js",
13
+ "bootstrap.template.js"
14
+ ],
15
+ "scripts": {
16
+ "test": "echo \"Error: no test specified\" && exit 1"
17
+ },
18
+ "keywords": [
19
+ "bun",
20
+ "compile",
21
+ "native",
22
+ "bindings",
23
+ "bundler",
24
+ "sharp",
25
+ "sqlite",
26
+ "canvas",
27
+ "serialport",
28
+ "single-executable",
29
+ "packaging"
30
+ ],
31
+ "author": "Pedro Costa <pedrohenriqueteodorodacosta@gmail.com>",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/phtdacosta/bundlerbus.git"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0",
39
+ "bun": ">=1.0.0"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/phtdacosta/bundlerbus/issues"
43
+ },
44
+ "homepage": "https://github.com/phtdacosta/bundlerbus#readme",
45
+ "dependencies": {
46
+ "minimatch": "^10.1.1",
47
+ "tar-stream": "^3.1.7"
48
+ },
49
+ "peerDependencies": {},
50
+ "devDependencies": {}
51
+ }