add --minify and --obfuscate flags to elc JS pipeline
Adds two post-processing flags that produce production-ready browser JS in a single elc invocation, replacing extract-js.py in the web product pipeline: elc --target=js --bundle --minify source.el > output.min.js elc --target=js --bundle --obfuscate source.el > output.obf.js --minify shells out to terser (passes=2, no drop_console, drop_debugger). --obfuscate shells out to javascript-obfuscator with the same options as the old extract-js.py script. --obfuscate implies --minify. Tool discovery: checks ./node_modules/.bin/, ../node_modules/.bin/ (monorepo), then falls back to npx. Both flags require --target=js; passing either without it exits 1 with a clear error. Both tools receive a reserved-names list of globals referenced from HTML onclick= attributes (neuronDemoToggle, signInWith, NEURON_CFG, etc.) so they are not mangled. Implementation adds stdout_to_file(path)/stdout_restore() builtins to the C runtime so codegen's println-streamed output can be captured to a temp file before being piped through the external tools. Temp files use /tmp/elc-<pid>-<timestamp>.js naming and are cleaned up on success and failure. Rebuilds dist/platform/elc and dist/platform/elc.c. Self-hosting verified.
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
+827
-176
File diff suppressed because it is too large
Load Diff
@@ -155,6 +155,36 @@ el_val_t readline(void) {
|
||||
return el_wrap_str(el_strdup(buf));
|
||||
}
|
||||
|
||||
/* ── stdout redirect helpers ─────────────────────────────────────────────── *
|
||||
* Used by elc post-processing (--minify, --obfuscate): capture codegen *
|
||||
* output into a temp file, then pass it to the external tool. */
|
||||
|
||||
static int _stdout_saved_fd = -1;
|
||||
|
||||
/* stdout_to_file(path) — redirect stdout to <path>. Returns 1 on success. */
|
||||
el_val_t stdout_to_file(el_val_t pathv) {
|
||||
const char* path = EL_CSTR(pathv);
|
||||
if (!path || !*path) return (el_val_t)(int64_t)0;
|
||||
fflush(stdout);
|
||||
_stdout_saved_fd = dup(STDOUT_FILENO);
|
||||
if (_stdout_saved_fd < 0) return (el_val_t)(int64_t)0;
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd < 0) { close(_stdout_saved_fd); _stdout_saved_fd = -1; return (el_val_t)(int64_t)0; }
|
||||
dup2(fd, STDOUT_FILENO);
|
||||
close(fd);
|
||||
return (el_val_t)(int64_t)1;
|
||||
}
|
||||
|
||||
/* stdout_restore() — restore stdout from the saved fd. Returns 1 on success. */
|
||||
el_val_t stdout_restore(void) {
|
||||
if (_stdout_saved_fd < 0) return (el_val_t)(int64_t)0;
|
||||
fflush(stdout);
|
||||
dup2(_stdout_saved_fd, STDOUT_FILENO);
|
||||
close(_stdout_saved_fd);
|
||||
_stdout_saved_fd = -1;
|
||||
return (el_val_t)(int64_t)1;
|
||||
}
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t av, el_val_t bv) {
|
||||
|
||||
@@ -79,6 +79,8 @@ extern "C" {
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
el_val_t stdout_to_file(el_val_t path); /* redirect println to a file */
|
||||
el_val_t stdout_restore(void); /* restore stdout after capture */
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
|
||||
@@ -2056,6 +2056,11 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "bool_to_str") { return 1 }
|
||||
// Process
|
||||
if str_eq(name, "exit_program") { return 1 }
|
||||
// Process info
|
||||
if str_eq(name, "getpid_now") { return 0 }
|
||||
// stdout redirect (used by elc post-processing)
|
||||
if str_eq(name, "stdout_to_file") { return 1 }
|
||||
if str_eq(name, "stdout_restore") { return 0 }
|
||||
// Subprocess execution
|
||||
if str_eq(name, "exec_command") { return 1 }
|
||||
if str_eq(name, "exec_capture") { return 1 }
|
||||
|
||||
+196
-7
@@ -123,6 +123,100 @@ fn detect_bundle(argv: [String]) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Detect --minify flag in argv.
|
||||
fn detect_minify(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, "--minify") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Detect --obfuscate flag in argv.
|
||||
fn detect_obfuscate(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, "--obfuscate") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Build a unique temp file path: /tmp/elc-<pid>-<timestamp>.<suffix>
|
||||
fn make_temp_path(suffix: String) -> String {
|
||||
let pid: Int = getpid_now()
|
||||
let ts: Int = time_now()
|
||||
"/tmp/elc-" + native_int_to_str(pid) + "-" + native_int_to_str(ts) + "." + suffix
|
||||
}
|
||||
|
||||
// Reserved globals that terser and javascript-obfuscator must not mangle.
|
||||
// These are referenced from HTML onclick= attributes and other direct window usage.
|
||||
fn js_reserved_names() -> String {
|
||||
"neuronDemoToggle,neuronDemoSend,neuronDemoReset,signInWith,signInWithEmail,signUpWithEmail,sendMagicLink,signOut,resetPassword,sendResetEmail,updatePassword,showSignIn,showSignUp,hideReset,setSort,addFamilyMember,removeFamilyMember,copyForPlatform,entHeadcountChange,NEURON_CFG"
|
||||
}
|
||||
|
||||
// Find a CLI tool by checking node_modules paths first, then falling back to npx.
|
||||
// src_dir is the directory of the source file being compiled.
|
||||
// Returns the command string to invoke the tool, or "" if not found.
|
||||
fn find_node_tool(tool_name: String, src_dir: String) -> String {
|
||||
// 1. Check ./node_modules/.bin/<tool> relative to source file
|
||||
let cand1: String = src_dir + "/node_modules/.bin/" + tool_name
|
||||
let check1: String = str_trim(exec_capture("test -x " + cand1 + " && echo yes 2>/dev/null"))
|
||||
if str_eq(check1, "yes") { return cand1 }
|
||||
// 2. Check ../node_modules/.bin/<tool> (monorepo layout)
|
||||
let parent_dir: String = dirname_of(src_dir)
|
||||
let cand2: String = parent_dir + "/node_modules/.bin/" + tool_name
|
||||
let check2: String = str_trim(exec_capture("test -x " + cand2 + " && echo yes 2>/dev/null"))
|
||||
if str_eq(check2, "yes") { return cand2 }
|
||||
// 3. Fall back to npx if it is on PATH. npx will use the globally cached
|
||||
// package or download on first use. Use --no to avoid auto-install if
|
||||
// the package is not already cached; if that fails, try with --yes.
|
||||
let npx_path: String = str_trim(exec_capture("which npx 2>/dev/null"))
|
||||
if !str_eq(npx_path, "") { return "npx --yes " + tool_name }
|
||||
return ""
|
||||
}
|
||||
|
||||
// apply_minify — run terser on js_path, write result to out_path.
|
||||
// Returns true on success, false on failure.
|
||||
fn apply_minify(js_path: String, out_path: String, src_dir: String) -> Bool {
|
||||
let terser: String = find_node_tool("terser", src_dir)
|
||||
if str_eq(terser, "") {
|
||||
println("el-compiler: error: terser not found. Run 'npm install terser' in your project directory.")
|
||||
return false
|
||||
}
|
||||
let names: String = js_reserved_names()
|
||||
// Single-quote the mangle reserved list so the shell does not glob-expand
|
||||
// the bracket expression. The compress options are safe without quoting.
|
||||
let compress_opts: String = "passes=2,drop_console=false,drop_debugger=true"
|
||||
let mangle_reserved: String = "'reserved=[" + names + "]'"
|
||||
let cmd: String = terser + " " + js_path + " --compress " + compress_opts + " --mangle " + mangle_reserved + " --output " + out_path
|
||||
let ret: Int = exec_command(cmd)
|
||||
if ret == 0 { return true }
|
||||
println("el-compiler: error: terser failed (exit " + native_int_to_str(ret) + ")")
|
||||
return false
|
||||
}
|
||||
|
||||
// apply_obfuscate — run javascript-obfuscator on js_path, write result to out_path.
|
||||
// Returns true on success, false on failure.
|
||||
fn apply_obfuscate(js_path: String, out_path: String, src_dir: String) -> Bool {
|
||||
let obfuscator: String = find_node_tool("javascript-obfuscator", src_dir)
|
||||
if str_eq(obfuscator, "") {
|
||||
println("el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory.")
|
||||
return false
|
||||
}
|
||||
let names: String = js_reserved_names()
|
||||
let cmd: String = obfuscator + " " + js_path + " --output " + out_path + " --compact true --simplify true --string-array true --string-array-encoding base64 --string-array-threshold 0.75 --identifier-names-generator hexadecimal --rename-globals false --self-defending false --reserved-names " + names
|
||||
let ret: Int = exec_command(cmd)
|
||||
if ret == 0 { return true }
|
||||
println("el-compiler: error: javascript-obfuscator failed (exit " + native_int_to_str(ret) + ")")
|
||||
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.
|
||||
@@ -295,14 +389,83 @@ fn resolve_imports(src_path: String) -> String {
|
||||
return str_join(prefix_chunks, "") + str_join(body_chunks, "")
|
||||
}
|
||||
|
||||
// run_with_postprocess — codegen + minify + optional obfuscate pipeline.
|
||||
//
|
||||
// Called from main() when --minify or --obfuscate is active. Redirects stdout
|
||||
// to a temp file during codegen so the output can be passed through the
|
||||
// external tools (terser, javascript-obfuscator) before final emission.
|
||||
//
|
||||
// Pipeline: codegen -> terser -> (javascript-obfuscator) -> stdout or file
|
||||
fn run_with_postprocess(tgt: String, source: String, src_path: String, do_bundle: Bool, do_obfuscate: Bool, argc: Int, positional: [String]) -> Void {
|
||||
let src_dir: String = dirname_of(src_path)
|
||||
let tmp_gen: String = make_temp_path("js")
|
||||
let tmp_min: String = make_temp_path("min.js")
|
||||
|
||||
// Redirect stdout to tmp_gen so codegen println output is captured.
|
||||
stdout_to_file(tmp_gen)
|
||||
if do_bundle {
|
||||
let runtime_path: String = resolve_runtime_path(src_path)
|
||||
compile_dispatch_bundle(tgt, source, runtime_path)
|
||||
} else {
|
||||
compile_dispatch(tgt, source)
|
||||
}
|
||||
stdout_restore()
|
||||
|
||||
// Run terser: tmp_gen -> tmp_min
|
||||
let ok_min: Bool = apply_minify(tmp_gen, tmp_min, src_dir)
|
||||
if !ok_min {
|
||||
exec_command("rm -f " + tmp_gen + " " + tmp_min)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Determine final result path (either tmp_min or post-obfuscation file).
|
||||
// Use state to pass the final path out of the optional obfuscation branch.
|
||||
state_set("__elc_final_js", tmp_min)
|
||||
|
||||
if do_obfuscate {
|
||||
let tmp_obf: String = make_temp_path("obf.js")
|
||||
let ok_obf: Bool = apply_obfuscate(tmp_min, tmp_obf, src_dir)
|
||||
if !ok_obf {
|
||||
exec_command("rm -f " + tmp_gen + " " + tmp_min + " " + tmp_obf)
|
||||
exit(1)
|
||||
}
|
||||
state_set("__elc_final_js", tmp_obf)
|
||||
}
|
||||
|
||||
let final_path: String = state_get("__elc_final_js")
|
||||
let final_js: String = fs_read(final_path)
|
||||
|
||||
// Clean up all temp files.
|
||||
exec_command("rm -f " + tmp_gen + " " + tmp_min)
|
||||
if do_obfuscate {
|
||||
exec_command("rm -f " + final_path)
|
||||
}
|
||||
|
||||
if argc >= 2 {
|
||||
let out_path: String = native_list_get(positional, 1)
|
||||
let ok: Bool = fs_write(out_path, final_js)
|
||||
if ok {
|
||||
return
|
||||
} else {
|
||||
println("el-compiler: failed to write output")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
// No output file: print final JS to stdout.
|
||||
print(final_js)
|
||||
}
|
||||
|
||||
// main — CLI entry point.
|
||||
//
|
||||
// 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
|
||||
// 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=js --bundle --minify <source.el> # emit minified IIFE to stdout
|
||||
// elc --target=js --bundle --obfuscate <source.el> # emit minified+obfuscated 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
|
||||
// elc --target=js --bundle --minify <source.el> <out.min.js> # write minified JS to file
|
||||
fn main() -> Void {
|
||||
let argv: [String] = args()
|
||||
// Use `tgt` not `target`: `target` is a reserved keyword in the lexer
|
||||
@@ -311,12 +474,27 @@ fn main() -> Void {
|
||||
let tgt: String = detect_target(argv)
|
||||
let do_emit_header: Bool = detect_emit_header(argv)
|
||||
let do_bundle: Bool = detect_bundle(argv)
|
||||
let do_minify: Bool = detect_minify(argv)
|
||||
let do_obfuscate: Bool = detect_obfuscate(argv)
|
||||
// --obfuscate implies --minify: obfuscating unminified code is pointless.
|
||||
if do_obfuscate {
|
||||
let do_minify = true
|
||||
}
|
||||
let positional: [String] = strip_flags(argv)
|
||||
let argc: Int = native_list_len(positional)
|
||||
if argc < 1 {
|
||||
println("el-compiler: usage: elc [--target=c|js] [--bundle] [--emit-header] <source.el> [<output>]")
|
||||
println("el-compiler: usage: elc [--target=c|js] [--bundle] [--minify] [--obfuscate] [--emit-header] <source.el> [<output>]")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// --minify and --obfuscate require --target=js
|
||||
if do_minify {
|
||||
if !str_eq(tgt, "js") {
|
||||
println("el-compiler: error: --minify and --obfuscate require --target=js")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
let src_path: String = native_list_get(positional, 0)
|
||||
|
||||
// When --emit-header is requested, parse the source file directly
|
||||
@@ -332,6 +510,17 @@ fn main() -> Void {
|
||||
}
|
||||
|
||||
let source: String = resolve_imports(src_path)
|
||||
|
||||
// When post-processing (--minify or --obfuscate) is requested, redirect
|
||||
// stdout to a temp file so codegen output can be captured and piped through
|
||||
// the external tools. After codegen, restore stdout before emitting the
|
||||
// final result.
|
||||
if do_minify {
|
||||
run_with_postprocess(tgt, source, src_path, do_bundle, do_obfuscate, argc, positional)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Standard path (no post-processing).
|
||||
let out: String = ""
|
||||
if do_bundle {
|
||||
let runtime_path: String = resolve_runtime_path(src_path)
|
||||
|
||||
@@ -240,6 +240,93 @@ The argv parser scans for a `--target=<lang>` token; remaining positional args a
|
||||
|
||||
---
|
||||
|
||||
## 8a. Production output — `--minify` and `--obfuscate`
|
||||
|
||||
Two post-processing flags produce production-ready browser JS in a single compiler invocation, replacing any external post-processing scripts.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
elc --target=js --bundle --minify source.el > output.min.js
|
||||
elc --target=js --bundle --obfuscate source.el > output.obf.js
|
||||
elc --target=js --bundle --minify --obfuscate source.el > output.final.js
|
||||
```
|
||||
|
||||
Both flags require `--target=js`. Passing either without `--target=js` prints an error and exits with code 1.
|
||||
|
||||
`--obfuscate` implies `--minify` — obfuscating unminified code produces no benefit and only increases output size.
|
||||
|
||||
### Pipeline order
|
||||
|
||||
```
|
||||
generate JS -> (if --bundle, wrap in IIFE) -> (if --minify, run terser) -> (if --obfuscate, run javascript-obfuscator) -> output
|
||||
```
|
||||
|
||||
### Tool discovery
|
||||
|
||||
The compiler looks for each tool in this order:
|
||||
|
||||
1. `<src_dir>/node_modules/.bin/<tool>` — local install next to source file
|
||||
2. `<src_dir>/../node_modules/.bin/<tool>` — one level up (monorepo layout)
|
||||
3. `npx --yes <tool>` — fall back to npx (uses globally cached package or downloads on first use)
|
||||
|
||||
If no path resolves and npx is not on `PATH`, the compiler prints a clear error and exits non-zero:
|
||||
|
||||
```
|
||||
el-compiler: error: terser not found. Run 'npm install terser' in your project directory.
|
||||
el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory.
|
||||
```
|
||||
|
||||
### Minification (terser)
|
||||
|
||||
Command issued internally:
|
||||
|
||||
```
|
||||
terser <tmpfile> --compress passes=2,drop_console=false,drop_debugger=true \
|
||||
--mangle 'reserved=[<reserved>]' --output <tmpfile.min>
|
||||
```
|
||||
|
||||
### Obfuscation (javascript-obfuscator)
|
||||
|
||||
Command issued internally (runs after minification):
|
||||
|
||||
```
|
||||
javascript-obfuscator <input> --output <output>
|
||||
--compact true
|
||||
--simplify true
|
||||
--string-array true
|
||||
--string-array-encoding base64
|
||||
--string-array-threshold 0.75
|
||||
--identifier-names-generator hexadecimal
|
||||
--rename-globals false
|
||||
--self-defending false
|
||||
--reserved-names <reserved>
|
||||
```
|
||||
|
||||
### Reserved names
|
||||
|
||||
These identifiers are protected from renaming by both tools. They are referenced directly from HTML `onclick=` attributes and other global-scope callsites:
|
||||
|
||||
```
|
||||
neuronDemoToggle, neuronDemoSend, neuronDemoReset,
|
||||
signInWith, signInWithEmail, signUpWithEmail, sendMagicLink,
|
||||
signOut, resetPassword, sendResetEmail, updatePassword,
|
||||
showSignIn, showSignUp, hideReset,
|
||||
setSort, addFamilyMember, removeFamilyMember, copyForPlatform, entHeadcountChange,
|
||||
NEURON_CFG
|
||||
```
|
||||
|
||||
### Temp files
|
||||
|
||||
The compiler uses `/tmp/elc-<pid>-<timestamp>.js` naming for temp files. All temp files are cleaned up on both success and failure paths.
|
||||
|
||||
### Implementation notes
|
||||
|
||||
- The compiler adds `stdout_to_file(path)` / `stdout_restore()` builtins to the C runtime (`el_runtime.c`) to capture codegen output (which is streamed via `println`) into a temp file before passing it to the external tools.
|
||||
- `--minify` and `--obfuscate` error messages are printed after stdout is restored, so they always reach the terminal regardless of output redirection.
|
||||
|
||||
---
|
||||
|
||||
## 9. The path to compiling el-ui/runtime through this backend
|
||||
|
||||
This is the real-world test. `el-ui/runtime/src/` is currently 5 hand-written `.js` files. The path to authoring them in El:
|
||||
|
||||
Reference in New Issue
Block a user