diff --git a/README.md b/README.md index f0793b5..17eae84 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Depot works on Javascript workspaces that have been created by Depot, specifical * For libraries, transpiles with [Typescript] * For scripts and websites, bundles with [Vite] * `depot test` - runs tests with [Vitest] -* `depot fmt` - formats source files with [Prettier] +* `depot fmt` - formats source files with [Biome] * `depot doc` - generates documentation with [Typedoc] A few benefits of using Depot: @@ -93,6 +93,6 @@ Depot is used in a few of our projects: [Typescript]: https://www.typescriptlang.org/ [Vite]: https://vitejs.dev/ [Vitest]: https://vitest.dev/ -[Prettier]: https://prettier.io/ +[Biome]: https://biomejs.dev/ [Typedoc]: https://typedoc.org/ [pnpm]: https://pnpm.io/ diff --git a/crates/depot/src/commands/build.rs b/crates/depot/src/commands/build.rs index 27d6e0a..e04c5c2 100644 --- a/crates/depot/src/commands/build.rs +++ b/crates/depot/src/commands/build.rs @@ -30,7 +30,7 @@ pub struct BuildArgs { #[clap(short, long, action)] pub watch: bool, - /// Fail if eslint finds a lint issue + /// Fail if biome finds a lint issue #[clap(short, long, action)] pub lint_fail: bool, } @@ -62,7 +62,7 @@ impl PackageCommand for BuildCommand { Target::Lib => processes.push(self.copy_assets(pkg).boxed()), } - processes.extend([self.tsc(pkg).boxed(), self.eslint(pkg).boxed()]); + processes.extend([self.tsc(pkg).boxed(), self.biome(pkg).boxed()]); try_join_all(processes).await?; @@ -105,15 +105,16 @@ impl BuildCommand { .await } - async fn eslint(&self, pkg: &Package) -> Result<()> { - let process = pkg.start_process("eslint", |cmd| { + async fn biome(&self, pkg: &Package) -> Result<()> { + let process = pkg.start_process("biome", |cmd| { + cmd.arg("check"); cmd.args(pkg.source_files()); - cmd.arg("--color"); + cmd.arg("--colors=force"); // TODO: watch mode })?; let status = process.wait().await?; - ensure!(!self.args.lint_fail || status.success(), "eslint failed"); + ensure!(!self.args.lint_fail || status.success(), "biome failed"); Ok(()) } diff --git a/crates/depot/src/commands/fix.rs b/crates/depot/src/commands/fix.rs index 258d7ae..fdcd706 100644 --- a/crates/depot/src/commands/fix.rs +++ b/crates/depot/src/commands/fix.rs @@ -2,12 +2,12 @@ use anyhow::{Context, Result}; use crate::workspace::{package::Package, Command, CoreCommand, PackageCommand}; -/// Fix eslint issues where possible +/// Fix biome issues where possible #[derive(clap::Parser, Debug)] pub struct FixArgs { /// Additional arguments to pass to prettier #[arg(last = true)] - pub eslint_args: Option, + pub biome_args: Option, } #[derive(Debug)] @@ -35,13 +35,14 @@ impl CoreCommand for FixCommand { #[async_trait::async_trait] impl PackageCommand for FixCommand { async fn run_pkg(&self, pkg: &Package) -> Result<()> { - let extra = match &self.args.eslint_args { + let extra = match &self.args.biome_args { Some(args) => shlex::split(args).context("Failed to parse prettier args")?, None => Vec::new(), }; let _ = pkg - .exec("eslint", |cmd| { + .exec("biome", |cmd| { + cmd.arg("check"); cmd.arg("--fix"); cmd.args(pkg.source_files()); cmd.args(extra); diff --git a/crates/depot/src/commands/fmt.rs b/crates/depot/src/commands/fmt.rs index bfb9199..854073a 100644 --- a/crates/depot/src/commands/fmt.rs +++ b/crates/depot/src/commands/fmt.rs @@ -45,8 +45,11 @@ impl PackageCommand for FmtCommand { }; pkg - .exec("prettier", |cmd| { - cmd.arg(if self.args.check { "-c" } else { "-w" }); + .exec("biome", |cmd| { + cmd.arg("format"); + if !self.args.check { + cmd.arg("--write"); + } cmd.args(pkg.source_files()); cmd.args(extra); }) diff --git a/crates/depot/src/commands/new.rs b/crates/depot/src/commands/new.rs index 2053e7b..73fec6f 100644 --- a/crates/depot/src/commands/new.rs +++ b/crates/depot/src/commands/new.rs @@ -149,7 +149,7 @@ impl NewCommand { ("pnpm-workspace.yaml".into(), PNPM_WORKSPACE.into()), ]; files.extend(self.make_tsconfig()?); - files.extend(self.make_eslint_config()?); + files.extend(Self::make_biome_config()?); files.extend(self.make_typedoc_config()?); files.extend(Self::make_prettier_config()); files.extend(Self::make_gitignore()); @@ -242,7 +242,9 @@ impl NewCommand { // Allows special Vite things like importing files with ?raw files.push(( "src/bindings/vite.d.ts".into(), - r#"/// "#.into(), + r#"/// +"# + .into(), )); } } @@ -252,77 +254,28 @@ impl NewCommand { Ok(files) } - fn make_eslint_config(&self) -> Result { - let mut config = json!({ - "env": { - "es2021": true, + fn make_biome_config() -> Result { + let config = json!({ + "$schema": "https://biomejs.dev/schemas/1.8.2/schema.json", + "organizeImports": { + "enabled": true }, - "extends": ["eslint:recommended"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 13, - "sourceType": "module", + "formatter": { + "enabled": true, + "indentStyle": "space" }, - "plugins": ["@typescript-eslint", "prettier", "eslint-plugin-unused-imports"], - "ignorePatterns": ["*.d.ts"], - "rules": { - "no-empty-pattern": "off", - "no-undef": "off", - "no-unused-vars": "off", - "no-cond-assign": "off", - "@typescript-eslint/no-unused-vars": "off", - "unused-imports/no-unused-imports": "error", - "unused-imports/no-unused-vars": [ - "warn", - { - "vars": "all", - "varsIgnorePattern": "^_", - "args": "after-used", - "argsIgnorePattern": "^_", - }, - ], - "no-constant-condition": ["error", { "checkLoops": false }], - "prettier/prettier": "error", - }, - }); - - if !self.args.workspace && self.ws_opt.is_some() { - config = json!({ - "extends": "../../.eslintrc.cjs" - }); - - let platform_config = match self.args.platform { - Platform::Browser => json!({ - "env": {"browser": true}, - }), - Platform::Node => json!({ - "env": {"node": true}, - }), - }; - - json_merge(&mut config, platform_config); - } - - if self.args.react { - let react_config = json!({ - "plugins": ["react"], + "linter": { + "enabled": true, "rules": { - "react/prop-types": "off", - "react/no-unescaped-entities": "off", - }, - "settings": { - "react": { - "version": "detect", - }, + "recommended": true, + "correctness": {"noUnusedImports": "warn"}, + "style": {"noNonNullAssertion": "off", "useConst": "off"} } - }); - - json_merge(&mut config, react_config); - } + } + }); let config_str = serde_json::to_string_pretty(&config)?; - let src = format!("module.exports = {config_str}"); - Ok(vec![(".eslintrc.cjs".into(), src.into())]) + Ok(vec![("biome.json".into(), config_str.into())]) } #[allow(clippy::too_many_lines)] @@ -539,7 +492,7 @@ export default defineConfig(({{ mode }}) => ({{ fn install_ws_dependencies(&self, root: &Path, is_workspace: bool) -> Result<()> { #[rustfmt::skip] - let mut ws_dependencies: Vec<&str> = vec![ + let ws_dependencies: Vec<&str> = vec![ // Building "vite", @@ -550,32 +503,29 @@ export default defineConfig(({{ mode }}) => ({{ "typescript", "@types/node", - // Linting - "eslint@8", - "@typescript-eslint/eslint-plugin@7", - "@typescript-eslint/parser@7", - "eslint-plugin-prettier@4", - "eslint-plugin-unused-imports@3", - - // Formatting - "prettier@2", - "@trivago/prettier-plugin-sort-imports@4", + // Linting and formatting + "@biomejs/biome", // Documentation generation "typedoc" ]; - if self.args.react { - ws_dependencies.extend(["eslint-plugin-react", "eslint-plugin-react-hooks"]); - } - self.run_pnpm(|pnpm| { pnpm.args(["add", "--save-dev"]).args(&ws_dependencies); if is_workspace { pnpm.arg("--workspace-root"); } pnpm.current_dir(root); - }) + })?; + + // Temporary fix for installing native modules on M-series Macs. + // --force ensures that the arm64 package is installed for Biome. + self.run_pnpm(|pnpm| { + pnpm.args(["install", "--force"]); + pnpm.current_dir(root); + })?; + + Ok(()) } fn make_index_html(js_entry_point: &str, css_entry_point: &str) -> String { @@ -747,7 +697,7 @@ export default defineConfig(({{ mode }}) => ({{ ), ]); files.extend(self.make_tsconfig()?); - files.extend(self.make_eslint_config()?); + files.extend(Self::make_biome_config()?); files.extend(self.make_vite_config(src_path)); if self.ws_opt.is_none() { diff --git a/crates/depot/src/workspace/mod.rs b/crates/depot/src/workspace/mod.rs index 1846af5..cb0b898 100644 --- a/crates/depot/src/workspace/mod.rs +++ b/crates/depot/src/workspace/mod.rs @@ -325,6 +325,7 @@ impl WorkspaceInner { let mut cmd = tokio::process::Command::new(script_path); cmd.current_dir(&self.root); + cmd.env("NODE_PATH", self.root.join("node_modules")); configure(&mut cmd); diff --git a/crates/depot/src/workspace/package.rs b/crates/depot/src/workspace/package.rs index 677d71e..2124006 100644 --- a/crates/depot/src/workspace/package.rs +++ b/crates/depot/src/workspace/package.rs @@ -148,6 +148,7 @@ pub struct PackageInner { // Metadata pub root: PathBuf, pub manifest: PackageManifest, + #[allow(unused)] pub platform: Platform, pub target: Target, pub name: PackageName, @@ -300,13 +301,17 @@ impl PackageInner { } pub fn source_files(&self) -> impl Iterator + '_ { + // TODO: make this configurable + let source_extensions = hashset! { "ts", "tsx", "html" }; + ["src", "tests"] .into_iter() .flat_map(|dir| self.iter_files(dir)) - .filter_map(|path| { + .filter_map(move |path| { let ext = path.extension()?; - let is_src_file = ext == "ts" || ext == "tsx"; - is_src_file.then_some(path) + source_extensions + .contains(ext.to_str().unwrap()) + .then_some(path) }) } diff --git a/crates/depot/src/workspace/process.rs b/crates/depot/src/workspace/process.rs index 9420e39..f6c8fa8 100644 --- a/crates/depot/src/workspace/process.rs +++ b/crates/depot/src/workspace/process.rs @@ -24,6 +24,7 @@ pub enum OutputChannel { /// A string emitted by a shell command on a given [`OutputChannel`]. pub struct LogLine { pub line: String, + #[allow(unused)] // We may eventually want to distinguish stdout/stderr in the logs pub channel: OutputChannel, } diff --git a/crates/depot/src/workspace/runner.rs b/crates/depot/src/workspace/runner.rs index b592bb0..1894821 100644 --- a/crates/depot/src/workspace/runner.rs +++ b/crates/depot/src/workspace/runner.rs @@ -113,7 +113,11 @@ impl Workspace { }) } - fn build_task_graph(&self, cmd_graph: &CommandGraph) -> (TaskGraph, HashMap) { + fn build_task_graph( + &self, + cmd_graph: &CommandGraph, + runtime: &Option, + ) -> (TaskGraph, HashMap) { let futures = RefCell::new(HashMap::new()); let task_pool = RefCell::new(HashMap::new()); @@ -125,6 +129,7 @@ impl Workspace { .entry($key.clone()) .or_insert_with(|| { let can_skip = !self.common.no_incremental + && !matches!(runtime, Some(CommandRuntime::RunForever)) && match $files { Some(files) => { let fingerprints = self.fingerprints.read().unwrap(); @@ -190,8 +195,9 @@ impl Workspace { } pub async fn run(&self, root: Command) -> Result<()> { + let runtime = root.runtime(); let cmd_graph = build_command_graph(&root); - let (task_graph, mut task_futures) = self.build_task_graph(&cmd_graph); + let (task_graph, mut task_futures) = self.build_task_graph(&cmd_graph, &runtime); let log_should_exit: Arc = Arc::new(Notify::new()); let runner_should_exit: Arc = Arc::new(Notify::new()); @@ -199,7 +205,6 @@ impl Workspace { let runner_should_exit_fut = runner_should_exit.notified(); tokio::pin!(runner_should_exit_fut); - let runtime = root.runtime(); let cleanup_logs = self.spawn_log_thread(&log_should_exit, &runner_should_exit, &runtime); let mut running_futures = Vec::new(); diff --git a/crates/depot/tests/tests/fmt.rs b/crates/depot/tests/tests/fmt.rs index 7b2b965..c4c7a6e 100644 --- a/crates/depot/tests/tests/fmt.rs +++ b/crates/depot/tests/tests/fmt.rs @@ -2,7 +2,7 @@ use depot_test_utils::{project, workspace_single_lib}; #[test] fn basic() { - let p = project(); + let p = project().persist(); p.file("src/lib.ts", "let x = 1 + 2;"); p.depot("fmt"); assert_eq!(p.read("src/lib.ts"), "let x = 1 + 2;\n"); diff --git a/crates/depot/tests/tests/new.rs b/crates/depot/tests/tests/new.rs index ca8e505..45afede 100644 --- a/crates/depot/tests/tests/new.rs +++ b/crates/depot/tests/tests/new.rs @@ -3,5 +3,5 @@ use depot_test_utils::project; #[test] fn formatting() { let p = project(); - p.depot("fmt -- --check"); + p.depot("fmt --check"); }