1pub mod codegen;
2pub mod stubwasi;
3
4use std::path::{Component as PathComponent, Path, PathBuf};
5
6use anyhow::{Context, Result, anyhow};
7use bytes::Bytes;
8use stubwasi::stub_wasi_imports;
9use wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER;
10use wasmtime::component::{Component as WasmtimeComponent, Linker, ResourceTable};
11use wasmtime::{Config, Engine, Store};
12use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
13use wasmtime_wasi::{DirPerms, FilePerms, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
14use wasmtime_wizer::{WasmtimeWizerComponent, Wizer};
15use wit_parser::Resolve;
16
17include!(concat!(env!("OUT_DIR"), "/output.rs"));
18
19wasmtime::component::bindgen!({
20 path: "wit/init.wit",
21 world: "init",
22 exports: { default: async },
23});
24
25struct Ctx {
26 wasi: WasiCtx,
27 table: ResourceTable,
28}
29
30impl WasiView for Ctx {
31 fn ctx(&mut self) -> WasiCtxView<'_> {
32 WasiCtxView {
33 ctx: &mut self.wasi,
34 table: &mut self.table,
35 }
36 }
37}
38
39pub struct ComponentizeOpts<'a> {
41 pub wit_path: &'a Path,
43 pub js_source: &'a str,
45 pub js_path: Option<&'a Path>,
47 pub module_root: Option<&'a Path>,
49 pub world_name: Option<&'a str>,
51 pub stub_wasi: bool,
53 pub disable_gc: bool,
55 pub runtime: Runtime<'a>,
57}
58
59#[derive(Clone, Copy, Debug)]
61pub enum Runtime<'a> {
62 Default,
67 OptSize,
72 DefaultSync,
78 OptSizeSync,
84 Custom(&'a [u8]),
86}
87
88impl Default for Runtime<'_> {
89 fn default() -> Self {
90 default_builtin_runtime()
91 }
92}
93
94pub fn default_builtin_runtime() -> Runtime<'static> {
96 if cfg!(feature = "opt-size") {
97 Runtime::OptSize
98 } else {
99 Runtime::Default
100 }
101}
102
103pub async fn componentize(opts: &ComponentizeOpts<'_>) -> Result<Vec<u8>> {
105 let mut resolve = Resolve::default();
106 let (pkg_id, _) = resolve.push_path(opts.wit_path)?;
107 let world_id = resolve.select_world(&[pkg_id], opts.world_name)?;
108
109 let shim = codegen::generate_shim(&resolve, world_id);
110 let module_resolution = module_resolution(opts)?;
111 let mut wit_dylib = wit_dylib::create(&resolve, world_id, None);
112
113 wit_component::embed_component_metadata(
114 &mut wit_dylib,
115 &resolve,
116 world_id,
117 wit_component::StringEncoding::UTF8,
118 )?;
119
120 let pre_wizer_component = wit_component::Linker::default()
121 .validate(true)
122 .library(
123 "componentize_qjs_runtime.wasm",
124 runtime_wasm(opts.runtime),
125 false,
126 )?
127 .library("wit-dylib.wasm", &wit_dylib, false)?
128 .adapter(
129 "wasi_snapshot_preview1",
130 WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER,
131 )?
132 .encode()
133 .context("failed to link and encode component")?;
134
135 let mut component = wizer_init(
136 &pre_wizer_component,
137 &shim,
138 opts.js_source,
139 module_resolution.as_ref(),
140 opts.disable_gc,
141 )
142 .await?;
143
144 if opts.stub_wasi {
145 component = stub_wasi_imports(&component).context("failed to stub WASI imports")?;
146 }
147
148 Ok(component)
149}
150
151pub fn default_runtime_wasm() -> &'static [u8] {
153 DEFAULT_RUNTIME_WASM
154}
155
156pub fn opt_size_runtime_wasm() -> &'static [u8] {
158 OPT_SIZE_RUNTIME_WASM
159}
160
161pub fn default_sync_runtime_wasm() -> &'static [u8] {
163 DEFAULT_SYNC_RUNTIME_WASM
164}
165
166pub fn opt_size_sync_runtime_wasm() -> &'static [u8] {
168 OPT_SIZE_SYNC_RUNTIME_WASM
169}
170
171fn runtime_wasm(runtime: Runtime<'_>) -> &[u8] {
172 match runtime {
173 Runtime::Default => DEFAULT_RUNTIME_WASM,
174 Runtime::OptSize => OPT_SIZE_RUNTIME_WASM,
175 Runtime::DefaultSync => DEFAULT_SYNC_RUNTIME_WASM,
176 Runtime::OptSizeSync => OPT_SIZE_SYNC_RUNTIME_WASM,
177 Runtime::Custom(wasm) => wasm,
178 }
179}
180
181struct ModuleResolution {
182 host_root: PathBuf,
183 guest_entry_path: String,
184}
185
186fn module_resolution(opts: &ComponentizeOpts<'_>) -> Result<Option<ModuleResolution>> {
187 let Some(js_path) = opts.js_path else {
188 if opts.module_root.is_some() {
189 return Err(anyhow!("module_root requires js_path"));
190 }
191 return Ok(None);
192 };
193
194 let host_entry = js_path
195 .canonicalize()
196 .with_context(|| format!("failed to resolve JS entry path {}", js_path.display()))?;
197 if !host_entry.is_file() {
198 return Err(anyhow!(
199 "JS entry path is not a file: {}",
200 host_entry.display()
201 ));
202 }
203
204 let host_root = match opts.module_root {
205 Some(root) => root
206 .canonicalize()
207 .with_context(|| format!("failed to resolve module root {}", root.display()))?,
208 None => default_module_root(&host_entry)?,
209 };
210 if !host_root.is_dir() {
211 return Err(anyhow!(
212 "module root is not a directory: {}",
213 host_root.display()
214 ));
215 }
216
217 let relative_entry = host_entry.strip_prefix(&host_root).with_context(|| {
218 format!(
219 "JS entry path {} is not under module root {}",
220 host_entry.display(),
221 host_root.display()
222 )
223 })?;
224 let guest_entry_path = guest_absolute_path(relative_entry)?;
225
226 Ok(Some(ModuleResolution {
227 host_root,
228 guest_entry_path,
229 }))
230}
231
232fn default_module_root(host_entry: &Path) -> Result<PathBuf> {
233 let cwd = std::env::current_dir()
234 .context("failed to read current directory")?
235 .canonicalize()
236 .context("failed to resolve current directory")?;
237
238 if host_entry.starts_with(&cwd) {
239 return Ok(cwd);
240 }
241
242 host_entry
243 .parent()
244 .map(Path::to_path_buf)
245 .ok_or_else(|| anyhow!("JS entry path has no parent: {}", host_entry.display()))
246}
247
248fn guest_absolute_path(relative: &Path) -> Result<String> {
249 let mut guest = String::from("/");
250 let mut first = true;
251
252 for component in relative.components() {
253 let PathComponent::Normal(part) = component else {
254 return Err(anyhow!(
255 "JS entry path contains unsupported component: {}",
256 relative.display()
257 ));
258 };
259 let part = part.to_str().ok_or_else(|| {
260 anyhow!(
261 "JS entry path contains non-UTF-8 component: {}",
262 relative.display()
263 )
264 })?;
265
266 if !first {
267 guest.push('/');
268 }
269 guest.push_str(part);
270 first = false;
271 }
272
273 if first {
274 return Err(anyhow!("JS entry path cannot be the module root"));
275 }
276
277 Ok(guest)
278}
279
280async fn wizer_init(
281 component: &[u8],
282 shim: &str,
283 js: &str,
284 module_resolution: Option<&ModuleResolution>,
285 disable_gc: bool,
286) -> Result<Vec<u8>> {
287 let stdout = MemoryOutputPipe::new(10000);
288 let stderr = MemoryOutputPipe::new(10000);
289
290 let mut wasi = WasiCtxBuilder::new();
291 wasi.stdin(MemoryInputPipe::new(Bytes::new()))
292 .stdout(stdout.clone())
293 .stderr(stderr.clone());
294 if let Some(resolution) = module_resolution {
295 wasi.preopened_dir(&resolution.host_root, "/", DirPerms::READ, FilePerms::READ)
296 .map_err(|err| {
297 anyhow!(
298 "failed to preopen module root {}: {err}",
299 resolution.host_root.display()
300 )
301 })?;
302 }
303 let wasi = wasi.build();
304
305 let table = ResourceTable::new();
306 let mut config = Config::new();
307 config.wasm_component_model(true);
308 config.wasm_component_model_async(true);
309
310 let engine = Engine::new(&config)?;
311 let mut store = Store::new(&engine, Ctx { wasi, table });
312
313 let wizer = Wizer::new();
314 let (cx, instrumented) = wizer.instrument_component(component)?;
315 let comp = WasmtimeComponent::new(&engine, &instrumented)?;
316
317 let mut linker = Linker::new(&engine);
318 linker.allow_shadowing(true);
319 linker.define_unknown_imports_as_traps(&comp)?;
320 wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
321 wasmtime_wasi::p3::add_to_linker(&mut linker)?;
322 let instance = linker.instantiate_async(&mut store, &comp).await?;
323
324 let init = Init::new(&mut store, &instance)?;
325 init.call_init(
326 &mut store,
327 shim,
328 js,
329 module_resolution.map(|resolution| resolution.guest_entry_path.as_str()),
330 disable_gc,
331 )
332 .await?
333 .map_err(|e| anyhow!("{e}"))
334 .with_context(move || {
335 format!(
336 "{}{}",
337 String::from_utf8_lossy(&stdout.contents()),
338 String::from_utf8_lossy(&stderr.contents())
339 )
340 })?;
341
342 let component = wizer
343 .snapshot_component(
344 cx,
345 &mut WasmtimeWizerComponent {
346 store: &mut store,
347 instance,
348 },
349 )
350 .await?;
351
352 Ok(component)
353}