# Esbuild and Browserify recipe

> Unpack browserify standalone bundles and esbuild/Bun scope-hoisted output: detection markers (__export, __commonJS), strict vs auto heuristic split, and testcase verification commands.

- Repository: pionxzh/wakaru
- GitHub: https://github.com/pionxzh/wakaru
- Human docs: https://grok-wiki.com/public/docs/pionxzh-wakaru-77a438a6cc6b
- Complete Markdown: https://grok-wiki.com/public/docs/pionxzh-wakaru-77a438a6cc6b/llms-full.txt

## Source Files

- `testcases/browserify/README.md`
- `testcases/browserify/dist/index.js`
- `crates/core/src/unpacker/browserify.rs`
- `crates/core/src/unpacker/esbuild.rs`
- `crates/core/src/unpacker/scope_hoist.rs`

---

---
title: "Esbuild and Browserify recipe"
description: "Unpack browserify standalone bundles and esbuild/Bun scope-hoisted output: detection markers (__export, __commonJS), strict vs auto heuristic split, and testcase verification commands."
---

Wakaru splits Browserify and esbuild/Bun bundles through `crates/core/src/unpacker/`: Browserify is detected at position 5 in `detect_bundle_candidate` (after webpack4/5 and before SystemJS), matching a triple-argument outer call whose first argument is a numeric-key module map; esbuild/Bun is detected at position 6 via lazy-helper factories (`__esm`, `__commonJS`) and/or `__export` namespace boundaries. Extracted module strings then pass through the two-phase unpack driver (rules through `UnEsm`, cross-module facts, late cleanup). Bundles without structural markers can still split when `--unpack=auto` enables `heuristic_split`, which falls back to `scope_hoist::split_scope_hoisted` reference-graph clustering.

## Detection order

Structural detectors run in fixed order; first match wins:

| Order | Format | `BundleFormat` |
|-------|--------|----------------|
| 1–3 | webpack5, webpack4, webpack5 chunk | `Webpack5`, `Webpack4` |
| 4 | Browserify standalone | `Browserify` |
| 5 | SystemJS | `SystemJs` |
| 6 | esbuild / Bun | `Esbuild` |
| — | Heuristic fallback (`--unpack=auto` only) | `ScopeHoisted` |

Pure ESM scope-hoisted output from Rollup, Vite, or esbuild/Bun **without** `__export` or `__commonJS` markers does not match the esbuild structural detector and is only split by the heuristic path.

## Browserify standalone bundles

### Structural signature

Browserify emits a self-executing wrapper whose outer expression is a call with exactly three arguments:

1. **Module map** — object literal keyed by non-negative integer module IDs; each value is a two-element array `[factory, deps]`.
2. **Cache object** — typically `{}` (argument 1; not inspected for detection).
3. **Entry IDs** — array of numeric entry module IDs (argument 2).

The detector scans top-level expression statements for `Callee::Expr` nested inside another `CallExpr`, then validates the module-map shape.

### Extraction and normalization

For each module entry, Wakaru:

- Extracts the factory function (named `FnExpr` or `ArrowExpr`).
- Builds a synthetic ES module from the factory body.
- Runs `resolver` marks and renames factory parameters to `require`, `module`, and `exports` when minified names differ.
- Applies the fixer and emits standalone module code.

Output filenames:

| Role | Filename |
|------|----------|
| Single entry | `entry.js` |
| Multiple entries | `entry-{id}.js` per entry ID |
| Dependency module | `module-{id}.js` |

The testcase bundle at `testcases/browserify/dist/index.js` is a minified standalone with numeric modules `1`–`3` and `(require, module, exports)` factories.

## Esbuild and Bun structural detection

Bun's bundler emits the same helper shapes as esbuild; both route through `esbuild::detect_from_module`.

### Phase 1: marker pre-check

Before cloning the AST, the detector scans top-level `var` initializers for:

- **Lazy helpers** — arrow with ≤2 params whose body is another arrow or function (`is_lazy_helper`). Covers minified `__esm` and non-minified `__require` wrappers.
- **`__export` helper shape** — two-param arrow whose body is a single `for…in` over the second parameter.

Detection aborts early when both `helper_syms` and `has_export_helper_shape` are empty.

### `__commonJS` and `__esm` factories

Factories match `var X = helper(arg)` where `helper` is a collected lazy-helper symbol:

| Form | Pattern | Filename source |
|------|---------|-----------------|
| Non-minified CJS | `__commonJS({ "src/foo.js"(exports, module) { … } })` | Sanitized path key |
| Minified | `y(() => { … })` | `{var_name}.js` |

