playablebundler
v1.0.3
Published
Custom HTML bundler/compiler from scratch
Readme
pcbuild — HTML Bundler / Compiler
A custom HTML bundler built from scratch in TypeScript. No Webpack, Rollup, Vite, Parcel, esbuild, or template engines. Designed as a compiler with multiple distinct phases operating on an HTML AST.
Pipeline
Input HTML
↓
HTML Parser (tokenizer + recursive descent → AST)
↓
Directive Resolver (validates all @directives)
↓
IncludePass — recursively resolves @include, detects circular includes
↓
CSSPass — collects @css files, inlines into a single <style> tag
↓
JSPass — collects @js entry points, triggers JS bundler
↓
AssetPass — copies @asset files to dist, rewrites paths
↓
InlinePass — inlines @inline text files as text nodes
↓
OptimizationPass — removes empty text nodes, merges adjacent text, strips comments
↓
MinifyPass — collapses whitespace (when --minify is set)
↓
Emitter — generates index.html from the final AST
↓
dist/index.htmlEvery transformation modifies the AST. No string-based replacements.
Directives
Place these in your HTML:
@include("./components/header.html")Recursively loads and inlines the file. Nested includes work. Circular includes throw an error with a readable stack trace.
@css("./styles/main.css")Reads the CSS file and inlines it inside a single <style> tag in <head>.
@js("./scripts/main.js")Parses the entry module with acorn, builds a dependency graph, detects circular imports, and bundles every imported module into a single <script> tag wrapped in a runtime module loader.
@asset("./images/logo.png")Copies the file into dist preserving the relative path.
@inline("./shaders/basic.glsl")Reads the file and inlines its contents as a text node in the HTML.
JavaScript Bundler
The JS bundler (no eval):
- Parses every module with acorn
- Resolves relative imports (with extension resolution)
- Builds a directed dependency graph
- Detects circular imports
- Generates a single script with a runtime module loader
The runtime uses a module registry and cache:
(function() {
var __def = {}, __cache = {};
function __r(id) { /* require implementation */ }
__def["scripts/utils.js"] = function(module, exports, require) { /* ... */ };
__def["scripts/main.js"] = function(module, exports, require) { /* ... */ };
__r("scripts/main.js");
})();Supports:
import default from './module'import { named } from './module'import * as namespace from './module'export defaultexport const / function / classexport { named }export * fromandexport { } from(re-exports)- Circular module references (pre-cached exports)
CLI
# Build once
pcbuild build --entry src/index.html --output dist
# Watch mode (incremental rebuilds)
pcbuild watch --entry src/index.html --output dist
# Dev server (watch + HTTP server)
pcbuild dev --entry src/index.html --output dist --port 3000
# Options
--entry, -e <path> Entry HTML file (default: ./src/index.html)
--output, -o <path> Output directory (default: ./dist)
--minify, -m Enable minification
--port, -p <number> Dev server port (default: 3000)
--root, -r <path> Root directory (default: cwd)Programmatic API
import { compile, watch, dev } from 'pcbuild';
// Single build
await compile({
entry: './src/index.html',
output: './dist',
minify: true,
});
// Watch mode
await watch({ entry: './src/index.html', output: './dist' });
// Dev server
await dev({ entry: './src/index.html', output: './dist' }, 3000);Architecture
src/
├── index.ts # Public API
├── cli.ts # CLI entry point
├── compiler.ts # Compiler orchestrator, plugin hooks
├── project.ts # Project management, file cache
├── types.ts # All interfaces, AST enums, context types
├── ast/
│ └── traverser.ts # Visitor-pattern AST traversal
├── parser/
│ ├── html-parser.ts # HTML lexer + recursive descent parser → AST
│ └── js-parser.ts # Acorn-based JS module analysis
├── graph/
│ └── dependency-graph.ts # Directed graph with cycle detection
├── resolver/
│ ├── directive-resolver.ts # Directive validation
│ └── asset-resolver.ts # Asset path resolution
├── passes/
│ ├── include-pass.ts # @include resolution
│ ├── css-pass.ts # @css → <style>
│ ├── js-pass.ts # @js → JS bundler trigger
│ ├── asset-pass.ts # @asset copy + path rewrite
│ ├── inline-pass.ts # @inline text inlining
│ ├── optimization-pass.ts # Empty text removal, comment stripping
│ └── minify-pass.ts # Whitespace collapse
├── bundler/
│ └── js-bundler.ts # ES module bundler + runtime generator
├── emitter.ts # AST → HTML string output
├── watcher.ts # File watching (fs.watch recursive)
├── diagnostics.ts # Error types, circular error classes
├── reporter.ts # Colored terminal output
└── fs.ts # File system abstractionDesign principles
- Modular: every subsystem is an independent class
- AST-first: all transforms operate on the AST, never on strings
- Visitor-based: traversal uses a visitor pattern for extensibility
- Dependency injection: FileSystem, Project, and context are injected
- Plugin system: hooks at 6 lifecycle points (beforeParse → afterEmit)
Plugin API
class MyPlugin {
name = 'my-plugin';
beforeParse(context: CompilerContext) {}
afterParse(context: CompilerContext) {}
beforeTransform(context: CompilerContext) {}
afterTransform(context: CompilerContext) {}
beforeEmit(context: CompilerContext) {}
afterEmit(context: CompilerContext) {}
}import { Compiler } from 'pcbuild';
const compiler = new Compiler({ entry: 'src/index.html', output: 'dist' });
compiler.addPlugin(new MyPlugin());
await compiler.compile();Example
<!-- src/index.html -->
<!DOCTYPE html>
<html>
<head>
@css("./styles/main.css")
</head>
<body>
@include("./components/header.html")
<main>
<h1>Welcome</h1>
<img src="@asset("./images/logo.png")" alt="Logo">
</main>
@include("./components/footer.html")
@js("./scripts/main.js")
@inline("./shaders/basic.glsl")
</body>
</html>Running pcbuild build produces a single dist/index.html with:
- Inlined HTML partials
- Inlined CSS
- Bundled JavaScript (dependency graph resolved, one
<script>) - Copied assets with rewritten paths
- Inlined GLSL shader
- Minified output (when
--minifyis set)
Diagnostics
Errors include source file, line, and column:
error: /project/src/index.html:5:3 - File not found: /project/src/components/missing.html
error: /project/src/index.html:8:1 - Unknown directive: @unknown
error: Circular include detected:
-> components/a.html
-> components/b.html
-> components/a.html
error: Circular JS import detected:
-> src/main.js
-> src/a.js
-> src/main.js