Skip to main content

componentize_qjs_cli/
cli.rs

1use componentize_qjs::{componentize, ComponentizeOpts};
2
3use anyhow::{Context, Result};
4use clap::Parser;
5use oxc_allocator::Allocator;
6use oxc_codegen::Codegen;
7use oxc_minifier::{
8    CompressOptions, CompressOptionsKeepNames, CompressOptionsUnused, MangleOptions, Minifier,
9    MinifierOptions,
10};
11use oxc_parser::Parser as OxcParser;
12use oxc_span::SourceType;
13
14use std::fs;
15
16#[derive(Parser)]
17#[command(name = "componentize-qjs")]
18#[command(about = "Convert JavaScript to WebAssembly components using QuickJS")]
19pub struct CliArgs {
20    /// Path to the WIT file or directory
21    #[arg(short, long)]
22    pub wit: std::path::PathBuf,
23
24    /// Path to the JavaScript source file
25    #[arg(short, long)]
26    pub js: std::path::PathBuf,
27
28    /// Output path for the component
29    #[arg(short, long, default_value = "output.wasm")]
30    pub output: std::path::PathBuf,
31
32    /// World name to use from the WIT
33    #[arg(short = 'n', long)]
34    pub world: Option<String>,
35
36    /// Stub all WASI imports with traps
37    #[arg(long)]
38    pub stub_wasi: bool,
39
40    /// Minify the JS source via oxc before componentizing
41    #[arg(short = 'm', long)]
42    pub minify: bool,
43
44    /// Disable automatic garbage collection in the QuickJS runtime
45    #[arg(long)]
46    pub disable_gc: bool,
47}
48
49/// Run the componentize-qjs CLI with the given arguments.
50pub async fn run(args: Vec<String>) -> Result<()> {
51    let args =
52        CliArgs::try_parse_from(std::iter::once("componentize-qjs".to_string()).chain(args))?;
53
54    if !args.wit.exists() {
55        anyhow::bail!("WIT file/directory not found: {}", args.wit.display());
56    }
57    if !args.js.exists() {
58        anyhow::bail!("JavaScript file not found: {}", args.js.display());
59    }
60
61    let js_source = fs::read_to_string(&args.js)
62        .with_context(|| format!("failed to read JS file: {}", args.js.display()))?;
63
64    let js_source = if args.minify {
65        let allocator = Allocator::default();
66        let source_type = SourceType::mjs();
67        let ret = OxcParser::new(&allocator, &js_source, source_type).parse();
68        let mut program = ret.program;
69
70        let options = MinifierOptions {
71            mangle: Some(MangleOptions {
72                top_level: Some(false),
73                ..Default::default()
74            }),
75            compress: Some(CompressOptions {
76                unused: CompressOptionsUnused::Keep,
77                keep_names: CompressOptionsKeepNames::all_false(),
78                ..CompressOptions::default()
79            }),
80        };
81        let ret = Minifier::new(options).minify(&allocator, &mut program);
82        Codegen::new()
83            .with_scoping(ret.scoping)
84            .build(&program)
85            .code
86    } else {
87        js_source
88    };
89
90    println!("componentize-qjs");
91    println!("  WIT:    {}", args.wit.display());
92    println!("  JS:     {}", args.js.display());
93    println!("  Output: {}", args.output.display());
94
95    if args.stub_wasi {
96        println!("Stubbing WASI imports...");
97    }
98
99    let component = componentize(&ComponentizeOpts {
100        wit_path: &args.wit,
101        js_source: &js_source,
102        world_name: args.world.as_deref(),
103        stub_wasi: args.stub_wasi,
104        disable_gc: args.disable_gc,
105    })
106    .await?;
107
108    fs::write(&args.output, &component)
109        .with_context(|| format!("failed to write output to {}", args.output.display()))?;
110
111    println!("Component written to {}", args.output.display());
112    println!("  Size: {} bytes", component.len());
113
114    Ok(())
115}