componentize_qjs_cli/
cli.rs1use 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 #[arg(short, long)]
22 pub wit: std::path::PathBuf,
23
24 #[arg(short, long)]
26 pub js: std::path::PathBuf,
27
28 #[arg(short, long, default_value = "output.wasm")]
30 pub output: std::path::PathBuf,
31
32 #[arg(short = 'n', long)]
34 pub world: Option<String>,
35
36 #[arg(long)]
38 pub stub_wasi: bool,
39
40 #[arg(short = 'm', long)]
42 pub minify: bool,
43
44 #[arg(long)]
46 pub disable_gc: bool,
47
48 #[arg(long, conflicts_with = "runtime")]
50 pub opt_size: bool,
51
52 #[arg(long, value_name = "PATH")]
54 pub runtime: Option<std::path::PathBuf>,
55}
56
57pub 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}