Bundle Mode
Use Rolldown to bundle your app before packaging with Hakobu.
Bundle mode is an opt-in pre-processing step that runs a JavaScript bundler (Rolldown) before Hakobu's packaging pipeline. It is designed for projects that Hakobu's native analyzer cannot handle directly, such as TypeScript source, workspace monorepos, or dependency graphs that benefit from tree-shaking.
hakobu ./my-project --bundle --output ./dist/appWhen to Use Bundle Mode
Use bundle mode when your project needs compilation or resolution that Node.js alone cannot provide:
- TypeScript source -- Rolldown compiles TS to JS
- Workspace monorepos -- resolves
workspace:*dependencies into a single bundle - Complex dependency trees -- tree-shakes and produces a smaller payload
- Bun/Deno projects -- can bundle code written for other runtimes into Node-compatible JS
For plain JavaScript projects, Native Mode is simpler and more predictable.
How It Works
Source project --> Rolldown --> Bundled JS --> Hakobu packager --> Executable
(TS, monorepo) (step 0) (temp dir) (steps 1-8)- Rolldown bundles the entry and all reachable dependencies into one or more
.jschunks - Post-bundle patches fix known bundler-hostile patterns
- A minimal
package.jsonis generated alongside the bundle - The Hakobu packaging pipeline runs on the temp directory
- The temp directory is cleaned up after packaging (even on failure)
Bundle mode is a pre-processing step, not a different runtime. The packaged executable uses the same snapshot filesystem and runtime bootstrap as native mode.
CLI Usage
# Bundle mode with default bundler (Rolldown)
hakobu ./my-project --bundle --output ./dist/app
# Explicit bundler name (currently only 'rolldown')
hakobu ./my-project --bundle rolldown --output ./dist/app
# Specify entry (required if not discoverable from package.json)
hakobu ./my-project --bundle --entry src/cli.ts --output ./dist/app
# Keep specific modules external (not bundled)
hakobu ./my-project --bundle --external electron --external better-sqlite3
# Combine with target selection
hakobu ./my-project --bundle --target node24-linux-x64 --output ./dist/app-linuxProgrammatic API
import { packageApp } from '@hakobu/hakobu';
await packageApp({
projectRoot: '/path/to/project',
bundle: true, // or 'rolldown'
entry: 'src/index.ts', // optional if package.json has "main"
bundleExternal: ['electron'], // optional
output: './dist/app',
});Entry Resolution
When --entry is not specified, bundle mode resolves the entry from:
package.json"main"fieldpackage.json"module"fieldsrc/index.tssrc/index.jsindex.tsindex.js
If none of these exist, bundling fails with a clear error asking for --entry.
Semantic Differences from Native Mode
Bundle mode produces a working executable, but the internals differ from native mode in specific ways. These are tradeoffs, not bugs.
Output Shape
Rolldown produces code-split output and injects __dirname / __filename polyfills only into the specific chunks that need them. This means:
- No
node_modulesin the snapshot. Dependencies are bundled into JS output files. - Dynamic
import()with variable arguments may break. Only statically traceable imports can be bundled. - Source maps are included as sidecar
.mapfiles in the snapshot. Enable withNODE_OPTIONS=--enable-source-maps.
__dirname and __filename
Rolldown outputs ESM format, so CJS modules that use __dirname or __filename need polyfills. Hakobu's Rolldown adapter scans each emitted chunk and injects polyfills only where needed, preserving code-splitting for all other chunks.
The polyfill derives __dirname and __filename from import.meta.url, giving each chunk its own correct snapshot path at runtime.
External Modules
By default, the following are always kept external (not bundled):
| Pattern | Reason |
|---|---|
node:* | Node.js built-in modules |
bun:* | Bun-specific modules (stubbed) |
electron | Electron framework |
chromium-bidi | Optional Playwright/CDP dependency |
Add more externals with --external:
--external "better-sqlite3"
--external "@my-scope/*"Externalized modules must be available at runtime. They must either be Node.js built-ins or findable on disk (in node_modules relative to the executable, or via NODE_PATH).
Runtime Protocol Stubs
When bundled code imports from bun:* or deno:* protocols, Hakobu's ESM hooks provide stub modules at runtime. Code that conditionally uses these (with a try/catch fallback) will work. Code that unconditionally depends on runtime-specific features will fail with a clear error message.
Post-Bundle Compatibility Patches
Some packages use runtime self-introspection patterns that break after bundling. Hakobu applies targeted fixes automatically:
- playwright-core -- patches for
require.resolve("playwright-core/package.json")and related patterns - Relative package.json traversals -- patches for
require("../../package.json")patterns common in many packages
Patches are applied to the bundled output before packaging and logged individually. They only match specific known patterns -- not arbitrary require() calls.
Comparison: Native vs Bundle Mode
| Aspect | Native Mode | Bundle Mode |
|---|---|---|
| Input | JavaScript (JS/MJS/CJS) | Any (TS, JSX, etc.) |
| Dependencies | Traced from disk | Bundled into output files |
__dirname | Points to snapshot file dir | Points to snapshot entry dir (polyfilled) |
| Source maps | Preserved (file-level) | Sidecar .map files |
| Tree-shaking | No | Yes |
| Workspace deps | Must be built first | Resolved by Rolldown |
| File count | Many (mirrors source) | 1+ (code-split chunks) |
| Default | Yes | No (--bundle required) |
| Bundler required | No | Yes (rolldown) |
Limitations
- Rolldown is the only supported bundler. The adapter interface allows future bundlers, but only Rolldown is implemented today.
- Rolldown is an optional dependency. If it is not installed,
--bundlefails with a message telling you to install it. - Bytecode and bundle cannot be combined. Bundle produces ESM output, which does not support V8 bytecode compilation. Use one or the other.