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