summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBartek IwaƄczuk <biwanczuk@gmail.com>2023-11-17 02:28:38 +0100
committerGitHub <noreply@github.com>2023-11-17 01:28:38 +0000
commit9534e6e1131542653c4e266f712c4067af2c8ec0 (patch)
tree1a74cccdcdb6bb12c10ab3e2a34d93fd8c4ccc55
parent544923afdc67e9946453901642746f37f22c8e24 (diff)
feat(unstable): Workspaces support (#20410)
This commit adds unstable workspace support. This is extremely bare-bones and minimal first-pass at this. With this change `deno.json` supports specifying `workspaces` key, that accepts a list of subdirectories. Each workspace can have its own import map. It's required to specify a `"name"` and `"version"` properties in the configuration file for the workspace: ```jsonc // deno.json { "workspaces": [ "a", "b" }, "imports": { "express": "npm:express@5" } } ``` ``` jsonc // a/deno.json { "name": "a", "version": "1.0.2", "imports": { "kleur": "npm:kleur" } } ``` ```jsonc // b/deno.json { "name": "b", "version": "0.51.0", "imports": { "chalk": "npm:chalk" } } ``` `--unstable-workspaces` flag is required to use this feature: ``` $ deno run --unstable-workspaces mod.ts ``` --------- Co-authored-by: David Sherret <dsherret@gmail.com>
-rw-r--r--Cargo.lock22
-rw-r--r--cli/Cargo.toml10
-rw-r--r--cli/args/flags.rs11
-rw-r--r--cli/args/import_map.rs2
-rw-r--r--cli/args/mod.rs57
-rw-r--r--cli/graph_util.rs59
-rw-r--r--cli/module_loader.rs10
-rw-r--r--cli/tests/integration/run_tests.rs29
-rw-r--r--cli/tests/testdata/run/workspaces/basic/bar/deno.json8
-rw-r--r--cli/tests/testdata/run/workspaces/basic/bar/fizz/buzz.ts1
-rw-r--r--cli/tests/testdata/run/workspaces/basic/bar/mod.ts5
-rw-r--r--cli/tests/testdata/run/workspaces/basic/bar/some_mod/hello.ts1
-rw-r--r--cli/tests/testdata/run/workspaces/basic/deno.json9
-rw-r--r--cli/tests/testdata/run/workspaces/basic/foo/bar/hello.ts1
-rw-r--r--cli/tests/testdata/run/workspaces/basic/foo/deno.json8
-rw-r--r--cli/tests/testdata/run/workspaces/basic/foo/fizz/buzz.ts1
-rw-r--r--cli/tests/testdata/run/workspaces/basic/foo/mod.ts5
-rw-r--r--cli/tests/testdata/run/workspaces/basic/main.out21
-rw-r--r--cli/tests/testdata/run/workspaces/basic/main.ts5
-rw-r--r--cli/tests/testdata/run/workspaces/member_outside_root_dir/deno.json9
-rw-r--r--cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/bar/hello.ts1
-rw-r--r--cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/deno.json8
-rw-r--r--cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/fizz/buzz.ts1
-rw-r--r--cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/mod.ts5
-rw-r--r--cli/tests/testdata/run/workspaces/member_outside_root_dir/main.out1
-rw-r--r--cli/tests/testdata/run/workspaces/member_outside_root_dir/main.ts4
-rw-r--r--cli/tests/testdata/run/workspaces/nested_member/bar/deno.json8
-rw-r--r--cli/tests/testdata/run/workspaces/nested_member/bar/fizz/buzz.ts1
-rw-r--r--cli/tests/testdata/run/workspaces/nested_member/bar/mod.ts5
-rw-r--r--cli/tests/testdata/run/workspaces/nested_member/bar/some_mod/hello.ts1
-rw-r--r--cli/tests/testdata/run/workspaces/nested_member/deno.json6
-rw-r--r--cli/tests/testdata/run/workspaces/nested_member/foo/bar/deno.json7
-rw-r--r--cli/tests/testdata/run/workspaces/nested_member/foo/bar/hello.ts3
-rw-r--r--cli/tests/testdata/run/workspaces/nested_member/foo/deno.json7
-rw-r--r--cli/tests/testdata/run/workspaces/nested_member/foo/fizz/buzz.ts1
-rw-r--r--cli/tests/testdata/run/workspaces/nested_member/foo/mod.ts3
-rw-r--r--cli/tests/testdata/run/workspaces/nested_member/main.out1
-rw-r--r--cli/tests/testdata/run/workspaces/nested_member/main.ts4
38 files changed, 317 insertions, 24 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 545b8d697..11775d274 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1061,16 +1061,15 @@ dependencies = [
[[package]]
name = "deno_config"
-version = "0.5.0"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f4dd27020827a51857fbb47a21d2359f75db031e177821bff4e95368333a5a0"
+checksum = "40c6b9137fcc5c6f81d12214fc31cdddf2c64d0667a5458803e081ddd856d5b6"
dependencies = [
"anyhow",
"indexmap 2.0.2",
"jsonc-parser",
"log",
"percent-encoding",
- "pretty_assertions",
"serde",
"serde_json",
"url",
@@ -1156,9 +1155,9 @@ dependencies = [
[[package]]
name = "deno_doc"
-version = "0.73.1"
+version = "0.73.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0caa44ce342fb69b449ae750f8bf66dea6730ce7a00cc4449766d47b8626faf9"
+checksum = "d2854646e9edfea295844b392ab64b7f4fb7b25b608c82d40a2c2d39655b71db"
dependencies = [
"anyhow",
"cfg-if",
@@ -1179,9 +1178,9 @@ dependencies = [
[[package]]
name = "deno_emit"
-version = "0.31.3"
+version = "0.31.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "193d738bbe06f950a7be3f852f0d940839bba6f4c2cb1d2019eb0d6c727f7b03"
+checksum = "f8910a6da498d0eb2a28d9ea613c47291a86377a85b3771dd90d624004814aeb"
dependencies = [
"anyhow",
"base64 0.13.1",
@@ -1246,9 +1245,9 @@ dependencies = [
[[package]]
name = "deno_graph"
-version = "0.61.0"
+version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bd6700dd3cec2a7763760168c952cc632246a1ae0fc52d6f297e2802b994e69"
+checksum = "076c0b611c10901456b78c837408b9c40fe0c3602e767307d986f46f0cc56b51"
dependencies = [
"anyhow",
"async-trait",
@@ -2865,11 +2864,10 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
[[package]]
name = "import_map"
-version = "0.15.0"
+version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "632089ec08bd62e807311104122fb26d5c911ab172e2b9864be154a575979e29"
+checksum = "1e5bf51a0adfdc08afcb9e5a1c8f8c804227ec50d493c65e57e6d117d594bd1b"
dependencies = [
- "cfg-if",
"indexmap 1.9.3",
"log",
"serde",
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index c13e1bf91..37250f328 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -51,11 +51,11 @@ winres.workspace = true
[dependencies]
deno_ast = { workspace = true, features = ["bundler", "cjs", "codegen", "dep_graph", "module_specifier", "proposal", "react", "sourcemap", "transforms", "typescript", "view", "visit"] }
deno_cache_dir = "=0.6.1"
-deno_config = "=0.5.0"
+deno_config = "=0.6.4"
deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] }
-deno_doc = { version = "=0.73.1", features = ["html"] }
-deno_emit = "=0.31.3"
-deno_graph = "=0.61.0"
+deno_doc = { version = "=0.73.2", features = ["html"] }
+deno_emit = "=0.31.4"
+deno_graph = "=0.61.1"
deno_lint = { version = "=0.52.2", features = ["docs"] }
deno_lockfile.workspace = true
deno_npm = "0.15.2"
@@ -95,7 +95,7 @@ glob = "0.3.1"
hex.workspace = true
http.workspace = true
hyper.workspace = true
-import_map = "=0.15.0"
+import_map = { version = "=0.17.0", features = ["ext"] }
indexmap.workspace = true
jsonc-parser = { version = "=0.21.1", features = ["serde"] }
lazy-regex.workspace = true
diff --git a/cli/args/flags.rs b/cli/args/flags.rs
index 235743bda..6d1e41a19 100644
--- a/cli/args/flags.rs
+++ b/cli/args/flags.rs
@@ -431,6 +431,7 @@ pub struct Flags {
pub unstable: bool,
pub unstable_bare_node_builtins: bool,
pub unstable_byonm: bool,
+ pub unstable_workspaces: bool,
pub unstable_features: Vec<String>,
pub unsafely_ignore_certificate_errors: Option<Vec<String>>,
pub v8_flags: Vec<String>,
@@ -871,6 +872,7 @@ pub fn flags_from_vec(args: Vec<String>) -> clap::error::Result<Flags> {
flags.unstable_bare_node_builtins =
matches.get_flag("unstable-bare-node-builtins");
flags.unstable_byonm = matches.get_flag("unstable-byonm");
+ flags.unstable_workspaces = matches.get_flag("unstable-workspaces");
if matches.get_flag("quiet") {
flags.log_level = Some(Level::Error);
@@ -984,6 +986,15 @@ fn clap_root() -> Command {
.value_parser(FalseyValueParser::new())
.action(ArgAction::SetTrue)
.global(true),
+ )
+ .arg(
+ Arg::new("unstable-workspaces")
+ .long("unstable-workspaces")
+ .help("Enable unstable 'workspaces' feature")
+ .env("DENO_UNSTABLE_WORKSPACES")
+ .value_parser(FalseyValueParser::new())
+ .action(ArgAction::SetTrue)
+ .global(true),
);
for (flag_name, help, _) in crate::UNSTABLE_GRANULAR_FLAGS {
diff --git a/cli/args/import_map.rs b/cli/args/import_map.rs
index 9d1b2bbda..5ebb425eb 100644
--- a/cli/args/import_map.rs
+++ b/cli/args/import_map.rs
@@ -36,7 +36,7 @@ pub async fn resolve_import_map_from_specifier(
import_map_from_value(specifier, value)
}
-fn import_map_from_value(
+pub fn import_map_from_value(
specifier: &Url,
json_value: serde_json::Value,
) -> Result<ImportMap, AnyError> {
diff --git a/cli/args/mod.rs b/cli/args/mod.rs
index 24d1237aa..dd8de2a6f 100644
--- a/cli/args/mod.rs
+++ b/cli/args/mod.rs
@@ -29,6 +29,7 @@ pub use deno_config::TsConfig;
pub use deno_config::TsConfigForEmit;
pub use deno_config::TsConfigType;
pub use deno_config::TsTypeLib;
+pub use deno_config::WorkspaceConfig;
pub use flags::*;
pub use lockfile::Lockfile;
pub use lockfile::LockfileError;
@@ -619,6 +620,7 @@ pub struct CliOptions {
maybe_package_json: Option<PackageJson>,
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
overrides: CliOptionOverrides,
+ maybe_workspace_config: Option<WorkspaceConfig>,
}
impl CliOptions {
@@ -652,6 +654,20 @@ impl CliOptions {
.with_context(|| "Resolving node_modules folder.")?;
let maybe_vendor_folder =
resolve_vendor_folder(&initial_cwd, &flags, maybe_config_file.as_ref());
+ let maybe_workspace_config =
+ if let Some(config_file) = maybe_config_file.as_ref() {
+ config_file.to_workspace_config()?
+ } else {
+ None
+ };
+
+ // TODO(bartlomieju): remove in v1.39 or v1.40.
+ if let Some(wsconfig) = &maybe_workspace_config {
+ if !wsconfig.members.is_empty() && !flags.unstable_workspaces {
+ eprintln!("Use of unstable 'workspaces' feature. The --unstable-workspaces flags must be provided.");
+ std::process::exit(70);
+ }
+ }
if let Some(env_file_name) = &flags.env_file {
if (from_filename(env_file_name)).is_err() {
@@ -668,6 +684,7 @@ impl CliOptions {
maybe_node_modules_folder,
maybe_vendor_folder,
overrides: Default::default(),
+ maybe_workspace_config,
})
}
@@ -813,6 +830,41 @@ impl CliOptions {
&self,
file_fetcher: &FileFetcher,
) -> Result<Option<ImportMap>, AnyError> {
+ if let Some(workspace_config) = self.maybe_workspace_config.as_ref() {
+ let base_import_map_config = ::import_map::ext::ImportMapConfig {
+ base_url: self.maybe_config_file.as_ref().unwrap().specifier.clone(),
+ import_map_value: workspace_config.base_import_map_value.clone(),
+ };
+ let children_configs = workspace_config
+ .members
+ .iter()
+ .map(|member| {
+ let import_map_value = member.config_file.to_import_map_value();
+ ::import_map::ext::ImportMapConfig {
+ base_url: member.config_file.specifier.clone(),
+ import_map_value,
+ }
+ })
+ .collect();
+
+ let maybe_import_map = ::import_map::ext::create_synthetic_import_map(
+ base_import_map_config,
+ children_configs,
+ );
+ if let Some((_import_map_url, import_map)) = maybe_import_map {
+ log::debug!(
+ "Workspace config generated this import map {}",
+ serde_json::to_string_pretty(&import_map).unwrap()
+ );
+ return import_map::import_map_from_value(
+ // TODO(bartlomieju): maybe should be stored on the workspace config?
+ &self.maybe_config_file.as_ref().unwrap().specifier,
+ import_map,
+ )
+ .map(Some);
+ }
+ }
+
let import_map_specifier = match self.resolve_import_map_specifier()? {
Some(specifier) => specifier,
None => return Ok(None),
@@ -940,6 +992,7 @@ impl CliOptions {
maybe_config_file: self.maybe_config_file.clone(),
maybe_package_json: self.maybe_package_json.clone(),
maybe_lockfile: self.maybe_lockfile.clone(),
+ maybe_workspace_config: self.maybe_workspace_config.clone(),
overrides: self.overrides.clone(),
}
}
@@ -1037,6 +1090,10 @@ impl CliOptions {
&self.maybe_config_file
}
+ pub fn maybe_workspace_config(&self) -> &Option<WorkspaceConfig> {
+ &self.maybe_workspace_config
+ }
+
pub fn maybe_package_json(&self) -> &Option<PackageJson> {
&self.maybe_package_json
}
diff --git a/cli/graph_util.rs b/cli/graph_util.rs
index f2713a9db..727037748 100644
--- a/cli/graph_util.rs
+++ b/cli/graph_util.rs
@@ -19,6 +19,7 @@ use crate::util::sync::TaskQueue;
use crate::util::sync::TaskQueuePermit;
use deno_core::anyhow::bail;
+use deno_core::anyhow::Context;
use deno_core::error::custom_error;
use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
@@ -269,6 +270,12 @@ impl ModuleGraphBuilder {
options: CreateGraphOptions<'_>,
) -> Result<deno_graph::ModuleGraph, AnyError> {
let maybe_imports = self.options.to_maybe_imports()?;
+ let maybe_workspace_config = self.options.maybe_workspace_config();
+ let workspace_members = if let Some(wc) = maybe_workspace_config {
+ workspace_config_to_workspace_members(wc)?
+ } else {
+ vec![]
+ };
let cli_resolver = self.resolver.clone();
let graph_resolver = cli_resolver.as_graph_resolver();
@@ -291,8 +298,7 @@ impl ModuleGraphBuilder {
npm_resolver: Some(graph_npm_resolver),
module_analyzer: Some(options.analyzer),
reporter: maybe_file_watcher_reporter,
- // todo(dsherret): workspace support
- workspace_members: vec![],
+ workspace_members,
},
)
.await?;
@@ -312,6 +318,12 @@ impl ModuleGraphBuilder {
) -> Result<Arc<deno_graph::ModuleGraph>, AnyError> {
let mut cache = self.create_graph_loader();
let maybe_imports = self.options.to_maybe_imports()?;
+ let maybe_workspace_config = self.options.maybe_workspace_config();
+ let workspace_members = if let Some(wc) = maybe_workspace_config {
+ workspace_config_to_workspace_members(wc)?
+ } else {
+ vec![]
+ };
let cli_resolver = self.resolver.clone();
let graph_resolver = cli_resolver.as_graph_resolver();
let graph_npm_resolver = cli_resolver.as_graph_npm_resolver();
@@ -336,8 +348,7 @@ impl ModuleGraphBuilder {
npm_resolver: Some(graph_npm_resolver),
module_analyzer: Some(&analyzer),
reporter: maybe_file_watcher_reporter,
- // todo(dsherret): workspace support
- workspace_members: vec![],
+ workspace_members,
},
)
.await?;
@@ -719,6 +730,46 @@ impl deno_graph::source::Reporter for FileWatcherReporter {
}
}
+pub fn workspace_config_to_workspace_members(
+ workspace_config: &deno_config::WorkspaceConfig,
+) -> Result<Vec<deno_graph::WorkspaceMember>, AnyError> {
+ workspace_config
+ .members
+ .iter()
+ .map(|member| {
+ workspace_member_config_try_into_workspace_member(member).with_context(
+ || {
+ format!(
+ "Failed to resolve configuration for '{}' workspace member at '{}'",
+ member.member_name,
+ member.config_file.specifier.as_str()
+ )
+ },
+ )
+ })
+ .collect()
+}
+
+fn workspace_member_config_try_into_workspace_member(
+ config: &deno_config::WorkspaceMemberConfig,
+) -> Result<deno_graph::WorkspaceMember, AnyError> {
+ let nv = deno_semver::package::PackageNv {
+ name: config.package_name.clone(),
+ version: deno_semver::Version::parse_standard(&config.package_version)?,
+ };
+ Ok(deno_graph::WorkspaceMember {
+ base: ModuleSpecifier::from_directory_path(&config.path).unwrap(),
+ nv,
+ exports: config
+ .config_file
+ .to_exports_config()?
+ .into_map()
+ // todo(dsherret): deno_graph should use an IndexMap
+ .into_iter()
+ .collect(),
+ })
+}
+
#[cfg(test)]
mod test {
use std::sync::Arc;
diff --git a/cli/module_loader.rs b/cli/module_loader.rs
index c8b2a36df..d8ab2d8c9 100644
--- a/cli/module_loader.rs
+++ b/cli/module_loader.rs
@@ -8,6 +8,7 @@ use crate::cache::ParsedSourceCache;
use crate::emit::Emitter;
use crate::graph_util::graph_lock_or_exit;
use crate::graph_util::graph_valid_with_cli_options;
+use crate::graph_util::workspace_config_to_workspace_members;
use crate::graph_util::FileWatcherReporter;
use crate::graph_util::ModuleGraphBuilder;
use crate::graph_util::ModuleGraphContainer;
@@ -119,6 +120,12 @@ impl ModuleLoadPreparer {
let mut cache = self.module_graph_builder.create_fetch_cacher(permissions);
let maybe_imports = self.options.to_maybe_imports()?;
+ let maybe_workspace_config = self.options.maybe_workspace_config();
+ let workspace_members = if let Some(wc) = maybe_workspace_config {
+ workspace_config_to_workspace_members(wc)?
+ } else {
+ vec![]
+ };
let graph_resolver = self.resolver.as_graph_resolver();
let graph_npm_resolver = self.resolver.as_graph_npm_resolver();
let maybe_file_watcher_reporter = self
@@ -152,8 +159,7 @@ impl ModuleLoadPreparer {
npm_resolver: Some(graph_npm_resolver),
module_analyzer: Some(&analyzer),
reporter: maybe_file_watcher_reporter,
- // todo(dsherret): workspace support
- workspace_members: vec![],
+ workspace_members,
},
)
.await?;
diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs
index c24464eba..306d4df67 100644
--- a/cli/tests/integration/run_tests.rs
+++ b/cli/tests/integration/run_tests.rs
@@ -4759,3 +4759,32 @@ itest!(explicit_resource_management {
args: "run --quiet --check run/explicit_resource_management/main.ts",
output: "run/explicit_resource_management/main.out",
});
+
+itest!(workspaces_basic {
+ args: "run -L debug -A --unstable-workspaces main.ts",
+ output: "run/workspaces/basic/main.out",
+ cwd: Some("run/workspaces/basic/"),
+ copy_temp_dir: Some("run/workspaces/basic/"),
+ envs: env_vars_for_npm_tests(),
+ http_server: true,
+});
+
+itest!(workspaces_member_outside_root_dir {
+ args: "run -A --unstable-workspaces main.ts",
+ output: "run/workspaces/member_outside_root_dir/main.out",
+ cwd: Some("run/workspaces/member_outside_root_dir/"),
+ copy_temp_dir: Some("run/workspaces/member_outside_root_dir/"),
+ envs: env_vars_for_npm_tests(),
+ http_server: true,
+ exit_code: 1,
+});
+
+itest!(workspaces_nested_member {
+ args: "run -A --unstable-workspaces main.ts",
+ output: "run/workspaces/nested_member/main.out",
+ cwd: Some("run/workspaces/nested_member/"),
+ copy_temp_dir: Some("run/workspaces/nested_member/"),
+ envs: env_vars_for_npm_tests(),
+ http_server: true,
+ exit_code: 1,
+});
diff --git a/cli/tests/testdata/run/workspaces/basic/bar/deno.json b/cli/tests/testdata/run/workspaces/basic/bar/deno.json
new file mode 100644
index 000000000..ef3bfc37a
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/basic/bar/deno.json
@@ -0,0 +1,8 @@
+{
+ "name": "asdfasdfasdf",
+ "version": "0.0.0",
+ "imports": {
+ "@/": "./",
+ "secret_mod/": "./some_mod/"
+ }
+}
diff --git a/cli/tests/testdata/run/workspaces/basic/bar/fizz/buzz.ts b/cli/tests/testdata/run/workspaces/basic/bar/fizz/buzz.ts
new file mode 100644
index 000000000..f88d62fcc
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/basic/bar/fizz/buzz.ts
@@ -0,0 +1 @@
+export const buzz = "buzz from bar";
diff --git a/cli/tests/testdata/run/workspaces/basic/bar/mod.ts b/cli/tests/testdata/run/workspaces/basic/bar/mod.ts
new file mode 100644
index 000000000..6f898e389
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/basic/bar/mod.ts
@@ -0,0 +1,5 @@
+import { hello } from "secret_mod/hello.ts";
+import { buzz } from "@/fizz/buzz.ts";
+
+console.log(hello);
+console.log(buzz);
diff --git a/cli/tests/testdata/run/workspaces/basic/bar/some_mod/hello.ts b/cli/tests/testdata/run/workspaces/basic/bar/some_mod/hello.ts
new file mode 100644
index 000000000..1013de8d2
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/basic/bar/some_mod/hello.ts
@@ -0,0 +1 @@
+export const hello = "hello from bar";
diff --git a/cli/tests/testdata/run/workspaces/basic/deno.json b/cli/tests/testdata/run/workspaces/basic/deno.json
new file mode 100644
index 000000000..b971c4f3d
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/basic/deno.json
@@ -0,0 +1,9 @@
+{
+ "workspaces": [
+ "foo",
+ "bar"
+ ],
+ "imports": {
+ "chalk": "npm:chalk"
+ }
+}
diff --git a/cli/tests/testdata/run/workspaces/basic/foo/bar/hello.ts b/cli/tests/testdata/run/workspaces/basic/foo/bar/hello.ts
new file mode 100644
index 000000000..c8a7e57c4
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/basic/foo/bar/hello.ts
@@ -0,0 +1 @@
+export const hello = "hello from foo";
diff --git a/cli/tests/testdata/run/workspaces/basic/foo/deno.json b/cli/tests/testdata/run/workspaces/basic/foo/deno.json
new file mode 100644
index 000000000..46d84f06f
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/basic/foo/deno.json
@@ -0,0 +1,8 @@
+{
+ "name": "qwerqwer",
+ "version": "0.0.0",
+ "imports": {
+ "~/": "./",
+ "foo/": "./bar/"
+ }
+}
diff --git a/cli/tests/testdata/run/workspaces/basic/foo/fizz/buzz.ts b/cli/tests/testdata/run/workspaces/basic/foo/fizz/buzz.ts
new file mode 100644
index 000000000..4e03777d1
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/basic/foo/fizz/buzz.ts
@@ -0,0 +1 @@
+export const buzz = "buzz from foo";
diff --git a/cli/tests/testdata/run/workspaces/basic/foo/mod.ts b/cli/tests/testdata/run/workspaces/basic/foo/mod.ts
new file mode 100644
index 000000000..d7b16dcc0
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/basic/foo/mod.ts
@@ -0,0 +1,5 @@
+import { hello } from "foo/hello.ts";
+import { buzz } from "~/fizz/buzz.ts";
+
+console.log(hello);
+console.log(buzz);
diff --git a/cli/tests/testdata/run/workspaces/basic/main.out b/cli/tests/testdata/run/workspaces/basic/main.out
new file mode 100644
index 000000000..77e0de4d1
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/basic/main.out
@@ -0,0 +1,21 @@
+[WILDCARD]Workspace config generated this import map {
+ "imports": {
+ "chalk": "npm:chalk"
+ },
+ "scopes": {
+ "./foo/": {
+ "~/": "./foo/",
+ "foo/": "./foo/bar/"
+ },
+ "./bar/": {
+ "@/": "./bar/",
+ "secret_mod/": "./bar/some_mod/"
+ }
+ }
+}
+[WILDCARD]
+hello from foo
+buzz from foo
+hello from bar
+buzz from bar
+[Function: chalk][WILDCARD] \ No newline at end of file
diff --git a/cli/tests/testdata/run/workspaces/basic/main.ts b/cli/tests/testdata/run/workspaces/basic/main.ts
new file mode 100644
index 000000000..380c97619
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/basic/main.ts
@@ -0,0 +1,5 @@
+import chalk from "chalk";
+import "./foo/mod.ts";
+import "./bar/mod.ts";
+
+console.log(chalk);
diff --git a/cli/tests/testdata/run/workspaces/member_outside_root_dir/deno.json b/cli/tests/testdata/run/workspaces/member_outside_root_dir/deno.json
new file mode 100644
index 000000000..25feefad8
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/member_outside_root_dir/deno.json
@@ -0,0 +1,9 @@
+{
+ "workspaces": [
+ "foo",
+ "../other_folder"
+ ],
+ "imports": {
+ "chalk": "npm:chalk"
+ }
+}
diff --git a/cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/bar/hello.ts b/cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/bar/hello.ts
new file mode 100644
index 000000000..c8a7e57c4
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/bar/hello.ts
@@ -0,0 +1 @@
+export const hello = "hello from foo";
diff --git a/cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/deno.json b/cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/deno.json
new file mode 100644
index 000000000..46d84f06f
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/deno.json
@@ -0,0 +1,8 @@
+{
+ "name": "qwerqwer",
+ "version": "0.0.0",
+ "imports": {
+ "~/": "./",
+ "foo/": "./bar/"
+ }
+}
diff --git a/cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/fizz/buzz.ts b/cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/fizz/buzz.ts
new file mode 100644
index 000000000..4e03777d1
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/fizz/buzz.ts
@@ -0,0 +1 @@
+export const buzz = "buzz from foo";
diff --git a/cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/mod.ts b/cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/mod.ts
new file mode 100644
index 000000000..d7b16dcc0
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/member_outside_root_dir/foo/mod.ts
@@ -0,0 +1,5 @@
+import { hello } from "foo/hello.ts";
+import { buzz } from "~/fizz/buzz.ts";
+
+console.log(hello);
+console.log(buzz);
diff --git a/cli/tests/testdata/run/workspaces/member_outside_root_dir/main.out b/cli/tests/testdata/run/workspaces/member_outside_root_dir/main.out
new file mode 100644
index 000000000..205d95aea
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/member_outside_root_dir/main.out
@@ -0,0 +1 @@
+error: Workspace member '../other_folder' is outside root configuration directory[WILDCARD] \ No newline at end of file
diff --git a/cli/tests/testdata/run/workspaces/member_outside_root_dir/main.ts b/cli/tests/testdata/run/workspaces/member_outside_root_dir/main.ts
new file mode 100644
index 000000000..182fd8517
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/member_outside_root_dir/main.ts
@@ -0,0 +1,4 @@
+import chalk from "chalk";
+import "./foo/mod.ts";
+
+console.log(chalk);
diff --git a/cli/tests/testdata/run/workspaces/nested_member/bar/deno.json b/cli/tests/testdata/run/workspaces/nested_member/bar/deno.json
new file mode 100644
index 000000000..ef3bfc37a
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/nested_member/bar/deno.json
@@ -0,0 +1,8 @@
+{
+ "name": "asdfasdfasdf",
+ "version": "0.0.0",
+ "imports": {
+ "@/": "./",
+ "secret_mod/": "./some_mod/"
+ }
+}
diff --git a/cli/tests/testdata/run/workspaces/nested_member/bar/fizz/buzz.ts b/cli/tests/testdata/run/workspaces/nested_member/bar/fizz/buzz.ts
new file mode 100644
index 000000000..f88d62fcc
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/nested_member/bar/fizz/buzz.ts
@@ -0,0 +1 @@
+export const buzz = "buzz from bar";
diff --git a/cli/tests/testdata/run/workspaces/nested_member/bar/mod.ts b/cli/tests/testdata/run/workspaces/nested_member/bar/mod.ts
new file mode 100644
index 000000000..6f898e389
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/nested_member/bar/mod.ts
@@ -0,0 +1,5 @@
+import { hello } from "secret_mod/hello.ts";
+import { buzz } from "@/fizz/buzz.ts";
+
+console.log(hello);
+console.log(buzz);
diff --git a/cli/tests/testdata/run/workspaces/nested_member/bar/some_mod/hello.ts b/cli/tests/testdata/run/workspaces/nested_member/bar/some_mod/hello.ts
new file mode 100644
index 000000000..1013de8d2
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/nested_member/bar/some_mod/hello.ts
@@ -0,0 +1 @@
+export const hello = "hello from bar";
diff --git a/cli/tests/testdata/run/workspaces/nested_member/deno.json b/cli/tests/testdata/run/workspaces/nested_member/deno.json
new file mode 100644
index 000000000..6d9c09d4d
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/nested_member/deno.json
@@ -0,0 +1,6 @@
+{
+ "workspaces": [
+ "foo",
+ "foo/bar"
+ ]
+}
diff --git a/cli/tests/testdata/run/workspaces/nested_member/foo/bar/deno.json b/cli/tests/testdata/run/workspaces/nested_member/foo/bar/deno.json
new file mode 100644
index 000000000..d40328b36
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/nested_member/foo/bar/deno.json
@@ -0,0 +1,7 @@
+{
+ "name": "bar",
+ "version": "0.0.0",
+ "imports": {
+ "chalk": "npm:chalk"
+ }
+}
diff --git a/cli/tests/testdata/run/workspaces/nested_member/foo/bar/hello.ts b/cli/tests/testdata/run/workspaces/nested_member/foo/bar/hello.ts
new file mode 100644
index 000000000..9c1023153
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/nested_member/foo/bar/hello.ts
@@ -0,0 +1,3 @@
+import chalk from "chalk";
+
+export default chalk;
diff --git a/cli/tests/testdata/run/workspaces/nested_member/foo/deno.json b/cli/tests/testdata/run/workspaces/nested_member/foo/deno.json
new file mode 100644
index 000000000..68e053b02
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/nested_member/foo/deno.json
@@ -0,0 +1,7 @@
+{
+ "name": "qwerqwer",
+ "version": "0.0.0",
+ "imports": {
+ "~/": "./"
+ }
+}
diff --git a/cli/tests/testdata/run/workspaces/nested_member/foo/fizz/buzz.ts b/cli/tests/testdata/run/workspaces/nested_member/foo/fizz/buzz.ts
new file mode 100644
index 000000000..4e03777d1
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/nested_member/foo/fizz/buzz.ts
@@ -0,0 +1 @@
+export const buzz = "buzz from foo";
diff --git a/cli/tests/testdata/run/workspaces/nested_member/foo/mod.ts b/cli/tests/testdata/run/workspaces/nested_member/foo/mod.ts
new file mode 100644
index 000000000..b9d4d3c04
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/nested_member/foo/mod.ts
@@ -0,0 +1,3 @@
+import { buzz } from "~/fizz/buzz.ts";
+
+console.log(buzz);
diff --git a/cli/tests/testdata/run/workspaces/nested_member/main.out b/cli/tests/testdata/run/workspaces/nested_member/main.out
new file mode 100644
index 000000000..98598a306
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/nested_member/main.out
@@ -0,0 +1 @@
+error: Workspace member 'foo/bar' is nested within other workspace member 'foo'
diff --git a/cli/tests/testdata/run/workspaces/nested_member/main.ts b/cli/tests/testdata/run/workspaces/nested_member/main.ts
new file mode 100644
index 000000000..2bf53f7c2
--- /dev/null
+++ b/cli/tests/testdata/run/workspaces/nested_member/main.ts
@@ -0,0 +1,4 @@
+import "./foo/mod.ts";
+import chalk from "./foo/bar/hello.ts";
+
+console.log(chalk);