HakobuHakobu

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/app

When 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)
  1. Rolldown bundles the entry and all reachable dependencies into one or more .js chunks
  2. Post-bundle patches fix known bundler-hostile patterns
  3. A minimal package.json is generated alongside the bundle
  4. The Hakobu packaging pipeline runs on the temp directory
  5. 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-linux

Programmatic 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:

  1. package.json "main" field
  2. package.json "module" field
  3. src/index.ts
  4. src/index.js
  5. index.ts
  6. index.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_modules in 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 .map files in the snapshot. Enable with NODE_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):

PatternReason
node:*Node.js built-in modules
bun:*Bun-specific modules (stubbed)
electronElectron framework
chromium-bidiOptional 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

AspectNative ModeBundle Mode
InputJavaScript (JS/MJS/CJS)Any (TS, JSX, etc.)
DependenciesTraced from diskBundled into output files
__dirnamePoints to snapshot file dirPoints to snapshot entry dir (polyfilled)
Source mapsPreserved (file-level)Sidecar .map files
Tree-shakingNoYes
Workspace depsMust be built firstResolved by Rolldown
File countMany (mirrors source)1+ (code-split chunks)
DefaultYesNo (--bundle required)
Bundler requiredNoYes (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, --bundle fails 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.

On this page