add --bundle flag for self-contained IIFE output

elc --target=js --bundle source.el > output.js produces a single file
with no import statement that can drop directly into a <script> tag.

How it works:
  - detect_bundle() reads the --bundle flag from argv
  - resolve_runtime_path() looks for el_runtime.js next to the source file
  - compile_js_with_bundle() reads the runtime, calls codegen_js_bundle()
  - codegen_js_inner(bundle_mode=true):
    - emits ;(function() { "use strict"; at the top
    - inlines the runtime content (stripping ES export statements which
      are invalid inside an IIFE via js_strip_es_exports())
    - skips the const {...} = globalThis.__el destructure -- the inlined
      function declarations are already in scope within the IIFE
    - closes with })(); after main()

Usage: elc --target=js --bundle app.el > app.js
       Place el_runtime.js in the same directory as app.el.
This commit is contained in:
Will Anderson
2026-05-04 10:40:46 -05:00
parent 437ba0a4dd
commit 422442b14e
3 changed files with 160 additions and 47 deletions
+62 -7
View File
@@ -29,7 +29,7 @@ fn compile(source: String) -> String {
codegen(stmts, source)
}
// compile_js full pipeline (JS target): source string -> JS source string
// compile_js full pipeline (JS target, module mode): source string -> JS source string
fn compile_js(source: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
@@ -38,6 +38,20 @@ fn compile_js(source: String) -> String {
codegen_js(stmts, source)
}
// compile_js_with_bundle JS target in bundle mode.
// Reads el_runtime.js from runtime_path and inlines it inside an IIFE.
fn compile_js_with_bundle(source: String, runtime_path: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
el_release(tokens)
let runtime_content: String = fs_read(runtime_path)
if str_eq(runtime_content, "") {
println("el-compiler: warning: --bundle: could not read runtime at " + runtime_path)
println("el-compiler: warning: bundle output will be incomplete")
}
codegen_js_bundle(stmts, source, runtime_content)
}
// compile_dispatch pick a backend based on the requested target.
// tgt = "c" | "js"
// (The parameter is named `tgt` because `target` is a reserved keyword
@@ -48,6 +62,12 @@ fn compile_dispatch(tgt: String, source: String) -> String {
compile(source)
}
// compile_dispatch_bundle like compile_dispatch but bundle mode for JS.
fn compile_dispatch_bundle(tgt: String, source: String, runtime_path: String) -> String {
if str_eq(tgt, "js") { return compile_js_with_bundle(source, runtime_path) }
compile(source)
}
// Detect a `--target=<lang>` flag in argv and return the target.
// Returns "c" if none specified or unrecognized.
fn detect_target(argv: [String]) -> String {
@@ -91,6 +111,32 @@ fn detect_emit_header(argv: [String]) -> Bool {
return false
}
// Detect --bundle flag in argv.
fn detect_bundle(argv: [String]) -> Bool {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_eq(a, "--bundle") { return true }
let i = i + 1
}
return false
}
// Resolve the runtime path for --bundle mode.
// Looks for el_runtime.js next to the source file first;
// if not found there, looks next to the elc binary itself.
// Returns "" if not found anywhere (caller emits a warning).
fn resolve_runtime_path(src_path: String) -> String {
let src_dir: String = dirname_of(src_path)
let candidate: String = src_dir + "/el_runtime.js"
let existing: String = fs_read(candidate)
if !str_eq(existing, "") {
return candidate
}
return ""
}
// Reconstruct an El type annotation string from a parsed type node.
fn type_node_to_el(t: Map<String, Any>) -> String {
let k: String = t["kind"]
@@ -251,10 +297,12 @@ fn resolve_imports(src_path: String) -> String {
// main CLI entry point.
//
// elc <source.el> # emit C to stdout
// elc --target=js <source.el> # emit JS to stdout
// elc --target=c <source.el> <out.c> # write C to file
// elc --target=js <source.el> <out.js> # write JS to file
// elc <source.el> # emit C to stdout
// elc --target=js <source.el> # emit JS (module) to stdout
// elc --target=js --bundle <source.el> # emit self-contained JS (IIFE) to stdout
// elc --target=c <source.el> <out.c> # write C to file
// elc --target=js <source.el> <out.js> # write JS to file
// elc --target=js --bundle <source.el> <out.js> # write bundled JS to file
fn main() -> Void {
let argv: [String] = args()
// Use `tgt` not `target`: `target` is a reserved keyword in the lexer
@@ -262,10 +310,11 @@ fn main() -> Void {
// because the function-name position has no token-class restriction.
let tgt: String = detect_target(argv)
let do_emit_header: Bool = detect_emit_header(argv)
let do_bundle: Bool = detect_bundle(argv)
let positional: [String] = strip_flags(argv)
let argc: Int = native_list_len(positional)
if argc < 1 {
println("el-compiler: usage: elc [--target=c|js] [--emit-header] <source.el> [<output>]")
println("el-compiler: usage: elc [--target=c|js] [--bundle] [--emit-header] <source.el> [<output>]")
exit(1)
}
let src_path: String = native_list_get(positional, 0)
@@ -283,7 +332,13 @@ fn main() -> Void {
}
let source: String = resolve_imports(src_path)
let out: String = compile_dispatch(tgt, source)
let out: String = ""
if do_bundle {
let runtime_path: String = resolve_runtime_path(src_path)
let out = compile_dispatch_bundle(tgt, source, runtime_path)
} else {
let out = compile_dispatch(tgt, source)
}
if argc >= 2 {
let out_path: String = native_list_get(positional, 1)
let ok: Bool = fs_write(out_path, out)