# How ego-lite's Build System Produces Both Per-File dist and Rollup Bundles

> Discover how ego-lite's build system creates per-file dist and Rollup bundles using a two-stage Node.js script with esbuild and Rollup for granular modules and executable artifacts.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-08-01

---

**ego-lite uses a two-stage Node.js build script that first transpiles individual TypeScript files with esbuild, then bundles the CLI entry point with Rollup to generate both granular modules and a single executable artifact.**

The `ego-browser` package in the citrolabs/ego-lite repository implements a hybrid build pipeline designed for flexibility. Developers get fine-grained per-file outputs for testing and library consumption, while end users receive a compact, single-file CLI bundle. This dual-output strategy is orchestrated entirely within `scripts/build.mjs`.

## The Two-Stage Build Architecture

The build system in `ego-browser` follows a deliberate sequence: **non-bundled transpilation first, bundled artifact second**. This ensures source maps remain accurate for individual files while the final CLI remains lean and self-contained.

### Stage 1: Per-File esbuild Transpilation

The script begins by invoking `esbuild.build()` with `bundle: false` (lines 58-64 of `scripts/build.mjs`). This configuration is critical—it instructs esbuild to treat each `.ts` file as an independent compilation unit rather than resolving imports into a single output.

Key configuration parameters include:

- `outdir: "dist"` — sets the output root directory
- `outbase: "."` — preserves the original directory structure relative to the project root
- `bundle: false` — disables bundling to maintain file-to-file correspondence

The result is a `dist/` directory that mirrors your source layout exactly. Files from `scripts/` and `src/` appear as individual `.js` files, enabling:

```bash

# Import specific modules for unit testing

import { browserRuntime } from './dist/src/browser-runtime.js'

```

This per-file ego-lite build output supports **incremental compilation** and simplifies debugging since stack traces map directly to original source positions.

### Stage 2: Rollup Bundle Generation

After esbuild completes, the script constructs a Rollup configuration (lines 68-82) targeting the main CLI entry point at [`src/index.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts). This stage produces the distributed executable.

The Rollup configuration treats Node.js built-ins as external (line 70) to avoid bundling core modules unnecessarily. Two plugins handle resolution and compilation:

- **`@rollup/plugin-node-resolve`** (line 72) — resolves `node_modules` dependencies
- **`@rollup/plugin-typescript`** (lines 73-80) — compiles TypeScript with project-specific settings

The bundle writes to [`dist/out/index.js`](https://github.com/citrolabs/ego-lite/blob/main/dist/out/index.js) with ESM format (`format: "esm"`), producing a single-file CLI artifact. This path is referenced directly in [`package.json`](https://github.com/citrolabs/ego-lite/blob/main/package.json) (line 7) via the `bin` field:

```json
{
  "bin": {
    "ego-browser": "./dist/out/index.js"
  }
}

```

## Embedding Runtime Documentation

Following bundle generation, the script executes `embedHelpDocs()` (lines 96-119). This utility:

1. Parses JSDoc comments from the Rollup output
2. Extracts help text for CLI commands
3. Injects the documentation into [`help-runtime.js`](https://github.com/citrolabs/ego-lite/blob/main/help-runtime.js)

The embedded approach eliminates runtime file system reads for help text, keeping the CLI fully self-contained.

## Resulting Build Artifacts

| Artifact | Tool | Location | Purpose |
|----------|------|----------|---------|
| Per-file modules | esbuild | `dist/**/*.js` | Library imports, testing, debugging |
| CLI bundle | Rollup | [`dist/out/index.js`](https://github.com/citrolabs/ego-lite/blob/main/dist/out/index.js) | Executable command-line tool |

## Running the Build Locally

Execute the complete pipeline as defined in [`package.json`](https://github.com/citrolabs/ego-lite/blob/main/package.json):

```bash
npm run build   # triggers node scripts/build.mjs

```

The resulting structure:

```text
dist/
├── src/
│   ├── helpers.js
│   ├── browser-runtime.js
│   └── index.js          # Also available standalone

├── scripts/
│   └── *.js              # Build utilities transpiled

└── out/
    └── index.js          # Bundled CLI for distribution

```

## Using the Built CLI

The Rollup bundle executes directly without additional dependencies:

```bash
./dist/out/index.js <<'JS'
await ego.navigate('https://example.com')
await ego.click('button#submit')
JS

```

## Key Implementation Files

| File | Role |
|------|------|
| `scripts/build.mjs` | Orchestrates esbuild and Rollup stages |
| [`package.json`](https://github.com/citrolabs/ego-lite/blob/main/package.json) | Declares CLI entry point and npm scripts |
| [`tsconfig.json`](https://github.com/citrolabs/ego-lite/blob/main/tsconfig.json) | Shared TypeScript configuration |
| [`src/index.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts) | Rollup entry point for bundle generation |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Helper context injected into user scripts |

## Summary

- **esbuild with `bundle: false`** generates per-file `dist/` outputs preserving source structure for modular consumption
- **Rollup with `@rollup/plugin-typescript`** creates a single [`dist/out/index.js`](https://github.com/citrolabs/ego-lite/blob/main/dist/out/index.js) bundle optimized for CLI distribution
- **`embedHelpDocs()`** inlines help documentation to keep the executable self-contained
- The [`package.json`](https://github.com/citrolabs/ego-lite/blob/main/package.json) `bin` field points exclusively to the Rollup bundle, while the esbuild outputs remain available for programmatic imports

## Frequently Asked Questions

### Why does ego-lite use two different bundlers instead of one?

esbuild handles fast, incremental transpilation of dozens of files efficiently, while Rollup produces superior tree-shaking and cleaner output for Node.js CLI distribution. The combination leverages each tool's strengths: esbuild for speed and granularity, Rollup for bundle optimization.

### Can I import individual modules from the per-file dist output?

Yes. The esbuild-generated files in `dist/src/` and `dist/scripts/` are fully resolvable ESM modules. Import them directly for testing or extending functionality without pulling in the entire CLI bundle.

### Is the Rollup bundle strictly required for the CLI to function?

Yes. The [`package.json`](https://github.com/citrolabs/ego-lite/blob/main/package.json) bin entry points exclusively to [`dist/out/index.js`](https://github.com/citrolabs/ego-lite/blob/main/dist/out/index.js). While you could theoretically execute [`dist/src/index.js`](https://github.com/citrolabs/ego-lite/blob/main/dist/src/index.js), it would require manual dependency resolution since esbuild does not bundle `node_modules`. The Rollup step ensures all third-party imports are resolved and inlined.

### What TypeScript configuration does ego-lite's build system use?

Both esbuild and Rollup reference [`tsconfig.json`](https://github.com/citrolabs/ego-lite/blob/main/tsconfig.json) in the `ego-browser` package root. The `@rollup/plugin-typescript` plugin explicitly passes these settings through its configuration object (lines 73-80 of `build.mjs`), ensuring consistent compiler behavior across both build stages.