diff --git a/src/build.rs b/src/build.rs index 15f02de..5e07dc1 100644 --- a/src/build.rs +++ b/src/build.rs @@ -18,10 +18,12 @@ use console::style; use indicatif::{ProgressBar, ProgressStyle}; use log::log_enabled; use serde::Serialize; +use std::ffi::OsString; use std::fmt; use std::fs::File; use std::io::{stdout, Write}; use std::path::PathBuf; +use std::process::Stdio; use std::time::{Duration, Instant}; use self::compile::compiler_args; @@ -506,3 +508,18 @@ pub fn build( } } } + +pub fn pass_through_legacy(args: &[OsString]) -> i32 { + let project_root = helpers::get_abs_path("."); + let workspace_root = helpers::get_workspace_root(&project_root); + + let bsb_path = helpers::get_rescript_legacy(&project_root, workspace_root); + + let status = std::process::Command::new(bsb_path) + .args(args) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status(); + + status.map(|s| s.code().unwrap_or(1)).unwrap_or(1) +} diff --git a/src/build/clean.rs b/src/build/clean.rs index ba53e4f..5792b09 100644 --- a/src/build/clean.rs +++ b/src/build/clean.rs @@ -338,7 +338,7 @@ pub fn cleanup_after_build(build_state: &BuildState) { }); } -pub fn clean(path: &str, show_progress: bool, bsc_path: Option, build_dev_deps: bool) -> Result<()> { +pub fn clean(path: &str, show_progress: bool, bsc_path: Option) -> Result<()> { let project_root = helpers::get_abs_path(path); let workspace_root = helpers::get_workspace_root(&project_root); let packages = packages::make( @@ -346,8 +346,9 @@ pub fn clean(path: &str, show_progress: bool, bsc_path: Option, build_de &project_root, &workspace_root, show_progress, - // Always clean dev dependencies - build_dev_deps, + // Build the package tree with dev dependencies. + // They should always be cleaned if they are there. + true, )?; let root_config_name = packages::read_package_name(&project_root)?; let bsc_path = match bsc_path { diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..13fb1f1 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,166 @@ +use std::ffi::OsString; + +use clap::{Args, Parser, Subcommand}; +use clap_verbosity_flag::InfoLevel; + +/// Rewatch is an alternative build system for the Rescript Compiler bsb (which uses Ninja internally). It strives +/// to deliver consistent and faster builds in monorepo setups with multiple packages, where the +/// default build system fails to pick up changed interfaces across multiple packages. +#[derive(Parser, Debug)] +#[command(version)] +#[command(args_conflicts_with_subcommands = true)] +pub struct Cli { + /// Verbosity: + /// -v -> Debug + /// -vv -> Trace + /// -q -> Warn + /// -qq -> Error + /// -qqq -> Off. + /// Default (/ no argument given): 'info' + #[command(flatten)] + pub verbose: clap_verbosity_flag::Verbosity, + + /// The command to run. If not provided it will default to build. + #[command(subcommand)] + pub command: Option, + + /// The relative path to where the main rescript.json resides. IE - the root of your project. + #[arg(default_value = ".")] + pub folder: String, + + #[command(flatten)] + pub build_args: BuildArgs, +} + +#[derive(Args, Debug, Clone)] +pub struct BuildArgs { + /// Filter files by regex + /// + /// Filter allows for a regex to be supplied which will filter the files to be compiled. For + /// instance, to filter out test files for compilation while doing feature work. + #[arg(short, long)] + pub filter: Option, + + /// Action after build + /// + /// This allows one to pass an additional command to the watcher, which allows it to run when + /// finished. For instance, to play a sound when done compiling, or to run a test suite. + /// NOTE - You may need to add '--color=always' to your subcommand in case you want to output + /// colour as well + #[arg(short, long)] + pub after_build: Option, + + /// Create source_dirs.json + /// + /// This creates a source_dirs.json file at the root of the monorepo, which is needed when you + /// want to use Reanalyze + #[arg(short, long, default_value_t = false, num_args = 0..=1)] + pub create_sourcedirs: bool, + + /// Build development dependencies + /// + /// This is the flag to also compile development dependencies + /// It's important to know that we currently do not discern between project src, and + /// dependencies. So enabling this flag will enable building _all_ development dependencies of + /// _all_ packages + #[arg(long, default_value_t = false, num_args = 0..=1)] + pub dev: bool, + + /// Disable timing on the output + #[arg(short, long, default_value_t = false, num_args = 0..=1)] + pub no_timing: bool, + + /// Path to bsc + #[arg(long)] + pub bsc_path: Option, +} + +#[derive(Args, Debug)] +pub struct WatchArgs { + /// Filter files by regex + /// + /// Filter allows for a regex to be supplied which will filter the files to be compiled. For + /// instance, to filter out test files for compilation while doing feature work. + #[arg(short, long)] + pub filter: Option, + + /// Action after build + /// + /// This allows one to pass an additional command to the watcher, which allows it to run when + /// finished. For instance, to play a sound when done compiling, or to run a test suite. + /// NOTE - You may need to add '--color=always' to your subcommand in case you want to output + /// colour as well + #[arg(short, long)] + pub after_build: Option, + + /// Create source_dirs.json + /// + /// This creates a source_dirs.json file at the root of the monorepo, which is needed when you + /// want to use Reanalyze + #[arg(short, long, default_value_t = false, num_args = 0..=1)] + pub create_sourcedirs: bool, + + /// Build development dependencies + /// + /// This is the flag to also compile development dependencies + /// It's important to know that we currently do not discern between project src, and + /// dependencies. So enabling this flag will enable building _all_ development dependencies of + /// _all_ packages + #[arg(long, default_value_t = false, num_args = 0..=1)] + pub dev: bool, + + /// Path to bsc + #[arg(long)] + pub bsc_path: Option, +} + +#[derive(Subcommand, Debug)] +pub enum Command { + /// Build using Rewatch + Build(BuildArgs), + /// Build, then start a watcher + Watch(WatchArgs), + /// Clean the build artifacts + Clean { + /// Path to bsc + #[arg(long)] + bsc_path: Option, + }, + /// Alias to `legacy format`. + #[command(disable_help_flag = true)] + Format { + #[arg(allow_hyphen_values = true, num_args = 0..)] + format_args: Vec, + }, + /// Alias to `legacy dump`. + #[command(disable_help_flag = true)] + Dump { + #[arg(allow_hyphen_values = true, num_args = 0..)] + dump_args: Vec, + }, + /// This prints the compiler arguments. It expects the path to a rescript.json file. + CompilerArgs { + /// Path to a rescript.json file + #[command()] + path: String, + + #[arg(long, default_value_t = false, num_args = 0..=1)] + dev: bool, + + /// To be used in conjunction with compiler_args + #[arg(long)] + rescript_version: Option, + + /// A custom path to bsc + #[arg(long)] + bsc_path: Option, + }, + /// Use the legacy build system. + /// + /// After this command is encountered, the rest of the arguments are passed to the legacy build system. + #[command(disable_help_flag = true)] + Legacy { + #[arg(allow_hyphen_values = true, num_args = 0..)] + legacy_args: Vec, + }, +} diff --git a/src/helpers.rs b/src/helpers.rs index c4a7343..3f98628 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -150,6 +150,25 @@ pub fn create_path_for_path(path: &Path) { fs::DirBuilder::new().recursive(true).create(path).unwrap(); } +pub fn get_rescript_legacy(root_path: &str, workspace_root: Option) -> String { + match ( + PathBuf::from(format!("{}/node_modules/rescript/rescript-legacy", root_path)).canonicalize(), + workspace_root.map(|workspace_root| { + PathBuf::from(format!( + "{}/node_modules/rescript/rescript-legacy", + workspace_root + )) + .canonicalize() + }), + ) { + (Ok(path), _) => path, + (_, Some(Ok(path))) => path, + _ => panic!("Could not find rescript-legacy"), + } + .to_string_lossy() + .to_string() +} + pub fn get_bsc(root_path: &str, workspace_root: Option) -> String { let subfolder = match (std::env::consts::OS, std::env::consts::ARCH) { ("macos", "aarch64") => "darwinarm64", diff --git a/src/lib.rs b/src/lib.rs index 9dc6f55..2df92a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod build; +pub mod cli; pub mod cmd; pub mod config; pub mod helpers; diff --git a/src/main.rs b/src/main.rs index 3ff4768..be5f97f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,88 +1,14 @@ use anyhow::Result; -use clap::{Parser, ValueEnum}; -use clap_verbosity_flag::InfoLevel; +use clap::Parser; use log::LevelFilter; use regex::Regex; -use std::io::Write; +use std::{ffi::OsString, io::Write}; -use rewatch::{build, cmd, lock, watcher}; - -#[derive(Debug, Clone, ValueEnum)] -enum Command { - /// Build using Rewatch - Build, - /// Build, then start a watcher - Watch, - /// Clean the build artifacts - Clean, -} - -/// Rewatch is an alternative build system for the Rescript Compiler bsb (which uses Ninja internally). It strives -/// to deliver consistent and faster builds in monorepo setups with multiple packages, where the -/// default build system fails to pick up changed interfaces across multiple packages. -#[derive(Parser, Debug)] -#[command(version)] -struct Args { - #[arg(value_enum)] - command: Option, - - /// The relative path to where the main rescript.json resides. IE - the root of your project. - folder: Option, - - /// Filter allows for a regex to be supplied which will filter the files to be compiled. For - /// instance, to filter out test files for compilation while doing feature work. - #[arg(short, long)] - filter: Option, - - /// This allows one to pass an additional command to the watcher, which allows it to run when - /// finished. For instance, to play a sound when done compiling, or to run a test suite. - /// NOTE - You may need to add '--color=always' to your subcommand in case you want to output - /// colour as well - #[arg(short, long)] - after_build: Option, - - // Disable timing on the output - #[arg(short, long, default_value = "false", num_args = 0..=1)] - no_timing: bool, - - /// Verbosity: - /// -v -> Debug - /// -vv -> Trace - /// -q -> Warn - /// -qq -> Error - /// -qqq -> Off. - /// Default (/ no argument given): 'info' - #[command(flatten)] - verbose: clap_verbosity_flag::Verbosity, - - /// This creates a source_dirs.json file at the root of the monorepo, which is needed when you - /// want to use Reanalyze - #[arg(short, long, default_value_t = false, num_args = 0..=1)] - create_sourcedirs: bool, - - /// This prints the compiler arguments. It expects the path to a rescript.json file. - /// This also requires --bsc-path and --rescript-version to be present - #[arg(long)] - compiler_args: Option, - - /// This is the flag to also compile development dependencies - /// It's important to know that we currently do not discern between project src, and - /// dependencies. So enabling this flag will enable building _all_ development dependencies of - /// _all_ packages - #[arg(long, default_value_t = false, num_args = 0..=1)] - dev: bool, - - /// To be used in conjunction with compiler_args - #[arg(long)] - rescript_version: Option, - - /// A custom path to bsc - #[arg(long)] - bsc_path: Option, -} +use rewatch::{build, cli, cmd, lock, watcher}; fn main() -> Result<()> { - let args = Args::parse(); + let args = cli::Cli::parse(); + let log_level_filter = args.verbose.log_level_filter(); env_logger::Builder::new() @@ -91,69 +17,100 @@ fn main() -> Result<()> { .target(env_logger::fmt::Target::Stdout) .init(); - let command = args.command.unwrap_or(Command::Build); - let folder = args.folder.unwrap_or(".".to_string()); - let filter = args - .filter - .map(|filter| Regex::new(filter.as_ref()).expect("Could not parse regex")); + let command = args.command.unwrap_or(cli::Command::Build(args.build_args)); - match args.compiler_args { - None => (), - Some(path) => { + // handle those commands early, because we don't need a lock for them + match &command { + cli::Command::Legacy { legacy_args } => { + let code = build::pass_through_legacy(legacy_args); + std::process::exit(code); + } + cli::Command::Format { format_args } => { + let mut args: Vec = vec!["format".into()]; + args.append(&mut format_args.clone()); + let code = build::pass_through_legacy(args.as_slice()); + std::process::exit(code); + } + cli::Command::Dump { dump_args } => { + let mut args: Vec = vec!["dump".into()]; + args.append(&mut dump_args.clone()); + let code = build::pass_through_legacy(args.as_slice()); + std::process::exit(code); + } + cli::Command::CompilerArgs { + path, + dev, + rescript_version, + bsc_path, + } => { println!( "{}", - build::get_compiler_args(&path, args.rescript_version, args.bsc_path, args.dev)? + build::get_compiler_args(&path, rescript_version.clone(), bsc_path.clone(), *dev)? ); std::process::exit(0); } + _ => (), } // The 'normal run' mode will show the 'pretty' formatted progress. But if we turn off the log // level, we should never show that. let show_progress = log_level_filter == LevelFilter::Info; - match lock::get(&folder) { + match lock::get(&args.folder) { lock::Lock::Error(ref e) => { println!("Could not start Rewatch: {e}"); std::process::exit(1) } lock::Lock::Aquired(_) => match command { - Command::Clean => build::clean::clean(&folder, show_progress, args.bsc_path, args.dev), - Command::Build => { + cli::Command::Clean { bsc_path } => build::clean::clean(&args.folder, show_progress, bsc_path), + cli::Command::Build(build_args) => { + let filter = build_args + .filter + .map(|filter| Regex::new(filter.as_ref()).expect("Could not parse regex")); + match build::build( &filter, - &folder, + &args.folder, show_progress, - args.no_timing, - args.create_sourcedirs, - args.bsc_path, - args.dev, + build_args.no_timing, + build_args.create_sourcedirs, + build_args.bsc_path, + build_args.dev, ) { Err(e) => { println!("{e}"); std::process::exit(1) } Ok(_) => { - if let Some(args_after_build) = args.after_build { + if let Some(args_after_build) = build_args.after_build { cmd::run(args_after_build) } std::process::exit(0) } }; } - Command::Watch => { + cli::Command::Watch(watch_args) => { + let filter = watch_args + .filter + .map(|filter| Regex::new(filter.as_ref()).expect("Could not parse regex")); watcher::start( &filter, show_progress, - &folder, - args.after_build, - args.create_sourcedirs, - args.dev, - args.bsc_path, + &args.folder, + watch_args.after_build, + watch_args.create_sourcedirs, + watch_args.dev, + watch_args.bsc_path, ); Ok(()) } + cli::Command::CompilerArgs { .. } + | cli::Command::Legacy { .. } + | cli::Command::Format { .. } + | cli::Command::Dump { .. } => { + unreachable!("command already handled") + } }, } } diff --git a/tests/compile.sh b/tests/compile.sh index dd39bf5..447bc9c 100755 --- a/tests/compile.sh +++ b/tests/compile.sh @@ -33,36 +33,36 @@ fi node ./packages/main/src/Main.mjs > ./packages/main/src/output.txt mv ./packages/main/src/Main.res ./packages/main/src/Main2.res -rewatch build &> ../tests/snapshots/rename-file.txt +rewatch build --no-timing &> ../tests/snapshots/rename-file.txt mv ./packages/main/src/Main2.res ./packages/main/src/Main.res # Rename a file with a dependent - this should trigger an error mv ./packages/main/src/InternalDep.res ./packages/main/src/InternalDep2.res -rewatch build &> ../tests/snapshots/rename-file-internal-dep.txt +rewatch build --no-timing &> ../tests/snapshots/rename-file-internal-dep.txt # replace the absolute path so the snapshot is the same on all machines replace "s/$(pwd | sed "s/\//\\\\\//g")//g" ../tests/snapshots/rename-file-internal-dep.txt mv ./packages/main/src/InternalDep2.res ./packages/main/src/InternalDep.res # Rename a file with a dependent in a namespaced package - this should trigger an error (regression) mv ./packages/new-namespace/src/Other_module.res ./packages/new-namespace/src/Other_module2.res -rewatch build &> ../tests/snapshots/rename-file-internal-dep-namespace.txt +rewatch build --no-timing &> ../tests/snapshots/rename-file-internal-dep-namespace.txt # replace the absolute path so the snapshot is the same on all machines replace "s/$(pwd | sed "s/\//\\\\\//g")//g" ../tests/snapshots/rename-file-internal-dep-namespace.txt mv ./packages/new-namespace/src/Other_module2.res ./packages/new-namespace/src/Other_module.res rewatch build &> /dev/null mv ./packages/main/src/ModuleWithInterface.resi ./packages/main/src/ModuleWithInterface2.resi -rewatch build &> ../tests/snapshots/rename-interface-file.txt +rewatch build --no-timing &> ../tests/snapshots/rename-interface-file.txt mv ./packages/main/src/ModuleWithInterface2.resi ./packages/main/src/ModuleWithInterface.resi rewatch build &> /dev/null mv ./packages/main/src/ModuleWithInterface.res ./packages/main/src/ModuleWithInterface2.res -rewatch build &> ../tests/snapshots/rename-file-with-interface.txt +rewatch build --no-timing &> ../tests/snapshots/rename-file-with-interface.txt mv ./packages/main/src/ModuleWithInterface2.res ./packages/main/src/ModuleWithInterface.res rewatch build &> /dev/null # when deleting a file that other files depend on, the compile should fail rm packages/dep02/src/Dep02.res -rewatch build &> ../tests/snapshots/remove-file.txt +rewatch build --no-timing &> ../tests/snapshots/remove-file.txt # replace the absolute path so the snapshot is the same on all machines replace "s/$(pwd | sed "s/\//\\\\\//g")//g" ../tests/snapshots/remove-file.txt git checkout -- packages/dep02/src/Dep02.res @@ -70,7 +70,7 @@ rewatch build &> /dev/null # it should show an error when we have a dependency cycle echo 'Dep01.log()' >> packages/new-namespace/src/NS_alias.res -rewatch build &> ../tests/snapshots/dependency-cycle.txt +rewatch build --no-timing &> ../tests/snapshots/dependency-cycle.txt git checkout -- packages/new-namespace/src/NS_alias.res # it should compile dev dependencies with the --dev flag @@ -91,7 +91,7 @@ else exit 1 fi -rewatch clean --dev &> /dev/null +rewatch clean &> /dev/null file_count=$(find ./packages/with-dev-deps -name *.mjs | wc -l) if [ "$file_count" -eq 0 ]; then diff --git a/tests/utils.sh b/tests/utils.sh index f2ee211..8ddca1b 100644 --- a/tests/utils.sh +++ b/tests/utils.sh @@ -3,8 +3,8 @@ overwrite() { echo -e "\r\033[1A\033[0K$@"; } success() { echo -e "- ✅ \033[32m$1\033[0m"; } error() { echo -e "- 🛑 \033[31m$1\033[0m"; } bold() { echo -e "\033[1m$1\033[0m"; } -rewatch() { RUST_BACKTRACE=1 $REWATCH_EXECUTABLE --no-timing=true $@; } -rewatch_bg() { RUST_BACKTRACE=1 nohup $REWATCH_EXECUTABLE --no-timing=true $@; } +rewatch() { RUST_BACKTRACE=1 $REWATCH_EXECUTABLE $@; } +rewatch_bg() { RUST_BACKTRACE=1 nohup $REWATCH_EXECUTABLE $@; } replace() { if [[ $OSTYPE == 'darwin'* ]];