npm package: @selfage/bundler_cli
Description: CLI for bundling and running bundled frontend or backend TypeScript files.
Author: Yuxuan Xie
Latest version: 5.7.2

# @seflage/bundler_cli

Provides an **extremely opinionated** bundling tool for developing frontend and backend in TypeScript **only**, powered by `browserify` and `uglify-js`, supporting importing asset files without extra plugins. Command line tool name is `bundage`.

Note that despite TypeScript can compile with various options, we expect you to set `"module": "commonjs"` and `"moduleResolution": "node"`, due to the use of `browserify`.

## Install

`npm install @selfage/bundler_cli`

## Quick Start

```bash
# Run TypeScript in Node.js (compile + bundle + execute)
bundage nrun my_script.ts

# Run tests in headless Chrome (compile + bundle + test in real browser)
bundage prun my_test.ts

# Debug tests visually in Chrome
bundage prun my_test.ts --no-headless --debug

# Bundle multiple web apps for production
bundage bwa -o ./dist

# Bundle a Node.js server with assets
bundage bns server.ts server.js -t ./dist
```

**💡 Tip:** Run `bundage <command> -h` to see all options for any command.

## Quick Decision: Is This Tool Right for You?

**✅ Use `bundage` if you:**
- Want to write **everything in TypeScript** (no HTML/CSS files)
- Prefer **programmatic DOM creation** over templates
- Value **type safety** over traditional web development patterns
- Are building **data-driven UIs** or **Node.js services**

**❌ Use webpack/Vite instead if you:**
- Need to write **HTML templates** and **CSS files**
- Use frameworks like **React, Vue, Angular, or Svelte**
- Want **CSS preprocessors** (Sass, Less, PostCSS)
- Prefer the **traditional** separation of HTML/CSS/JS

**Still unsure?** Read the [Philosophy section](#philosophy-typescript-first-no-htmlcss-files) below.

## Philosophy: TypeScript-First, No HTML/CSS Files

### Everything is TypeScript

Unlike traditional web bundlers (webpack, Vite, Rollup), `bundage` assumes **all your UI is created programmatically in TypeScript**. There are no separate HTML or CSS files to author. After all, if I can develop backend in TypeScript only, why not for frontend?

**What bundage does:**
```typescript
// app.ts - Your entire application
import { E } from '@selfage/element/factory';
import { Ref } from '@selfage/ref';
import image = require('./logo.png');

// Create DOM elements programmatically
let button = new Ref<HTMLButtonElement>();
let container = E.div(
  {
    class: 'container',
    style: `background: #f0f0f0; padding: 20px;`
  },
  E.h1({}, E.text('My App')),
  E.img({ src: image }),
  E.button({ ref: button }, E.text('Click Me'))
);
button.val.addEventListener('click', () => alert('Clicked!'));

document.body.append(container.element);
```

The bundler generates a **minimal HTML shell** that just loads your bundled JS:
```html
<!-- Generated by bundage -->
<html>
  <head><script src="bundle.js"></script></head>
  <body></body>
</html>
```

### Comparison with Other Bundlers

| Aspect | bundage (this) | webpack/Vite/Rollup |
|--------|----------------|---------------------|
| **HTML Files** | ❌ Auto-generated shell only | ✅ Author index.html directly |
| **CSS Files** | ❌ Inline styles or CSS-in-JS | ✅ Import .css files |
| **DOM Creation** | ✅ Programmatic (TypeScript) | 🔧 Template-based (HTML) |
| **Style Approach** | TypeScript strings/objects | CSS modules, PostCSS, Sass |
| **Component Model** | Type-safe factory functions | JSX, Vue templates, Svelte |
| **Asset Imports** | ✅ Images, fonts, etc. | ✅ Images, CSS, fonts, etc. |

### Traditional Approach (webpack/Vite)

```html
<!-- index.html - You write this -->
<html>
  <body>
    <div class="container">
      <h1>My App</h1>
      <img src="./logo.png">
      <button id="myBtn">Click Me</button>
    </div>
  </body>
</html>
```

```css
/* style.css - You write this */
.container {
  background: #f0f0f0;
  padding: 20px;
}
```

```typescript
// app.ts - Just add behavior
document.getElementById('myBtn').onclick = () => alert('Clicked!');
```

```typescript
// Entry point
import './style.css';
import './app.ts';
```

### Why This Matters

**✅ You should use bundage if:**
- You prefer **type-safe DOM manipulation** (no `querySelector` runtime errors)
- You want **everything in TypeScript** (no context switching between HTML/CSS/TS)
- You use component libraries like `@selfage/element` or similar
- You're building **programmatic UIs** or **data-driven interfaces**
- You want to avoid the complexity of JSX/template compilation

**❌ You should NOT use bundage if:**
- You prefer writing **HTML templates** directly
- Your designers provide **static HTML/CSS** files
- You want to use **CSS preprocessors** (Sass, Less, PostCSS)
- You're using **frameworks** like React, Vue, Angular, or Svelte
- You need **existing HTML/CSS** from templates or themes

### Asset Handling

**What IS supported:**
```typescript
// Images, fonts, data files - any binary assets
import logoPath = require('./logo.png');
import fontPath = require('./font.woff2');
import dataPath = require('./config.json');

