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 +351 -0
- package/bootstrap.template.js +104 -0
- package/bundler.js +169 -0
- package/cli.js +165 -0
- package/package.json +51 -0
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
|
+
}
|