Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

#### :nail_care: Polish

- Rewatch cli: do not show build command options in the root help. https://github.com/rescript-lang/rescript/pull/7715

#### :house: Internal

# 12.0.0-beta.13
Expand Down
11 changes: 5 additions & 6 deletions rewatch/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ pub enum FileExtension {
/// ReScript - Fast, Simple, Fully Typed JavaScript from the Future
#[derive(Parser, Debug)]
#[command(version)]
#[command(args_conflicts_with_subcommands = true)]
#[command(
after_help = "Note: If no command is provided, the build command is run by default. See `rescript help build` for more information."
)]
pub struct Cli {
/// Verbosity:
/// -v -> Debug
Expand All @@ -35,10 +37,7 @@ pub struct Cli {

/// The command to run. If not provided it will default to build.
#[command(subcommand)]
pub command: Option<Command>,

#[command(flatten)]
pub build_args: BuildArgs,
pub command: Command,
}

#[derive(Args, Debug, Clone)]
Expand Down Expand Up @@ -181,7 +180,7 @@ impl From<BuildArgs> for WatchArgs {

#[derive(Subcommand, Clone, Debug)]
pub enum Command {
/// Build the project
/// Build the project (default command)
Build(BuildArgs),
/// Build, then start a watcher
Watch(WatchArgs),
Expand Down
120 changes: 115 additions & 5 deletions rewatch/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
use anyhow::Result;
use clap::Parser;
use clap::{Parser, error::ErrorKind};
use log::LevelFilter;
use std::{io::Write, path::Path};
use std::{env, io::Write, path::Path};

use rescript::{build, cli, cmd, format, lock, watcher};

fn main() -> Result<()> {
let args = cli::Cli::parse();
let raw_args: Vec<String> = env::args().collect();
let cli = parse_cli(raw_args).unwrap_or_else(|err| err.exit());

let log_level_filter = args.verbose.log_level_filter();
let log_level_filter = cli.verbose.log_level_filter();

env_logger::Builder::new()
.format(|buf, record| writeln!(buf, "{}:\n{}", record.level(), record.args()))
.filter_level(log_level_filter)
.target(env_logger::fmt::Target::Stdout)
.init();

let mut command = args.command.unwrap_or(cli::Command::Build(args.build_args));
let mut command = cli.command;

if let cli::Command::Build(build_args) = &command {
if build_args.watch {
Expand Down Expand Up @@ -111,3 +112,112 @@ fn get_lock(folder: &str) -> lock::Lock {
acquired_lock => acquired_lock,
}
}

fn parse_cli(raw_args: Vec<String>) -> Result<cli::Cli, clap::Error> {
match cli::Cli::try_parse_from(&raw_args) {
Ok(cli) => Ok(cli),
Err(err) => {
if should_default_to_build(&err, &raw_args) {
let mut fallback_args = raw_args.clone();
let insert_at = index_after_global_flags(&fallback_args);
fallback_args.insert(insert_at, "build".into());

match cli::Cli::try_parse_from(&fallback_args) {
Ok(cli) => Ok(cli),
Err(fallback_err) => Err(fallback_err),
}
} else {
Err(err)
}
}
}
}

fn should_default_to_build(err: &clap::Error, args: &[String]) -> bool {
match err.kind() {
ErrorKind::MissingSubcommand | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => true,
ErrorKind::UnknownArgument | ErrorKind::InvalidSubcommand => {
args.iter().skip(1).any(|arg| !is_global_flag(arg))
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Avoid defaulting to build when a subcommand was supplied

The fallback parsing in should_default_to_build treats any UnknownArgument or InvalidSubcommand as an omitted subcommand as long as a non‑global token exists. When a user explicitly runs another subcommand but mistypes an option that only build understands, the second parse injects build and succeeds. For example, rescript watch --no-timing (an invalid option for watch) is reinterpreted as rescript build watch --no-timing, so the tool performs a one‑off build of the watch folder and exits instead of starting the watcher. That silent command switch regresses error reporting and will surprise users trying to run watch/clean/format. The fallback should only fire when no subcommand token is present, not when an explicit subcommand failed to parse.

Useful? React with 👍 / 👎.

_ => false,
}
}

fn index_after_global_flags(args: &[String]) -> usize {
let mut idx = 1;
while let Some(arg) = args.get(idx) {
if is_global_flag(arg) {
idx += 1;
} else {
break;
}
}
idx.min(args.len())
}

fn is_global_flag(arg: &str) -> bool {
matches!(
arg,
"-v" | "-vv"
| "-vvv"
| "-vvvv"
| "-q"
| "-qq"
| "-qqq"
| "-qqqq"
| "--verbose"
| "--quiet"
| "-h"
| "--help"
| "-V"
| "--version"
)
}

#[cfg(test)]
mod tests {
use super::*;

fn parse(args: &[&str]) -> Result<cli::Cli, clap::Error> {
parse_cli(args.iter().map(|arg| arg.to_string()).collect())
}

#[test]
fn defaults_to_build_without_args() {
let cli = parse(&["rescript"]).expect("expected default build command");

match cli.command {
cli::Command::Build(build_args) => assert_eq!(build_args.folder.folder, "."),
other => panic!("expected build command, got {other:?}"),
}
}

#[test]
fn defaults_to_build_with_folder_shortcut() {
let cli = parse(&["rescript", "someFolder"]).expect("expected build command");

match cli.command {
cli::Command::Build(build_args) => assert_eq!(build_args.folder.folder, "someFolder"),
other => panic!("expected build command, got {other:?}"),
}
}

#[test]
fn respects_global_flag_before_subcommand() {
let cli = parse(&["rescript", "-v", "watch"]).expect("expected watch command");

assert!(matches!(cli.command, cli::Command::Watch(_)));
}

#[test]
fn help_flag_does_not_default_to_build() {
let err = parse(&["rescript", "--help"]).expect_err("expected clap help error");
assert_eq!(err.kind(), ErrorKind::DisplayHelp);
}

#[test]
fn version_flag_does_not_default_to_build() {
let err = parse(&["rescript", "--version"]).expect_err("expected clap version error");
assert_eq!(err.kind(), ErrorKind::DisplayVersion);
}
}
Loading