// Use in your TypeScript-generated DOM
E.img({ src: logoPath });
```

**What is NOT supported:**
```typescript
// ❌ Cannot import CSS files
import './styles.css';  // Won't work

// ❌ Cannot use HTML files as templates
import template from './template.html';  // Won't work
```

**Solution:** Write styles inline or use CSS-in-JS:
```typescript
// Inline styles
E.div(
  { style: `color: blue; font-size: 16px;` },
  E.text('Styled content')
);

// Or style programmatically
let div = E.div({}, E.text('Content'));
div.element.style.color = 'blue';
div.element.style.fontSize = '16px';
```

### Real-World Example

**bundage way:**
```typescript
// main.ts
import { E } from '@selfage/element/factory';

class App {
  public constructor(document: Document) {
    document.body.append(
      E.div(
        {
          class: 'app',
          style: 'max-width: 800px; margin: 0 auto;'
        },
        E.div(
          {
            class: 'header',
            style: 'background: #333; color: white; padding: 20px;'
          },
          E.h1({}, E.text('My application'))
        )
      )
    );
  }
}

new App(document);
```

**Traditional way (Vite + React):**
```tsx
// App.tsx
import './App.css';

export function App() {
  return (
    <div className="app">
      <div className="header">
        <h1>My Application</h1>
      </div>
    </div>
  );
}
```

```css
/* App.css */
.app { max-width: 800px; margin: 0 auto; }
.header { background: #333; color: white; padding: 20px; }
```

### Summary

`bundage` is designed for developers who want to **write everything in TypeScript**, with type safety from top to bottom. If you prefer the traditional separation of HTML/CSS/JS, or use frameworks that rely on templates, this tool is **not** for you. Use webpack, Vite, or Rollup instead.

## Why This Approach Excels for Testing

The TypeScript-first philosophy makes **browser testing extremely simple**:

### Testing with `bundage prun`

```typescript
// my_component_test.ts
import { E } from '@selfage/element/factory';

// Create component
let button = E.button({}, E.text('Click'));
button.addEventListener('click', () => alert('works!'));
document.body.append(button);

// Test it
button.click();  // Type-safe!
console.log('Button text:', button.textContent);

// Test with real assets
import imagePath = require('./test-image.png');
let img = E.img({ src: imagePath });
document.body.append(img);
console.log('Image loaded from:', img.src);
```

```bash
# Run in headless Chrome with one command
bundage prun my_component_test.ts -a .png

# Debug visually with Chrome open
bundage prun my_component_test.ts -a .png --no-headless --debug
```

**vs. Traditional Testing (Jest + React Testing Library):**
```typescript
// Setup required: jest.config.js, babel config, etc.
import { render, fireEvent } from '@testing-library/react';

test('button works', () => {
  const { getByText } = render(<Button>Click</Button>);
  fireEvent.click(getByText('Click'));
  // Mock DOM, no real rendering
});
```

### Advantages

| Feature | bundage prun | Jest + Testing Library |
|---------|--------------|----------------------|
| **Setup** | Zero config | Complex config files |
| **DOM** | Real Chrome | jsdom (limited) |
| **Assets** | Real loading | Mocked |
| **Debugging** | Chrome DevTools | Node debugger |
| **Visual** | `--no-headless` flag | Not possible |
| **Type Safety** | Full TypeScript | Partial (test utils) |

### Testing Comparison

**bundage approach:**
```typescript
// Everything is type-safe TypeScript
import { E } from '@selfage/element/factory';
import { Ref } from '@selfage/ref';

let button = new Ref<HTMLButtonElement>();
let app = E.div(
  {},
  E.button({ ref: button }, E.text('Click'))
);
button.val.addEventListener('click', handleClick);

// Tests run in real Chrome
bundage prun test.ts -a .png .css --no-headless
```

**Traditional approach:**
```jsx
// JSX needs compilation, types are partial
import { render } from '@testing-library/react';

function App() {
  return <div><button onClick={handleClick}>Click</button></div>;
}

// Tests run in jsdom (not real browser)
jest test.ts
```

## Commands

`bundage` provides four main commands. Use `-h` flag to see detailed options for each:

```bash
bundage nrun -h    # Run in Node.js
bundage prun -h    # Run in Puppeteer (headless Chrome)
bundage bwa -h     # Bundle Web Apps
bundage bns -h     # Bundle Node Server
```

### Command Overview

**`bundage nrun <sourceFile> [args...]`** (alias: `runInNode`)
- Compiles, bundles, and runs TypeScript in Node.js
- Pass-through args: `bundage nrun script.ts -- --arg1 value1`

**`bundage prun <sourceFile> [args...]`** (alias: `runInPuppeteer`)
- Runs in headless Chrome with real DOM (based on [@selfage/puppeteer_test_executor](https://www.npmjs.com/package/@selfage/puppeteer_test_executor))
- Additional flags: `-p <port>` (default: 8000), `-nh` (visible browser mode)
- Pass-through args: `bundage prun test.ts -- --test-flag`

**`bundage bwa`** (alias: `bundleWebApps`)
- Bundles multiple SPAs from config file (default: `./web_app_entries.yaml`)
- Generates HTML, minifies, and compresses with Gzip
- Config schema: [WebAppEntries](https://github.com/selfage/bundler_cli/blob/main/web_app_entries_def.ts)
- Additional flags: `-ec <configFile>`, `-o <outDir>`

**`bundage bns <serverSourceFile> <serverOutputFile>`** (alias: `bundleNodeServer`)
- Bundles Node.js server and copies assets (NPM modules kept as external dependencies)
- Additional flags: `-f <fromDir>`, `-t <toDir>`

### Common Options

These options are available across most commands:

- **`-e, --extra-files <files...>`** - Extra TypeScript files to bundle before the source file
- **`-i, --inline-js <code...>`** - Inline JavaScript code to bundle before all files
- **`-a, --asset-exts <exts...>`** - File extensions to treat as assets (e.g., `-a .png .jpg`)
  - Alternatively: add `"assetExts": [".png", ".jpg"]` in your `package.json`
- **`-s, --skip-minify`** - Skip minification (useful for debugging)
- **`-d, --debug`** - Include inline source maps and source code
- **`-c, --tsconfig-file <file>`** - Path to `tsconfig.json` (defaults to `./tsconfig.json`)

## Options explained

### Extra file

See [this answer](https://stackoverflow.com/questions/38906359/create-a-global-variable-in-typescript/67040805#67040805) for how to use extra files to properly define global `environment` variable with the help of globalThis.

### Asset files

As explained in all helper manuals, you can also `import imagePath = require('./image.png');` with `-a .png .gif .jpg` flag, which doesn't really import the image but only import its path. When bundle for Node, it's the relative path from the bundled output file. When bundle for browser, it's the url path that is expected to be mapped to a relative path from the root directory. E.g., the url path would work if you start a [http-server](https://www.npmjs.com/package/http-server) at `--root-dir`. Now you can load images, CSS files or assets without worrying about their actual URL paths -- they will be inferred at bundling time.

Equivalently, you can define `"assetExts": [".png", ".gif", ".jpg"]` in your `package.json` file to save you from typing the list everytime.

### Debug

Note that `--debug` doesn't guarantee stack traces will be mapped to TypeScript source code. You could consider using `source-map-support` package. E.g., you can `import 'source-map-support/register';` in your main file.

## Puppeteer executor environment

[Puppetter](https://www.npmjs.com/package/puppeteer) is essentially a headless Chrome. The term "Puppeteer executor environment" refers to the runtime environment provided by `$ bundage prun` or `$ bundage pexe` which is based on Puppeteer and provides APIs for testing purposes within browser context.

### Pass-through arguments

Pass-through arguments are made available by writing them to the temp HTML file, which can then be accessed by the JS code running in it. See [@selfage/puppeteer_executor_api#access-argv](https://github.com/selfage/puppeteer_executor_api#access-argv) for more details.

### Function APIs

Functions are made avaialble thanks to Puppeteer's `exposeFunction`. See [@selfage/puppeteer_executor_api#all-apis](https://github.com/selfage/puppeteer_executor_api#all-apis) for all available APIs. It's easier to use with `@selfage/puppeteer_test_runner` which cleans up after each test, e.g. closes the browser.

## General API access

Each sub-command corresponds to an API as the following.

`runInNode` -> `import { runInNode } from '@selfage/bundler_cli/runner_in_node';`

`runInPuppeteer` -> `import { runInPuppeteer } from '@selfage/bundler_cli/runner_in_puppeteer';`

`bundleWebApps` -> `import { bundleWebApps } from '@selfage/bundler_cli/web_apps_bundler';`

`bundleNodeServer` -> `import { bundleNodeServer } from '@selfage/bundler_cli/node_server_bundler';`