Skip to main content

componentize_qjs_cli/
cli.rs

1use componentize_qjs::{ComponentizeOpts, Runtime, componentize};
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    /// Use the built-in runtime optimized for smaller generated components
49    #[arg(long, conflicts_with = "runtime")]
50    pub opt_size: bool,
51
52    /// Path to a custom QuickJS runtime Wasm module
53    #[arg(long, value_name = "PATH")]
54    pub runtime: Option<std::path::PathBuf>,
55}
56
57/// Run the componentize-qjs CLI with the given arguments.
58pub async fn run(args: Vec<String>) -> Result<()> {
59    let args =
60        CliArgs::try_parse_from(std::iter::once("componentize-qjs".to_string()).chain(args))?;
61
62    if !args.wit.exists() {
63        anyhow::bail!("WIT file/directory not found: {}", args.wit.display());
64    }
65    if !args.js.exists() {
66        anyhow::bail!("JavaScript file not found: {}", args.js.display());
67    }
68    if let Some(runtime_file) = &args.runtime
69        && !runtime_file.exists()
70    {
71        anyhow::bail!("Runtime file not found: {}", runtime_file.display());
72    }
73
74    let js_source = fs::read_to_string(&args.js)
75        .with_context(|| format!("failed to read JS file: {}", args.js.display()))?;
76
77    let js_source = if args.minify {
78        let allocator = Allocator::default();
79        let source_type = SourceType::mjs();
80        let ret = OxcParser::new(&allocator, &js_source, source_type).parse();
81        let mut program = ret.program;
82
83        let options = MinifierOptions {
84            mangle: Some(MangleOptions {
85                top_level: Some(false),
86                ..Default::default()
87            }),
88            compress: Some(CompressOptions {
89                unused: CompressOptionsUnused::Keep,
90                keep_names: CompressOptionsKeepNames::all_false(),
91                ..CompressOptions::default()
92            }),
93        };
94        let ret = Minifier::new(options).minify(&allocator, &mut program);
95        Codegen::new()
96            .with_scoping(ret.scoping)
97            .build(&program)
98            .code
99    } else {
100        js_source
101    };
102
103    println!("componentize-qjs");
104    println!("  WIT:    {}", args.wit.display());
105    println!("  JS:     {}", args.js.display());
106    println!("  Output: {}", args.output.display());
107
108    let runtime = match &args.runtime {
109        Some(file) => Runtime::Custom(&fs::read(file)?),
110        None if args.opt_size => Runtime::OptSize,
111        None => Runtime::default(),
112    };
113
114    if args.stub_wasi {
115        println!("Stubbing WASI imports...");
116    }
117
118    let component = componentize(&ComponentizeOpts {
119        wit_path: &args.wit,
120        js_source: &js_source,
121        world_name: args.world.as_deref(),
122        stub_wasi: args.stub_wasi,
123        disable_gc: args.disable_gc,
124        runtime,
125    })
126    .await?;
127
128    fs::write(&args.output, &component)
129        .with_context(|| format!("failed to write output to {}", args.output.display()))?;
130
131    println!("Component written to {}", args.output.display());
132    println!("  Size: {} bytes", component.len());
133
134    Ok(())
135}