Repository: bytecodealliance/wasm-tools
Pull request #1657 (2024-07-09 20:45:01)

Improve the output of `wasm-tools component wit`
This commit implements a few minor improvements I've been wanting to make this this subcommand, notably changing the default output to use the multi-package form if needed to print everything in a component instead of just the "top"


Patch:
diff --git a/crates/wasmparser/src/readers/core/operators.rs b/crates/wasmparser/src/readers/core/operators.rs
index fe775476..9fa2731a 100644
--- a/crates/wasmparser/src/readers/core/operators.rs
+++ b/crates/wasmparser/src/readers/core/operators.rs
@@ -88,6 +88,14 @@ impl Ieee32 {
     }
 }
 
+impl From<f32> for Ieee32 {
+    fn from(value: f32) -> Self {
+        Ieee32 {
+            0: u32::from_le_bytes(value.to_le_bytes()),
+        }
+    }
+}
+
 impl From<Ieee32> for f32 {
     fn from(bits: Ieee32) -> f32 {
         f32::from_bits(bits.bits())
@@ -108,6 +116,14 @@ impl Ieee64 {
     }
 }
 
+impl From<f64> for Ieee64 {
+    fn from(value: f64) -> Self {
+        Ieee64 {
+            0: u64::from_le_bytes(value.to_le_bytes()),
+        }
+    }
+}
+
 impl From<Ieee64> for f64 {
     fn from(bits: Ieee64) -> f64 {
         f64::from_bits(bits.bits())
diff --git a/crates/wit-component/src/printing.rs b/crates/wit-component/src/printing.rs
index 28406a50..5c2ce581 100644
--- a/crates/wit-component/src/printing.rs
+++ b/crates/wit-component/src/printing.rs
@@ -51,8 +51,13 @@ impl WitPrinter {
     }
 
     /// Print a set of one or more WIT packages into a string.
-    pub fn print(&mut self, resolve: &Resolve, pkg_ids: &[PackageId]) -> Result<String> {
-        let has_multiple_packages = pkg_ids.len() > 1;
+    pub fn print(
+        &mut self,
+        resolve: &Resolve,
+        pkg_ids: &[PackageId],
+        force_print_package_in_curlies: bool,
+    ) -> Result<String> {
+        let print_package_in_curlies = force_print_package_in_curlies || pkg_ids.len() > 1;
         for (i, pkg_id) in pkg_ids.into_iter().enumerate() {
             if i > 0 {
                 self.output.push_str("\n\n");
@@ -68,9 +73,8 @@ impl WitPrinter {
                 self.output.push_str(&format!("@{version}"));
             }
 
-            if has_multiple_packages {
-                self.output.push_str("{");
-                self.output.indent += 1
+            if print_package_in_curlies {
+                self.output.push_str(" {\n");
             } else {
                 self.print_semicolon();
                 self.output.push_str("\n\n");
@@ -96,9 +100,8 @@ impl WitPrinter {
                 writeln!(&mut self.output, "}}")?;
             }
 
-            if has_multiple_packages {
-                self.output.push_str("}");
-                self.output.indent -= 1
+            if print_package_in_curlies {
+                self.output.push_str("}\n");
             }
         }
 
diff --git a/crates/wit-encoder/src/ident.rs b/crates/wit-encoder/src/ident.rs
index 4b7116f5..c336173c 100644
--- a/crates/wit-encoder/src/ident.rs
+++ b/crates/wit-encoder/src/ident.rs
@@ -5,12 +5,7 @@ pub struct Ident(Cow<'static, str>);
 
 impl Ident {
     pub fn new(s: impl Into<Cow<'static, str>>) -> Self {
-        let s: Cow<'static, str> = s.into();
-        if is_keyword(&s) {
-            Self(Cow::Owned(format!("%{}", s)))
-        } else {
-            Self(s)
-        }
+        Self(s.into())
     }
 }
 
@@ -25,6 +20,9 @@ where
 
 impl fmt::Display for Ident {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if is_keyword(&self.0) {
+            write!(f, "%")?;
+        }
         self.0.fmt(f)
     }
 }
@@ -37,8 +35,8 @@ impl AsRef<str> for Ident {
 
 fn is_keyword(name: &str) -> bool {
     match name {
-        "u8" | "u16" | "u32" | "u64" | "s8" | "s16" | "s32" | "s64" | "float32" | "float64"
-        | "char" | "bool" | "string" | "tuple" | "list" | "option" | "result" | "use" | "type"
+        "u8" | "u16" | "u32" | "u64" | "s8" | "s16" | "s32" | "s64" | "f32" | "f64" | "char"
+        | "bool" | "string" | "tuple" | "list" | "option" | "result" | "use" | "type"
         | "resource" | "func" | "record" | "enum" | "flags" | "variant" | "static"
         | "interface" | "world" | "import" | "export" | "package" | "own" | "borrow" => true,
         _ => false,
diff --git a/crates/wit-encoder/src/interface.rs b/crates/wit-encoder/src/interface.rs
index 5641426c..bf082716 100644
--- a/crates/wit-encoder/src/interface.rs
+++ b/crates/wit-encoder/src/interface.rs
@@ -1,6 +1,6 @@
 use std::fmt;
 
-use crate::{Docs, Ident, Render, RenderOpts, StandaloneFunc, TypeDef};
+use crate::{Docs, Ident, Render, RenderOpts, StandaloneFunc, TypeDef, Use};
 
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub struct Interface {
@@ -34,6 +34,11 @@ impl Interface {
         self.items.push(InterfaceItem::Function(function));
     }
 
+    /// Add a `Use` to the interface
+    pub fn use_(&mut self, use_: Use) {
+        self.items.push(InterfaceItem::Use(use_));
+    }
+
     pub fn items(&self) -> &[InterfaceItem] {
         &self.items
     }
@@ -51,6 +56,7 @@ impl Interface {
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub enum InterfaceItem {
     TypeDef(TypeDef),
+    Use(Use),
     Function(StandaloneFunc),
 }
 
@@ -73,6 +79,9 @@ impl Render for InterfaceItems {
                     }
                     write!(f, ";\n")?;
                 }
+                InterfaceItem::Use(use_) => {
+                    use_.render(f, opts)?;
+                }
             }
         }
         Ok(())
diff --git a/crates/wit-encoder/src/lib.rs b/crates/wit-encoder/src/lib.rs
index c4e8eb4f..902ffc00 100644
--- a/crates/wit-encoder/src/lib.rs
+++ b/crates/wit-encoder/src/lib.rs
@@ -17,6 +17,7 @@ mod resource;
 mod result;
 mod tuple;
 mod ty;
+mod use_;
 mod variant;
 mod world;
 
@@ -34,5 +35,6 @@ pub use resource::*;
 pub use result::*;
 pub use tuple::*;
 pub use ty::*;
+pub use use_::*;
 pub use variant::*;
 pub use world::*;
diff --git a/crates/wit-encoder/src/use_.rs b/crates/wit-encoder/src/use_.rs
new file mode 100644
index 00000000..9322c15d
--- /dev/null
+++ b/crates/wit-encoder/src/use_.rs
@@ -0,0 +1,44 @@
+use std::fmt;
+
+use crate::{Ident, Render};
+
+/// Enable the union of a world with another world
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Use {
+    target: Ident,
+    use_names_list: Vec<(String, Option<String>)>,
+}
+
+impl Use {
+    pub fn new(use_target: impl Into<Ident>) -> Self {
+        Self {
+            target: use_target.into(),
+            use_names_list: vec![],
+        }
+    }
+
+    pub fn item(&mut self, id: &str, alias: Option<&str>) {
+        self.use_names_list
+            .push((id.to_string(), alias.map(|s| s.to_string())));
+    }
+}
+
+impl Render for Use {
+    fn render(&self, f: &mut fmt::Formatter<'_>, opts: &crate::RenderOpts) -> fmt::Result {
+        let len = self.use_names_list.len();
+
+        write!(f, "{}use {}.{{ ", opts.spaces(), self.target)?;
+        for (i, (id, alias)) in self.use_names_list.iter().enumerate() {
+            if let Some(alias) = alias {
+                write!(f, "{id} as {alias}")?;
+            } else {
+                write!(f, "{id}")?;
+            }
+            if i < len - 1 {
+                write!(f, ", ")?;
+            }
+        }
+        write!(f, " }};\n")?;
+        Ok(())
+    }
+}
diff --git a/crates/wit-encoder/src/world.rs b/crates/wit-encoder/src/world.rs
index 7203524b..5f933209 100644
--- a/crates/wit-encoder/src/world.rs
+++ b/crates/wit-encoder/src/world.rs
@@ -1,6 +1,6 @@
 use std::fmt;
 
-use crate::{ident::Ident, Docs, Include, Interface, Render, RenderOpts, StandaloneFunc};
+use crate::{ident::Ident, Docs, Include, Interface, Render, RenderOpts, StandaloneFunc, Use};
 
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub struct World {
@@ -55,6 +55,9 @@ impl World {
     pub fn include(&mut self, include: Include) {
         self.item(WorldItem::Include(include));
     }
+    pub fn use_(&mut self, use_: Use) {
+        self.item(WorldItem::Use(use_));
+    }
 
     /// Set the documentation
     pub fn docs(&mut self, docs: Option<impl Into<Docs>>) {
@@ -143,6 +146,7 @@ impl Render for World {
                     render_function(f, opts, function)?;
                 }
                 WorldItem::Include(include) => include.render(f, opts)?,
+                WorldItem::Use(use_) => use_.render(f, opts)?,
             }
         }
         let opts = &opts.outdent();
@@ -173,6 +177,9 @@ pub enum WorldItem {
 
     /// Include type
     Include(Include),
+
+    /// Use
+    Use(Use),
 }
 
 impl WorldItem {
diff --git a/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs
index b15cd8c4..5cc66782 100644
--- a/fuzz/src/roundtrip_wit.rs
+++ b/fuzz/src/roundtrip_wit.rs
@@ -86,7 +86,7 @@ fn roundtrip_through_printing(file: &str, resolve: &Resolve, wasm: &[u8]) {
     for (id, pkg) in resolve.packages.iter() {
         let mut map = SourceMap::new();
         let pkg_name = &pkg.name;
-        let doc = WitPrinter::default().print(resolve, &[id]).unwrap();
+        let doc = WitPrinter::default().print(resolve, &[id], false).unwrap();
         write_file(&format!("{file}-{pkg_name}.wit"), &doc);
         map.push(format!("{pkg_name}.wit").as_ref(), doc);
         let unresolved = map.parse().unwrap();
diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs
index d546f828..1b15b2f5 100644
--- a/src/bin/wasm-tools/component.rs
+++ b/src/bin/wasm-tools/component.rs
@@ -523,9 +523,7 @@ impl WitOpts {
         // This interprets all of the output options and performs such a task.
         if self.json {
             self.emit_json(&decoded)?;
-            return Ok(());
-        }
-        if self.wasm || self.wat {
+        } else if self.wasm || self.wat {
             self.emit_wasm(&decoded)?;
         } else {
             self.emit_wit(&decoded)?;
@@ -618,7 +616,6 @@ impl WitOpts {
         assert!(!self.wasm && !self.wat);
 
         let resolve = decoded.resolve();
-        let main = decoded.packages();
 
         let mut printer = WitPrinter::default();
         printer.emit_docs(!self.no_docs);
@@ -641,28 +638,31 @@ impl WitOpts {
                     *cnt += 1;
                 }
 
+                let main = decoded.packages();
                 for (id, pkg) in resolve.packages.iter() {
-                    let output = printer.print(resolve, &[id])?;
-                    let out_dir = if main.contains(&id) {
+                    let is_main = main.contains(&id);
+                    let output = printer.print(resolve, &[id], is_main)?;
+                    let out_dir = if is_main {
                         dir.clone()
                     } else {
-                        let dir = dir.join("deps");
-                        let packages_with_same_name = &names[&pkg.name.name];
-                        if packages_with_same_name.len() == 1 {
-                            dir.join(&pkg.name.name)
+                        dir.join("deps")
+                    };
+                    let packages_with_same_name = &names[&pkg.name.name];
+                    let stem = if packages_with_same_name.len() == 1 {
+                        pkg.name.name.clone()
+                    } else {
+                        let packages_with_same_namespace =
+                            packages_with_same_name[&pkg.name.namespace];
+                        if packages_with_same_namespace == 1 {
+                            format!("{}:{}", pkg.name.namespace, pkg.name.name)
                         } else {
-                            let packages_with_same_namespace =
-                                packages_with_same_name[&pkg.name.namespace];
-                            if packages_with_same_namespace == 1 {
-                                dir.join(format!("{}:{}", pkg.name.namespace, pkg.name.name))
-                            } else {
-                                dir.join(pkg.name.to_string())
-                            }
+                            pkg.name.to_string()
                         }
                     };
                     std::fs::create_dir_all(&out_dir)
                         .with_context(|| format!("failed to create directory: {out_dir:?}"))?;
-                    let path = out_dir.join("main.wit");
+                    let filename = format!("{stem}.wit");
+                    let path = out_dir.join(&filename);
                     std::fs::write(&path, &output)
                         .with_context(|| format!("failed to write file: {path:?}"))?;
                     println!("Writing: {}", path.display());
@@ -672,8 +672,7 @@ impl WitOpts {
                 self.output.output(
                     &self.general,
                     Output::Wit {
-                        resolve: &resolve,
-                        ids: &main,
+                        wit: &decoded,
                         printer,
                     },
                 )?;
diff --git a/src/lib.rs b/src/lib.rs
index b9536eff..70210b83 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -143,8 +143,7 @@ pub struct OutputArg {
 pub enum Output<'a> {
     #[cfg(feature = "component")]
     Wit {
-        resolve: &'a wit_parser::Resolve,
-        ids: &'a [wit_parser::PackageId],
+        wit: &'a wit_component::DecodedWasm,
         printer: wit_component::WitPrinter,
     },
     Wasm(&'a [u8]),
@@ -233,12 +232,14 @@ impl OutputArg {
             }
             Output::Json(s) => self.output_str(s),
             #[cfg(feature = "component")]
-            Output::Wit {
-                resolve,
-                ids,
-                mut printer,
-            } => {
-                let output = printer.print(resolve, ids)?;
+            Output::Wit { wit, mut printer } => {
+                let resolve = wit.resolve();
+                let ids = resolve
+                    .packages
+                    .iter()
+                    .map(|(id, _)| id)
+                    .collect::<Vec<_>>();
+                let output = printer.print(resolve, &ids, false)?;
                 self.output_str(&output)
             }
         }


Test patch:
diff --git a/crates/wit-component/tests/components.rs b/crates/wit-component/tests/components.rs
index de2ff79c..9d00e2f7 100644
--- a/crates/wit-component/tests/components.rs
+++ b/crates/wit-component/tests/components.rs
@@ -171,7 +171,7 @@ fn run_test(path: &Path) -> Result<()> {
                 }
             };
         let wit = WitPrinter::default()
-            .print(&resolve, &[pkg])
+            .print(&resolve, &[pkg], false)
             .context("failed to print WIT")?;
         assert_output(&wit, &component_wit_path)?;
 
diff --git a/crates/wit-component/tests/interfaces.rs b/crates/wit-component/tests/interfaces.rs
index 87c7ac2d..55a4cadd 100644
--- a/crates/wit-component/tests/interfaces.rs
+++ b/crates/wit-component/tests/interfaces.rs
@@ -96,7 +96,7 @@ fn run_test(path: &Path, is_dir: bool) -> Result<()> {
 }
 
 fn assert_print(resolve: &Resolve, pkg_ids: &[PackageId], path: &Path, is_dir: bool) -> Result<()> {
-    let output = WitPrinter::default().print(resolve, &pkg_ids)?;
+    let output = WitPrinter::default().print(resolve, &pkg_ids, false)?;
     for pkg_id in pkg_ids {
         let pkg = &resolve.packages[*pkg_id];
         let expected = if is_dir {
diff --git a/crates/wit-component/tests/merge.rs b/crates/wit-component/tests/merge.rs
index f9a70859..3dd25da9 100644
--- a/crates/wit-component/tests/merge.rs
+++ b/crates/wit-component/tests/merge.rs
@@ -46,7 +46,7 @@ fn merging() -> Result<()> {
                         .join("merge")
                         .join(&pkg.name.name)
                         .with_extension("wit");
-                    let output = WitPrinter::default().print(&into, &[id])?;
+                    let output = WitPrinter::default().print(&into, &[id], false)?;
                     assert_output(&expected, &output)?;
                 }
             }
diff --git a/crates/wit-encoder/tests/type_defs.rs b/crates/wit-encoder/tests/type_defs.rs
index fb2d211a..2f96a9a6 100644
--- a/crates/wit-encoder/tests/type_defs.rs
+++ b/crates/wit-encoder/tests/type_defs.rs
@@ -103,6 +103,7 @@ const PACKAGE: &str = indoc::indoc! {"
       type t46 = t44;
       type foo = bar;
       type bar = u32;
+      type %f64 = f64;
       resource t50 {
       }
       resource t51 {
@@ -211,6 +212,8 @@ fn types() {
         interface.type_def(TypeDef::type_("foo", Type::named("bar")));
         interface.type_def(TypeDef::type_("bar", Type::U32));
 
+        interface.type_def(TypeDef::type_("f64", Type::F64));
+
         interface.type_def(TypeDef::resource("t50", Vec::<ResourceFunc>::new()));
         interface.type_def(TypeDef::resource(
             "t51",
diff --git a/crates/wit-encoder/tests/use.rs b/crates/wit-encoder/tests/use.rs
new file mode 100644
index 00000000..c3d891c6
--- /dev/null
+++ b/crates/wit-encoder/tests/use.rs
@@ -0,0 +1,45 @@
+use pretty_assertions::assert_eq;
+use wit_encoder::{Interface, Package, PackageName, ResourceFunc, TypeDef, Use, World};
+
+const PACKAGE: &str = indoc::indoc! {"
+    package foo:foo;
+
+    interface foo {
+      resource bar {
+      }
+    }
+
+    interface bar {
+      use foo.{ bar as foobar };
+      resource baz {
+      }
+    }
+
+    world baz {
+      use bar.{ baz };
+    }
+"};
+
+#[test]
+fn concrete_types() {
+    let mut package = Package::new(PackageName::new("foo", "foo", None));
+
+    let mut interface = Interface::new("foo");
+    interface.type_def(TypeDef::resource("bar", Vec::<ResourceFunc>::new()));
+    package.interface(interface);
+
+    let mut interface = Interface::new("bar");
+    let mut use_ = Use::new("foo");
+    use_.item("bar", Some("foobar"));
+    interface.use_(use_);
+    interface.type_def(TypeDef::resource("baz", Vec::<ResourceFunc>::new()));
+    package.interface(interface);
+
+    let mut world = World::new("baz");
+    let mut use_ = Use::new("bar");
+    use_.item("baz", None);
+    world.use_(use_);
+    package.world(world);
+
+    assert_eq!(package.to_string(), PACKAGE);
+}
diff --git a/tests/cli.rs b/tests/cli.rs
index 3c8d2cfd..2e9d1664 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -32,6 +32,7 @@ use std::env;
 use std::io::Write;
 use std::path::{Path, PathBuf};
 use std::process::{Command, Output, Stdio};
+use tempfile::TempDir;
 
 fn main() {
     let mut tests = Vec::new();
@@ -76,6 +77,7 @@ fn run_test(test: &Path, bless: bool) -> Result<()> {
 
     let mut cmd = wasm_tools_exe();
     let mut stdin = None;
+    let tempdir = TempDir::new()?;
     for arg in line.split_whitespace() {
         if arg == "|" {
             let output = execute(&mut cmd, stdin.as_deref(), false)?;
@@ -83,6 +85,8 @@ fn run_test(test: &Path, bless: bool) -> Result<()> {
             cmd = wasm_tools_exe();
         } else if arg == "%" {
             cmd.arg(test);
+        } else if arg == "%tmpdir" {
+            cmd.arg(tempdir.path());
         } else {
             cmd.arg(arg);
         }
@@ -94,12 +98,14 @@ fn run_test(test: &Path, bless: bool) -> Result<()> {
         bless,
         &output.stdout,
         &test.with_extension(&format!("{extension}.stdout")),
+        &tempdir,
     )
     .context("failed to check stdout expectation (auto-update with BLESS=1)")?;
     assert_output(
         bless,
         &output.stderr,
         &test.with_extension(&format!("{extension}.stderr")),
+        &tempdir,
     )
     .context("failed to check stderr expectation (auto-update with BLESS=1)")?;
     Ok(())
@@ -145,7 +151,14 @@ fn execute(cmd: &mut Command, stdin: Option<&[u8]>, should_fail: bool) -> Result
     Ok(output)
 }
 
-fn assert_output(bless: bool, output: &[u8], path: &Path) -> Result<()> {
+fn assert_output(bless: bool, output: &[u8], path: &Path, tempdir: &TempDir) -> Result<()> {
+    let tempdir = tempdir.path().to_str().unwrap();
+    // sanitize the output to be consistent across platforms and handle per-test
+    // differences such as `%tmpdir`.
+    let output = String::from_utf8_lossy(output)
+        .replace(tempdir, "%tmpdir")
+        .replace("\\", "/");
+
     if bless {
         if output.is_empty() {
             drop(std::fs::remove_file(path));
@@ -162,7 +175,6 @@ fn assert_output(bless: bool, output: &[u8], path: &Path) -> Result<()> {
             Ok(())
         }
     } else {
-        let output = std::str::from_utf8(output)?;
         let contents = std::fs::read_to_string(path)
             .with_context(|| format!("failed to read {path:?}"))?
             .replace("\r\n", "\n");
diff --git a/tests/cli/print-core-wasm-wit-multiple-packages.wit.stdout b/tests/cli/print-core-wasm-wit-multiple-packages.wit.stdout
index 75feb032..57c65fce 100644
--- a/tests/cli/print-core-wasm-wit-multiple-packages.wit.stdout
+++ b/tests/cli/print-core-wasm-wit-multiple-packages.wit.stdout
@@ -1,5 +1,16 @@
-package root:root;
+package root:root {
+  world root {
+    import bar:bar/my-interface;
+  }
+}
+
+
+package bar:bar {
+  interface my-interface {
+    foo: func();
+  }
 
-world root {
-  import bar:bar/my-interface;
+  world my-world {
+    import my-interface;
+  }
 }
diff --git a/tests/cli/print-core-wasm-wit.wit.stdout b/tests/cli/print-core-wasm-wit.wit.stdout
index e560f82e..c1eb1f63 100644
--- a/tests/cli/print-core-wasm-wit.wit.stdout
+++ b/tests/cli/print-core-wasm-wit.wit.stdout
@@ -1,5 +1,16 @@
-package root:root;
+package root:root {
+  world root {
+    import foo:foo/my-interface;
+  }
+}
+
+
+package foo:foo {
+  interface my-interface {
+    foo: func();
+  }
 
-world root {
-  import foo:foo/my-interface;
+  world my-world {
+    import my-interface;
+  }
 }
diff --git a/tests/cli/wit-directory-output-in-deps-folder.wit b/tests/cli/wit-directory-output-in-deps-folder.wit
new file mode 100644
index 00000000..58ff346e
--- /dev/null
+++ b/tests/cli/wit-directory-output-in-deps-folder.wit
@@ -0,0 +1,14 @@
+// RUN: component embed --dummy % --world a:b/c | component wit --out-dir %tmpdir
+
+package a:b {
+  interface foo {}
+
+  world c {
+    import foo;
+    import a:c/foo;
+  }
+}
+
+package a:c {
+  interface foo {}
+}
diff --git a/tests/cli/wit-directory-output-in-deps-folder.wit.stdout b/tests/cli/wit-directory-output-in-deps-folder.wit.stdout
new file mode 100644
index 00000000..8c2fc622
--- /dev/null
+++ b/tests/cli/wit-directory-output-in-deps-folder.wit.stdout
@@ -0,0 +1,3 @@
+Writing: %tmpdir/root.wit
+Writing: %tmpdir/deps/b.wit
+Writing: %tmpdir/deps/c.wit
diff --git a/tests/cli/wit-directory-output-valid.wit b/tests/cli/wit-directory-output-valid.wit
new file mode 100644
index 00000000..01e60dca
--- /dev/null
+++ b/tests/cli/wit-directory-output-valid.wit
@@ -0,0 +1,14 @@
+// RUN: component wit % --out-dir %tmpdir | component wit %tmpdir
+
+package a:b {
+  interface foo {}
+
+  world c {
+    import foo;
+    import a:c/foo;
+  }
+}
+
+package a:c {
+  interface foo {}
+}
diff --git a/tests/cli/wit-directory-output-valid.wit.stdout b/tests/cli/wit-directory-output-valid.wit.stdout
new file mode 100644
index 00000000..33576bc7
--- /dev/null
+++ b/tests/cli/wit-directory-output-valid.wit.stdout
@@ -0,0 +1,16 @@
+package a:c {
+  interface foo {
+  }
+
+}
+
+
+package a:b {
+  interface foo {
+  }
+
+  world c {
+    import foo;
+    import a:c/foo;
+  }
+}
diff --git a/tests/cli/wit-directory-output.wit b/tests/cli/wit-directory-output.wit
new file mode 100644
index 00000000..88d6f83d
--- /dev/null
+++ b/tests/cli/wit-directory-output.wit
@@ -0,0 +1,14 @@
+// RUN: component wit % --out-dir %tmpdir
+
+package a:b {
+  interface foo {}
+
+  world c {
+    import foo;
+    import a:c/foo;
+  }
+}
+
+package a:c {
+  interface foo {}
+}
diff --git a/tests/cli/wit-directory-output.wit.stdout b/tests/cli/wit-directory-output.wit.stdout
new file mode 100644
index 00000000..9524cea5
--- /dev/null
+++ b/tests/cli/wit-directory-output.wit.stdout
@@ -0,0 +1,2 @@
+Writing: %tmpdir/c.wit
+Writing: %tmpdir/b.wit