@selfage/bundler_cli
v5.7.2
Published
CLI for bundling and running bundled frontend or backend TypeScript files.
Downloads
32
Readme
@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
# 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 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:
// 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:
<!-- 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)
<!-- 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>/* style.css - You write this */
.container {
background: #f0f0f0;
padding: 20px;
}// app.ts - Just add behavior
document.getElementById('myBtn').onclick = () => alert('Clicked!');// Entry point
import './style.css';
import './app.ts';Why This Matters
✅ You should use bundage if:
- You prefer type-safe DOM manipulation (no
querySelectorruntime errors) - You want everything in TypeScript (no context switching between HTML/CSS/TS)
- You use component libraries like
@selfage/elementor 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:
// 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:
// ❌ Cannot import CSS files
import './styles.css'; // Won't work
// ❌ Cannot use HTML files as templates
import template from './template.html'; // Won't workSolution: Write styles inline or use CSS-in-JS:
// 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:
// 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):
// App.tsx
import './App.css';
export function App() {
return (
<div className="app">
<div className="header">
<h1>My Application</h1>
</div>
</div>
);
}/* 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
// 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);# 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 --debugvs. Traditional Testing (Jest + React Testing Library):
// 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:
// 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-headlessTraditional approach:
// 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.tsCommands
bundage provides four main commands. Use -h flag to see detailed options for each:
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 ServerCommand 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)
- 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
- 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 yourpackage.json
- Alternatively: add
-s, --skip-minify- Skip minification (useful for debugging)-d, --debug- Include inline source maps and source code-c, --tsconfig-file <file>- Path totsconfig.json(defaults to./tsconfig.json)
Options explained
Extra file
See this answer 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 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 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 for more details.
Function APIs
Functions are made avaialble thanks to Puppeteer's exposeFunction. See @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';