`__commonJS` helpers are distinguished by lazy-helper shape **plus** a reference to `.exports` in the helper body (`collect_commonjs_helper_syms`).

A bundle qualifies when it has CJS factories **or** at least **five** lazy factories (`has_factories`).

### `__export` scope-hoisted boundaries

Scope-hoisted modules are delimited by adjacent pairs:

```javascript
var ns_a = {};
__export(ns_a, { greet: () => greet, … });
```

`detect_export_helper` finds the `__export` binding; `collect_scope_hoisted_boundaries` enumerates namespace + call pairs. Exported bindings are promoted to `export` declarations; cross-boundary references become synthesized `import`/`export` edges. `__toESM` and dynamic-require helpers are detected by body shape and rewritten during emission (not left as synthetic imports).

Scope-only bundles (no factories) still match when `__export` boundaries exist and either multiple namespaces are present or a single namespace is re-exported at module scope.

## Strict vs auto heuristic split

| Mode | CLI flag | `heuristic_split` | Behavior |
|------|----------|-------------------|----------|
| Auto (default) | `--unpack` / `--unpack=auto` | `true` | Structural detectors, then `scope_hoist` fallback |
| Strict | `--unpack=strict` | `false` | Structural detectors only; unmatched input decompiles as one file |

**Auto fallback flow** (`driver/unpack/mod.rs`):

1. `try_unpack_bundle` — webpack → browserify → systemjs → esbuild.
2. If `None` and `heuristic_split`: `scope_hoist::split_scope_hoisted`.
3. If split yields `>1` modules → unpack with `BundleFormat::ScopeHoisted`.
4. Otherwise → single-file `decompile` → `module.js`.

**Nested scope split** (auto + `--level aggressive`): after a structural match, `maybe_split_scope_hoisted_modules` re-runs the heuristic splitter on each extracted module. Import paths must resolve against sibling filenames or the parent module is kept intact.

Directory scanning (`is_detected_unpack_input`) uses the same gate: strict mode skips heuristic candidates; auto mode includes files that heuristic-split into multiple modules.

## Heuristic scope-hoist splitter

`scope_hoist::split_scope_hoisted` handles flat concatenated ESM without bundler markers (Rollup/Vite output, IIFE-wrapped esbuild ESM).

| Gate | Threshold |
|------|-----------|
| Minimum declarations | 10 top-level items with declared names |
| Minimum clusters | 2 after union-find clustering |
| Module cluster size | ≥2 declarations (else folded into entry) |

Clustering uses five merge signals: mutual references, adjacent dependency chains, inert helpers, adjacency + shared references, and exclusive-consumer merges (conservative fan-out guard). The entry cluster absorbs small clusters, `ModuleDecl` items, and startup code.

During emission, esbuild-specific helpers are recovered when present:

- **Dynamic require** — `typeof require`, `require.apply(this, arguments)`, and `"Dynamic require of"` message.
- **`__toESM`** — `__esModule` member check plus `"default"` `defineProperty`.

## CLI commands

### Browserify testcase

```bash
# Rebuild fixture (optional)
cd testcases/browserify && pnpm install && pnpm build

# Unpack with full decompile pipeline
cargo run -p wakaru-cli -- testcases/browserify/dist/index.js --unpack -o /tmp/browserify-out/

# Raw extraction (no rule pipeline)
cargo run -p wakaru-cli -- testcases/browserify/dist/index.js --unpack --raw -o /tmp/browserify-raw/
```

Expected: multiple modules including `entry.js`; factory bodies decompiled to `import`/`export` rather than `require()` calls.

### Esbuild/Bun fixtures

Generated bundles live under `crates/core/tests/bundles/esbuild-gen/dist/`. Regenerate with Node.js and Bun:

```bash
cd crates/core/tests/bundles/esbuild-gen && bash generate.sh
```

```bash
# Mixed CJS factories + scope-hoisted namespaces
cargo run -p wakaru-cli -- \
  crates/core/tests/bundles/esbuild-gen/dist/es-mixed/bundle.js \
  --unpack -o /tmp/esbuild-mixed/

# Scope-only ESM (structural __export detection)
cargo run -p wakaru-cli -- \
  crates/core/tests/bundles/esbuild-gen/dist/es-scope-only/bundle.js \
  --unpack -o /tmp/esbuild-scope/

# Bun minified comparison fixture
cargo run -p wakaru-cli -- \
  crates/core/tests/bundles/esbuild-gen/dist/bun-mixed-min/bundle.js \
  --unpack -o /tmp/bun-mixed/
```

