diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 279df82657f..bf54fe3bdd1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -72,6 +72,11 @@ jobs: bins: cargo-make, cargo-nextest, cargo-llvm-cov components: llvm-tools-preview cache: false + # Required for "globals" tests + - uses: moonrepo/setup-toolchain@v0 + if: ${{ runner.os == 'Linux' }} + - run: proto install bun 1.0.0 + if: ${{ runner.os == 'Linux' }} - uses: mozilla-actions/sccache-action@v0.0.3 - name: Run tests if: ${{ env.WITH_COVERAGE == 'false' }} diff --git a/Cargo.lock b/Cargo.lock index c7ef281265b..198e5d065cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2922,6 +2922,7 @@ version = "0.1.0" dependencies = [ "miette", "moon_action_graph", + "moon_bun_platform", "moon_config", "moon_deno_platform", "moon_node_platform", @@ -3113,18 +3114,55 @@ dependencies = [ "yarn-lock-parser", ] +[[package]] +name = "moon_bun_platform" +version = "0.1.0" +dependencies = [ + "miette", + "moon", + "moon_action_context", + "moon_bun_lang", + "moon_bun_tool", + "moon_common", + "moon_config", + "moon_hash", + "moon_lang", + "moon_logger", + "moon_node_lang", + "moon_platform", + "moon_process", + "moon_project", + "moon_project_graph", + "moon_task", + "moon_terminal", + "moon_test_utils", + "moon_tool", + "moon_typescript_platform", + "moon_utils", + "proto_core", + "rustc-hash", + "serde", + "serde_json", + "starbase_styles", + "starbase_utils", + "tokio", +] + [[package]] name = "moon_bun_tool" version = "0.1.0" dependencies = [ "miette", + "moon_bun_lang", "moon_config", "moon_logger", "moon_platform_runtime", + "moon_process", "moon_terminal", "moon_tool", "proto_core", "rustc-hash", + "starbase_utils", ] [[package]] @@ -3929,6 +3967,7 @@ name = "moon_test_utils2" version = "0.1.0" dependencies = [ "miette", + "moon_bun_platform", "moon_config", "moon_node_platform", "moon_platform", diff --git a/crates/bun/lang/src/lib.rs b/crates/bun/lang/src/lib.rs index 6dbc12fcb3b..c6581ec134a 100644 --- a/crates/bun/lang/src/lib.rs +++ b/crates/bun/lang/src/lib.rs @@ -3,6 +3,7 @@ mod bun_lockb; use moon_lang::{DependencyManager, Language}; pub use bun_lockb::*; +pub use moon_lang::LockfileDependencyVersions; pub const BUN: Language = Language { binary: "bun", @@ -13,7 +14,7 @@ pub const BUN: Language = Language { // Dependency managers -pub const BUN_INSTALL: DependencyManager = DependencyManager { +pub const BUNPM: DependencyManager = DependencyManager { binary: "bun install", config_files: &["bunfig.toml"], lockfile: "bun.lockb", diff --git a/crates/bun/platform/Cargo.toml b/crates/bun/platform/Cargo.toml new file mode 100644 index 00000000000..e2044515f83 --- /dev/null +++ b/crates/bun/platform/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "moon_bun_platform" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +moon_action_context = { path = "../../core/action-context" } +moon_bun_lang = { path = "../lang" } +moon_bun_tool = { path = "../tool" } +moon_common = { path = "../../../nextgen/common" } +moon_config = { path = "../../../nextgen/config" } +moon_hash = { path = "../../../nextgen/hash" } +moon_lang = { path = "../../core/lang" } +moon_logger = { path = "../../core/logger" } +moon_node_lang = { path = "../../node/lang" } +moon_platform = { path = "../../core/platform" } +moon_process = { path = "../../../nextgen/process" } +moon_project = { path = "../../../nextgen/project" } +moon_task = { path = "../../../nextgen/task" } +moon_terminal = { path = "../../core/terminal" } +moon_tool = { path = "../../core/tool" } +moon_typescript_platform = { path = "../../typescript/platform" } +moon_utils = { path = "../../core/utils" } +miette = { workspace = true } +proto_core = { workspace = true } +rustc-hash = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +starbase_styles = { workspace = true } +starbase_utils = { workspace = true } +tokio = { workspace = true } + +[dev-dependencies] +moon = { path = "../../core/moon" } +moon_project_graph = { path = "../../../nextgen/project-graph" } +moon_test_utils = { path = "../../core/test-utils" } diff --git a/crates/bun/platform/src/actions/install_deps.rs b/crates/bun/platform/src/actions/install_deps.rs new file mode 100644 index 00000000000..a66b354a0e0 --- /dev/null +++ b/crates/bun/platform/src/actions/install_deps.rs @@ -0,0 +1,32 @@ +use moon_bun_lang::BUN; +use moon_bun_tool::BunTool; +use moon_lang::has_vendor_installed_dependencies; +use moon_logger::{debug, info}; +use moon_terminal::{print_checkpoint, Checkpoint}; +use moon_tool::DependencyManager; +use moon_utils::{is_ci, is_test_env}; +use std::path::Path; + +const LOG_TARGET: &str = "moon:bun-platform:install-deps"; + +pub async fn install_deps(bun: &BunTool, working_dir: &Path) -> miette::Result<()> { + // When in CI, we can avoid installing dependencies because + // we can assume they've already been installed before moon runs! + if is_ci() && has_vendor_installed_dependencies(working_dir, &BUN) { + info!( + target: LOG_TARGET, + "In a CI environment and dependencies already exist, skipping install" + ); + + return Ok(()); + } + + debug!(target: LOG_TARGET, "Installing dependencies"); + + print_checkpoint("bun install", Checkpoint::Setup); + + bun.install_dependencies(&(), working_dir, !is_test_env()) + .await?; + + Ok(()) +} diff --git a/crates/bun/platform/src/actions/mod.rs b/crates/bun/platform/src/actions/mod.rs new file mode 100644 index 00000000000..bd1fad7d307 --- /dev/null +++ b/crates/bun/platform/src/actions/mod.rs @@ -0,0 +1,5 @@ +mod install_deps; +mod run_target; + +pub use install_deps::*; +pub use run_target::*; diff --git a/crates/bun/platform/src/actions/run_target.rs b/crates/bun/platform/src/actions/run_target.rs new file mode 100644 index 00000000000..a8bd47fcdc8 --- /dev/null +++ b/crates/bun/platform/src/actions/run_target.rs @@ -0,0 +1,145 @@ +use crate::target_hash::BunTargetHash; +use moon_bun_tool::BunTool; +use moon_config::{HasherConfig, HasherOptimization}; +use moon_node_lang::{ + node::{self, BinFile}, + PackageJson, +}; +use moon_process::Command; +use moon_project::Project; +use moon_task::Task; +use moon_tool::{prepend_path_env_var, DependencyManager, Tool, ToolError}; +use moon_utils::path; +use rustc_hash::FxHashMap; +use std::path::Path; + +fn find_package_bin( + command: &mut Command, + starting_dir: &Path, + working_dir: &Path, + bin_name: &str, +) -> miette::Result> { + let possible_bin_path = match node::find_package_bin(starting_dir, bin_name)? { + Some(bin) => bin, + None => { + // moon isn't installed as a node module, but probably + // exists globally, so let's go with that instead of failing + if bin_name == "moon" { + return Ok(Some(Command::new(bin_name))); + } + + return Err(ToolError::MissingBinary("node module".into(), bin_name.to_owned()).into()); + } + }; + + match possible_bin_path { + // Rust, Go + BinFile::Binary(bin_path) => { + return Ok(Some(Command::new(bin_path))); + } + // JavaScript + BinFile::Script(bin_path) => { + command.arg(path::to_string( + path::relative_from(bin_path, working_dir).unwrap(), + )?); + } + // Other (Bash) + BinFile::Other(bin_path, parent_cmd) => { + let mut cmd = Command::new(parent_cmd); + cmd.arg(bin_path); + + return Ok(Some(cmd)); + } + }; + + Ok(None) +} + +pub fn create_target_command( + bun: &BunTool, + project: &Project, + task: &Task, + working_dir: &Path, +) -> miette::Result { + let mut command = Command::new(bun.get_bin_path()?); + + match task.command.as_str() { + "bun" | "bunx" => { + if task.command == "bunx" { + command.arg("x"); + } + } + bin => { + if let Some(new_command) = + find_package_bin(&mut command, &project.root, working_dir, bin)? + { + command = new_command; + } + } + }; + + if !bun.global { + command.env( + "PATH", + prepend_path_env_var([bun.tool.get_bin_path()?.parent().unwrap()]), + ); + } + + command.args(&task.args).envs(&task.env); + + Ok(command) +} + +// This is like the function above, but is for situations where the tool +// has not been configured, and should default to the global "bun" found +// in the user's shell. +pub fn create_target_command_without_tool( + project: &Project, + task: &Task, + working_dir: &Path, +) -> miette::Result { + let mut command = Command::new(&task.command); + + if task.command != "bun" && task.command != "bunx" { + if let Some(new_command) = + find_package_bin(&mut command, &project.root, working_dir, &task.command)? + { + command = new_command; + } + } + + command.args(&task.args).envs(&task.env); + + Ok(command) +} + +pub async fn create_target_hasher( + bun: Option<&BunTool>, + project: &Project, + workspace_root: &Path, + hasher_config: &HasherConfig, +) -> miette::Result { + let mut hasher = BunTargetHash::new( + bun.map(|n| n.config.version.as_ref().map(|v| v.to_string())) + .unwrap_or_default(), + ); + + let resolved_dependencies = + if matches!(hasher_config.optimization, HasherOptimization::Accuracy) && bun.is_some() { + bun.unwrap() + .get_resolved_dependencies(&project.root) + .await? + } else { + FxHashMap::default() + }; + + if let Some(root_package) = PackageJson::read(workspace_root)? { + hasher.hash_package_json(&root_package, &resolved_dependencies); + } + + if let Some(package) = PackageJson::read(&project.root)? { + hasher.hash_package_json(&package, &resolved_dependencies); + } + + Ok(hasher) +} diff --git a/crates/bun/platform/src/bun_platform.rs b/crates/bun/platform/src/bun_platform.rs new file mode 100644 index 00000000000..6fc888266ca --- /dev/null +++ b/crates/bun/platform/src/bun_platform.rs @@ -0,0 +1,380 @@ +use crate::actions; +use moon_action_context::ActionContext; +use moon_bun_lang::BUNPM; +use moon_bun_tool::BunTool; +use moon_common::Id; +use moon_config::{ + BunConfig, DependencyConfig, DependencyScope, DependencySource, HasherConfig, PlatformType, + ProjectConfig, ProjectsAliasesMap, ProjectsSourcesMap, TypeScriptConfig, UnresolvedVersionSpec, +}; +use moon_hash::{ContentHasher, DepsHash}; +use moon_logger::{debug, warn}; +use moon_node_lang::{node::get_package_manager_workspaces, PackageJson}; +use moon_platform::{Platform, Runtime, RuntimeReq}; +use moon_process::Command; +use moon_project::Project; +use moon_task::Task; +use moon_tool::{Tool, ToolManager}; +use moon_typescript_platform::TypeScriptTargetHash; +use moon_utils::async_trait; +use proto_core::ProtoEnvironment; +use rustc_hash::FxHashMap; +use starbase_styles::color; +use starbase_utils::glob::GlobSet; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + sync::Arc, +}; + +const LOG_TARGET: &str = "moon:bun-platform"; + +pub struct BunPlatform { + pub config: BunConfig, + + package_names: FxHashMap, + + proto_env: Arc, + + toolchain: ToolManager, + + typescript_config: Option, + + #[allow(dead_code)] + pub workspace_root: PathBuf, +} + +impl BunPlatform { + pub fn new( + config: &BunConfig, + typescript_config: &Option, + workspace_root: &Path, + proto_env: Arc, + ) -> Self { + BunPlatform { + config: config.to_owned(), + package_names: FxHashMap::default(), + proto_env, + toolchain: ToolManager::new(Runtime::new(PlatformType::Bun, RuntimeReq::Global)), + typescript_config: typescript_config.to_owned(), + workspace_root: workspace_root.to_path_buf(), + } + } +} + +#[async_trait] +impl Platform for BunPlatform { + fn get_type(&self) -> PlatformType { + PlatformType::Bun + } + + fn get_runtime_from_config(&self, project_config: Option<&ProjectConfig>) -> Runtime { + if let Some(config) = &project_config { + if let Some(bun_config) = &config.toolchain.bun { + if let Some(version) = &bun_config.version { + return Runtime::new_override( + PlatformType::Bun, + RuntimeReq::Toolchain(version.to_owned()), + ); + } + } + } + + if let Some(version) = &self.config.version { + return Runtime::new(PlatformType::Bun, RuntimeReq::Toolchain(version.to_owned())); + } + + Runtime::new(PlatformType::Bun, RuntimeReq::Global) + } + + fn matches(&self, platform: &PlatformType, runtime: Option<&Runtime>) -> bool { + if matches!(platform, PlatformType::Bun) { + return true; + } + + if let Some(runtime) = &runtime { + return matches!(runtime.platform, PlatformType::Bun); + } + + false + } + + // PROJECT GRAPH + + fn is_project_in_dependency_workspace(&self, project_source: &str) -> miette::Result { + let mut in_workspace = false; + + // Root package is always considered within the workspace + if project_source.is_empty() || project_source == "." { + return Ok(true); + } + + if let Some(globs) = get_package_manager_workspaces(self.workspace_root.to_owned())? { + in_workspace = GlobSet::new(&globs)?.matches(project_source); + } + + Ok(in_workspace) + } + + fn load_project_graph_aliases( + &mut self, + projects_map: &ProjectsSourcesMap, + aliases_map: &mut ProjectsAliasesMap, + ) -> miette::Result<()> { + debug!( + target: LOG_TARGET, + "Loading names (aliases) from project {}'s", + color::file(BUNPM.manifest) + ); + + for (project_id, project_source) in projects_map { + if let Some(package_json) = + PackageJson::read(project_source.to_path(&self.workspace_root))? + { + if let Some(package_name) = package_json.name { + let alias = package_name.clone(); + + self.package_names + .insert(package_name.clone(), project_id.to_owned()); + + if let Some(existing_source) = projects_map.get(&alias) { + if existing_source != project_source { + warn!( + target: LOG_TARGET, + "A project already exists with the ID {} ({}), skipping alias of the same name ({})", + color::id(&alias), + color::file(existing_source), + color::file(project_source) + ); + + continue; + } + } + + if aliases_map.contains_key(&alias) { + // Ignore warning here since the duplicate may have come + // from the Node.js platform! + continue; + } + + aliases_map.insert(alias, project_id.to_owned()); + } + } + } + + Ok(()) + } + + fn load_project_implicit_dependencies( + &self, + project_id: &str, + project_source: &str, + ) -> miette::Result> { + let mut implicit_deps = vec![]; + + debug!( + target: LOG_TARGET, + "Scanning {} for implicit dependency relations", + color::id(project_id), + ); + + if let Some(package_json) = PackageJson::read(self.workspace_root.join(project_source))? { + let mut find_implicit_relations = + |package_deps: &BTreeMap, scope: &DependencyScope| { + for dep_name in package_deps.keys() { + if let Some(dep_project_id) = self.package_names.get(dep_name) { + implicit_deps.push(DependencyConfig { + id: dep_project_id.to_owned(), + scope: *scope, + source: DependencySource::Implicit, + via: Some(dep_name.clone()), + }); + } + } + }; + + if let Some(dependencies) = &package_json.dependencies { + find_implicit_relations(dependencies, &DependencyScope::Production); + } + + if let Some(dev_dependencies) = &package_json.dev_dependencies { + find_implicit_relations(dev_dependencies, &DependencyScope::Development); + } + + if let Some(peer_dependencies) = &package_json.peer_dependencies { + find_implicit_relations(peer_dependencies, &DependencyScope::Peer); + } + } + + Ok(implicit_deps) + } + + // TOOLCHAIN + + fn is_toolchain_enabled(&self) -> miette::Result { + Ok(self.config.version.is_some()) + } + + fn get_tool(&self) -> miette::Result> { + let tool = self.toolchain.get()?; + + Ok(Box::new(tool)) + } + + fn get_tool_for_version(&self, req: RuntimeReq) -> miette::Result> { + let tool = self.toolchain.get_for_version(&req)?; + + Ok(Box::new(tool)) + } + + fn get_dependency_configs(&self) -> miette::Result> { + Ok(Some((BUNPM.lockfile.to_owned(), BUNPM.manifest.to_owned()))) + } + + async fn setup_toolchain(&mut self) -> miette::Result<()> { + let req = match &self.config.version { + Some(v) => RuntimeReq::Toolchain(v.to_owned()), + None => RuntimeReq::Global, + }; + + let mut last_versions = FxHashMap::default(); + + if !self.toolchain.has(&req) { + self.toolchain.register( + &req, + BunTool::new(&self.proto_env, &self.config, &req).await?, + ); + } + + self.toolchain.setup(&req, &mut last_versions).await?; + + Ok(()) + } + + async fn teardown_toolchain(&mut self) -> miette::Result<()> { + self.toolchain.teardown_all().await?; + + Ok(()) + } + + // ACTIONS + + async fn setup_tool( + &mut self, + _context: &ActionContext, + runtime: &Runtime, + last_versions: &mut FxHashMap, + ) -> miette::Result { + let req = &runtime.requirement; + + if !self.toolchain.has(req) { + self.toolchain + .register(req, BunTool::new(&self.proto_env, &self.config, req).await?); + } + + Ok(self.toolchain.setup(req, last_versions).await?) + } + + async fn install_deps( + &self, + _context: &ActionContext, + runtime: &Runtime, + working_dir: &Path, + ) -> miette::Result<()> { + actions::install_deps( + self.toolchain.get_for_version(&runtime.requirement)?, + working_dir, + ) + .await?; + + Ok(()) + } + + async fn sync_project( + &self, + _context: &ActionContext, + _project: &Project, + _dependencies: &FxHashMap>, + ) -> miette::Result { + Ok(false) + } + + async fn hash_manifest_deps( + &self, + manifest_path: &Path, + hasher: &mut ContentHasher, + _hasher_config: &HasherConfig, + ) -> miette::Result<()> { + if let Ok(Some(package)) = PackageJson::read(manifest_path) { + let name = package.name.unwrap_or_else(|| "unknown".into()); + let mut hash = DepsHash::new(name); + + if let Some(peer_deps) = &package.peer_dependencies { + hash.add_deps(peer_deps); + } + + if let Some(dev_deps) = &package.dev_dependencies { + hash.add_deps(dev_deps); + } + + if let Some(deps) = &package.dependencies { + hash.add_deps(deps); + } + + hasher.hash_content(hash)?; + } + + Ok(()) + } + + async fn hash_run_target( + &self, + project: &Project, + runtime: &Runtime, + hasher: &mut ContentHasher, + hasher_config: &HasherConfig, + ) -> miette::Result<()> { + let node_hash = actions::create_target_hasher( + self.toolchain.get_for_version(&runtime.requirement).ok(), + project, + &self.workspace_root, + hasher_config, + ) + .await?; + + hasher.hash_content(node_hash)?; + + if let Some(typescript_config) = &self.typescript_config { + let ts_hash = TypeScriptTargetHash::generate( + typescript_config, + &self.workspace_root, + &project.root, + )?; + + hasher.hash_content(ts_hash)?; + } + Ok(()) + } + + async fn create_run_target_command( + &self, + _context: &ActionContext, + project: &Project, + task: &Task, + runtime: &Runtime, + working_dir: &Path, + ) -> miette::Result { + let command = if self.is_toolchain_enabled()? { + actions::create_target_command( + self.toolchain.get_for_version(&runtime.requirement)?, + project, + task, + working_dir, + )? + } else { + actions::create_target_command_without_tool(project, task, working_dir)? + }; + + Ok(command) + } +} diff --git a/crates/bun/platform/src/lib.rs b/crates/bun/platform/src/lib.rs new file mode 100644 index 00000000000..73f7f83b31e --- /dev/null +++ b/crates/bun/platform/src/lib.rs @@ -0,0 +1,5 @@ +mod actions; +mod bun_platform; +mod target_hash; + +pub use bun_platform::*; diff --git a/crates/bun/platform/src/target_hash.rs b/crates/bun/platform/src/target_hash.rs new file mode 100644 index 00000000000..3ec488319c1 --- /dev/null +++ b/crates/bun/platform/src/target_hash.rs @@ -0,0 +1,57 @@ +use moon_hash::hash_content; +use moon_lang::LockfileDependencyVersions; +use moon_node_lang::PackageJson; +use std::collections::BTreeMap; + +hash_content!( + pub struct BunTargetHash { + // Bun version + bun_version: String, + + // All the dependencies of the project (including dev and peer), + // and the hashes corresponding with their versions + dependencies: BTreeMap>, + } +); + +impl BunTargetHash { + pub fn new(bun_version: Option) -> Self { + BunTargetHash { + bun_version: bun_version.unwrap_or_else(|| "unknown".into()), + dependencies: BTreeMap::new(), + } + } + + /// Hash `package.json` dependencies as version changes should bust the cache. + pub fn hash_package_json( + &mut self, + package: &PackageJson, + resolved_deps: &LockfileDependencyVersions, + ) { + let copy_deps = |deps: &BTreeMap, + hashed: &mut BTreeMap>| { + for (name, version_range) in deps { + if let Some(resolved_versions) = resolved_deps.get(name) { + let mut sorted_deps = resolved_versions.to_owned().clone(); + sorted_deps.sort(); + hashed.insert(name.to_owned(), sorted_deps); + } else { + // No match, just use the range itself + hashed.insert(name.to_owned(), vec![version_range.to_owned()]); + } + } + }; + + if let Some(peer_deps) = &package.peer_dependencies { + copy_deps(peer_deps, &mut self.dependencies); + } + + if let Some(dev_deps) = &package.dev_dependencies { + copy_deps(dev_deps, &mut self.dependencies); + } + + if let Some(deps) = &package.dependencies { + copy_deps(deps, &mut self.dependencies); + } + } +} diff --git a/crates/bun/tool/Cargo.toml b/crates/bun/tool/Cargo.toml index bd081dfa14f..cd5f67c9025 100644 --- a/crates/bun/tool/Cargo.toml +++ b/crates/bun/tool/Cargo.toml @@ -5,11 +5,14 @@ edition = "2021" publish = false [dependencies] +moon_bun_lang = { path = "../lang" } moon_config = { path = "../../../nextgen/config" } moon_logger = { path = "../../core/logger" } moon_platform_runtime = { path = "../../../nextgen/platform-runtime" } +moon_process = { path = "../../../nextgen/process" } moon_terminal = { path = "../../core/terminal" } moon_tool = { path = "../../core/tool" } miette = { workspace = true } proto_core = { workspace = true } rustc-hash = { workspace = true } +starbase_utils = { workspace = true } diff --git a/crates/bun/tool/src/bun_tool.rs b/crates/bun/tool/src/bun_tool.rs index 06a9ff79798..a8a0625dd18 100644 --- a/crates/bun/tool/src/bun_tool.rs +++ b/crates/bun/tool/src/bun_tool.rs @@ -1,11 +1,18 @@ +use moon_bun_lang::{load_lockfile_dependencies, LockfileDependencyVersions, BUNPM}; use moon_config::BunConfig; use moon_logger::debug; use moon_platform_runtime::RuntimeReq; +use moon_process::{output_to_string, Command}; use moon_terminal::{print_checkpoint, Checkpoint}; -use moon_tool::{async_trait, load_tool_plugin, use_global_tool_on_path, Tool}; +use moon_tool::{ + async_trait, load_tool_plugin, prepend_path_env_var, use_global_tool_on_path, + DependencyManager, Tool, +}; use proto_core::{Id, ProtoEnvironment, Tool as ProtoTool, UnresolvedVersionSpec}; use rustc_hash::FxHashMap; -use std::path::PathBuf; +use starbase_utils::fs; +use std::env; +use std::path::{Path, PathBuf}; pub struct BunTool { pub config: BunConfig, @@ -27,7 +34,7 @@ impl BunTool { tool: load_tool_plugin(&Id::raw("bun"), proto, config.plugin.as_ref().unwrap()).await?, }; - if use_global_tool_on_path() || req.is_global() { + if use_global_tool_on_path() || req.is_global() || bun.config.version.is_none() { bun.global = true; bun.config.version = None; } else { @@ -45,52 +52,65 @@ impl Tool for BunTool { } fn get_bin_path(&self) -> miette::Result { - Ok(PathBuf::from("bun")) + Ok(if self.global { + "bun".into() + } else { + self.tool.get_bin_path()?.to_path_buf() + }) } async fn setup( &mut self, last_versions: &mut FxHashMap, ) -> miette::Result { - let mut installed = 0; + let mut count = 0; + let version = self.config.version.as_ref(); - let Some(version) = &self.config.version else { - return Ok(installed); + let Some(version) = version else { + return Ok(count); }; if self.global { debug!("Using global binary in PATH"); - } else if self.tool.is_setup(version).await? { + + return Ok(count); + } + + if self.tool.is_setup(version).await? { + self.tool.locate_globals_dir().await?; + debug!("Bun has already been setup"); - // When offline and the tool doesn't exist, fallback to the global binary - } else if proto_core::is_offline() { + return Ok(count); + } + + // When offline and the tool doesn't exist, fallback to the global binary + if proto_core::is_offline() { debug!( "No internet connection and Bun has not been setup, falling back to global binary in PATH" ); self.global = true; - // Otherwise try and install the tool - } else { - let setup = match last_versions.get("bun") { - Some(last) => version != last, - None => true, - }; - - if setup || !self.tool.get_tool_dir().exists() { - print_checkpoint(format!("installing bun {version}"), Checkpoint::Setup); - - if self.tool.setup(version, false).await? { - last_versions.insert("bun".into(), version.to_owned()); - installed += 1; - } + return Ok(count); + } + + if let Some(last) = last_versions.get("bun") { + if last == version && self.tool.get_tool_dir().exists() { + return Ok(count); } } + print_checkpoint(format!("installing bun {version}"), Checkpoint::Setup); + + if self.tool.setup(version, false).await? { + last_versions.insert("bun".into(), version.to_owned()); + count += 1; + } + self.tool.locate_globals_dir().await?; - Ok(installed) + Ok(count) } async fn teardown(&mut self) -> miette::Result<()> { @@ -99,3 +119,105 @@ impl Tool for BunTool { Ok(()) } } + +#[async_trait] +impl DependencyManager<()> for BunTool { + fn create_command(&self, _parent: &()) -> miette::Result { + let mut cmd = if self.global { + Command::new("bun") + } else { + Command::new(self.get_bin_path()?) + }; + + if !self.global { + cmd.env( + "PATH", + prepend_path_env_var([self.tool.get_bin_path()?.parent().unwrap()]), + ); + } + + Ok(cmd) + } + + async fn dedupe_dependencies( + &self, + _parent: &(), + _working_dir: &Path, + _log: bool, + ) -> miette::Result<()> { + // Not supported! + + Ok(()) + } + + fn get_lock_filename(&self) -> String { + String::from(BUNPM.lockfile) + } + + fn get_manifest_filename(&self) -> String { + String::from(BUNPM.manifest) + } + + async fn get_resolved_dependencies( + &self, + project_root: &Path, + ) -> miette::Result { + let Some(lockfile_path) = fs::find_upwards(BUNPM.lockfile, project_root) else { + return Ok(FxHashMap::default()); + }; + + // Bun lockfiles are binary, so we need to convert them to text first + // using Bun itself! + let mut cmd = self.create_command(&())?; + cmd.arg(BUNPM.lockfile); + cmd.cwd(lockfile_path.parent().unwrap()); + + let output = cmd.create_async().exec_capture_output().await?; + + Ok(load_lockfile_dependencies(output_to_string( + &output.stdout, + ))?) + } + + async fn install_dependencies( + &self, + parent: &(), + working_dir: &Path, + log: bool, + ) -> miette::Result<()> { + let mut cmd = self.create_command(parent)?; + + cmd.args(["install"]) + .cwd(working_dir) + .set_print_command(log); + + let mut cmd = cmd.create_async(); + + if env::var("MOON_TEST_HIDE_INSTALL_OUTPUT").is_ok() { + cmd.exec_capture_output().await?; + } else { + cmd.exec_stream_output().await?; + } + + Ok(()) + } + + async fn install_focused_dependencies( + &self, + parent: &(), + _package_names: &[String], // Not supporetd + _production_only: bool, + ) -> miette::Result<()> { + let mut cmd = self.create_command(parent)?; + cmd.args(["install"]); + + // NOTE: This seems to *not* install any dependencies + // if production_only { + // cmd.arg("--production"); + // } + + cmd.create_async().exec_stream_output().await?; + + Ok(()) + } +} diff --git a/crates/cli/src/commands/docker/prune.rs b/crates/cli/src/commands/docker/prune.rs index c27b08ea3f1..b60bbd8f18c 100644 --- a/crates/cli/src/commands/docker/prune.rs +++ b/crates/cli/src/commands/docker/prune.rs @@ -10,6 +10,7 @@ use moon_project_graph::ProjectGraph; use moon_rust_lang::{CARGO, RUST}; use moon_rust_tool::RustTool; use moon_terminal::safe_exit; +use moon_tool::DependencyManager; use moon_workspace::Workspace; use rustc_hash::FxHashSet; use starbase::system; @@ -19,12 +20,36 @@ use starbase_utils::json; use std::path::Path; pub async fn prune_bun( - _bun: &BunTool, - _workspace_root: &Path, - _project_graph: &ProjectGraph, - _manifest: &DockerManifest, + bun: &BunTool, + workspace_root: &Path, + project_graph: &ProjectGraph, + manifest: &DockerManifest, ) -> AppResult { - // TODO + let mut package_names = vec![]; + + for project_id in &manifest.focused_projects { + if let Some(source) = project_graph.sources().get(project_id) { + if let Some(package_json) = PackageJson::read(source.to_path(workspace_root))? { + if let Some(package_name) = package_json.name { + package_names.push(package_name); + } + } + } + } + + // Some package managers do not delete stale node modules + if let Some(vendor_dir) = NODE.vendor_dir { + fs::remove_dir_all(workspace_root.join(vendor_dir))?; + + for source in project_graph.sources().values() { + fs::remove_dir_all(source.join(vendor_dir).to_path(workspace_root))?; + } + } + + // Install production only dependencies for focused projects + bun.install_focused_dependencies(&(), &package_names, true) + .await?; + Ok(()) } diff --git a/crates/cli/tests/run_bun_test.rs b/crates/cli/tests/run_bun_test.rs new file mode 100644 index 00000000000..ee70e899843 --- /dev/null +++ b/crates/cli/tests/run_bun_test.rs @@ -0,0 +1,427 @@ +use moon_config::PartialBunConfig; +use moon_test_utils::{ + assert_snapshot, create_sandbox_with_config, get_bun_fixture_configs, predicates::prelude::*, + Sandbox, +}; + +fn bun_sandbox() -> Sandbox { + bun_sandbox_with_config(|_| {}) +} + +fn bun_sandbox_with_config(callback: C) -> Sandbox +where + C: FnOnce(&mut PartialBunConfig), +{ + let (workspace_config, mut toolchain_config, tasks_config) = get_bun_fixture_configs(); + + if let Some(bun_config) = &mut toolchain_config.bun { + callback(bun_config); + } + + let sandbox = create_sandbox_with_config( + "bun", + Some(workspace_config), + Some(toolchain_config), + Some(tasks_config), + ); + + sandbox.enable_git(); + sandbox +} + +// TODO: Bun doesn't support Windows yet! +#[cfg(not(windows))] +mod bun { + use super::*; + + #[test] + fn runs_self() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:version"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn runs_standard_script() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:standard"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn runs_cjs_files() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:cjs"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn runs_mjs_files() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:mjs"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn supports_top_level_await() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:topLevelAwait"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn handles_process_exit_zero() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:processExitZero"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn handles_process_exit_nonzero() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:processExitNonZero"); + }); + + if cfg!(windows) { + assert.code(1); + } else { + assert_snapshot!(assert.output()); + } + } + + #[test] + fn handles_process_exit_code_zero() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:exitCodeZero"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn handles_process_exit_code_nonzero() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:exitCodeNonZero"); + }); + + if cfg!(windows) { + assert.code(1); + } else { + assert_snapshot!(assert.output()); + } + } + + #[test] + fn handles_throw_error() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:throwError"); + }); + + let output = assert.output(); + + // Output contains file paths that we cant snapshot + assert!(predicate::str::contains("error: Oops").eval(&output)); + } + + #[test] + fn handles_unhandled_promise() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:unhandledPromise"); + }); + + if cfg!(windows) { + assert.code(1); + } else { + assert_snapshot!(assert.output()); + } + } + + #[test] + fn passes_args_through() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run") + .arg("bun:passthroughArgs") + .arg("--") + .arg("-aBc") + .arg("--opt") + .arg("value") + .arg("--optCamel=value") + .arg("foo") + .arg("'bar baz'") + .arg("--opt-kebab") + .arg("123"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn sets_env_vars() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:envVars"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn inherits_moon_env_vars() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:envVarsMoon"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn forces_cache_to_write_only() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:envVarsMoon").arg("--updateCache"); + }); + + assert!(predicate::str::contains("MOON_CACHE=write").eval(&assert.output())); + } + + #[test] + fn runs_from_project_root() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:runFromProject"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn runs_from_workspace_root() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:runFromWorkspace"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn runs_node_module_bin_from_workspace_root() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:runFromWorkspaceBin"); + }); + + assert_snapshot!(assert.output()); + } + + #[test] + fn retries_on_failure_till_count() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:retryCount"); + }); + + let output = assert.output(); + + assert!(predicate::str::contains("1 exit code").eval(&output)); + } + + #[test] + fn can_run_many_targets() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:cjs").arg("bun:mjs"); + }); + + let output = assert.output(); + + assert!(predicate::str::contains("bun:cjs | stdout").eval(&output)); + assert!(predicate::str::contains("bun:mjs | stdout").eval(&output)); + assert!(predicate::str::contains("bun:cjs | stderr").eval(&output)); + assert!(predicate::str::contains("bun:mjs | stderr").eval(&output)); + } + + mod package_manager { + use super::*; + + #[test] + fn can_install_a_dep() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("packageManager:installDep"); + }); + + assert.success(); + } + + #[test] + fn can_run_a_deps_bin() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("packageManager:runDep"); + }); + + assert!( + predicate::str::contains("All matched files use Prettier code style!") + .eval(&assert.output()) + ); + + assert.success(); + } + + #[test] + fn can_run_a_script() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("packageManager:runScript"); + }); + + assert!(predicate::str::contains("test").eval(&assert.output())); + + assert.success(); + } + } + + mod workspace_overrides { + use super::*; + + #[test] + fn can_override_version() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run") + .arg("bun:version") + .arg("versionOverride:version"); + }); + + let output = assert.output(); + + assert!(predicate::str::contains("1.0.0").eval(&output)); + assert!(predicate::str::contains("0.8.0").eval(&output)); + + assert.success(); + } + } + + mod affected_files { + use super::*; + + #[test] + fn uses_dot_when_not_affected() { + let sandbox = bun_sandbox(); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:affectedFiles"); + }); + + let output = assert.output(); + + assert!(predicate::str::contains("Args: .\n").eval(&output)); + assert!(predicate::str::contains("Env: .\n").eval(&output)); + } + + #[test] + fn uses_rel_paths_when_affected() { + let sandbox = bun_sandbox(); + + sandbox.create_file("base/input1.js", ""); + sandbox.create_file("base/input2.js", ""); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("bun:affectedFiles").arg("--affected"); + }); + + let output = assert.output(); + + assert!(predicate::str::contains("Args: ./input1.js ./input2.js\n").eval(&output)); + assert!(predicate::str::contains("Env: input1.js,input2.js\n").eval(&output)); + } + + #[test] + fn sets_args_only() { + let sandbox = bun_sandbox(); + + sandbox.create_file("base/input1.js", ""); + sandbox.create_file("base/input2.js", ""); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run") + .arg("bun:affectedFilesArgs") + .arg("--affected"); + }); + + let output = assert.output(); + + assert!(predicate::str::contains("Args: ./input1.js ./input2.js\n").eval(&output)); + assert!(predicate::str::contains("Env: \n").eval(&output)); + } + + #[test] + fn sets_env_var_only() { + let sandbox = bun_sandbox(); + + sandbox.create_file("base/input1.js", ""); + sandbox.create_file("base/input2.js", ""); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run") + .arg("bun:affectedFilesEnvVar") + .arg("--affected"); + }); + + let output = assert.output(); + + assert!(predicate::str::contains("Args: \n").eval(&output)); + assert!(predicate::str::contains("Env: input1.js,input2.js\n").eval(&output)); + } + } +} diff --git a/crates/cli/tests/run_node_test.rs b/crates/cli/tests/run_node_test.rs index fb7ef698bad..6ef12f343a1 100644 --- a/crates/cli/tests/run_node_test.rs +++ b/crates/cli/tests/run_node_test.rs @@ -10,17 +10,7 @@ use rustc_hash::FxHashMap; use std::fs::read_to_string; fn node_sandbox() -> Sandbox { - let (workspace_config, toolchain_config, tasks_config) = get_node_fixture_configs(); - - let sandbox = create_sandbox_with_config( - "node", - Some(workspace_config), - Some(toolchain_config), - Some(tasks_config), - ); - - sandbox.enable_git(); - sandbox + node_sandbox_with_config(|_| {}) } fn node_sandbox_with_config(callback: C) -> Sandbox diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__handles_process_exit_code_nonzero.snap b/crates/cli/tests/snapshots/run_bun_test__bun__handles_process_exit_code_nonzero.snap new file mode 100644 index 00000000000..80e7051cf79 --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__handles_process_exit_code_nonzero.snap @@ -0,0 +1,18 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:exitCodeNonZero +stdout +This should appear! +▪▪▪▪ bun:exitCodeNonZero (100ms) +stderr +Error: target_runner::run_failed + + × Task bun:exitCodeNonZero failed to run. View hash details with moon query + │ hash hash1234. + ╰─▶ Process bun failed with a 1 exit code. + + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__handles_process_exit_code_zero.snap b/crates/cli/tests/snapshots/run_bun_test__bun__handles_process_exit_code_zero.snap new file mode 100644 index 00000000000..c2ef2512a18 --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__handles_process_exit_code_zero.snap @@ -0,0 +1,16 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:exitCodeZero +stdout +This should appear! +▪▪▪▪ bun:exitCodeZero (100ms) + +Tasks: 1 completed + Time: 100ms + +stderr + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__handles_process_exit_nonzero.snap b/crates/cli/tests/snapshots/run_bun_test__bun__handles_process_exit_nonzero.snap new file mode 100644 index 00000000000..5c471e018ac --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__handles_process_exit_nonzero.snap @@ -0,0 +1,17 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:processExitNonZero +stdout +▪▪▪▪ bun:processExitNonZero (100ms) +stderr +Error: target_runner::run_failed + + × Task bun:processExitNonZero failed to run. View hash details with moon + │ query hash hash1234. + ╰─▶ Process bun failed with a 1 exit code. + + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__handles_process_exit_zero.snap b/crates/cli/tests/snapshots/run_bun_test__bun__handles_process_exit_zero.snap new file mode 100644 index 00000000000..6e9915dfaad --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__handles_process_exit_zero.snap @@ -0,0 +1,15 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:processExitZero +stdout +▪▪▪▪ bun:processExitZero (100ms) + +Tasks: 1 completed + Time: 100ms + +stderr + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__handles_unhandled_promise.snap b/crates/cli/tests/snapshots/run_bun_test__bun__handles_unhandled_promise.snap new file mode 100644 index 00000000000..b20bda233c6 --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__handles_unhandled_promise.snap @@ -0,0 +1,18 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:unhandledPromise +stdout +▪▪▪▪ bun:unhandledPromise (100ms) +stderr +error: Oops +Error: target_runner::run_failed + + × Task bun:unhandledPromise failed to run. View hash details with moon query + │ hash hash1234. + ╰─▶ Process bun failed with a 1 exit code. + + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__inherits_moon_env_vars.snap b/crates/cli/tests/snapshots/run_bun_test__bun__inherits_moon_env_vars.snap new file mode 100644 index 00000000000..5481a18c2ef --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__inherits_moon_env_vars.snap @@ -0,0 +1,26 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:envVarsMoon +MOON_CACHE=read-write +MOON_CACHE_DIR=/.moon/cache +MOON_HOME=/.moon-home +MOON_LOG=trace +MOON_PROJECT_ID=bun +MOON_PROJECT_ROOT=/base +MOON_PROJECT_SNAPSHOT=/.moon/cache/states/bun/snapshot.json +MOON_PROJECT_SOURCE=base +MOON_RUNNING_ACTION=run-task +MOON_TARGET=bun:envVarsMoon +MOON_TOOLCHAIN_DIR=~/.proto +MOON_WORKING_DIR= +MOON_WORKSPACE_ROOT= +▪▪▪▪ bun:envVarsMoon (100ms) + +Tasks: 1 completed + Time: 100ms + + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__passes_args_through.snap b/crates/cli/tests/snapshots/run_bun_test__bun__passes_args_through.snap new file mode 100644 index 00000000000..f5514ff0179 --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__passes_args_through.snap @@ -0,0 +1,14 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:passthroughArgs +Args: -aBc --opt value --optCamel=value foo 'bar baz' --opt-kebab 123 +▪▪▪▪ bun:passthroughArgs (100ms) + +Tasks: 1 completed + Time: 100ms + + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__runs_cjs_files.snap b/crates/cli/tests/snapshots/run_bun_test__bun__runs_cjs_files.snap new file mode 100644 index 00000000000..e3744f51bea --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__runs_cjs_files.snap @@ -0,0 +1,15 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:cjs +stdout +▪▪▪▪ bun:cjs (100ms) + +Tasks: 1 completed + Time: 100ms + +stderr + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__runs_from_project_root.snap b/crates/cli/tests/snapshots/run_bun_test__bun__runs_from_project_root.snap new file mode 100644 index 00000000000..106cdbb6668 --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__runs_from_project_root.snap @@ -0,0 +1,14 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:runFromProject +/base +▪▪▪▪ bun:runFromProject (100ms) + +Tasks: 1 completed + Time: 100ms + + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__runs_from_workspace_root.snap b/crates/cli/tests/snapshots/run_bun_test__bun__runs_from_workspace_root.snap new file mode 100644 index 00000000000..da030186f69 --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__runs_from_workspace_root.snap @@ -0,0 +1,14 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:runFromWorkspace + +▪▪▪▪ bun:runFromWorkspace (100ms) + +Tasks: 1 completed + Time: 100ms + + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__runs_mjs_files.snap b/crates/cli/tests/snapshots/run_bun_test__bun__runs_mjs_files.snap new file mode 100644 index 00000000000..d7507fbc9a0 --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__runs_mjs_files.snap @@ -0,0 +1,15 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:mjs +stdout +▪▪▪▪ bun:mjs (100ms) + +Tasks: 1 completed + Time: 100ms + +stderr + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__runs_node_module_bin_from_workspace_root.snap b/crates/cli/tests/snapshots/run_bun_test__bun__runs_node_module_bin_from_workspace_root.snap new file mode 100644 index 00000000000..fda88ca036c --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__runs_node_module_bin_from_workspace_root.snap @@ -0,0 +1,15 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:runFromWorkspaceBin +Checking formatting... +All matched files use Prettier code style! +▪▪▪▪ bun:runFromWorkspaceBin (100ms) + +Tasks: 1 completed + Time: 100ms + + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__runs_package_managers.snap b/crates/cli/tests/snapshots/run_bun_test__bun__runs_package_managers.snap new file mode 100644 index 00000000000..8273f46a983 --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__runs_package_managers.snap @@ -0,0 +1,11 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +Error: project::task::unknown + + × Unknown task npm for project bun. + help: Has this task been configured? + + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__runs_self.snap b/crates/cli/tests/snapshots/run_bun_test__bun__runs_self.snap new file mode 100644 index 00000000000..91267f70c68 --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__runs_self.snap @@ -0,0 +1,14 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:version +1.0.0 +▪▪▪▪ bun:version (100ms) + +Tasks: 1 completed + Time: 100ms + + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__runs_standard_script.snap b/crates/cli/tests/snapshots/run_bun_test__bun__runs_standard_script.snap new file mode 100644 index 00000000000..5aae790253e --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__runs_standard_script.snap @@ -0,0 +1,15 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:standard +stdout +▪▪▪▪ bun:standard (100ms) + +Tasks: 1 completed + Time: 100ms + +stderr + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__sets_env_vars.snap b/crates/cli/tests/snapshots/run_bun_test__bun__sets_env_vars.snap new file mode 100644 index 00000000000..2b873741ff8 --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__sets_env_vars.snap @@ -0,0 +1,16 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:envVars +MOON_FOO=abc +MOON_BAR=123 +MOON_BAZ=true +▪▪▪▪ bun:envVars (100ms) + +Tasks: 1 completed + Time: 100ms + + + diff --git a/crates/cli/tests/snapshots/run_bun_test__bun__supports_top_level_await.snap b/crates/cli/tests/snapshots/run_bun_test__bun__supports_top_level_await.snap new file mode 100644 index 00000000000..2f44f01f24f --- /dev/null +++ b/crates/cli/tests/snapshots/run_bun_test__bun__supports_top_level_await.snap @@ -0,0 +1,16 @@ +--- +source: crates/cli/tests/run_bun_test.rs +expression: assert.output() +--- +▪▪▪▪ bun install +▪▪▪▪ bun:topLevelAwait +before +awaiting +after +▪▪▪▪ bun:topLevelAwait (100ms) + +Tasks: 1 completed + Time: 100ms + + + diff --git a/crates/core/moon/Cargo.toml b/crates/core/moon/Cargo.toml index 0aad8788f05..605cc9b6cf2 100644 --- a/crates/core/moon/Cargo.toml +++ b/crates/core/moon/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dependencies] moon_action_graph = { path = "../../../nextgen/action-graph" } moon_config = { path = "../../../nextgen/config" } +moon_bun_platform = { path = "../../bun/platform" } moon_deno_platform = { path = "../../deno/platform" } moon_node_platform = { path = "../../node/platform" } moon_platform = { path = "../platform" } diff --git a/crates/core/moon/src/lib.rs b/crates/core/moon/src/lib.rs index 2164dd6c1d4..2c07d90699e 100644 --- a/crates/core/moon/src/lib.rs +++ b/crates/core/moon/src/lib.rs @@ -1,4 +1,5 @@ use moon_action_graph::ActionGraphBuilder; +use moon_bun_platform::BunPlatform; use moon_config::LanguageType; use moon_deno_platform::DenoPlatform; use moon_node_platform::NodePlatform; @@ -74,7 +75,17 @@ pub async fn load_workspace_from(path: &Path) -> miette::Result { // Primarily for testing registry.reset(); - // TODO bun + if let Some(bun_config) = &workspace.toolchain_config.bun { + registry.register( + PlatformType::Bun, + Box::new(BunPlatform::new( + bun_config, + &workspace.toolchain_config.typescript, + &workspace.root, + Arc::clone(&workspace.proto_env), + )), + ); + } if let Some(deno_config) = &workspace.toolchain_config.deno { registry.register( diff --git a/crates/core/platform-detector/src/language_files.rs b/crates/core/platform-detector/src/language_files.rs index f25cb23503d..5b588cc8fc5 100644 --- a/crates/core/platform-detector/src/language_files.rs +++ b/crates/core/platform-detector/src/language_files.rs @@ -1,4 +1,4 @@ -use moon_bun_lang::BUN_INSTALL; +use moon_bun_lang::BUNPM; use moon_config::LanguageType; use moon_deno_lang::{DENO_DEPS, DVM}; use moon_go_lang::{G, GOENV, GOMOD, GVM}; @@ -34,7 +34,7 @@ pub fn detect_language_files(language: &LanguageType) -> Vec { } LanguageType::JavaScript | LanguageType::TypeScript => { // Bun - extract_depman_files(&BUN_INSTALL, &mut files); + extract_depman_files(&BUNPM, &mut files); // Deno extract_depman_files(&DENO_DEPS, &mut files); diff --git a/crates/core/platform-detector/src/project_language.rs b/crates/core/platform-detector/src/project_language.rs index 70131ceb7c2..98c1c0df0be 100644 --- a/crates/core/platform-detector/src/project_language.rs +++ b/crates/core/platform-detector/src/project_language.rs @@ -1,4 +1,4 @@ -use moon_bun_lang::BUN_INSTALL; +use moon_bun_lang::BUNPM; use moon_config::LanguageType; use moon_deno_lang::{DENO_DEPS, DVM}; use moon_go_lang::{G, GOENV, GOMOD, GVM}; @@ -69,7 +69,7 @@ pub fn detect_project_language(root: &Path) -> LanguageType { || is_using_version_manager(root, &NVM) || is_using_version_manager(root, &NODENV) // Bun - || is_using_dependency_manager(root, &BUN_INSTALL, true) + || is_using_dependency_manager(root, &BUNPM, true) { return LanguageType::JavaScript; } diff --git a/crates/core/test-utils/src/configs.rs b/crates/core/test-utils/src/configs.rs index 1532d0b323c..93d1f0dc086 100644 --- a/crates/core/test-utils/src/configs.rs +++ b/crates/core/test-utils/src/configs.rs @@ -1,10 +1,10 @@ use crate::create_input_paths; use moon_config::{ - InputPath, NodePackageManager, PartialBunpmConfig, PartialInheritedTasksConfig, - PartialNodeConfig, PartialNpmConfig, PartialPnpmConfig, PartialTaskCommandArgs, - PartialTaskConfig, PartialToolchainConfig, PartialTypeScriptConfig, PartialWorkspaceConfig, - PartialWorkspaceProjects, PartialWorkspaceProjectsConfig, PartialYarnConfig, - UnresolvedVersionSpec, + InputPath, NodePackageManager, PartialBunConfig, PartialBunpmConfig, + PartialInheritedTasksConfig, PartialNodeConfig, PartialNpmConfig, PartialPnpmConfig, + PartialTaskCommandArgs, PartialTaskConfig, PartialToolchainConfig, PartialTypeScriptConfig, + PartialWorkspaceConfig, PartialWorkspaceProjects, PartialWorkspaceProjectsConfig, + PartialYarnConfig, UnresolvedVersionSpec, }; use rustc_hash::FxHashMap; use std::collections::BTreeMap; @@ -273,7 +273,52 @@ pub fn get_tasks_fixture_configs() -> ( (workspace_config, toolchain_config, tasks_config) } -// NODE.JS +// JAVASCRIPT + +pub fn get_bun_fixture_configs() -> ( + PartialWorkspaceConfig, + PartialToolchainConfig, + PartialInheritedTasksConfig, +) { + let workspace_config = PartialWorkspaceConfig { + projects: Some(PartialWorkspaceProjects::Sources(FxHashMap::from_iter([ + ("bun".into(), "base".to_owned()), + ("packageManager".into(), "package-manager".to_owned()), + ("versionOverride".into(), "version-override".to_owned()), + ]))), + ..PartialWorkspaceConfig::default() + }; + + let mut toolchain_config = get_default_toolchain(); + toolchain_config.node = None; + toolchain_config.bun = Some(PartialBunConfig { + version: Some(UnresolvedVersionSpec::parse("1.0.0").unwrap()), + ..PartialBunConfig::default() + }); + + let tasks_config = PartialInheritedTasksConfig { + tasks: Some(BTreeMap::from_iter([ + ( + "version".into(), + PartialTaskConfig { + command: Some(PartialTaskCommandArgs::String("bun".into())), + args: Some(PartialTaskCommandArgs::String("--version".into())), + ..PartialTaskConfig::default() + }, + ), + ( + "noop".into(), + PartialTaskConfig { + command: Some(PartialTaskCommandArgs::String("noop".into())), + ..PartialTaskConfig::default() + }, + ), + ])), + ..PartialInheritedTasksConfig::default() + }; + + (workspace_config, toolchain_config, tasks_config) +} pub fn get_node_fixture_configs() -> ( PartialWorkspaceConfig, diff --git a/crates/node/platform/src/actions/install_deps.rs b/crates/node/platform/src/actions/install_deps.rs index 709f7f2a1bc..762630e3c34 100644 --- a/crates/node/platform/src/actions/install_deps.rs +++ b/crates/node/platform/src/actions/install_deps.rs @@ -1,6 +1,6 @@ use moon_config::NodePackageManager; use moon_lang::has_vendor_installed_dependencies; -use moon_logger::{debug, warn}; +use moon_logger::{debug, info}; use moon_node_lang::NODE; use moon_node_tool::NodeTool; use moon_terminal::{print_checkpoint, Checkpoint}; @@ -13,7 +13,7 @@ pub async fn install_deps(node: &NodeTool, working_dir: &Path) -> miette::Result // When in CI, we can avoid installing dependencies because // we can assume they've already been installed before moon runs! if is_ci() && has_vendor_installed_dependencies(working_dir, &NODE) { - warn!( + info!( target: LOG_TARGET, "In a CI environment and dependencies already exist, skipping install" ); diff --git a/crates/node/platform/src/lib.rs b/crates/node/platform/src/lib.rs index 5abfb1ee18f..92def6a16e0 100644 --- a/crates/node/platform/src/lib.rs +++ b/crates/node/platform/src/lib.rs @@ -1,9 +1,9 @@ pub mod actions; -mod platform; +mod node_platform; mod target_hash; pub mod task; -pub use platform::NodePlatform; +pub use node_platform::NodePlatform; pub use target_hash::NodeTargetHash; use moon_common::Id; diff --git a/crates/node/platform/src/platform.rs b/crates/node/platform/src/node_platform.rs similarity index 100% rename from crates/node/platform/src/platform.rs rename to crates/node/platform/src/node_platform.rs diff --git a/crates/node/tool/src/bun_tool.rs b/crates/node/tool/src/bun_tool.rs index 7618752ce25..6e3f8fcf3c3 100644 --- a/crates/node/tool/src/bun_tool.rs +++ b/crates/node/tool/src/bun_tool.rs @@ -39,8 +39,6 @@ impl BunTool { fn internal_create_command(&self) -> miette::Result { Ok(if self.global { Command::new("bun") - } else if let Some(shim) = self.get_shim_path() { - Command::new(shim) } else { Command::new(self.get_bin_path()?) }) @@ -61,14 +59,6 @@ impl Tool for BunTool { }) } - fn get_shim_path(&self) -> Option { - if self.global { - return None; - } - - self.tool.get_shim_path().map(|p| p.to_path_buf()) - } - async fn setup( &mut self, last_versions: &mut FxHashMap, diff --git a/nextgen/config/src/project/overrides_config.rs b/nextgen/config/src/project/overrides_config.rs index a494926c2c8..6c111bed10f 100644 --- a/nextgen/config/src/project/overrides_config.rs +++ b/nextgen/config/src/project/overrides_config.rs @@ -24,6 +24,9 @@ cacheable!( cacheable!( #[derive(Clone, Config, Debug)] pub struct ProjectToolchainConfig { + #[setting(nested)] + pub bun: Option, + #[setting(nested)] pub node: Option, diff --git a/nextgen/config/tests/toolchain_config_test.rs b/nextgen/config/tests/toolchain_config_test.rs index 4a89798fe8f..de1514600fd 100644 --- a/nextgen/config/tests/toolchain_config_test.rs +++ b/nextgen/config/tests/toolchain_config_test.rs @@ -76,6 +76,112 @@ mod toolchain_config { } } + mod bun { + use super::*; + + #[test] + fn uses_defaults() { + let config = test_load_config(FILENAME, "bun: {}", |path| { + ToolchainConfig::load_from(path, &ToolsConfig::default()) + }); + + let cfg = config.bun.unwrap(); + + assert!(cfg.plugin.is_some()); + } + + #[test] + fn enables_via_proto() { + let config = test_load_config(FILENAME, "{}", |path| { + let mut proto = ToolsConfig::default(); + proto.tools.insert( + Id::raw("bun"), + UnresolvedVersionSpec::parse("1.0.0").unwrap(), + ); + + ToolchainConfig::load_from(path, &proto) + }); + + assert!(config.bun.is_some()); + assert_eq!( + config.bun.unwrap().version.unwrap(), + UnresolvedVersionSpec::parse("1.0.0").unwrap() + ); + } + + #[test] + fn inherits_plugin_locator() { + let config = test_load_config(FILENAME, "bun: {}", |path| { + let mut tools = ToolsConfig::default(); + tools.inherit_builtin_plugins(); + + ToolchainConfig::load_from(path, &tools) + }); + + assert_eq!( + config.bun.unwrap().plugin.unwrap(), + PluginLocator::SourceUrl { + url: "https://github.com/moonrepo/bun-plugin/releases/download/v0.4.0/bun_plugin.wasm".into() + } + ); + } + + #[test] + fn proto_version_doesnt_override() { + let config = test_load_config( + FILENAME, + r" +bun: + version: 1.0.0 +", + |path| { + let mut proto = ToolsConfig::default(); + proto.tools.insert( + Id::raw("bun"), + UnresolvedVersionSpec::parse("2.0.0").unwrap(), + ); + + ToolchainConfig::load_from(path, &proto) + }, + ); + + assert!(config.bun.is_some()); + assert_eq!( + config.bun.unwrap().version.unwrap(), + UnresolvedVersionSpec::parse("1.0.0").unwrap() + ); + } + + #[test] + fn inherits_version_from_env_var() { + env::set_var("MOON_BUN_VERSION", "1.0.0"); + + let config = test_load_config( + FILENAME, + r" +bun: + version: 3.0.0 +", + |path| { + let mut proto = ToolsConfig::default(); + proto.tools.insert( + Id::raw("bun"), + UnresolvedVersionSpec::parse("2.0.0").unwrap(), + ); + + ToolchainConfig::load_from(path, &proto) + }, + ); + + env::remove_var("MOON_BUN_VERSION"); + + assert_eq!( + config.bun.unwrap().version.unwrap(), + UnresolvedVersionSpec::parse("1.0.0").unwrap() + ); + } + } + mod deno { use super::*; diff --git a/nextgen/test-utils/Cargo.toml b/nextgen/test-utils/Cargo.toml index f290514b133..e953223519e 100644 --- a/nextgen/test-utils/Cargo.toml +++ b/nextgen/test-utils/Cargo.toml @@ -18,6 +18,7 @@ starbase_sandbox = { workspace = true } # TODO moon_platform = { path = "../../crates/core/platform" } +moon_bun_platform = { path = "../../crates/bun/platform" } moon_node_platform = { path = "../../crates/node/platform" } moon_rust_platform = { path = "../../crates/rust/platform" } moon_system_platform = { path = "../../crates/system/platform" } diff --git a/nextgen/test-utils/src/platform_manager.rs b/nextgen/test-utils/src/platform_manager.rs index b0f2cf75ea1..c4bf28994bd 100644 --- a/nextgen/test-utils/src/platform_manager.rs +++ b/nextgen/test-utils/src/platform_manager.rs @@ -1,3 +1,4 @@ +use moon_bun_platform::BunPlatform; use moon_config::{PlatformType, ToolchainConfig, ToolsConfig}; use moon_node_platform::NodePlatform; use moon_platform::PlatformManager; @@ -12,7 +13,12 @@ pub async fn generate_platform_manager_from_sandbox(root: &Path) -> PlatformMana let config = ToolchainConfig::load_from(root, &ToolsConfig::default()).unwrap(); let mut manager = PlatformManager::default(); - // TODO bun + if let Some(bun_config) = &config.bun { + manager.register( + PlatformType::Bun, + Box::new(BunPlatform::new(bun_config, &None, root, proto.clone())), + ); + } if let Some(node_config) = &config.node { manager.register( diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index c07806665cc..55e12f278ed 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -10,6 +10,19 @@ - More accurately monitors signals (ctrl+c) and shutdowns. - Tasks can now be configured with a timeout. +## Unreleased + +#### 🚀 Updates + +- Integrated full Bun support (tier 1-3). + - Will download and install Bun into the toolchain when a `version` is configured. + - Will parse the `bun.lockb` lockfile to extract and resolve dependencies. + - Will hash manifests and inputs for Bun specific caching. + - Added a `bun` setting to `.moon/toolchain.yml`. + - Added a `toolchain.bun` setting to `moon.yml`. + - Updated `moon bin` and `moon docker` commands to support Bun. + - Updated task `platform` to support "bun". + ## 1.16.0 #### 🚀 Updates diff --git a/packages/types/src/project-config.ts b/packages/types/src/project-config.ts index 2635b266acf..fcbf73b1977 100644 --- a/packages/types/src/project-config.ts +++ b/packages/types/src/project-config.ts @@ -60,6 +60,7 @@ export interface PartialProjectToolchainTypeScriptConfig { } export interface PartialProjectToolchainConfig { + bun?: PartialProjectToolchainCommonToolConfig | null; node?: PartialProjectToolchainCommonToolConfig | null; rust?: PartialProjectToolchainCommonToolConfig | null; typescript?: PartialProjectToolchainTypeScriptConfig | null; @@ -135,6 +136,7 @@ export interface ProjectToolchainTypeScriptConfig { } export interface ProjectToolchainConfig { + bun: ProjectToolchainCommonToolConfig | null; node: ProjectToolchainCommonToolConfig | null; rust: ProjectToolchainCommonToolConfig | null; typescript: ProjectToolchainTypeScriptConfig | null; diff --git a/scripts/new-language.md b/scripts/new-language.md index e4c251b7462..4598402ac17 100644 --- a/scripts/new-language.md +++ b/scripts/new-language.md @@ -123,7 +123,8 @@ pub struct ToolchainConfig { - [ ] Created config template file - [ ] Updated `ToolchainConfig` struct - [ ] Ran `cargo make schemas` and updated the JSON schemas -- [ ] Add `.prototools` support in `crates/core/config/src/toolchain/config.rs` +- [ ] Add `.prototools` support in `nextgen/config/src/toolchain_config.rs` +- [ ] Add tests to `nextgen/config/tests/toolchain_config_test.rs` ### Add variant to `PlatformType` enum in `moon_config` @@ -150,21 +151,6 @@ ProjectLanguage::Kotlin => PlatformType::Kotlin, - [ ] Updated enum -### Add variant to `Runtime` enum in `moon_platform_runtime` - -This determines the language + version of a tool to run within the platform. - -```rust -pub enum Runtime { - // ... - Kotlin(Version), -} -``` - -- [ ] Updated enum -- [ ] Updated TypeScript types at `packages/types/src/common.ts` -- [ ] Verified all `match` callsites handle the new variant - ### Update `moon_platform_detector` crate Tasks run against the platform, so we can now attempt to detect this. @@ -175,7 +161,7 @@ Tasks run against the platform, so we can now attempt to detect this. Every language will have a "tool" crate that implements the moon `Tool` trait (and eventually the proto `Tool` trait). This trait defines a handful of methods for how to install and execute the -toon. +tool. ```rust #[derive(Debug)] @@ -251,9 +237,8 @@ Furthermore, when applicable, also add version support from `.prototools`. kotlin = "1.2.3" ``` -- [ ] Updated config struct: `crates/core/config/src/toolchain/.rs` -- [ ] Supported proto version in `crates/core/config/src/toolchain/config.rs` -- [ ] Added tests: `crates/core/config/tests/toolchain_test.rs` +- [ ] Updated config struct: `nextgen/config/src/toolchain/.rs` +- [ ] Supported proto version in `nextgen/config/src/toolchain_config.rs` - [ ] Ran `cargo make schemas` and updated the JSON schemas ### Integrate proto tool into moon tool crate @@ -290,9 +275,8 @@ Refer to the Node.js implementation for examples (it can mostly be copied). Different projects may have different version requirements, so we need to support this through project-level toolchain overrides. -- [ ] Updated `crates/core/config/src/project/toolchain.rs` +- [ ] Updated `nextgen/config/src/project/overrides_config.rs` - [ ] Updated `get_runtime_from_config` in platform crate -- [ ] Updated `packages/types/src/project-config.ts` ### Integrate `--profile` option diff --git a/tests/fixtures/bun/base/affectedFiles.js b/tests/fixtures/bun/base/affectedFiles.js new file mode 100644 index 00000000000..56600e0cd0a --- /dev/null +++ b/tests/fixtures/bun/base/affectedFiles.js @@ -0,0 +1,2 @@ +console.log('Args:', process.argv.slice(2).join(' ')); +console.log('Env:', process.env.MOON_AFFECTED_FILES || ''); diff --git a/tests/fixtures/bun/base/args.js b/tests/fixtures/bun/base/args.js new file mode 100644 index 00000000000..03e8e5b9b2f --- /dev/null +++ b/tests/fixtures/bun/base/args.js @@ -0,0 +1 @@ +console.log('Args:', process.argv.slice(2).join(' ')); diff --git a/tests/fixtures/bun/base/binExecArgs.js b/tests/fixtures/bun/base/binExecArgs.js new file mode 100644 index 00000000000..efbc46d5630 --- /dev/null +++ b/tests/fixtures/bun/base/binExecArgs.js @@ -0,0 +1 @@ +console.log(process.execArgv); diff --git a/tests/fixtures/bun/base/cjsFile.cjs b/tests/fixtures/bun/base/cjsFile.cjs new file mode 100644 index 00000000000..91382f66725 --- /dev/null +++ b/tests/fixtures/bun/base/cjsFile.cjs @@ -0,0 +1 @@ +require('./standard'); diff --git a/tests/fixtures/bun/base/cwd.js b/tests/fixtures/bun/base/cwd.js new file mode 100644 index 00000000000..47d4aa44cd6 --- /dev/null +++ b/tests/fixtures/bun/base/cwd.js @@ -0,0 +1 @@ +console.log(process.cwd().replace(/\\/g, '/')); diff --git a/tests/fixtures/bun/base/envVars.js b/tests/fixtures/bun/base/envVars.js new file mode 100644 index 00000000000..44b0fda6bce --- /dev/null +++ b/tests/fixtures/bun/base/envVars.js @@ -0,0 +1,5 @@ +['MOON_FOO', 'MOON_BAR', 'MOON_BAZ'].forEach((key) => { + if (process.env[key]) { + console.log(`${key}=${process.env[key].replace(/\\/g, '/')}`); + } +}); diff --git a/tests/fixtures/bun/base/envVarsMoon.js b/tests/fixtures/bun/base/envVarsMoon.js new file mode 100644 index 00000000000..b019c61c0fa --- /dev/null +++ b/tests/fixtures/bun/base/envVarsMoon.js @@ -0,0 +1,12 @@ +Object.entries(process.env) + .sort((a, d) => a[0].localeCompare(d[0])) + .forEach(([key, value]) => { + if ( + key.startsWith('MOON_') && + !key.startsWith('MOON_TEST') && + key !== 'MOON_VERSION' && + key !== 'MOON_APP_LOG' + ) { + console.log(`${key}=${value.replace(/\\/g, '/')}`); + } + }); diff --git a/tests/fixtures/bun/base/execBin.js b/tests/fixtures/bun/base/execBin.js new file mode 100644 index 00000000000..3c1b7c265ec --- /dev/null +++ b/tests/fixtures/bun/base/execBin.js @@ -0,0 +1,3 @@ +const { spawnSync } = require('child_process'); + +spawnSync('node', ['--version'], { stdio: 'inherit' }); diff --git a/tests/fixtures/bun/base/exitCodeNonZero.js b/tests/fixtures/bun/base/exitCodeNonZero.js new file mode 100644 index 00000000000..1cea9d1c8c3 --- /dev/null +++ b/tests/fixtures/bun/base/exitCodeNonZero.js @@ -0,0 +1,6 @@ +console.log('stdout'); +console.error('stderr'); + +process.exitCode = 1; + +console.log('This should appear!'); diff --git a/tests/fixtures/bun/base/exitCodeZero.js b/tests/fixtures/bun/base/exitCodeZero.js new file mode 100644 index 00000000000..887c40bee28 --- /dev/null +++ b/tests/fixtures/bun/base/exitCodeZero.js @@ -0,0 +1,6 @@ +console.log('stdout'); +console.error('stderr'); + +process.exitCode = 0; + +console.log('This should appear!'); diff --git a/tests/fixtures/bun/base/mjsFile.mjs b/tests/fixtures/bun/base/mjsFile.mjs new file mode 100644 index 00000000000..a5b6817de5c --- /dev/null +++ b/tests/fixtures/bun/base/mjsFile.mjs @@ -0,0 +1 @@ +import './standard.js'; diff --git a/tests/fixtures/bun/base/moon.yml b/tests/fixtures/bun/base/moon.yml new file mode 100644 index 00000000000..a2bed73ceb7 --- /dev/null +++ b/tests/fixtures/bun/base/moon.yml @@ -0,0 +1,84 @@ +language: javascript +platform: bun + +tasks: + standard: + command: bun + args: ./standard.js + cjs: + command: bun + args: ./cjsFile.cjs + mjs: + command: bun + args: ./mjsFile.mjs + execBinSelf: + command: bun + args: ./execBin.js + + # Runner cases + exitCodeNonZero: + command: bun + args: ./exitCodeNonZero.js + exitCodeZero: + command: bun + args: ./exitCodeZero.js + processExitNonZero: + command: bun + args: ./processExitNonZero.js + processExitZero: + command: bun + args: ./processExitZero.js + throwError: + command: bun + args: ./throwError.js + unhandledPromise: + command: bun + args: ./unhandledPromise.js + topLevelAwait: + command: bun + args: ./topLevelAwait.mjs + passthroughArgs: + command: bun + args: ./args.js + envVars: + command: bun + args: ./envVars.js + env: + MOON_FOO: abc + MOON_BAR: '123' + MOON_BAZ: 'true' + envVarsMoon: + command: bun + args: ./envVarsMoon.js + runFromProject: + command: bun + args: ./cwd.js + runFromWorkspace: + command: bun + args: ./base/cwd.js + options: + runFromWorkspaceRoot: true + runFromWorkspaceBin: + command: prettier --check '*.json' + options: + runFromWorkspaceRoot: true + retryCount: + command: bun + args: ./processExitNonZero.js + options: + retryCount: 3 + binExecArgs: + command: bun + args: ./binExecArgs.js + affectedFiles: + command: bun ./affectedFiles.js + options: + affectedFiles: true + affectedFilesArgs: + command: bun ./affectedFiles.js + options: + affectedFiles: 'args' + affectedFilesEnvVar: + command: bun ./affectedFiles.js + options: + affectedFiles: 'env' diff --git a/tests/fixtures/bun/base/package.json b/tests/fixtures/bun/base/package.json new file mode 100644 index 00000000000..b3baa628016 --- /dev/null +++ b/tests/fixtures/bun/base/package.json @@ -0,0 +1,7 @@ +{ + "name": "test-bun-base", + "version": "1.0.0", + "dependencies": { + "prettier": "2.7.0" + } +} diff --git a/tests/fixtures/bun/base/processExitNonZero.js b/tests/fixtures/bun/base/processExitNonZero.js new file mode 100644 index 00000000000..2fe41b280bb --- /dev/null +++ b/tests/fixtures/bun/base/processExitNonZero.js @@ -0,0 +1,6 @@ +console.log('stdout'); +console.error('stderr'); + +process.exit(1); + +console.log('This should not appear!'); diff --git a/tests/fixtures/bun/base/processExitZero.js b/tests/fixtures/bun/base/processExitZero.js new file mode 100644 index 00000000000..eb885b8c763 --- /dev/null +++ b/tests/fixtures/bun/base/processExitZero.js @@ -0,0 +1,6 @@ +console.log('stdout'); +console.error('stderr'); + +process.exit(0); + +console.log('This should not appear!'); diff --git a/tests/fixtures/bun/base/standard.js b/tests/fixtures/bun/base/standard.js new file mode 100644 index 00000000000..8d4f87187c2 --- /dev/null +++ b/tests/fixtures/bun/base/standard.js @@ -0,0 +1,2 @@ +console.log('stdout'); +console.error('stderr'); diff --git a/tests/fixtures/bun/base/throwError.js b/tests/fixtures/bun/base/throwError.js new file mode 100644 index 00000000000..9c5b8d2297f --- /dev/null +++ b/tests/fixtures/bun/base/throwError.js @@ -0,0 +1,4 @@ +console.log('stdout'); +console.error('stderr'); + +throw new Error('Oops'); diff --git a/tests/fixtures/bun/base/topLevelAwait.mjs b/tests/fixtures/bun/base/topLevelAwait.mjs new file mode 100644 index 00000000000..50945a9865e --- /dev/null +++ b/tests/fixtures/bun/base/topLevelAwait.mjs @@ -0,0 +1,10 @@ +console.log('before'); + +await new Promise((resolve) => { + setTimeout(() => { + console.log('awaiting'); + resolve(); + }, 100); +}); + +console.log('after'); diff --git a/tests/fixtures/bun/base/unhandledPromise.js b/tests/fixtures/bun/base/unhandledPromise.js new file mode 100644 index 00000000000..909d23eeb44 --- /dev/null +++ b/tests/fixtures/bun/base/unhandledPromise.js @@ -0,0 +1,6 @@ +console.log('stdout'); +console.error('stderr'); + +new Promise((resolve, reject) => { + reject('Oops'); +}); diff --git a/tests/fixtures/bun/package-manager/moon.yml b/tests/fixtures/bun/package-manager/moon.yml new file mode 100644 index 00000000000..3311dd48cd4 --- /dev/null +++ b/tests/fixtures/bun/package-manager/moon.yml @@ -0,0 +1,12 @@ +language: javascript +platform: bun + +tasks: + installDep: + command: bun + args: install react@17.0.0 + runDep: + command: prettier + args: --check *.yml + runScript: + command: bun run test diff --git a/tests/fixtures/bun/package-manager/package.json b/tests/fixtures/bun/package-manager/package.json new file mode 100644 index 00000000000..88a7bf1153b --- /dev/null +++ b/tests/fixtures/bun/package-manager/package.json @@ -0,0 +1,10 @@ +{ + "name": "test-bun-package-manager", + "version": "1.0.0", + "scripts": { + "test": "echo 'test'" + }, + "dependencies": { + "react": "17.0.0" + } +} diff --git a/tests/fixtures/bun/package.json b/tests/fixtures/bun/package.json new file mode 100644 index 00000000000..7efa0076622 --- /dev/null +++ b/tests/fixtures/bun/package.json @@ -0,0 +1,9 @@ +{ + "name": "test-bun", + "private": true, + "workspaces": [ + "base", + "package-manager", + "version-override" + ] +} diff --git a/tests/fixtures/bun/version-override/moon.yml b/tests/fixtures/bun/version-override/moon.yml new file mode 100644 index 00000000000..b2b0a89e401 --- /dev/null +++ b/tests/fixtures/bun/version-override/moon.yml @@ -0,0 +1,6 @@ +language: javascript +platform: bun + +toolchain: + bun: + version: '0.8.0' diff --git a/tests/fixtures/bun/version-override/package.json b/tests/fixtures/bun/version-override/package.json new file mode 100644 index 00000000000..80efb4938fa --- /dev/null +++ b/tests/fixtures/bun/version-override/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-bun-version-override", + "version": "1.0.0" +} diff --git a/website/docs/__partials__/create-task/bun/args.mdx b/website/docs/__partials__/create-task/bun/args.mdx new file mode 100644 index 00000000000..42366ac5ce8 --- /dev/null +++ b/website/docs/__partials__/create-task/bun/args.mdx @@ -0,0 +1,8 @@ +```yaml title="/moon.yml" {6} +language: 'javascript' +platform: 'bun' + +tasks: + build: + command: 'webpack build --mode production --no-stats' +``` diff --git a/website/docs/__partials__/create-task/bun/base.mdx b/website/docs/__partials__/create-task/bun/base.mdx new file mode 100644 index 00000000000..2b04efd58c1 --- /dev/null +++ b/website/docs/__partials__/create-task/bun/base.mdx @@ -0,0 +1,8 @@ +```yaml title="/moon.yml" {5,6} +language: 'javascript' +platform: 'bun' + +tasks: + build: + command: 'webpack build' +``` diff --git a/website/docs/__partials__/create-task/bun/filegroups.mdx b/website/docs/__partials__/create-task/bun/filegroups.mdx new file mode 100644 index 00000000000..0ced7c25bee --- /dev/null +++ b/website/docs/__partials__/create-task/bun/filegroups.mdx @@ -0,0 +1,17 @@ +```yaml title="/moon.yml" {11} +language: 'javascript' +platform: 'bun' + +fileGroups: + # ... + +tasks: + build: + command: 'webpack build --mode production --no-stats --output-path @out(0)' + inputs: + - '@globs(sources)' + - 'webpack.config.js' + - '/webpack-shared.config.js' + outputs: + - 'build' +``` diff --git a/website/docs/__partials__/create-task/bun/inputs.mdx b/website/docs/__partials__/create-task/bun/inputs.mdx new file mode 100644 index 00000000000..5c8c0ee4353 --- /dev/null +++ b/website/docs/__partials__/create-task/bun/inputs.mdx @@ -0,0 +1,12 @@ +```yaml title="/moon.yml" {7-10} +language: 'javascript' +platform: 'bun' + +tasks: + build: + command: 'webpack build --mode production --no-stats' + inputs: + - 'src/**/*' + - 'webpack.config.js' + - '/webpack-shared.config.js' +``` diff --git a/website/docs/__partials__/create-task/bun/outputs.mdx b/website/docs/__partials__/create-task/bun/outputs.mdx new file mode 100644 index 00000000000..a7824d18f5f --- /dev/null +++ b/website/docs/__partials__/create-task/bun/outputs.mdx @@ -0,0 +1,14 @@ +```yaml title="/moon.yml" {6,11,12} +language: 'javascript' +platform: 'bun' + +tasks: + build: + command: 'webpack build --mode production --no-stats --output-path @out(0)' + inputs: + - 'src/**/*' + - 'webpack.config.js' + - '/webpack-shared.config.js' + outputs: + - 'build' +``` diff --git a/website/docs/__partials__/migrate/bun/migrate.mdx b/website/docs/__partials__/migrate/bun/migrate.mdx new file mode 100644 index 00000000000..cb755d1b2c3 --- /dev/null +++ b/website/docs/__partials__/migrate/bun/migrate.mdx @@ -0,0 +1,41 @@ +```yaml title="/moon.yml" +language: 'typescript' +platform: 'bun' + +fileGroups: + sources: + - 'src/**/*' + - '*.ts' + - 'bunfig.toml' + tests: + - 'tests/**/*' + +tasks: + dev: + command: 'bun run dev' + inputs: + - '@globs(sources)' + local: true + format: + command: 'bun run format' + inputs: + - '@globs(sources)' + - '@globs(tests)' + lint: + command: 'bun run lint' + inputs: + - '@globs(sources)' + - '@globs(tests)' + test: + command: 'bun test --bail' + inputs: + - '@globs(sources)' + - '@globs(tests)' + typecheck: + command: 'bun run typecheck' + inputs: + - '@globs(sources)' + - '@globs(tests)' + - 'tsconfig.json' + - '/tsconfig.json' +``` diff --git a/website/docs/__partials__/migrate/bun/scripts.mdx b/website/docs/__partials__/migrate/bun/scripts.mdx new file mode 100644 index 00000000000..cd787dd7a2d --- /dev/null +++ b/website/docs/__partials__/migrate/bun/scripts.mdx @@ -0,0 +1,21 @@ +Bun provides a built-in script runner through the [`bun run` command](https://bun.sh/docs/cli/run). +While Bun scripts are great, they're not as efficient as moon. They _do not_ support granular +inputs, smart hashing, incremental caching, and all the other performance benefits that moon +provides. + +With that being said, you _do not_ have to migrate away from Bun scripts. Instead, you can simply +run them from within moon tasks. This gives you the best of both worlds. + +```yaml title="/moon.yml" +language: 'typescript' + +tasks: + analyze: + command: 'bun run analyze' + inputs: + - '@globs(sources)' + - '@globs(tests)' +``` + +> When Bun scripts are ran through moon tasks, the current working directory is set to the project +> root. diff --git a/website/docs/concepts/cache.mdx b/website/docs/concepts/cache.mdx index 2bee28b0593..bc51d5d4822 100644 --- a/website/docs/concepts/cache.mdx +++ b/website/docs/concepts/cache.mdx @@ -27,8 +27,8 @@ Our smart hashing currently takes the following sources into account: - Deno version. - `deno.json`/`deps.ts` imports, import maps, and scopes. - `tsconfig.json` compiler options (when applicable). -- **For Node.js tasks**: - - Node.js version. +- **For Bun and Node.js tasks**: + - Bun/Node.js version. - `package.json` dependencies (including development and peer). - `tsconfig.json` compiler options (when applicable). diff --git a/website/docs/concepts/toolchain.mdx b/website/docs/concepts/toolchain.mdx index eea3e4e8426..9f9ebbf2cf0 100644 --- a/website/docs/concepts/toolchain.mdx +++ b/website/docs/concepts/toolchain.mdx @@ -65,6 +65,11 @@ locally. The following tools are currently managed by the toolchain. +### Bun + +- Configured with: [`bun`](../config/toolchain#bun) +- Installed to: `~/.proto/tools/bun/x.x.x` + ### Deno - Configured with: [`deno`](../config/toolchain#deno) diff --git a/website/docs/config/project.mdx b/website/docs/config/project.mdx index d38007a9a90..6506564ee39 100644 --- a/website/docs/config/project.mdx +++ b/website/docs/config/project.mdx @@ -418,6 +418,8 @@ For interoperability reasons, the following command names have special handling. - `noop`, `no-op`, `nop` - Marks the task as a "no operation". Will not execute a command in the action pipeline but can define dependencies. +- When `platform` is "bun": + - `bun`, `bunx` - Uses the binaries from the toolchain. - When `platform` is "deno": - Will execute with `deno` binary. - When `platform` is "node": @@ -622,6 +624,8 @@ The `platform` field defines the platform (language runtime) the command runs on its executable, and which tool to execute it with. By default moon will set to a value based on the project's [`language`](#language) or default [`platform`](#platform). +- `bun` - Command is a binary within `node_modules` and will be executed with Bun. + - `deno` - Command is executed with Deno, or is a Deno binary located in `~/.deno/bin`. - `node` - Command is a binary within `node_modules` and will be executed with Node.js. - `rust` - Command is executed with Cargo, or is a Cargo binary located in `~/.cargo/bin`. @@ -960,6 +964,23 @@ Dictates how a project interacts with settings defined at the top-level. +### `bun` + + + +Configures Bun for this project and overrides the top-level [`bun`](./toolchain#bun) setting. + +#### `version` + +Defines the explicit Bun [version specification](../concepts/toolchain#version-specification) to use +when _running tasks_ for this project. + +```yaml title="moon.yml" {2,3} +toolchain: + bun: + version: '1.0.0' +``` + ### `node` diff --git a/website/docs/config/toolchain.mdx b/website/docs/config/toolchain.mdx index 412b98b0dd9..40fe5e388e8 100644 --- a/website/docs/config/toolchain.mdx +++ b/website/docs/config/toolchain.mdx @@ -40,6 +40,27 @@ taking precedence over those defined in the extended configuration. ## JavaScript +## `bun` + + + +Enables and configures [Bun](../guides/javascript/bun-handbook). + +### `version` + + + +Defines the explicit Bun toolchain +[version specification](../concepts/toolchain#version-specification) to use. If this field is _not +defined_, the global `bun` binary will be used. + +```yaml title=".moon/toolchain.yml" {2} +bun: + version: '1.0.0' +``` + +> Version can also be defined with [`.prototools`](../proto/config). + ## `deno` diff --git a/website/docs/create-task.mdx b/website/docs/create-task.mdx index 6fd0d8831ee..abcc24ec425 100644 --- a/website/docs/create-task.mdx +++ b/website/docs/create-task.mdx @@ -30,6 +30,7 @@ a project using [`moon.yml`](./config/project). Begin by creating the `moon.yml` file at the root of a project and add `build` to the [`tasks`](./config/project#tasks) field, with a [`command`](./config/project#command) parameter. +import BaseBun from './__partials__/create-task/bun/base.mdx'; import BaseDeno from './__partials__/create-task/deno/base.mdx'; import BaseGo from './__partials__/create-task/go/base.mdx'; import BaseNode from './__partials__/create-task/node/base.mdx'; @@ -39,6 +40,7 @@ import BaseRuby from './__partials__/create-task/ruby/base.mdx'; import BaseRust from './__partials__/create-task/rust/base.mdx'; + @@ -51,6 +53,7 @@ import BaseRust from './__partials__/create-task/rust/base.mdx'; By itself, this isn't doing much, so let's add some arguments. Arguments can also be defined with the [`args`](./config/project#args) setting. +import ArgsBun from './__partials__/create-task/bun/args.mdx'; import ArgsDeno from './__partials__/create-task/deno/args.mdx'; import ArgsGo from './__partials__/create-task/go/args.mdx'; import ArgsNode from './__partials__/create-task/node/args.mdx'; @@ -60,6 +63,7 @@ import ArgsRuby from './__partials__/create-task/ruby/args.mdx'; import ArgsRust from './__partials__/create-task/rust/args.mdx'; + @@ -86,6 +90,7 @@ inputs to calculate whether to run, or to return the previous run state from the If you're a bit confused, let's demonstrate this by expanding the task with the [`inputs`](./config/project#inputs) setting. +import InputsBun from './__partials__/create-task/bun/inputs.mdx'; import InputsDeno from './__partials__/create-task/deno/inputs.mdx'; import InputsGo from './__partials__/create-task/go/inputs.mdx'; import InputsNode from './__partials__/create-task/node/inputs.mdx'; @@ -95,6 +100,7 @@ import InputsRuby from './__partials__/create-task/ruby/inputs.mdx'; import InputsRust from './__partials__/create-task/rust/inputs.mdx'; + @@ -134,6 +140,7 @@ then immediately exits. No more waiting for long builds! Continuing our example, let's route the built files and expand our task with the [`outputs`](./config/project#outputs) setting. +import OutputsBun from './__partials__/create-task/bun/outputs.mdx'; import OutputsDeno from './__partials__/create-task/deno/outputs.mdx'; import OutputsGo from './__partials__/create-task/go/outputs.mdx'; import OutputsNode from './__partials__/create-task/node/outputs.mdx'; @@ -143,6 +150,7 @@ import OutputsRuby from './__partials__/create-task/ruby/outputs.mdx'; import OutputsRust from './__partials__/create-task/rust/outputs.mdx'; + @@ -200,6 +208,7 @@ We can then replace the inputs in our task above with these new file groups usin [`@files`](./concepts/token#files) token functions. Tokens are an advanced feature, so please refer to their documentation for more information! +import FilegroupsBun from './__partials__/create-task/bun/filegroups.mdx'; import FilegroupsDeno from './__partials__/create-task/deno/filegroups.mdx'; import FilegroupsGo from './__partials__/create-task/go/filegroups.mdx'; import FilegroupsNode from './__partials__/create-task/node/filegroups.mdx'; @@ -209,6 +218,7 @@ import FilegroupsRuby from './__partials__/create-task/ruby/filegroups.mdx'; import FilegroupsRust from './__partials__/create-task/rust/filegroups.mdx'; + diff --git a/website/docs/guides/javascript/bun-handbook.mdx b/website/docs/guides/javascript/bun-handbook.mdx new file mode 100644 index 00000000000..e8c28ddb7d7 --- /dev/null +++ b/website/docs/guides/javascript/bun-handbook.mdx @@ -0,0 +1,113 @@ +--- +title: Bun handbook +toc_max_heading_level: 6 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Utilizing JavaScript (and TypeScript) in a monorepo can be a daunting task, especially when using +Bun (or Node.js), as there are many ways to structure your code and to configure your tools. With +this handbook, we'll help guide you through this process. + +:::info + +This guide is a living document and will continue to be updated over time! + +::: + +## moon setup + +For this part of the handbook, we'll be focusing on [moon](/moon), our task runner. To start, +languages in moon act like plugins, where their functionality and support _is not_ enabled unless +explicitly configured. We follow this approach to avoid unnecessary overhead. + +### Enabling the language + +To enable JavaScript support via Bun, define the [`bun`](../../config/toolchain#bun) setting in +[`.moon/toolchain.yml`](../../config/toolchain), even if an empty object. + +```yaml title=".moon/toolchain.yml" +# Enable Bun +bun: {} +``` + +Or by pinning a `bun` version in [`.prototools`](../../proto/config) in the workspace root. + +```toml title=".prototools" +bun = "1.0.0" +``` + +This will enable the Bun platform and provide the following automations around its ecosystem: + +- Node modules will automatically be installed if dependencies in `package.json` have changed, or + the lockfile has changed, since the last time a task has ran. + - We'll also take `package.json` workspaces into account and install modules in the correct + location; either the workspace root, in a project, or both. +- Relationships between projects will automatically be discovered based on `dependencies`, + `devDependencies`, and `peerDependencies` in `package.json`. + +### Utilizing the toolchain + +When a language is enabled, moon by default will assume that the language's binary is available +within the current environment (typically on `PATH`). This has the downside of requiring all +developers and machines to manually install the correct version of the language, _and to stay in +sync_. + +Instead, you can utilize [moon's toolchain](../../concepts/toolchain), which will download and +install the language in the background, and ensure every task is executed using the exact version +across all machines. + +Enabling the toolchain is as simple as defining the [`bun.version`](../../config/toolchain#version) +setting. + +```yaml title=".moon/toolchain.yml" +# Enable Bun toolchain with an explicit version +bun: + version: '1.0.0' +``` + +> Versions can also be defined with [`.prototools`](../../proto/config). + +### Configuring the `platform` + +Since the JavaScript ecosystem supports multiple runtimes, moon is unable to automatically detect +the correct runtime for all scenarios. Does the existence of a `package.json` mean Node.js or Bun? +We don't know, and default to Node.js because of its popularity. + +To work around this, you can set `platform` to "bun" at the task-level or project-level. + +```yaml title="moon.yml" +# For all tasks in the project +platform: 'bun' + +tasks: + build: + command: 'webpack' + # For this specific task + platform: 'bun' +``` + +> The task-level `platform` only needs to be set if executing a `node_modules` binary! The `bun` +> binary automatically sets the platform to Bun. + +### Using `package.json` scripts + +If you're looking to prototype moon, or reduce the migration effort to moon tasks, you can configure +moon tasks to execute `package.json` scripts using `bun run`, instead of migrating everything. + +```yaml title="moon.yml" +tasks: + build: + command: 'bun run build' +``` + +## Handbook + +:::info + +Refer to the [Node.js handbook](./node-handbook) for more information on repository structure, +dependency management, and more. Since both runtimes are extremely similar, the information in that +handbook also applies to Bun! + +::: diff --git a/website/docs/install.mdx b/website/docs/install.mdx index d3bf821c294..8f3f034f7a8 100644 --- a/website/docs/install.mdx +++ b/website/docs/install.mdx @@ -69,7 +69,7 @@ irm https://moonrepo.dev/install/moon.ps1 | iex This will install moon to `~\.moon\bin` and prepend to the `PATH` environment variable for the current session. To persist across sessions, update `PATH` manually. -### Node.js +### npm moon is also packaged and shipped as a single binary through the [`@moonrepo/cli`](https://www.npmjs.com/package/@moonrepo/cli) npm package. Begin by installing this diff --git a/website/docs/intro.mdx b/website/docs/intro.mdx index 974dd4fd4cb..55228c3245f 100644 --- a/website/docs/intro.mdx +++ b/website/docs/intro.mdx @@ -73,7 +73,7 @@ incremental integrate and improve them over time. The 4 tiers are as follows: | | Tier 0 | Tier 1 | Tier 2 | Tier 3 | | :---------------------------- | :----: | :----: | :----: | :----: | | Bash/Batch | 🟢 | 🟢 | | | -| Bun (JavaScript, TypeScript) | 🟢 | 🟢 | | 🟣 | +| Bun (JavaScript, TypeScript) | 🟢 | 🟢 | 🟢 | 🟢 | | Deno (JavaScript, TypeScript) | 🟢 | 🟢 | 🟣 | | | Go | 🟢 | 🟢 | | | | Node (JavaScript, TypeScript) | 🟢 | 🟢 | 🟢 | 🟢 | diff --git a/website/docs/migrate-to-moon.mdx b/website/docs/migrate-to-moon.mdx index 7be6f4466cf..d9d6bbfccdc 100644 --- a/website/docs/migrate-to-moon.mdx +++ b/website/docs/migrate-to-moon.mdx @@ -19,6 +19,7 @@ enough to scale for large codebases. An example of what this may look like can be found below. This _may_ look like a lot, but it pays dividends in the long run. +import MigrateBun from './__partials__/migrate/bun/migrate.mdx'; import MigrateDeno from './__partials__/migrate/deno/migrate.mdx'; import MigrateGo from './__partials__/migrate/go/migrate.mdx'; import MigrateNode from './__partials__/migrate/node/migrate.mdx'; @@ -28,6 +29,7 @@ import MigrateRuby from './__partials__/migrate/ruby/migrate.mdx'; import MigrateRust from './__partials__/migrate/rust/migrate.mdx'; + @@ -39,6 +41,7 @@ import MigrateRust from './__partials__/migrate/rust/migrate.mdx'; ## Continue using scripts +import ScriptsBun from './__partials__/migrate/bun/scripts.mdx'; import ScriptsDeno from './__partials__/migrate/deno/scripts.mdx'; import ScriptsGo from './__partials__/migrate/go/scripts.mdx'; import ScriptsNode from './__partials__/migrate/node/scripts.mdx'; @@ -48,6 +51,7 @@ import ScriptsRuby from './__partials__/migrate/ruby/scripts.mdx'; import ScriptsRust from './__partials__/migrate/rust/scripts.mdx'; + diff --git a/website/sidebars.js b/website/sidebars.js index 346a5822d06..1f3b0e4403e 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -211,6 +211,7 @@ const sidebars = { label: 'JavaScript', collapsed: false, items: [ + 'guides/javascript/bun-handbook', 'guides/javascript/deno-handbook', 'guides/javascript/node-handbook', 'guides/profile', diff --git a/website/src/components/AddDepsTabs.tsx b/website/src/components/AddDepsTabs.tsx index 44d38dc1576..56556953a27 100644 --- a/website/src/components/AddDepsTabs.tsx +++ b/website/src/components/AddDepsTabs.tsx @@ -66,6 +66,24 @@ function getPnpm(props: AddDepsTabsProps, workspaces: boolean) { return cmd; } +function getBun(props: AddDepsTabsProps) { + let cmd = `bun install `; + + if (props.dev) { + cmd += '--dev '; + } else if (props.peer) { + cmd += '--peer '; + } + + // if (props.package) { + // cmd += `--workspace ${props.package} `; + // } + + cmd += props.dep; + + return cmd; +} + export default function AddDepsTabs(props: AddDepsTabsProps) { let yarn1 = getYarn(props, false, true); let pnpm = getPnpm(props, false); @@ -87,6 +105,7 @@ export default function AddDepsTabs(props: AddDepsTabsProps) { { label: 'Yarn (classic)', value: 'yarn1' }, { label: 'npm', value: 'npm' }, { label: 'pnpm', value: 'pnpm' }, + { label: 'Bun', value: 'bun' }, ]} > @@ -101,6 +120,9 @@ export default function AddDepsTabs(props: AddDepsTabsProps) { {pnpm} + + {getBun(props)} + ); } diff --git a/website/src/components/ComparisonTable.tsx b/website/src/components/ComparisonTable.tsx index b4b18444e50..f06e634006f 100644 --- a/website/src/components/ComparisonTable.tsx +++ b/website/src/components/ComparisonTable.tsx @@ -105,7 +105,7 @@ const toolchainRows: Comparison[] = [ { feature: 'Supported dependency managers', support: { - moon: 'npm, pnpm, yarn', + moon: 'npm, pnpm, yarn, bun', nx: 'npm, pnpm, yarn', turborepo: 'npm, pnpm, yarn', }, diff --git a/website/src/components/LangSelector.tsx b/website/src/components/LangSelector.tsx index a9a1fea05e0..a1a61027961 100644 --- a/website/src/components/LangSelector.tsx +++ b/website/src/components/LangSelector.tsx @@ -73,6 +73,7 @@ export default function LangSelector() { onChange={handleChange} className="outline-none min-w-0 bg-white border border-solid border-gray-400 dark:border-transparent rounded-md p-0.5 text-sm text-gray-800 placeholder-gray-600 h-full font-sans" > + diff --git a/website/static/schemas/project.json b/website/static/schemas/project.json index 4df83f38531..3a99c1e92f4 100644 --- a/website/static/schemas/project.json +++ b/website/static/schemas/project.json @@ -429,6 +429,16 @@ "title": "PartialProjectToolchainConfig", "type": "object", "properties": { + "bun": { + "anyOf": [ + { + "$ref": "#/definitions/PartialProjectToolchainCommonToolConfig" + }, + { + "type": "null" + } + ] + }, "node": { "anyOf": [ {