Compare strict vs auto on markerless bundles:

```bash
cargo run -p wakaru-cli -- flat-bundle.js --unpack=strict -o /tmp/strict/   # single module if no markers
cargo run -p wakaru-cli -- flat-bundle.js --unpack=auto   -o /tmp/auto/     # heuristic split when ≥10 decls
```

## Test verification

Run the focused test binaries after changes:

```bash
# Browserify pipeline snapshot
cargo test -p wakaru-core --test bundle_unpack browserify_unpack_extracts_multiple_modules

# Full browserify + webpack5 bundle suite
cargo test -p wakaru-core --test bundle_unpack

# Esbuild detection, scope-hoisted extraction, heuristic helpers
cargo test -p wakaru-core --test esbuild_unpack

# Generated esbuild/Bun fixture integration
cargo test -p wakaru-core --test esbuild_fixtures

# Heuristic scope-hoist unit tests
cargo test -p wakaru-core -- unpacker::scope_hoist
```

Faster iteration with nextest:

```bash
cargo nextest run -p wakaru-core -E 'test(bundle_unpack) | test(esbuild_unpack) | test(esbuild_fixtures)'
```

Success signals:

- `browserify_unpack_extracts_multiple_modules`: `pairs.len() > 1` and `entry.js` present.
- `esbuild_detects_minified_lazy_helper` / `esbuild_detects_minified_cjs_helper`: ≥6 modules (5 factories + entry).
- `esbuild_scope_hoisted_modules_are_extracted`: ≥8 modules including `ns_a.js`, `ns_b.js`.
- `heuristic_scope_hoist_restores_esbuild_dynamic_require_imports`: `heuristic_split: true`; `require("react")` restored, no synthetic `import { r }`.

Definition-of-done pipeline checks from `AGENTS.md` still apply after rule changes:

```bash
cargo test -p wakaru-core --test noop_pipeline
cargo test -p wakaru-core --test bundle_unpack
cargo fmt --check
cargo clippy -p wakaru-core --all-targets -- -D warnings
```

## Format comparison

```mermaid
flowchart TD
  input[Bundle input] --> structural[try_unpack_bundle]
  structural -->|Browserify match| bfy[browserify.rs extract]
  structural -->|esbuild markers| esb[esbuild.rs extract]
  structural -->|no match| heuristic{heuristic_split?}
  heuristic -->|yes| sh[scope_hoist.rs cluster]
  heuristic -->|no| single[decompile as module.js]
  bfy --> pipeline[Two-phase unpack driver]
  esb --> pipeline
  sh --> pipeline
```

| Aspect | Browserify | esbuild structural | Heuristic scope-hoist |
|--------|------------|--------------------|-----------------------|
| Primary signal | Numeric module map + triple-arg call | Lazy helpers / `__export` pairs | Reference graph on top-level decls |
| Typical input | `browserify -o bundle.js` | `esbuild --bundle --format=esm` | Rollup/Vite flat ESM, markerless esbuild ESM |
| Entry filename | `entry.js` | `entry.js` | `entry.js` |
| Strict mode | Detected | Detected (with markers) | Skipped |

## Failure modes

| Symptom | Likely cause |
|---------|--------------|
| Single `module.js` output | Strict mode on markerless bundle; or heuristic gates not met (<10 declarations, <2 clusters) |
| Browserify not detected | Input lacks triple-arg outer call or non-integer module keys |
| esbuild not detected | Fewer than five lazy factories and no CJS factories; no `__export` boundaries |
| Empty or partial split | Parse failure in `try_unpack_bundle` (returns error, not `None`) |
| Directory scan skips files | `--unpack=strict` and file has no structural signature |

## Related pages

<Card href="/bundle-formats-and-unpacking" title="Bundle formats and unpacking" description="Full detection order, raw vs full unpack, and multi-file directory semantics." />

<Card href="/unpack-bundles" title="Unpack bundles" description="Operational guide for --unpack modes, --raw, --force, and directory scanning." />

<Card href="/webpack-bundle-recipe" title="Webpack bundle recipe" description="Parallel workflow for webpack4/webpack5 testcase verification." />

<Card href="/testing-and-snapshots" title="Testing and snapshots" description="nextest workflow, snapshot acceptance, and required pipeline test matrix." />

<Card href="/decompile-pipeline" title="Decompile pipeline" description="What happens to extracted modules in the two-phase unpack driver." />
