diff --git a/.clippy.toml b/.clippy.toml index fd0b1c45c2..40215aa466 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -12,6 +12,7 @@ disallowed-methods = [ { path = "str::replace", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_replace` instead." }, { path = "str::replacen", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_replacen` instead." }, { path = "std::env::current_dir", reason = "To get an `AbsolutePathBuf`, Use `vite_path::current_dir` instead." }, + { path = "std::io::IsTerminal::is_terminal", reason = "Use `vite_powershell::is_stdin_terminal` for stdin, or `vite_shared::is_stdout_terminal` / `vite_shared::is_stderr_terminal`, which memoize the lookup." }, ] disallowed-types = [ diff --git a/Cargo.lock b/Cargo.lock index e142086f4f..97cf67d74c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2337,7 +2337,7 @@ dependencies = [ [[package]] name = "fspy" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "anyhow", "bstr", @@ -2372,7 +2372,7 @@ dependencies = [ [[package]] name = "fspy_detours_sys" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "cc", "winapi", @@ -2381,7 +2381,7 @@ dependencies = [ [[package]] name = "fspy_preload_unix" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "anyhow", "bstr", @@ -2396,7 +2396,7 @@ dependencies = [ [[package]] name = "fspy_preload_windows" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "constcat", "fspy_detours_sys", @@ -2412,7 +2412,7 @@ dependencies = [ [[package]] name = "fspy_seccomp_unotify" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "futures-util", "libc", @@ -2429,7 +2429,7 @@ dependencies = [ [[package]] name = "fspy_shared" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "bitflags 2.11.0", "bstr", @@ -2448,7 +2448,7 @@ dependencies = [ [[package]] name = "fspy_shared_unix" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "anyhow", "base64 0.22.1", @@ -3611,7 +3611,7 @@ dependencies = [ [[package]] name = "materialized_artifact" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "tempfile", ] @@ -3619,7 +3619,7 @@ dependencies = [ [[package]] name = "materialized_artifact_build" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "xxhash-rust", ] @@ -3843,7 +3843,7 @@ dependencies = [ [[package]] name = "native_str" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "bumpalo", "bytemuck", @@ -5596,7 +5596,7 @@ dependencies = [ [[package]] name = "pty_terminal" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "anyhow", "portable-pty", @@ -5606,7 +5606,7 @@ dependencies = [ [[package]] name = "pty_terminal_test" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "anyhow", "portable-pty", @@ -5617,7 +5617,7 @@ dependencies = [ [[package]] name = "pty_terminal_test_client" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" [[package]] name = "quote" @@ -7512,7 +7512,7 @@ dependencies = [ [[package]] name = "snapshot_test" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "serde", "serde_json", @@ -8500,6 +8500,7 @@ dependencies = [ "owo-colors", "petgraph 0.8.3", "pretty_assertions", + "pty_terminal_test_client", "rolldown_binding", "rustc-hash", "serde", @@ -8512,6 +8513,7 @@ dependencies = [ "vite_migration", "vite_path", "vite_pm_cli", + "vite_select", "vite_shared", "vite_static_config", "vite_str", @@ -8584,7 +8586,7 @@ dependencies = [ [[package]] name = "vite_glob" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "globset", "thiserror 2.0.18", @@ -8632,7 +8634,7 @@ dependencies = [ [[package]] name = "vite_graph_ser" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "petgraph 0.8.3", "serde", @@ -8732,7 +8734,7 @@ dependencies = [ [[package]] name = "vite_path" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "diff-struct", "os_str_bytes", @@ -8764,7 +8766,7 @@ dependencies = [ [[package]] name = "vite_powershell" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "vite_path", "which", @@ -8773,7 +8775,7 @@ dependencies = [ [[package]] name = "vite_select" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "anyhow", "crossterm", @@ -8820,13 +8822,14 @@ dependencies = [ "tracing", "tracing-subscriber", "vite_path", + "vite_powershell", "vite_str", ] [[package]] name = "vite_shell" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "brush-parser 0.4.0", "diff-struct", @@ -8853,7 +8856,7 @@ dependencies = [ [[package]] name = "vite_str" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "compact_str", "diff-struct", @@ -8864,7 +8867,7 @@ dependencies = [ [[package]] name = "vite_task" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "anstream", "anyhow", @@ -8913,7 +8916,7 @@ dependencies = [ [[package]] name = "vite_task_client" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "native_str", "rustc-hash", @@ -8926,7 +8929,7 @@ dependencies = [ [[package]] name = "vite_task_client_napi" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "napi", "napi-build", @@ -8938,7 +8941,7 @@ dependencies = [ [[package]] name = "vite_task_graph" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "anyhow", "async-trait", @@ -8960,7 +8963,7 @@ dependencies = [ [[package]] name = "vite_task_ipc_shared" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "native_str", "rustc-hash", @@ -8970,7 +8973,7 @@ dependencies = [ [[package]] name = "vite_task_plan" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "anyhow", "async-trait", @@ -8998,7 +9001,7 @@ dependencies = [ [[package]] name = "vite_task_server" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "futures", "native_str", @@ -9022,7 +9025,7 @@ version = "0.0.0" [[package]] name = "vite_workspace" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=6cfa6e47a1a5fdadd215e1556766cc753603a539#6cfa6e47a1a5fdadd215e1556766cc753603a539" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=a301aff2705ab7c663cc847dcc7c42f178f1fb68#a301aff2705ab7c663cc847dcc7c42f178f1fb68" dependencies = [ "clap", "petgraph 0.8.3", diff --git a/Cargo.toml b/Cargo.toml index a8f065cb6a..788c407cf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -194,7 +194,7 @@ dunce = "1.0.5" fast-glob = "1.0.0" flate2 = { version = "=1.1.9", features = ["zlib-rs"] } form_urlencoded = "1.2.1" -fspy = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "6cfa6e47a1a5fdadd215e1556766cc753603a539" } +fspy = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "a301aff2705ab7c663cc847dcc7c42f178f1fb68" } futures = "0.3.31" futures-util = "0.3.31" glob = "0.3.2" @@ -251,8 +251,8 @@ pretty_assertions = "1.4.1" phf = "0.13.0" prettyplease = "0.2.32" proc-macro2 = "1" -pty_terminal_test = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "6cfa6e47a1a5fdadd215e1556766cc753603a539" } -pty_terminal_test_client = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "6cfa6e47a1a5fdadd215e1556766cc753603a539" } +pty_terminal_test = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "a301aff2705ab7c663cc847dcc7c42f178f1fb68" } +pty_terminal_test_client = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "a301aff2705ab7c663cc847dcc7c42f178f1fb68" } quote = "1" rayon = "1.10.0" regex = "1.11.1" @@ -276,7 +276,7 @@ sha2 = "0.10.9" shell-escape = "0.1.5" simdutf8 = "0.1.5" smallvec = { version = "1.15.1", features = ["union"] } -snapshot_test = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "6cfa6e47a1a5fdadd215e1556766cc753603a539" } +snapshot_test = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "a301aff2705ab7c663cc847dcc7c42f178f1fb68" } string_cache = "0.9.0" sugar_path = { version = "2.0.1", features = ["cached_current_dir"] } supports-color = "3" @@ -313,11 +313,12 @@ vite_pm_cli = { path = "crates/vite_pm_cli" } vite_setup = { path = "crates/vite_setup" } vite_shared = { path = "crates/vite_shared" } vite_static_config = { path = "crates/vite_static_config" } -vite_path = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "6cfa6e47a1a5fdadd215e1556766cc753603a539" } -vite_powershell = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "6cfa6e47a1a5fdadd215e1556766cc753603a539" } -vite_str = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "6cfa6e47a1a5fdadd215e1556766cc753603a539" } -vite_task = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "6cfa6e47a1a5fdadd215e1556766cc753603a539" } -vite_workspace = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "6cfa6e47a1a5fdadd215e1556766cc753603a539" } +vite_path = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "a301aff2705ab7c663cc847dcc7c42f178f1fb68" } +vite_powershell = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "a301aff2705ab7c663cc847dcc7c42f178f1fb68" } +vite_select = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "a301aff2705ab7c663cc847dcc7c42f178f1fb68" } +vite_str = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "a301aff2705ab7c663cc847dcc7c42f178f1fb68" } +vite_task = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "a301aff2705ab7c663cc847dcc7c42f178f1fb68" } +vite_workspace = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "a301aff2705ab7c663cc847dcc7c42f178f1fb68" } walkdir = "2.5.0" wax = "0.6.0" which = "8.0.0" diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/apps/web/index.html b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/apps/web/index.html new file mode 100644 index 0000000000..ce6d6280c2 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/apps/web/index.html @@ -0,0 +1,6 @@ + + + +

web

+ + diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/apps/web/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/apps/web/package.json new file mode 100644 index 0000000000..64630869c1 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/apps/web/package.json @@ -0,0 +1 @@ +{ "name": "web", "private": true } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/package.json new file mode 100644 index 0000000000..05c1c2ef02 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/package.json @@ -0,0 +1 @@ +{ "name": "app-root-auto-select", "private": true, "workspaces": ["apps/*", "packages/*"] } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/packages/ui/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/packages/ui/package.json new file mode 100644 index 0000000000..70bb6aab17 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/packages/ui/package.json @@ -0,0 +1 @@ +{ "name": "ui", "private": true, "type": "module", "main": "src/index.ts" } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/packages/ui/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/packages/ui/src/index.ts new file mode 100644 index 0000000000..905c7a9bbb --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/packages/ui/src/index.ts @@ -0,0 +1,3 @@ +export function hello(name: string): string { + return `hello ${name}`; +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots.toml new file mode 100644 index 0000000000..0d74f9f251 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots.toml @@ -0,0 +1,10 @@ +[[case]] +name = "auto_select" +vp = ["local", "global"] +comment = """ +With exactly one likely-runnable package, a bare app command in an interactive +terminal auto-selects it, prints the Selected/Tip teaching lines, and runs +there (rfcs/cwd-flag.md). This TTY-only branch was untestable in the old +harness. +""" +steps = [["vp", "build"]] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots/auto_select.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots/auto_select.global.md new file mode 100644 index 0000000000..46cc92d7a2 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots/auto_select.global.md @@ -0,0 +1,21 @@ +# auto_select + +With exactly one likely-runnable package, a bare app command in an interactive +terminal auto-selects it, prints the Selected/Tip teaching lines, and runs +there (rfcs/cwd-flag.md). This TTY-only branch was untestable in the old +harness. + +## `vp build` + +``` +VITE+ - The Unified Toolchain for the Web + +Selected package: web (apps/web) +Tip: run this directly with `vp -C apps/web build` +vite building client environment for production... +✓ 2 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB + +✓ built in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots/auto_select.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots/auto_select.local.md new file mode 100644 index 0000000000..ceca69331c --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots/auto_select.local.md @@ -0,0 +1,19 @@ +# auto_select + +With exactly one likely-runnable package, a bare app command in an interactive +terminal auto-selects it, prints the Selected/Tip teaching lines, and runs +there (rfcs/cwd-flag.md). This TTY-only branch was untestable in the old +harness. + +## `vp build` + +``` +Selected package: web (apps/web) +Tip: run this directly with `vp -C apps/web build` +vite building client environment for production... +✓ 2 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB + +✓ built in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/missing/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/missing/package.json new file mode 100644 index 0000000000..f6a8d9de96 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/missing/package.json @@ -0,0 +1 @@ +{ "name": "missing-default", "private": true } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/missing/vite.config.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/missing/vite.config.ts new file mode 100644 index 0000000000..8204c22024 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/missing/vite.config.ts @@ -0,0 +1,3 @@ +export default { + defaultPackage: './packages/nope', +}; diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/package.json new file mode 100644 index 0000000000..b9fabc032d --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/package.json @@ -0,0 +1 @@ +{ "name": "app-root-default-package", "private": true } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/packages/ui/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/packages/ui/package.json new file mode 100644 index 0000000000..70bb6aab17 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/packages/ui/package.json @@ -0,0 +1 @@ +{ "name": "ui", "private": true, "type": "module", "main": "src/index.ts" } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/packages/ui/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/packages/ui/src/index.ts new file mode 100644 index 0000000000..905c7a9bbb --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/packages/ui/src/index.ts @@ -0,0 +1,3 @@ +export function hello(name: string): string { + return `hello ${name}`; +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots.toml new file mode 100644 index 0000000000..e85cd246d9 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots.toml @@ -0,0 +1,31 @@ +[[case]] +name = "default_package" +vp = ["local", "global"] +comment = """ +defaultPackage in the root config acts as an implicit -C for bare app +commands, including at a root that is not a JS workspace; vp prints a note +line and runs in the configured directory (rfcs/cwd-flag.md). +""" +steps = [ + ["vp", "pack"], + { argv = ["vpt", "list-dir", "packages/ui/dist"], comment = "output lands in the configured package" }, +] + +[[case]] +name = "default_package_missing" +vp = ["local", "global"] +comment = "defaultPackage pointing at a missing directory errors before any workspace lookup." +steps = [{ argv = ["vp", "build"], cwd = "missing" }] + +[[case]] +name = "unanalyzable_config_ignored" +vp = ["local", "global"] +comment = """ +Regression guard for spread/unanalyzable configs: a config that only parses +as an open map might hide defaultPackage behind the spread, but that must +not fail the command. Bare vp build falls through and runs in place. +""" +# PTY-rendered (not tty = false): non-TTY steps record raw bytes, and the +# raw Windows byte stream of a vite build differs invisibly from macOS; the +# grid rendering normalizes that and is what the assertion needs anyway. +steps = [{ argv = ["vp", "build"], cwd = "spread" }] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package.global.md new file mode 100644 index 0000000000..3e187a1d9b --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package.global.md @@ -0,0 +1,26 @@ +# default_package + +defaultPackage in the root config acts as an implicit -C for bare app +commands, including at a root that is not a JS workspace; vp prints a note +line and runs in the configured directory (rfcs/cwd-flag.md). + +## `vp pack` + +``` +VITE+ - The Unified Toolchain for the Web + +note: vp pack: using ./packages/ui (defaultPackage) +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` + +## `vpt list-dir packages/ui/dist` + +output lands in the configured package + +``` +index.mjs +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package.local.md new file mode 100644 index 0000000000..622f391b6c --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package.local.md @@ -0,0 +1,24 @@ +# default_package + +defaultPackage in the root config acts as an implicit -C for bare app +commands, including at a root that is not a JS workspace; vp prints a note +line and runs in the configured directory (rfcs/cwd-flag.md). + +## `vp pack` + +``` +note: vp pack: using ./packages/ui (defaultPackage) +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` + +## `vpt list-dir packages/ui/dist` + +output lands in the configured package + +``` +index.mjs +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_missing.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_missing.global.md new file mode 100644 index 0000000000..b05221c778 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_missing.global.md @@ -0,0 +1,13 @@ +# default_package_missing + +defaultPackage pointing at a missing directory errors before any workspace lookup. + +## `cd missing && vp build` + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: defaultPackage points to a missing directory: ./packages/nope +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_missing.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_missing.local.md new file mode 100644 index 0000000000..768869fad9 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_missing.local.md @@ -0,0 +1,11 @@ +# default_package_missing + +defaultPackage pointing at a missing directory errors before any workspace lookup. + +## `cd missing && vp build` + +**Exit code:** 1 + +``` +error: defaultPackage points to a missing directory: ./packages/nope +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/unanalyzable_config_ignored.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/unanalyzable_config_ignored.global.md new file mode 100644 index 0000000000..ea92615f0c --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/unanalyzable_config_ignored.global.md @@ -0,0 +1,18 @@ +# unanalyzable_config_ignored + +Regression guard for spread/unanalyzable configs: a config that only parses +as an open map might hide defaultPackage behind the spread, but that must +not fail the command. Bare vp build falls through and runs in place. + +## `cd spread && vp build` + +``` +VITE+ - The Unified Toolchain for the Web + +vite building client environment for production... +✓ 2 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB + +✓ built in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/unanalyzable_config_ignored.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/unanalyzable_config_ignored.local.md new file mode 100644 index 0000000000..1446589b7d --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/unanalyzable_config_ignored.local.md @@ -0,0 +1,16 @@ +# unanalyzable_config_ignored + +Regression guard for spread/unanalyzable configs: a config that only parses +as an open map might hide defaultPackage behind the spread, but that must +not fail the command. Bare vp build falls through and runs in place. + +## `cd spread && vp build` + +``` +vite building client environment for production... +✓ 2 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB + +✓ built in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/spread/index.html b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/spread/index.html new file mode 100644 index 0000000000..32b40097ee --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/spread/index.html @@ -0,0 +1,6 @@ + + + +

spread

+ + diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/spread/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/spread/package.json new file mode 100644 index 0000000000..eba4d545fe --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/spread/package.json @@ -0,0 +1 @@ +{ "name": "spread-config", "private": true } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/spread/vite.config.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/spread/vite.config.ts new file mode 100644 index 0000000000..aaf0089da5 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/spread/vite.config.ts @@ -0,0 +1,5 @@ +const base = {}; + +export default { + ...base, +}; diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/vite.config.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/vite.config.ts new file mode 100644 index 0000000000..36dab4eb9e --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/vite.config.ts @@ -0,0 +1,3 @@ +export default { + defaultPackage: './packages/ui', +}; diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/admin/index.html b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/admin/index.html new file mode 100644 index 0000000000..9a194fe836 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/admin/index.html @@ -0,0 +1,6 @@ + + + +

admin

+ + diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/admin/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/admin/package.json new file mode 100644 index 0000000000..bebad673a4 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/admin/package.json @@ -0,0 +1 @@ +{ "name": "admin", "private": true } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/web/index.html b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/web/index.html new file mode 100644 index 0000000000..ce6d6280c2 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/web/index.html @@ -0,0 +1,6 @@ + + + +

web

+ + diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/web/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/web/package.json new file mode 100644 index 0000000000..64630869c1 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/web/package.json @@ -0,0 +1 @@ +{ "name": "web", "private": true } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/package.json new file mode 100644 index 0000000000..b2d7158d1d --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/package.json @@ -0,0 +1,11 @@ +{ + "name": "app-root-listing", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "build": "vp build" + } +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/packages/ui/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/packages/ui/package.json new file mode 100644 index 0000000000..70bb6aab17 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/packages/ui/package.json @@ -0,0 +1 @@ +{ "name": "ui", "private": true, "type": "module", "main": "src/index.ts" } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/packages/ui/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/packages/ui/src/index.ts new file mode 100644 index 0000000000..905c7a9bbb --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/packages/ui/src/index.ts @@ -0,0 +1,3 @@ +export function hello(name: string): string { + return `hello ${name}`; +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots.toml new file mode 100644 index 0000000000..953b61ac07 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots.toml @@ -0,0 +1,50 @@ +[[case]] +name = "listing" +vp = ["local", "global"] +comment = """ +A bare app command at a workspace root without an interactive terminal prints +the ranked package listing with -C hints and exits 1 instead of building the +root (rfcs/cwd-flag.md). +""" +steps = [ + { argv = ["vp", "build"], tty = false, continue-on-failure = true }, + { argv = ["vp", "dev"], tty = false, comment = "dev at the root no longer starts a server against the root" }, +] + +[[case]] +name = "picker_select" +vp = ["local", "global"] +comment = """ +A bare app command at a workspace root with several candidates opens the +fuzzy package picker (the vp run selector component); typing filters, Enter +runs the selection as an implicit -C (rfcs/cwd-flag.md). +""" +steps = [ + { argv = ["vp", "build"], interactions = [ + { "expect-milestone" = "package-select::0" }, + { "write" = "web" }, + { "expect-milestone" = "package-select:web:0" }, + { "write-key" = "enter" }, + ] }, +] + +[[case]] +name = "picker_cancel" +vp = ["local", "global"] +comment = "Ctrl+C in the package picker cancels with exit 130 and runs nothing." +steps = [ + { argv = ["vp", "build"], interactions = [ + { "expect-milestone" = "package-select::0" }, + { "write-key" = "ctrl-c" }, + ] }, +] + +[[case]] +name = "script_at_root_elicits" +vp = ["local", "global"] +comment = """ +A root package script "build": "vp build" run through vp run gets the same +target elicitation as a direct bare invocation: the spawned vp prints the +listing and the task fails instead of silently building the root. +""" +steps = [{ argv = ["vp", "run", "build"], tty = false }] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/listing.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/listing.global.md new file mode 100644 index 0000000000..daf3c000d6 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/listing.global.md @@ -0,0 +1,39 @@ +# listing + +A bare app command at a workspace root without an interactive terminal prints +the ranked package listing with -C hints and exits 1 instead of building the +root (rfcs/cwd-flag.md). + +## `vp build` + +**Exit code:** 1 + +``` +error: `vp build` at the workspace root needs a target package. + + Packages in this workspace: + admin apps/admin + web apps/web + ui packages/ui + + Pass a directory: vp -C apps/admin build + Or run every package's build script: vp run -r build +``` + +## `vp dev` + +dev at the root no longer starts a server against the root + +**Exit code:** 1 + +``` +error: `vp dev` at the workspace root needs a target package. + + Packages in this workspace: + admin apps/admin + web apps/web + ui packages/ui + + Pass a directory: vp -C apps/admin dev + Or run every package's dev script: vp run -r dev +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/listing.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/listing.local.md new file mode 100644 index 0000000000..daf3c000d6 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/listing.local.md @@ -0,0 +1,39 @@ +# listing + +A bare app command at a workspace root without an interactive terminal prints +the ranked package listing with -C hints and exits 1 instead of building the +root (rfcs/cwd-flag.md). + +## `vp build` + +**Exit code:** 1 + +``` +error: `vp build` at the workspace root needs a target package. + + Packages in this workspace: + admin apps/admin + web apps/web + ui packages/ui + + Pass a directory: vp -C apps/admin build + Or run every package's build script: vp run -r build +``` + +## `vp dev` + +dev at the root no longer starts a server against the root + +**Exit code:** 1 + +``` +error: `vp dev` at the workspace root needs a target package. + + Packages in this workspace: + admin apps/admin + web apps/web + ui packages/ui + + Pass a directory: vp -C apps/admin dev + Or run every package's dev script: vp run -r dev +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/picker_cancel.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/picker_cancel.global.md new file mode 100644 index 0000000000..1731cd3bcf --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/picker_cancel.global.md @@ -0,0 +1,25 @@ +# picker_cancel + +Ctrl+C in the package picker cancels with exit 130 and runs nothing. + +## `vp build` + +**Exit code:** 130 + +**→ expect-milestone:** `package-select::0` + +``` +VITE+ - The Unified Toolchain for the Web + +Select a package to build (↑/↓, Enter to run, type to search): + + › admin apps/admin + web apps/web + ui packages/ui +``` + +**← write-key:** `ctrl-c` + +``` +VITE+ - The Unified Toolchain for the Web +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/picker_cancel.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/picker_cancel.local.md new file mode 100644 index 0000000000..0855d0a694 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/picker_cancel.local.md @@ -0,0 +1,22 @@ +# picker_cancel + +Ctrl+C in the package picker cancels with exit 130 and runs nothing. + +## `vp build` + +**Exit code:** 130 + +**→ expect-milestone:** `package-select::0` + +``` +Select a package to build (↑/↓, Enter to run, type to search): + + › admin apps/admin + web apps/web + ui packages/ui +``` + +**← write-key:** `ctrl-c` + +``` +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/picker_select.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/picker_select.global.md new file mode 100644 index 0000000000..a5f8c1fe5e --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/picker_select.global.md @@ -0,0 +1,46 @@ +# picker_select + +A bare app command at a workspace root with several candidates opens the +fuzzy package picker (the vp run selector component); typing filters, Enter +runs the selection as an implicit -C (rfcs/cwd-flag.md). + +## `vp build` + +**→ expect-milestone:** `package-select::0` + +``` +VITE+ - The Unified Toolchain for the Web + +Select a package to build (↑/↓, Enter to run, type to search): + + › admin apps/admin + web apps/web + ui packages/ui +``` + +**← write:** `web` + +**→ expect-milestone:** `package-select:web:0` + +``` +VITE+ - The Unified Toolchain for the Web + +Select a package to build (↑/↓, Enter to run, type to search): web + + › web apps/web +``` + +**← write-key:** `enter` + +``` +VITE+ - The Unified Toolchain for the Web + +Selected package: web (apps/web) +Tip: run this directly with `vp -C apps/web build` +vite building client environment for production... +✓ 2 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB + +✓ built in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/picker_select.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/picker_select.local.md new file mode 100644 index 0000000000..c2fd913b4c --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/picker_select.local.md @@ -0,0 +1,40 @@ +# picker_select + +A bare app command at a workspace root with several candidates opens the +fuzzy package picker (the vp run selector component); typing filters, Enter +runs the selection as an implicit -C (rfcs/cwd-flag.md). + +## `vp build` + +**→ expect-milestone:** `package-select::0` + +``` +Select a package to build (↑/↓, Enter to run, type to search): + + › admin apps/admin + web apps/web + ui packages/ui +``` + +**← write:** `web` + +**→ expect-milestone:** `package-select:web:0` + +``` +Select a package to build (↑/↓, Enter to run, type to search): web + + › web apps/web +``` + +**← write-key:** `enter` + +``` +Selected package: web (apps/web) +Tip: run this directly with `vp -C apps/web build` +vite building client environment for production... +✓ 2 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB + +✓ built in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/script_at_root_elicits.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/script_at_root_elicits.global.md new file mode 100644 index 0000000000..9b5daa0fa8 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/script_at_root_elicits.global.md @@ -0,0 +1,23 @@ +# script_at_root_elicits + +A root package script "build": "vp build" run through vp run gets the same +target elicitation as a direct bare invocation: the spawned vp prints the +listing and the task fails instead of silently building the root. + +## `vp run build` + +**Exit code:** 1 + +``` +$ vp build ⊘ cache disabled + +error: `vp build` at the workspace root needs a target package. + + Packages in this workspace: + admin apps/admin + web apps/web + ui packages/ui + + Pass a directory: vp -C apps/admin build + Or run every package's build script: vp run -r build +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/script_at_root_elicits.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/script_at_root_elicits.local.md new file mode 100644 index 0000000000..9b5daa0fa8 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/script_at_root_elicits.local.md @@ -0,0 +1,23 @@ +# script_at_root_elicits + +A root package script "build": "vp build" run through vp run gets the same +target elicitation as a direct bare invocation: the spawned vp prints the +listing and the task fails instead of silently building the root. + +## `vp run build` + +**Exit code:** 1 + +``` +$ vp build ⊘ cache disabled + +error: `vp build` at the workspace root needs a target package. + + Packages in this workspace: + admin apps/admin + web apps/web + ui packages/ui + + Pass a directory: vp -C apps/admin build + Or run every package's build script: vp run -r build +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/vite.config.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/vite.config.ts new file mode 100644 index 0000000000..9b4b8b8cd3 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/vite.config.ts @@ -0,0 +1,4 @@ +// Shared workspace config (lint/fmt style): must NOT make the root an app target. +export default { + lint: {}, +}; diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/index.html b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/index.html new file mode 100644 index 0000000000..31acf91c54 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/index.html @@ -0,0 +1,6 @@ + + + +

root app

+ + diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/package.json new file mode 100644 index 0000000000..78cbc31491 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/package.json @@ -0,0 +1 @@ +{ "name": "runnable-root", "private": true, "workspaces": ["packages/*"] } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/packages/ui/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/packages/ui/package.json new file mode 100644 index 0000000000..70bb6aab17 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/packages/ui/package.json @@ -0,0 +1 @@ +{ "name": "ui", "private": true, "type": "module", "main": "src/index.ts" } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/packages/ui/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/packages/ui/src/index.ts new file mode 100644 index 0000000000..62c987f2f1 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/packages/ui/src/index.ts @@ -0,0 +1 @@ +export const ui = true; diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/snapshots.toml new file mode 100644 index 0000000000..7629fe5ebf --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/snapshots.toml @@ -0,0 +1,10 @@ +[[case]] +name = "runnable_root_runs_in_place" +vp = ["local", "global"] +comment = """ +A workspace root that is itself a runnable app never elicits: bare vp build +runs in place (pre-elicitation behavior), without picker output. The +non-TTY shape of the same rule is pinned by settings_only_workspace, whose +tsdown output is byte-stable on Windows (a piped vite build is not). +""" +steps = [["vp", "build"]] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/snapshots/runnable_root_runs_in_place.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/snapshots/runnable_root_runs_in_place.global.md new file mode 100644 index 0000000000..d4f59d0950 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/snapshots/runnable_root_runs_in_place.global.md @@ -0,0 +1,19 @@ +# runnable_root_runs_in_place + +A workspace root that is itself a runnable app never elicits: bare vp build +runs in place (pre-elicitation behavior), without picker output. The +non-TTY shape of the same rule is pinned by settings_only_workspace, whose +tsdown output is byte-stable on Windows (a piped vite build is not). + +## `vp build` + +``` +VITE+ - The Unified Toolchain for the Web + +vite building client environment for production... +✓ 2 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB + +✓ built in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/snapshots/runnable_root_runs_in_place.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/snapshots/runnable_root_runs_in_place.local.md new file mode 100644 index 0000000000..a9a953de62 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_runnable_root/snapshots/runnable_root_runs_in_place.local.md @@ -0,0 +1,17 @@ +# runnable_root_runs_in_place + +A workspace root that is itself a runnable app never elicits: bare vp build +runs in place (pre-elicitation behavior), without picker output. The +non-TTY shape of the same rule is pinned by settings_only_workspace, whose +tsdown output is byte-stable on Windows (a piped vite build is not). + +## `vp build` + +``` +vite building client environment for production... +✓ 2 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB + +✓ built in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md index 7a51254a8e..33e4236631 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message.md @@ -58,6 +58,7 @@ Documentation: https://viteplus.dev/guide/ Options: -V, --version Print version + -C Run as if vp was started in instead of the current working directory -h, --help Print help ``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message_local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message_local.md index b0c2afef0e..074c6346c6 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message_local.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cli_helper_message/snapshots/cli_helper_message_local.md @@ -30,6 +30,7 @@ Package Manager Commands: install Install all dependencies, or add packages if package names are provided Options: + -C Run as if vp was started in instead of the current working directory -h, --help Print help ``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/package.json new file mode 100644 index 0000000000..dea0bba909 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/package.json @@ -0,0 +1,10 @@ +{ + "name": "cwd-flag", + "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "where:hello": "vp -C packages/hello run where" + } +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/packages/hello/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/packages/hello/package.json new file mode 100644 index 0000000000..c765a5d476 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/packages/hello/package.json @@ -0,0 +1,9 @@ +{ + "name": "hello", + "version": "1.0.0", + "type": "module", + "main": "src/index.ts", + "scripts": { + "where": "node -e \"console.log('cwd base: ' + require('node:path').basename(process.cwd()))\"" + } +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/packages/hello/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/packages/hello/src/index.ts new file mode 100644 index 0000000000..905c7a9bbb --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/packages/hello/src/index.ts @@ -0,0 +1,3 @@ +export function hello(name: string): string { + return `hello ${name}`; +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/snapshots.toml new file mode 100644 index 0000000000..1be84e1398 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/snapshots.toml @@ -0,0 +1,21 @@ +[[case]] +name = "cwd_flag" +vp = ["local", "global"] +comment = """ +The global -C flag runs any command as if vp was started in the directory: +pack and run behave byte-identically to the cd forms, a missing directory +errors, and the positional keeps upstream tsdown entry semantics +(rfcs/cwd-flag.md). +""" +steps = [ + { argv = ["vp", "-C", "packages/hello", "pack"], comment = "-C packs the package from the workspace root" }, + { argv = ["vpt", "list-dir", "packages/hello/dist"], comment = "output lands in the target package" }, + { argv = ["vpt", "rm", "-rf", "packages/hello/dist"], comment = "reset so both forms produce identical output" }, + { argv = ["vp", "pack"], cwd = "packages/hello", comment = "the cd form is equivalent" }, + { argv = ["vp", "-C", "packages/hello", "run", "where"], comment = "-C applies to vp run as well" }, + { argv = ["vp", "run", "where"], cwd = "packages/hello", comment = "equivalent cd form for run" }, + { argv = ["vp", "-C", "packages/missing", "build"], comment = "missing directory errors", continue-on-failure = true }, + { argv = ["vp", "pack", "packages/hello"], comment = "positional stays a tsdown entry resolved from the invocation directory" }, + { argv = ["vpt", "list-dir", "dist"], comment = "upstream semantics: output lands at the invocation directory" }, + { argv = ["vp", "run", "where:hello"], comment = "a script whose command starts with vp -C runs verbatim and honors the directory" }, +] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/snapshots/cwd_flag.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/snapshots/cwd_flag.global.md new file mode 100644 index 0000000000..0059c25184 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/snapshots/cwd_flag.global.md @@ -0,0 +1,117 @@ +# cwd_flag + +The global -C flag runs any command as if vp was started in the directory: +pack and run behave byte-identically to the cd forms, a missing directory +errors, and the positional keeps upstream tsdown entry semantics +(rfcs/cwd-flag.md). + +## `vp -C packages/hello pack` + +-C packs the package from the workspace root + +``` +VITE+ - The Unified Toolchain for the Web + +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` + +## `vpt list-dir packages/hello/dist` + +output lands in the target package + +``` +index.mjs +``` + +## `vpt rm -rf packages/hello/dist` + +reset so both forms produce identical output + +``` +``` + +## `cd packages/hello && vp pack` + +the cd form is equivalent + +``` +VITE+ - The Unified Toolchain for the Web + +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` + +## `vp -C packages/hello run where` + +-C applies to vp run as well + +``` +VITE+ - The Unified Toolchain for the Web + +~/packages/hello$ node -e "console.log('cwd base: ' + require('node:path').basename(process.cwd()))" ⊘ cache disabled +cwd base: hello +``` + +## `cd packages/hello && vp run where` + +equivalent cd form for run + +``` +VITE+ - The Unified Toolchain for the Web + +~/packages/hello$ node -e "console.log('cwd base: ' + require('node:path').basename(process.cwd()))" ⊘ cache disabled +cwd base: hello +``` + +## `vp -C packages/missing build` + +missing directory errors + +**Exit code:** 1 + +``` +directory not found: packages/missing +``` + +## `vp pack packages/hello` + +positional stays a tsdown entry resolved from the invocation directory + +``` +VITE+ - The Unified Toolchain for the Web + +ℹ entry: packages/hello +ℹ Build start +ℹ dist/hello.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` + +## `vpt list-dir dist` + +upstream semantics: output lands at the invocation directory + +``` +hello.mjs +``` + +## `vp run where:hello` + +a script whose command starts with vp -C runs verbatim and honors the directory + +``` +VITE+ - The Unified Toolchain for the Web + +$ vp -C packages/hello run where ⊘ cache disabled +VITE+ - The Unified Toolchain for the Web + +~/packages/hello$ node -e "console.log('cwd base: ' + require('node:path').basename(process.cwd()))" ⊘ cache disabled +cwd base: hello +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/snapshots/cwd_flag.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/snapshots/cwd_flag.local.md new file mode 100644 index 0000000000..e58f7923a2 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/cwd_flag/snapshots/cwd_flag.local.md @@ -0,0 +1,103 @@ +# cwd_flag + +The global -C flag runs any command as if vp was started in the directory: +pack and run behave byte-identically to the cd forms, a missing directory +errors, and the positional keeps upstream tsdown entry semantics +(rfcs/cwd-flag.md). + +## `vp -C packages/hello pack` + +-C packs the package from the workspace root + +``` +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` + +## `vpt list-dir packages/hello/dist` + +output lands in the target package + +``` +index.mjs +``` + +## `vpt rm -rf packages/hello/dist` + +reset so both forms produce identical output + +``` +``` + +## `cd packages/hello && vp pack` + +the cd form is equivalent + +``` +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` + +## `vp -C packages/hello run where` + +-C applies to vp run as well + +``` +~/packages/hello$ node -e "console.log('cwd base: ' + require('node:path').basename(process.cwd()))" ⊘ cache disabled +cwd base: hello +``` + +## `cd packages/hello && vp run where` + +equivalent cd form for run + +``` +~/packages/hello$ node -e "console.log('cwd base: ' + require('node:path').basename(process.cwd()))" ⊘ cache disabled +cwd base: hello +``` + +## `vp -C packages/missing build` + +missing directory errors + +**Exit code:** 1 + +``` +error: directory not found: packages/missing +``` + +## `vp pack packages/hello` + +positional stays a tsdown entry resolved from the invocation directory + +``` +ℹ entry: packages/hello +ℹ Build start +ℹ dist/hello.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` + +## `vpt list-dir dist` + +upstream semantics: output lands at the invocation directory + +``` +hello.mjs +``` + +## `vp run where:hello` + +a script whose command starts with vp -C runs verbatim and honors the directory + +``` +$ vp -C packages/hello run where ⊘ cache disabled +~/packages/hello$ node -e "console.log('cwd base: ' + require('node:path').basename(process.cwd()))" ⊘ cache disabled +cwd base: hello +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/package.json new file mode 100644 index 0000000000..ed5c75cb8f --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/package.json @@ -0,0 +1 @@ +{ "name": "member-default-package", "private": true, "workspaces": ["packages/*"] } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/packages/ui/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/packages/ui/package.json new file mode 100644 index 0000000000..70bb6aab17 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/packages/ui/package.json @@ -0,0 +1 @@ +{ "name": "ui", "private": true, "type": "module", "main": "src/index.ts" } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/packages/ui/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/packages/ui/src/index.ts new file mode 100644 index 0000000000..62c987f2f1 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/packages/ui/src/index.ts @@ -0,0 +1 @@ +export const ui = true; diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/packages/ui/vite.config.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/packages/ui/vite.config.ts new file mode 100644 index 0000000000..6021f747da --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/packages/ui/vite.config.ts @@ -0,0 +1,5 @@ +// A workspace member declaring defaultPackage: ignored below the workspace +// root, where the current directory already identifies the target. +export default { + defaultPackage: "./does-not-exist", +}; diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/snapshots.toml new file mode 100644 index 0000000000..f96ff0faf0 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/snapshots.toml @@ -0,0 +1,9 @@ +[[case]] +name = "member_config_ignored" +vp = ["local", "global"] +comment = """ +defaultPackage is a root-pointer concept: a workspace member's own config +declaring it (here pointing at a missing directory) must not redirect or +fail a command already running in that member; the pack runs in place. +""" +steps = [{ argv = ["vp", "pack"], cwd = "packages/ui", tty = false }] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/snapshots/member_config_ignored.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/snapshots/member_config_ignored.global.md new file mode 100644 index 0000000000..da45246d53 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/snapshots/member_config_ignored.global.md @@ -0,0 +1,15 @@ +# member_config_ignored + +defaultPackage is a root-pointer concept: a workspace member's own config +declaring it (here pointing at a missing directory) must not redirect or +fail a command already running in that member; the pack runs in place. + +## `cd packages/ui && vp pack` + +``` +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/snapshots/member_config_ignored.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/snapshots/member_config_ignored.local.md new file mode 100644 index 0000000000..da45246d53 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/member_default_package/snapshots/member_config_ignored.local.md @@ -0,0 +1,15 @@ +# member_config_ignored + +defaultPackage is a root-pointer concept: a workspace member's own config +declaring it (here pointing at a missing directory) must not redirect or +fail a command already running in that member; the pack runs in place. + +## `cd packages/ui && vp pack` + +``` +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/apps/web/index.html b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/apps/web/index.html new file mode 100644 index 0000000000..ce6d6280c2 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/apps/web/index.html @@ -0,0 +1,6 @@ + + + +

web

+ + diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/apps/web/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/apps/web/package.json new file mode 100644 index 0000000000..64630869c1 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/apps/web/package.json @@ -0,0 +1 @@ +{ "name": "web", "private": true } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/apps/web/vite.config.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/apps/web/vite.config.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/apps/web/vite.config.ts @@ -0,0 +1 @@ +export default {}; diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/package.json new file mode 100644 index 0000000000..b6e5ea319d --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/package.json @@ -0,0 +1 @@ +{ "name": "pack-default-entry", "private": true, "workspaces": ["apps/*", "packages/*"] } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/packages/lib/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/packages/lib/package.json new file mode 100644 index 0000000000..20968dbb63 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/packages/lib/package.json @@ -0,0 +1 @@ +{ "name": "lib", "private": true, "type": "module" } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/packages/lib/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/packages/lib/src/index.ts new file mode 100644 index 0000000000..a1398e8d76 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/packages/lib/src/index.ts @@ -0,0 +1,3 @@ +export function lib(name: string): string { + return `lib ${name}`; +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/packages/spread-only/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/packages/spread-only/package.json new file mode 100644 index 0000000000..318af1703a --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/packages/spread-only/package.json @@ -0,0 +1 @@ +{ "name": "spread-only", "private": true } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/packages/spread-only/vite.config.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/packages/spread-only/vite.config.ts new file mode 100644 index 0000000000..aaf0089da5 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/packages/spread-only/vite.config.ts @@ -0,0 +1,5 @@ +const base = {}; + +export default { + ...base, +}; diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/snapshots.toml new file mode 100644 index 0000000000..ff6e7598fb --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/snapshots.toml @@ -0,0 +1,14 @@ +[[case]] +name = "pack_default_entry" +vp = ["local", "global"] +comment = """ +Bare vp pack at the root auto-selects the only pack-runnable package: the +library whose sole signal is tsdown's default src/index.ts entry. The app's +vite.config.ts has no pack block, so it does not count as pack-runnable +(rfcs/cwd-flag.md, "The likely-runnable heuristic"); tsdown then packs via +its default entry with no pack config at all. +""" +steps = [ + ["vp", "pack"], + { argv = ["vpt", "list-dir", "packages/lib/dist"], comment = "output lands in the auto-selected library" }, +] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/snapshots/pack_default_entry.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/snapshots/pack_default_entry.global.md new file mode 100644 index 0000000000..b52a25aca4 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/snapshots/pack_default_entry.global.md @@ -0,0 +1,29 @@ +# pack_default_entry + +Bare vp pack at the root auto-selects the only pack-runnable package: the +library whose sole signal is tsdown's default src/index.ts entry. The app's +vite.config.ts has no pack block, so it does not count as pack-runnable +(rfcs/cwd-flag.md, "The likely-runnable heuristic"); tsdown then packs via +its default entry with no pack config at all. + +## `vp pack` + +``` +VITE+ - The Unified Toolchain for the Web + +Selected package: lib (packages/lib) +Tip: run this directly with `vp -C packages/lib pack` +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` + +## `vpt list-dir packages/lib/dist` + +output lands in the auto-selected library + +``` +index.mjs +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/snapshots/pack_default_entry.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/snapshots/pack_default_entry.local.md new file mode 100644 index 0000000000..27c8175b05 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/pack_default_entry/snapshots/pack_default_entry.local.md @@ -0,0 +1,27 @@ +# pack_default_entry + +Bare vp pack at the root auto-selects the only pack-runnable package: the +library whose sole signal is tsdown's default src/index.ts entry. The app's +vite.config.ts has no pack block, so it does not count as pack-runnable +(rfcs/cwd-flag.md, "The likely-runnable heuristic"); tsdown then packs via +its default entry with no pack config at all. + +## `vp pack` + +``` +Selected package: lib (packages/lib) +Tip: run this directly with `vp -C packages/lib pack` +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` + +## `vpt list-dir packages/lib/dist` + +output lands in the auto-selected library + +``` +index.mjs +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/package.json new file mode 100644 index 0000000000..af33ecea8a --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/package.json @@ -0,0 +1 @@ +{ "name": "settings-only", "private": true, "type": "module" } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/pnpm-workspace.yaml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/pnpm-workspace.yaml new file mode 100644 index 0000000000..4ff9cc2cbd --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/pnpm-workspace.yaml @@ -0,0 +1 @@ +minimumReleaseAge: 4320 diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/snapshots.toml new file mode 100644 index 0000000000..2a97170a7f --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/snapshots.toml @@ -0,0 +1,10 @@ +[[case]] +name = "pack_in_place" +vp = ["local", "global"] +comment = """ +Regression guard for the vp-config e2e shape: a single package whose +pnpm-workspace.yaml only carries settings (catalogs, minimumReleaseAge) is +a workspace root whose only runnable candidate is itself. Bare vp pack must +run in place, TTY or not, never print the target listing. +""" +steps = [{ argv = ["vp", "pack"], tty = false }] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/snapshots/pack_in_place.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/snapshots/pack_in_place.global.md new file mode 100644 index 0000000000..6f10cdbefc --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/snapshots/pack_in_place.global.md @@ -0,0 +1,16 @@ +# pack_in_place + +Regression guard for the vp-config e2e shape: a single package whose +pnpm-workspace.yaml only carries settings (catalogs, minimumReleaseAge) is +a workspace root whose only runnable candidate is itself. Bare vp pack must +run in place, TTY or not, never print the target listing. + +## `vp pack` + +``` +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/snapshots/pack_in_place.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/snapshots/pack_in_place.local.md new file mode 100644 index 0000000000..6f10cdbefc --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/snapshots/pack_in_place.local.md @@ -0,0 +1,16 @@ +# pack_in_place + +Regression guard for the vp-config e2e shape: a single package whose +pnpm-workspace.yaml only carries settings (catalogs, minimumReleaseAge) is +a workspace root whose only runnable candidate is itself. Bare vp pack must +run in place, TTY or not, never print the target listing. + +## `vp pack` + +``` +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/src/index.ts new file mode 100644 index 0000000000..7b8c293a75 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/src/index.ts @@ -0,0 +1 @@ +export const lib = true; diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/vite.config.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/vite.config.ts new file mode 100644 index 0000000000..580a281fd7 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/settings_only_workspace/vite.config.ts @@ -0,0 +1,3 @@ +export default { + pack: {}, +}; diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/index.html b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/index.html new file mode 100644 index 0000000000..d6845ec898 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/index.html @@ -0,0 +1,6 @@ + + + +

single package

+ + diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/package.json new file mode 100644 index 0000000000..10db26f25c --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/package.json @@ -0,0 +1 @@ +{ "name": "single-package", "private": true, "type": "module" } diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots.toml new file mode 100644 index 0000000000..dfcf4a5f86 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots.toml @@ -0,0 +1,19 @@ +[[case]] +name = "build_in_place" +vp = ["local", "global"] +comment = """ +Regression guard: a single-package repo (no workspaces field) never goes +through target elicitation. Bare vp build runs in place even in an +interactive terminal: no picker, no Selected line, no listing +(rfcs/cwd-flag.md, resolution order step "anywhere else"). +""" +steps = [["vp", "build"]] + +[[case]] +name = "pack_in_place" +vp = ["local", "global"] +comment = "Same guard for vp pack: the standalone library packs in place via tsdown's default entry." +steps = [ + ["vp", "pack"], + { argv = ["vpt", "list-dir", "dist"], comment = "output lands in the repo itself" }, +] diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots/build_in_place.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots/build_in_place.global.md new file mode 100644 index 0000000000..d4a89a25be --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots/build_in_place.global.md @@ -0,0 +1,19 @@ +# build_in_place + +Regression guard: a single-package repo (no workspaces field) never goes +through target elicitation. Bare vp build runs in place even in an +interactive terminal: no picker, no Selected line, no listing +(rfcs/cwd-flag.md, resolution order step "anywhere else"). + +## `vp build` + +``` +VITE+ - The Unified Toolchain for the Web + +vite building client environment for production... +✓ 2 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB + +✓ built in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots/build_in_place.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots/build_in_place.local.md new file mode 100644 index 0000000000..acb09f2cf9 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots/build_in_place.local.md @@ -0,0 +1,17 @@ +# build_in_place + +Regression guard: a single-package repo (no workspaces field) never goes +through target elicitation. Bare vp build runs in place even in an +interactive terminal: no picker, no Selected line, no listing +(rfcs/cwd-flag.md, resolution order step "anywhere else"). + +## `vp build` + +``` +vite building client environment for production... +✓ 2 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB + +✓ built in +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots/pack_in_place.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots/pack_in_place.global.md new file mode 100644 index 0000000000..9eb6e8bdd1 --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots/pack_in_place.global.md @@ -0,0 +1,23 @@ +# pack_in_place + +Same guard for vp pack: the standalone library packs in place via tsdown's default entry. + +## `vp pack` + +``` +VITE+ - The Unified Toolchain for the Web + +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` + +## `vpt list-dir dist` + +output lands in the repo itself + +``` +index.mjs +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots/pack_in_place.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots/pack_in_place.local.md new file mode 100644 index 0000000000..d8ce6886ae --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/snapshots/pack_in_place.local.md @@ -0,0 +1,21 @@ +# pack_in_place + +Same guard for vp pack: the standalone library packs in place via tsdown's default entry. + +## `vp pack` + +``` +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ 1 files, total: kB +✔ Build complete in +``` + +## `vpt list-dir dist` + +output lands in the repo itself + +``` +index.mjs +``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/src/index.ts new file mode 100644 index 0000000000..56e894462d --- /dev/null +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/single_package/src/index.ts @@ -0,0 +1,3 @@ +export function single(name: string): string { + return `single ${name}`; +} diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/vp_help/snapshots/help.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/vp_help/snapshots/help.global.md index c2f0d2f46e..fc51e24490 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/vp_help/snapshots/help.global.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/vp_help/snapshots/help.global.md @@ -58,5 +58,6 @@ Documentation: https://viteplus.dev/guide/ Options: -V, --version Print version + -C Run as if vp was started in instead of the current working directory -h, --help Print help ``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/vp_help/snapshots/help.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/vp_help/snapshots/help.local.md index 398c5d0f4d..166ac74621 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/vp_help/snapshots/help.local.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/vp_help/snapshots/help.local.md @@ -30,5 +30,6 @@ Package Manager Commands: install Install all dependencies, or add packages if package names are provided Options: + -C Run as if vp was started in instead of the current working directory -h, --help Print help ``` diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 2d91427f40..437ee17d33 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -3,7 +3,7 @@ //! This module defines the CLI structure using clap and routes commands //! to their appropriate handlers. -use std::{collections::HashSet, ffi::OsStr, io::IsTerminal, process::ExitStatus}; +use std::{collections::HashSet, ffi::OsStr, process::ExitStatus}; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; use clap_complete::ArgValueCompleter; @@ -53,6 +53,10 @@ pub struct Args { #[arg(short = 'V', long = "version")] pub version: bool, + /// Run as if vp was started in instead of the current working directory + #[arg(short = 'C', value_name = "DIR")] + pub chdir: Option, + #[clap(subcommand)] pub command: Option, } @@ -805,7 +809,7 @@ fn should_reinstall_node_mismatches( return true; } - if !std::io::stdin().is_terminal() || std::env::var_os("CI").is_some() { + if !vite_shared::is_stdin_terminal() || std::env::var_os("CI").is_some() { let package_names = packages.iter().map(|package| package.name.as_str()).collect::>().join(", "); output::warn(&format!( @@ -848,10 +852,19 @@ pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result Result { + // Apply the global `-C ` flag before anything reads cwd. main + // normally consumes a leading `-C` pre-parse; this covers orderings that + // reach clap (e.g. a second `-C`), with identical semantics — including + // the process-cwd change and PWD sync the shared helper performs. + if let Some(dir) = &args.chdir { + cwd = + crate::apply_chdir(&cwd, dir).map_err(|message| Error::UserMessage(message.into()))?; + } + // Handle --version flag (Category B: delegates to JS) if args.version { return commands::version::execute(cwd).await; diff --git a/crates/vite_global_cli/src/command_picker.rs b/crates/vite_global_cli/src/command_picker.rs index dc31289b9c..590f36c941 100644 --- a/crates/vite_global_cli/src/command_picker.rs +++ b/crates/vite_global_cli/src/command_picker.rs @@ -1,7 +1,7 @@ //! Interactive top-level command picker for `vp`. use std::{ - io::{self, IsTerminal, Write}, + io::{self, Write}, ops::ControlFlow, }; @@ -114,24 +114,10 @@ const COMMANDS: &[CommandEntry] = &[ }, ]; -const CI_ENV_VARS: &[&str] = &[ - "CI", - "CONTINUOUS_INTEGRATION", - "GITHUB_ACTIONS", - "GITLAB_CI", - "CIRCLECI", - "TRAVIS", - "JENKINS_URL", - "BUILDKITE", - "DRONE", - "CODEBUILD_BUILD_ID", - "TF_BUILD", -]; - pub fn pick_top_level_command_if_interactive( cwd: &AbsolutePath, ) -> io::Result { - if !should_enable_picker() { + if !vite_shared::is_interactive_terminal() { return Ok(TopLevelCommandPick::Skipped); } @@ -143,17 +129,6 @@ pub fn pick_top_level_command_if_interactive( }) } -fn should_enable_picker() -> bool { - std::io::stdin().is_terminal() - && std::io::stdout().is_terminal() - && std::env::var("TERM").map_or(true, |term| term != "dumb") - && !is_ci_environment() -} - -fn is_ci_environment() -> bool { - CI_ENV_VARS.iter().any(|key| std::env::var_os(key).is_some()) -} - fn run_picker(command_order: &[usize]) -> io::Result> { let mut stdout = io::stdout(); let mut selected_position = 0usize; diff --git a/crates/vite_global_cli/src/commands/env/list_remote.rs b/crates/vite_global_cli/src/commands/env/list_remote.rs index a42f160968..fa20b7c21e 100644 --- a/crates/vite_global_cli/src/commands/env/list_remote.rs +++ b/crates/vite_global_cli/src/commands/env/list_remote.rs @@ -117,8 +117,7 @@ fn strip_v(version: &str) -> &str { /// Whether colored output should be emitted on stdout. fn use_color() -> bool { - use std::io::IsTerminal; - std::io::stdout().is_terminal() && std::env::var_os("NO_COLOR").is_none() + vite_shared::is_stdout_terminal() && std::env::var_os("NO_COLOR").is_none() } /// Filter versions based on criteria. diff --git a/crates/vite_global_cli/src/commands/env/pin.rs b/crates/vite_global_cli/src/commands/env/pin.rs index 5f67440423..cd3f76c225 100644 --- a/crates/vite_global_cli/src/commands/env/pin.rs +++ b/crates/vite_global_cli/src/commands/env/pin.rs @@ -7,10 +7,7 @@ //! directory has no package.json. An explicit `--target` flag overrides the selection. //! An existing `engines.node` is never deleted or modified. -use std::{ - io::{IsTerminal, Write}, - process::ExitStatus, -}; +use std::{io::Write, process::ExitStatus}; use vite_js_runtime::NodeProvider; use vite_path::AbsolutePathBuf; @@ -290,7 +287,7 @@ async fn pin_node_version_file( // If a devEngines.runtime range is declared and no longer satisfied, offer to // sync it in interactive terminals and warn otherwise (rfcs/dev-engines.md) - check_dev_engines_sync(cwd, resolved_version, force, std::io::stdin().is_terminal()).await?; + check_dev_engines_sync(cwd, resolved_version, force, vite_shared::is_stdin_terminal()).await?; Ok(true) } diff --git a/crates/vite_global_cli/src/commands/global/install.rs b/crates/vite_global_cli/src/commands/global/install.rs index 0b09e52493..6c0399be4a 100644 --- a/crates/vite_global_cli/src/commands/global/install.rs +++ b/crates/vite_global_cli/src/commands/global/install.rs @@ -3,7 +3,7 @@ use std::{ collections::{HashMap, HashSet}, fs::{File, OpenOptions, TryLockError}, - io::{IsTerminal, Read, Write}, + io::{Read, Write}, process::Stdio, time::Duration, }; @@ -178,7 +178,7 @@ pub async fn install( )); let progress = ProgressBar::new(packages_count as u64); - if std::io::stderr().is_terminal() && std::env::var_os("CI").is_none() { + if vite_shared::is_stderr_terminal() && std::env::var_os("CI").is_none() { let style = ProgressStyle::with_template("{spinner:.cyan} {msg} ({pos}/{len})") .unwrap_or_else(|_| ProgressStyle::default_spinner()) .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); diff --git a/crates/vite_global_cli/src/commands/global/mod.rs b/crates/vite_global_cli/src/commands/global/mod.rs index 57530e8ae5..01eb8ef142 100644 --- a/crates/vite_global_cli/src/commands/global/mod.rs +++ b/crates/vite_global_cli/src/commands/global/mod.rs @@ -1,12 +1,6 @@ //! Managed global package utilities. -use std::{ - collections::HashMap, - fs::File, - io::{IsTerminal, Read}, - process::Stdio, - time::Duration, -}; +use std::{collections::HashMap, fs::File, io::Read, process::Stdio, time::Duration}; use flate2::read::GzDecoder; use futures::{StreamExt, stream::FuturesUnordered}; @@ -96,7 +90,7 @@ pub(crate) async fn latest_package_versions( let mut versions = HashMap::with_capacity(specs.len()); let progress = ProgressBar::new(specs.len() as u64); - if std::io::stderr().is_terminal() && std::env::var_os("CI").is_none() { + if vite_shared::is_stderr_terminal() && std::env::var_os("CI").is_none() { let style = ProgressStyle::with_template("{spinner:.cyan} {msg} ({pos}/{len})") .unwrap_or_else(|_| ProgressStyle::default_spinner()) .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); diff --git a/crates/vite_global_cli/src/commands/implode.rs b/crates/vite_global_cli/src/commands/implode.rs index e2f5ac8f30..fb7c563d38 100644 --- a/crates/vite_global_cli/src/commands/implode.rs +++ b/crates/vite_global_cli/src/commands/implode.rs @@ -1,9 +1,6 @@ //! `vp implode` — completely remove vp and all its data from this system. -use std::{ - io::{IsTerminal, Write}, - process::ExitStatus, -}; +use std::{io::Write, process::ExitStatus}; use directories::BaseDirs; use owo_colors::OwoColorize; @@ -132,7 +129,7 @@ fn confirm_implode( home_dir: &AbsolutePathBuf, affected_profiles: &[AffectedProfile], ) -> Result { - if !std::io::stdin().is_terminal() { + if !vite_shared::is_stdin_terminal() { return Err(Error::UserMessage( "Cannot prompt for confirmation: stdin is not a TTY. Use --yes to skip confirmation." .into(), diff --git a/crates/vite_global_cli/src/commands/vpr.rs b/crates/vite_global_cli/src/commands/vpr.rs index e043b9c8b0..8465c916f3 100644 --- a/crates/vite_global_cli/src/commands/vpr.rs +++ b/crates/vite_global_cli/src/commands/vpr.rs @@ -10,11 +10,33 @@ use vite_shared::output; /// /// Called from shim dispatch when `argv[0]` is `vpr`. pub async fn execute_vpr(args: &[String], cwd: &AbsolutePath) -> i32 { + // `vpr -C ` mirrors `vp -C run `: consume the + // global flag before treating the rest as run arguments. There is no + // clap parse on this path, so a missing value is reported here. + let mut args = args; + let mut cwd_buf = cwd.to_absolute_path_buf(); + match crate::parse_leading_chdir(args) { + Some((dir, consumed)) => { + cwd_buf = match crate::apply_chdir(cwd, &dir) { + Ok(target) => target, + Err(message) => { + output::raw_stderr(&message); + return 1; + } + }; + args = &args[consumed..]; + } + None if args.first().is_some_and(|arg| arg.starts_with("-C")) => { + output::raw_stderr("-C requires a directory argument"); + return 1; + } + None => {} + } + if crate::help::maybe_print_unified_delegate_help("run", args, true) { return 0; } - let cwd_buf = cwd.to_absolute_path_buf(); match super::delegate::execute(cwd_buf, "run", args).await { Ok(status) => status.code().unwrap_or(1), Err(e) => { diff --git a/crates/vite_global_cli/src/help.rs b/crates/vite_global_cli/src/help.rs index 6741a3cafe..0f9416a2f2 100644 --- a/crates/vite_global_cli/src/help.rs +++ b/crates/vite_global_cli/src/help.rs @@ -1,6 +1,6 @@ //! Unified help rendering for the global CLI. -use std::{fmt::Write as _, io::IsTerminal}; +use std::fmt::Write as _; use clap::{CommandFactory, error::ErrorKind}; use owo_colors::OwoColorize; @@ -112,7 +112,7 @@ fn write_documentation_footer(output: &mut String, documentation_url: &str) { } pub fn should_style_help() -> bool { - std::io::stdout().is_terminal() + vite_shared::is_stdout_terminal() && std::env::var_os("NO_COLOR").is_none() && std::env::var("CLICOLOR").map_or(true, |value| value != "0") && std::env::var("TERM").map_or(true, |term| term != "dumb") diff --git a/crates/vite_global_cli/src/main.rs b/crates/vite_global_cli/src/main.rs index 1fdd658238..ace35e84c7 100644 --- a/crates/vite_global_cli/src/main.rs +++ b/crates/vite_global_cli/src/main.rs @@ -24,7 +24,7 @@ mod upgrade_check; use std::{ env, - io::{IsTerminal, Write}, + io::Write, process::{ExitCode, ExitStatus}, }; @@ -39,6 +39,48 @@ use crate::cli::{ try_parse_args_from_with_options, }; +/// Parse a leading `-C ` / `-C=` / `-C` at the start of the +/// user arguments (argv without the binary name), mirroring clap's +/// short-option grammar (and bin.ts on the local path). Returns the +/// directory and how many tokens it consumed. +pub(crate) fn parse_leading_chdir(user_args: &[String]) -> Option<(String, usize)> { + let first = user_args.first()?; + if first == "-C" { + return user_args.get(1).map(|dir| (dir.clone(), 2)); + } + if let Some(rest) = first.strip_prefix("-C") { + let dir = rest.strip_prefix('=').unwrap_or(rest); + if !dir.is_empty() { + return Some((dir.to_string(), 1)); + } + } + None +} + +/// Apply `-C `: resolve against `cwd`, validate, change the process +/// cwd, and keep the POSIX `PWD` in sync like a real `cd`. Returns the new +/// cwd, or the user-facing error message (not printed here: the completion +/// peek must stay silent, and clap-path callers wrap it in their error type). +pub(crate) fn apply_chdir( + cwd: &vite_path::AbsolutePath, + dir: &str, +) -> Result { + let target = cwd.join(dir).clean(); + if !target.as_path().is_dir() { + return Err(format!("directory not found: {dir}")); + } + if let Err(e) = std::env::set_current_dir(target.as_path()) { + return Err(format!("Failed to change directory to {dir}: {e}")); + } + // Node tools commonly read process.env.PWD. + #[cfg(unix)] + // SAFETY: single-threaded startup, before any command logic runs. + unsafe { + std::env::set_var("PWD", target.as_path()); + } + Ok(target) +} + /// Normalize CLI arguments: /// - `vp list ...` / `vp ls ...` → `vp pm list ...` /// - `vp rebuild ...` → `vp pm rebuild ...` @@ -165,7 +207,7 @@ fn is_affirmative_response(input: &str) -> bool { } fn should_prompt_for_correction() -> bool { - std::io::stdin().is_terminal() && std::io::stderr().is_terminal() + vite_shared::is_stdin_terminal() && vite_shared::is_stderr_terminal() } fn prompt_to_run_suggested_command(suggestion: &str) -> bool { @@ -308,6 +350,16 @@ async fn main() -> ExitCode { } // Handle shell completion + // `complete()` exits before the normal `-C` handling below runs, so apply + // the target directory here too: cwd-sensitive completers (run tasks) + // should suggest from , where the completed command will run. + if env::var_os("VP_COMPLETE").is_some() + && let Some((dir, _)) = parse_leading_chdir(&args[1..]) + && let Ok(current) = vite_path::current_dir() + { + // Best-effort and silent: mid-typing values are often not (yet) dirs. + let _ = apply_chdir(¤t, &dir); + } CompleteEnv::with_factory(command_with_help).var("VP_COMPLETE").complete(); // Check for shim mode (invoked as node, npm, or npx) @@ -323,7 +375,7 @@ async fn main() -> ExitCode { } // Normal CLI mode - get current working directory - let cwd = match vite_path::current_dir() { + let mut cwd = match vite_path::current_dir() { Ok(path) => path, Err(e) => { output::error(&format!("Failed to get current directory: {e}")); @@ -331,6 +383,22 @@ async fn main() -> ExitCode { } }; + // Consume a leading `-C ` here, before anything inspects args or cwd, + // so alias normalization, the command picker, and in-process helpers all + // behave exactly as if vp had been started in . Setting the process + // cwd (before any command logic runs) keeps `vite_path::current_dir()` + // callers deep inside command implementations equivalent to the cd form. + if let Some((dir, consumed)) = parse_leading_chdir(&args[1..]) { + cwd = match apply_chdir(&cwd, &dir) { + Ok(target) => target, + Err(message) => { + output::raw_stderr(&message); + return ExitCode::FAILURE; + } + }; + args.drain(1..=consumed); + } + if args.len() == 1 { match command_picker::pick_top_level_command_if_interactive(&cwd) { Ok(command_picker::TopLevelCommandPick::Selected(selection)) => { @@ -455,7 +523,8 @@ mod tests { use super::{ extract_unknown_argument, has_pass_as_value_suggestion, is_affirmative_response, - normalize_args, replace_top_level_typoed_subcommand, try_parse_args_from, + normalize_args, parse_leading_chdir, replace_top_level_typoed_subcommand, + try_parse_args_from, }; fn s(v: &[&str]) -> Vec { @@ -469,6 +538,23 @@ mod tests { assert_eq!(normalized, s(&["vp", "env", "exec", "node", "script.js", "foo", "--flag"])); } + #[test] + fn parse_leading_chdir_accepts_all_clap_short_forms() { + // `main` consumes these before alias normalization and the picker, so + // `vp -C dir node --version` normalizes like `cd dir && vp node ...`. + assert_eq!(parse_leading_chdir(&s(&["-C", "apps/web", "dev"])), some_dir(2)); + assert_eq!(parse_leading_chdir(&s(&["-Capps/web", "dev"])), some_dir(1)); + assert_eq!(parse_leading_chdir(&s(&["-C=apps/web", "dev"])), some_dir(1)); + // Missing or empty value falls through to clap's own error. + assert_eq!(parse_leading_chdir(&s(&["-C"])), None); + assert_eq!(parse_leading_chdir(&s(&["-C="])), None); + assert_eq!(parse_leading_chdir(&s(&["dev"])), None); + } + + fn some_dir(consumed: usize) -> Option<(String, usize)> { + Some(("apps/web".to_string(), consumed)) + } + #[test] fn normalize_args_rewrites_bare_vp_node() { let input = s(&["vp", "node"]); diff --git a/crates/vite_global_cli/src/shim/dispatch.rs b/crates/vite_global_cli/src/shim/dispatch.rs index 47776f7a31..f2516466eb 100644 --- a/crates/vite_global_cli/src/shim/dispatch.rs +++ b/crates/vite_global_cli/src/shim/dispatch.rs @@ -231,8 +231,6 @@ fn check_npm_global_install_result( node_dir: &AbsolutePath, node_version: &str, ) { - use std::io::IsTerminal; - let Ok(bin_dir) = config::get_bin_dir() else { return }; // Derive bin dir from prefix (Unix: prefix/bin, Windows: prefix itself) @@ -249,7 +247,7 @@ fn check_npm_global_install_result( } } - let is_interactive = std::io::stdin().is_terminal(); + let is_interactive = vite_shared::is_stdin_terminal(); // (bin_name, source_path, package_name) let mut missing_bins: Vec<(String, AbsolutePathBuf, String)> = Vec::new(); let mut managed_conflicts: Vec<(String, String)> = Vec::new(); diff --git a/crates/vite_global_cli/src/upgrade_check.rs b/crates/vite_global_cli/src/upgrade_check.rs index d5fd005279..5d9f330c6a 100644 --- a/crates/vite_global_cli/src/upgrade_check.rs +++ b/crates/vite_global_cli/src/upgrade_check.rs @@ -4,10 +4,7 @@ //! result to `~/.vite-plus/.upgrade-check.json`. Displays a one-line notice on //! stderr when a newer version is available, at most once per 24 hours. -use std::{ - io::IsTerminal, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; use owo_colors::OwoColorize; use serde::{Deserialize, Serialize}; @@ -143,7 +140,7 @@ pub fn display_upgrade_notice(result: &UpgradeCheckResult) { /// Returns `false` for commands excluded by design, quiet modes, and /// machine-readable output flags (--silent, -s, --json, --parseable, --format json). pub fn should_run_for_command(args: &crate::cli::Args) -> bool { - if !cfg!(test) && !std::io::stderr().is_terminal() { + if !cfg!(test) && !vite_shared::is_stderr_terminal() { return false; } diff --git a/crates/vite_install/src/package_manager.rs b/crates/vite_install/src/package_manager.rs index e7fcb975f1..abd7bc6671 100644 --- a/crates/vite_install/src/package_manager.rs +++ b/crates/vite_install/src/package_manager.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, env, fmt, fs::{self, File}, - io::{self, BufReader, IsTerminal, Write}, + io::{self, BufReader, Write}, path::Path, }; @@ -1229,26 +1229,7 @@ async fn set_dev_engines_package_manager_field( } pub(crate) use vite_shared::format_path_prepended as format_path_env; - -/// Common CI environment variables -const CI_ENV_VARS: &[&str] = &[ - "CI", - "CONTINUOUS_INTEGRATION", - "GITHUB_ACTIONS", - "GITLAB_CI", - "CIRCLECI", - "TRAVIS", - "JENKINS_URL", - "BUILDKITE", - "DRONE", - "CODEBUILD_BUILD_ID", // AWS CodeBuild - "TF_BUILD", // Azure Pipelines -]; - -/// Check if running in a CI environment -fn is_ci_environment() -> bool { - CI_ENV_VARS.iter().any(|key| env::var(key).is_ok()) -} +use vite_shared::is_ci_environment; /// Interactive menu for selecting a package manager with keyboard navigation fn interactive_package_manager_menu() -> Result { @@ -1404,7 +1385,7 @@ fn prompt_package_manager_selection() -> Result { } // Check if stdin is a TTY (terminal) - if not, use default - if !io::stdin().is_terminal() { + if !vite_shared::is_stdin_terminal() { tracing::info!("Non-interactive environment detected. Using default package manager: pnpm"); return Ok(PackageManagerType::Pnpm); } diff --git a/crates/vite_js_runtime/src/download.rs b/crates/vite_js_runtime/src/download.rs index 5f28bf6f45..21b3949734 100644 --- a/crates/vite_js_runtime/src/download.rs +++ b/crates/vite_js_runtime/src/download.rs @@ -3,7 +3,7 @@ //! This module provides platform-agnostic utilities for downloading, //! verifying, and extracting runtime archives. -use std::{fs::File, io::IsTerminal, time::Duration}; +use std::{fs::File, time::Duration}; use backon::{ExponentialBuilder, Retryable}; use futures_util::StreamExt; @@ -45,7 +45,7 @@ pub async fn download_file( // across retry attempts; its position is reset at the start of every // attempt so a retried download doesn't double-count bytes. let is_ci = vite_shared::EnvConfig::get().is_ci; - let progress = if std::io::stderr().is_terminal() && !is_ci { + let progress = if vite_shared::is_stderr_terminal() && !is_ci { let pb = ProgressBar::new_spinner(); pb.set_style( ProgressStyle::default_spinner() diff --git a/crates/vite_setup/src/install.rs b/crates/vite_setup/src/install.rs index 879af4f4a5..ba20638f39 100644 --- a/crates/vite_setup/src/install.rs +++ b/crates/vite_setup/src/install.rs @@ -5,7 +5,7 @@ use std::{ env, - io::{Cursor, IsTerminal, Read as _, Write as _}, + io::{Cursor, Read as _, Write as _}, path::Path, process::{self, Output}, time::{SystemTime, UNIX_EPOCH}, @@ -132,7 +132,7 @@ fn is_affirmative_response(input: &str) -> bool { } fn should_prompt_release_age_override(silent: bool) -> bool { - !silent && std::io::stdin().is_terminal() && std::io::stderr().is_terminal() + !silent && vite_shared::is_stdin_terminal() && vite_shared::is_stderr_terminal() } fn prompt_release_age_override(version: &str) -> bool { diff --git a/crates/vite_shared/Cargo.toml b/crates/vite_shared/Cargo.toml index 62dbde59a6..3e426b6642 100644 --- a/crates/vite_shared/Cargo.toml +++ b/crates/vite_shared/Cargo.toml @@ -18,6 +18,7 @@ supports-color = "3" tracing = { workspace = true } tracing-subscriber = { workspace = true } vite_path = { workspace = true } +vite_powershell = { workspace = true } vite_str = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/crates/vite_shared/src/header.rs b/crates/vite_shared/src/header.rs index cf4163f0a1..f4ec220b32 100644 --- a/crates/vite_shared/src/header.rs +++ b/crates/vite_shared/src/header.rs @@ -8,10 +8,7 @@ //! - Stream-based response parsing (modelled after `terminal-colorsaurus`) //! - Gradient/fade generation and RGB ANSI coloring -use std::{ - io::IsTerminal, - sync::{LazyLock, OnceLock}, -}; +use std::sync::{LazyLock, OnceLock}; #[cfg(unix)] use std::{ io::Write, @@ -62,13 +59,11 @@ fn fg_rgb(color: Rgb) -> String { } fn should_colorize() -> bool { - let stdout = std::io::stdout(); - stdout.is_terminal() && on(Stream::Stdout).is_some() + crate::is_stdout_terminal() && on(Stream::Stdout).is_some() } fn supports_true_color() -> bool { - let stdout = std::io::stdout(); - stdout.is_terminal() && on(Stream::Stdout).is_some_and(|color| color.has_16m) + crate::is_stdout_terminal() && on(Stream::Stdout).is_some_and(|color| color.has_16m) } fn lerp(a: f64, b: f64, t: f64) -> f64 { @@ -190,7 +185,7 @@ fn parse_osc4_rgb(buffer: &str, index: u8) -> Option { fn is_osc_query_unsupported() -> bool { static UNSUPPORTED: OnceLock = OnceLock::new(); *UNSUPPORTED.get_or_init(|| { - if !std::io::stdout().is_terminal() || !std::io::stdin().is_terminal() { + if !crate::is_stdout_terminal() || !crate::is_stdin_terminal() { return true; } @@ -547,7 +542,7 @@ pub fn vite_plus_header() -> String { /// which is where `vp check --fix` typically runs. #[must_use] pub fn should_print_header() -> bool { - if !std::io::stdout().is_terminal() { + if !crate::is_stdout_terminal() { return false; } if std::env::var_os("GIT_INDEX_FILE").is_some() { diff --git a/crates/vite_shared/src/interactivity.rs b/crates/vite_shared/src/interactivity.rs new file mode 100644 index 0000000000..fbb17c1c4a --- /dev/null +++ b/crates/vite_shared/src/interactivity.rs @@ -0,0 +1,54 @@ +//! Shared CI-environment and interactive-terminal detection. + +pub use vite_powershell::is_stdin_terminal; + +/// Common CI environment variables. +const CI_ENV_VARS: &[&str] = &[ + "CI", + "CONTINUOUS_INTEGRATION", + "GITHUB_ACTIONS", + "GITLAB_CI", + "CIRCLECI", + "TRAVIS", + "JENKINS_URL", + "BUILDKITE", + "DRONE", + "CODEBUILD_BUILD_ID", // AWS CodeBuild + "TF_BUILD", // Azure Pipelines +]; + +/// Check if running in a CI environment. +pub fn is_ci_environment() -> bool { + CI_ENV_VARS.iter().any(|key| std::env::var_os(key).is_some()) +} + +/// Memoized `stdout` terminal check; the fd's TTY-ness cannot change +/// within a process. +#[expect(clippy::disallowed_methods, reason = "the memoizing wrapper itself")] +pub fn is_stdout_terminal() -> bool { + use std::{io::IsTerminal, sync::LazyLock}; + static IS_TTY: LazyLock = LazyLock::new(|| std::io::stdout().is_terminal()); + *IS_TTY +} + +/// Memoized `stderr` terminal check; the fd's TTY-ness cannot change +/// within a process. +#[expect(clippy::disallowed_methods, reason = "the memoizing wrapper itself")] +pub fn is_stderr_terminal() -> bool { + use std::{io::IsTerminal, sync::LazyLock}; + static IS_TTY: LazyLock = LazyLock::new(|| std::io::stderr().is_terminal()); + *IS_TTY +} + +/// True when vp can show interactive prompts: stdin and stdout are terminals, +/// the terminal is capable, and this is not a CI environment. +/// +/// The CI check is not redundant with the TTY checks: some CI systems run +/// commands in a PTY (Buildkite does by default), where an interactive prompt +/// would hang the job. +pub fn is_interactive_terminal() -> bool { + is_stdin_terminal() + && is_stdout_terminal() + && std::env::var("TERM").map_or(true, |term| term != "dumb") + && !is_ci_environment() +} diff --git a/crates/vite_shared/src/lib.rs b/crates/vite_shared/src/lib.rs index 5abf7b727f..a3823629ef 100644 --- a/crates/vite_shared/src/lib.rs +++ b/crates/vite_shared/src/lib.rs @@ -13,6 +13,7 @@ mod error; pub mod header; mod home; mod http; +mod interactivity; mod json_edit; pub mod output; mod package_json; @@ -25,6 +26,10 @@ pub use env_config::{EnvConfig, TestEnvGuard}; pub use error::format_error_chain; pub use home::{VP_BINARY_NAME, get_vp_home}; pub use http::shared_http_client; +pub use interactivity::{ + is_ci_environment, is_interactive_terminal, is_stderr_terminal, is_stdin_terminal, + is_stdout_terminal, +}; pub use json_edit::{JsonStyle, edit_json_object, insert_after}; pub use package_json::{ DevEngineDependency, DevEngineField, DevEngines, Engines, OnFail, PackageJson, dev_engine_entry, diff --git a/crates/vite_static_config/src/lib.rs b/crates/vite_static_config/src/lib.rs index cb7cca623d..d554f273bb 100644 --- a/crates/vite_static_config/src/lib.rs +++ b/crates/vite_static_config/src/lib.rs @@ -37,12 +37,13 @@ pub enum FieldValue { /// The map is exhaustive: every field is accounted for, absent keys do not exist. /// /// - [`FieldMapInner::Open`] — the object had at least one spread or computed-key -/// property. The map contains only [`serde_json::Value`] entries for keys -/// explicitly declared **after** the last such entry. Absent keys may exist via -/// the spread and are treated as [`FieldValue::NonStatic`] by [`FieldMap::get`]. +/// property. The map contains entries only for keys explicitly declared +/// **after** the last such entry (both static and `NonStatic` values). Absent +/// keys may exist via the spread and are treated as [`FieldValue::NonStatic`] +/// by [`FieldMap::get`], but not by [`FieldMap::get_declared`]. enum FieldMapInner { Closed(FxHashMap, FieldValue>), - Open(FxHashMap, serde_json::Value>), + Open(FxHashMap, FieldValue>), } /// Extracted fields from a vite config object. @@ -65,18 +66,30 @@ impl FieldMap { /// /// - [`Closed`](FieldMapInner::Closed): returns the stored value, or `None` /// if the field is definitively absent. - /// - [`Open`](FieldMapInner::Open): returns the stored `Json` value if - /// explicitly declared after the last spread/computed key, or - /// `Some(NonStatic)` for any other key (it may exist in the spread). + /// - [`Open`](FieldMapInner::Open): returns the stored value if explicitly + /// declared after the last spread/computed key, or `Some(NonStatic)` for + /// any other key (it may exist in the spread). #[must_use] pub fn get(&self, key: &str) -> Option { match &self.0 { FieldMapInner::Closed(map) => map.get(key).cloned(), FieldMapInner::Open(map) => { - Some(map.get(key).map_or(FieldValue::NonStatic, |v| FieldValue::Json(v.clone()))) + Some(map.get(key).cloned().unwrap_or(FieldValue::NonStatic)) } } } + + /// Like [`get`](Self::get), but only for fields the config explicitly + /// declares. Unlike `get`, an open map (spread, computed keys, or an + /// unanalyzable config) does NOT report undeclared keys as `NonStatic`: + /// they return `None`. Use this when a merely-possible field must not + /// change behavior (e.g. `defaultPackage`, where erroring on every + /// unanalyzable config would break unrelated commands). + #[must_use] + pub fn get_declared(&self, key: &str) -> Option { + let (FieldMapInner::Closed(map) | FieldMapInner::Open(map)) = &self.0; + map.get(key).cloned() + } } /// Config file names to try, in priority order. @@ -95,6 +108,11 @@ const CONFIG_FILE_NAMES: &[&str] = &[ "vite.config.cts", ]; +/// Returns true when `dir` directly contains a Vite config file. +pub fn has_config_file(dir: &AbsolutePath) -> bool { + resolve_config_path(dir).is_some() +} + /// Resolve the vite config file path in the given directory. /// /// Tries each config file name in priority order and returns the first one that exists. @@ -413,20 +431,12 @@ fn extract_object_fields(obj: &oxc_ast::ast::ObjectExpression<'_>) -> FieldMap { continue; }; - match &mut inner { - FieldMapInner::Closed(map) => { - let value = - expr_to_json(&prop.value).map_or(FieldValue::NonStatic, FieldValue::Json); - map.insert(Box::from(key.as_ref()), value); - } - FieldMapInner::Open(map) => { - // Only Json values are meaningful in Open — NonStatic is already implied - // for any absent key, so there's no need to record it explicitly. - if let Some(json) = expr_to_json(&prop.value) { - map.insert(Box::from(key.as_ref()), json); - } - } - } + // Both variants record explicit declarations, including NonStatic + // ones: `get_declared` must distinguish a written `key: expr` from a + // key that merely might exist behind a spread. + let (FieldMapInner::Closed(map) | FieldMapInner::Open(map)) = &mut inner; + let value = expr_to_json(&prop.value).map_or(FieldValue::NonStatic, FieldValue::Json); + map.insert(Box::from(key.as_ref()), value); } FieldMap(inner) @@ -528,6 +538,30 @@ mod tests { assert_eq!(map.get(key), Some(FieldValue::Json(expected))); } + #[test] + fn get_declared_ignores_undeclared_keys_in_open_maps() { + // A spread makes the map open: `get` reports any key as possibly + // present, but `get_declared` only reports explicitly written ones. + let map = parse("const base = {};\nexport default { ...base, run: { tasks: {} } };"); + assert_eq!(map.get("defaultPackage"), Some(FieldValue::NonStatic)); + assert_eq!(map.get_declared("defaultPackage"), None); + assert!(map.get_declared("run").is_some()); + + // Closed map: explicitly declared non-static values still surface. + let map = parse("export default { defaultPackage: process.env.DIR };"); + assert_eq!(map.get_declared("defaultPackage"), Some(FieldValue::NonStatic)); + + // Open map: an explicit non-static declaration after a spread is + // still a declaration, not an unknown. + let map = + parse("const base = {};\nexport default { ...base, defaultPackage: process.env.DIR };"); + assert_eq!(map.get_declared("defaultPackage"), Some(FieldValue::NonStatic)); + + // Closed map without the key: definitively absent either way. + let map = parse("export default { run: {} };"); + assert_eq!(map.get_declared("defaultPackage"), None); + } + /// Shorthand for asserting a field is `NonStatic`. fn assert_non_static(map: &FieldMap, key: &str) { assert_eq!( diff --git a/docs/config/index.md b/docs/config/index.md index d7f1b25543..d54665244c 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -33,3 +33,16 @@ Vite+ extends the basic Vite configuration with these additions: - [`test`](/config/test) for Vitest - [`pack`](/config/pack) for tsdown - [`staged`](/config/staged) for staged-file checks +- [`defaultPackage`](#defaultpackage) for the default target of bare app commands at a workspace root + +## defaultPackage + +Default target directory for `vp dev` / `vp build` / `vp preview` / `vp pack` when they are invoked bare in the directory containing the config, an implicit [`vp -C `](/guide/monorepo#app-commands): + +```ts [vite.config.ts] +export default { + defaultPackage: './frontend', +}; +``` + +The value must be a static string literal: vp reads it without executing the config, so it also works at repository roots without a vite-plus install (for example a Laravel or Rails repo whose Vite app lives in `frontend/`). An explicit `-C` or positional target always wins over the config. diff --git a/docs/guide/monorepo.md b/docs/guide/monorepo.md index def5b9aa08..da9b6e711a 100644 --- a/docs/guide/monorepo.md +++ b/docs/guide/monorepo.md @@ -145,16 +145,96 @@ This keeps the behavior centralized while letting each team or package own the p ## App Commands -The root `vite.config.ts` is most valuable for shared linting, formatting, staged checks, and task definitions. For project-specific development, build, and test behavior, use the setup that best matches each app: +The root `vite.config.ts` is most valuable for shared linting, formatting, staged checks, and task definitions. Development, build, preview, and packaging still act on a single app, so Vite+ makes the built-in commands monorepo-aware instead of forcing you to `cd` between packages. -- Pass a folder to built-in Vite commands when you want to target one app: +### Running at the workspace root + +`vp dev`, `vp build`, `vp preview`, and `vp pack` never silently act on the workspace root, which usually has no app of its own. Run them at the top of the monorepo and Vite+ works out which app you mean. + +When exactly one package looks like an app, vp runs it and shows you the direct command for next time: + +``` +$ vp dev +Selected package: web (apps/web) +Tip: run this directly with `vp -C apps/web dev` + + VITE+ v0.2.2 + + ➜ Local: http://localhost:5173/ + ➜ Network: use --host to expose +``` + +When several packages could be the target, vp opens a fuzzy package picker (the same selector as `vp run`). Typing filters, Enter runs the selection, and vp prints the direct command for next time: + +``` +$ vp build +Select a package to build (↑/↓, Enter to run, type to search): + + › admin apps/admin + web apps/web + ui packages/ui +``` + +``` +Selected package: web (apps/web) +Tip: run this directly with `vp -C apps/web build` + + ✓ built in 187ms +``` + +In non-interactive shells (CI, pipes, redirection), vp prints the same packages as a plain listing with ready-to-copy commands and exits 1: + +``` +$ vp build | cat +error: `vp build` at the workspace root needs a target package. + + Packages in this workspace: + admin apps/admin + web apps/web + @shop/ui packages/ui + + Pass a directory: vp -C apps/admin build + Or run every package's build script: vp run -r build +``` + +Packages that look runnable for the command are ranked first in both the picker and the listing: a `vite.config.*` or root `index.html` for `dev` / `build` / `preview`, and a `pack` config block or tsdown's default `src/index.ts` entry for `pack`. + +### Targeting a package with `-C` + +The global `-C` flag runs any command as if you had `cd`'d into a package. It works with every vp command and is identical to `cd && vp `: ```bash -vp dev apps/web -vp build apps/web +vp -C apps/web dev +vp -C apps/web build +vp -C packages/ui pack ``` -- Keep package-specific scripts in each package when the command differs per app: +Passing a folder as a positional (`vp dev apps/web`) still works, but keeps upstream Vite semantics: it sets Vite's `root` option without changing the working directory, so `process.cwd()` reads in configs and plugins resolve from where you ran vp. Prefer `-C` when the package should behave as if you had `cd`'d into it. + +### A fixed default with `defaultPackage` + +To always target one directory and skip the resolution above, set [`defaultPackage`](/config/#defaultpackage) in the root config: + +```ts [vite.config.ts] +export default { + defaultPackage: './apps/web', +}; +``` + +``` +$ vp dev +note: vp dev: using ./apps/web (defaultPackage) + + VITE+ v0.2.2 + + ➜ Local: http://localhost:5173/ +``` + +This is the right choice for framework monorepos that are not JavaScript workspaces, such as a Laravel or Rails app with a `frontend/` directory: there is no package list to resolve, so `defaultPackage` points vp straight at the app. Because vp reads it without executing the config, it works even when `vite-plus` is installed only inside that subdirectory. + +### Package scripts and workspace-wide tasks + +Keep package-specific scripts in each package when the command differs per app: ```json [apps/api/package.json] { @@ -165,7 +245,7 @@ vp build apps/web } ``` -- Run scripts across the workspace with `vp run`: +Run scripts across the whole workspace with `vp run`: ```bash vp run -r build diff --git a/packages/cli/bin/vpr b/packages/cli/bin/vpr index 1848381e60..8550f94294 100755 --- a/packages/cli/bin/vpr +++ b/packages/cli/bin/vpr @@ -4,6 +4,4 @@ import module from 'node:module'; if (module.enableCompileCache) { module.enableCompileCache(); } - -process.argv.splice(2, 0, 'run'); -await import('../dist/bin.js'); +import '../dist/bin.js'; diff --git a/packages/cli/binding/Cargo.toml b/packages/cli/binding/Cargo.toml index f2aa8611d6..a84fae3cd9 100644 --- a/packages/cli/binding/Cargo.toml +++ b/packages/cli/binding/Cargo.toml @@ -21,6 +21,7 @@ napi = { workspace = true } napi-derive = { workspace = true } petgraph = { workspace = true } owo-colors = { workspace = true } +pty_terminal_test_client = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["fs"] } @@ -31,6 +32,7 @@ vite_install = { workspace = true } vite_migration = { workspace = true } vite_pm_cli = { workspace = true } vite_path = { workspace = true } +vite_select = { workspace = true } vite_shared = { workspace = true } vite_static_config = { workspace = true } vite_str = { workspace = true } diff --git a/packages/cli/binding/src/check/analysis.rs b/packages/cli/binding/src/check/analysis.rs index e02d26fb54..da42c7f2d1 100644 --- a/packages/cli/binding/src/check/analysis.rs +++ b/packages/cli/binding/src/check/analysis.rs @@ -1,5 +1,3 @@ -use std::io::IsTerminal; - use owo_colors::OwoColorize; use vite_shared::output; @@ -141,7 +139,7 @@ pub(super) fn print_stdout_block(block: &str) { pub(super) fn print_summary_line(message: &str) { output::raw(""); - if std::io::stdout().is_terminal() && message.contains('`') { + if vite_shared::is_stdout_terminal() && message.contains('`') { let mut formatted = String::with_capacity(message.len()); let mut segments = message.split('`'); if let Some(first) = segments.next() { diff --git a/packages/cli/binding/src/cli/app_target.rs b/packages/cli/binding/src/cli/app_target.rs new file mode 100644 index 0000000000..0d917da160 --- /dev/null +++ b/packages/cli/binding/src/cli/app_target.rs @@ -0,0 +1,432 @@ +//! Target elicitation for bare app commands at a workspace root. +//! +//! A bare `vp dev`/`build`/`preview`/`pack` at a workspace root has no target +//! and would silently run against the root. Resolution order (rfcs/cwd-flag.md): +//! explicit `-C` and positional targets are handled before this code and skip +//! elicitation entirely; then `defaultPackage` from the config in the +//! invocation directory, then the interactive package picker (a package +//! listing plus exit 1 when the terminal is not interactive). + +use vite_error::Error; +use vite_path::AbsolutePathBuf; +use vite_shared::output; +use vite_task::ExitStatus; +use vite_workspace::WorkspaceFile; + +use super::types::SynthesizableSubcommand; + +/// Where a bare app command should run. +pub(super) enum AppTarget { + /// No elicitation applies; run in the invocation directory as today. + CurrentDir, + /// Run as if invoked in this directory (implicit `-C`). + Dir(AbsolutePathBuf), + /// Elicitation printed its output and decided the exit code. + Exit(ExitStatus), +} + +struct PackageRow { + name: vite_str::Str, + path: vite_str::Str, + absolute: AbsolutePathBuf, + runnable: bool, +} + +/// App commands are the single-target subcommands; everything else never +/// goes through elicitation. +fn app_command_parts(subcommand: &SynthesizableSubcommand) -> Option<(&'static str, &[String])> { + match subcommand { + SynthesizableSubcommand::Dev { args } => Some(("dev", args)), + SynthesizableSubcommand::Build { args } => Some(("build", args)), + SynthesizableSubcommand::Preview { args } => Some(("preview", args)), + SynthesizableSubcommand::Pack { args } => Some(("pack", args)), + _ => None, + } +} + +/// Boolean flags of the Vite CLI (dev/build/preview), from the shipped +/// `vp --help` (snap-tests/command-helper); keep in sync. Under +/// cac/mri parsing every OTHER flag — required-value, optional-value +/// (`--host [host]`), or unknown — consumes a following non-flag token as +/// its value, so only tokens no flag consumes are positional targets. +const VITE_BOOLEAN_FLAGS: &[&str] = &[ + "-w", + "--watch", + "--app", + "--clearScreen", + "--cors", + "--emptyOutDir", + "--experimentalBundle", + "--force", + "--profile", + "--strictPort", +]; + +/// Boolean flags of the bundled pack CLI (tsdown), from `vp pack --help`. +const PACK_BOOLEAN_FLAGS: &[&str] = &[ + "--attw", + "--clean", + "--devtools", + "--dts", + "--exe", + "--exports", + "--fail-on-warn", + "--failOnWarn", + "--minify", + "--no-write", + "--publint", + "--report", + "--shims", + "--sourcemap", + "--treeshake", + "--unbundle", + "--unused", +]; + +/// Bare = no positional target and no help-like flag. Mirrors the tools' +/// own cac/mri parsing: a non-flag token after any non-boolean flag is that +/// flag's value (the tool would never see it as a positional), while a token +/// after a boolean flag is a positional target and disables elicitation. +/// pack's workspace selectors already define their own target set and +/// disable elicitation outright. Help/version requests are answered by the +/// underlying tool and must never be redirected. +fn is_bare(command: &str, args: &[String]) -> bool { + let is_pack = command == "pack"; + let booleans = if is_pack { PACK_BOOLEAN_FLAGS } else { VITE_BOOLEAN_FLAGS }; + let mut iter = args.iter().peekable(); + while let Some(arg) = iter.next() { + if !arg.starts_with('-') || super::help::is_app_tool_help_or_version_flag(arg) { + return false; + } + // `--` terminates options: whatever follows is an explicit positional. + if arg == "--" { + return iter.next().is_none(); + } + // Workspace selectors and --root already specify pack's target; + // these previously-valid targeted invocations must keep forwarding. + if is_pack + && ["-W", "--workspace", "-F", "--filter", "--root"] + .iter() + .any(|f| arg == f || arg.strip_prefix(f).is_some_and(|r| r.starts_with('='))) + { + return false; + } + let is_boolean = booleans.contains(&arg.as_str()) || arg.starts_with("--no-"); + // An inline `=` already carries the value (`--port=3000`, `--env.FOO=bar`). + if !is_boolean + && !arg.contains('=') + && iter.peek().is_some_and(|next| !next.starts_with('-')) + { + iter.next(); + } + } + true +} + +/// Heuristic ranking signal: does `dir` look runnable for `command`? +/// Used for ordering and single-candidate auto-selection, never for hiding. +/// The rules are documented in rfcs/cwd-flag.md ("The likely-runnable +/// heuristic"); keep both in sync. +/// +/// The workspace root needs a stronger signal than member packages: a shared +/// root `vite.config.ts` (lint/fmt/tasks) is the normal monorepo setup and +/// must not make the root look like an app, or auto-select would run the +/// silent root build this feature exists to prevent. +fn looks_runnable(dir: &AbsolutePathBuf, command: &str, is_root: bool) -> bool { + match command { + // Bare `vp pack` succeeds when tsdown's default entry exists or the + // config explicitly declares a `pack` block (a spread that only + // might contain `pack` does not count: auto-select acts on this + // signal, so a false positive runs tsdown in a non-packable + // package). The one-stat entry check runs first: this executes per + // workspace package, and the config check reads and parses a file. + "pack" => { + dir.as_path().join("src/index.ts").is_file() + || vite_static_config::resolve_static_config(dir).get_declared("pack").is_some() + } + _ if is_root => dir.as_path().join("index.html").is_file(), + _ => vite_static_config::has_config_file(dir) || dir.as_path().join("index.html").is_file(), + } +} + +/// Resolve the `defaultPackage` value [`classify`] extracted from the +/// invocation root's `vite.config.*` (static extraction, so it works at +/// roots without a vite-plus install). The value must be a static string +/// literal naming an existing directory. +fn resolve_default_package( + command: &str, + cwd: &AbsolutePathBuf, + value: vite_static_config::FieldValue, +) -> AppTarget { + let fail = |msg: &str| { + output::error(msg); + AppTarget::Exit(ExitStatus(1)) + }; + match value { + vite_static_config::FieldValue::Json(serde_json::Value::String(dir)) => { + let target = cwd.join(&dir).clean(); + if !target.as_path().is_dir() { + return fail(&format!("defaultPackage points to a missing directory: {dir}")); + } + output::note(&format!("vp {command}: using {dir} (defaultPackage)")); + AppTarget::Dir(target) + } + vite_static_config::FieldValue::Json(other) => { + fail(&format!("defaultPackage must be a string of a directory, got: {other}")) + } + vite_static_config::FieldValue::NonStatic => fail( + "defaultPackage in vite.config.ts must be a static string literal so vp can read it without executing the config", + ), + } +} + +/// Fuzzy package picker on `vite_select`, the same component behind the +/// `vp run` task selector. Returns the selected row index, or `None` on +/// Ctrl+C. When the PTY snapshot harness sets `VP_EMIT_MILESTONES=1`, every +/// render emits a `package-select::` milestone (invisible +/// OSC 8 hyperlinks) for the tests to synchronize on — same gate as +/// packages/prompts/src/milestone.ts; real terminals never see the bytes. +fn run_package_picker(command: &str, rows: &[PackageRow]) -> Result, Error> { + let emit_milestones = std::env::var_os("VP_EMIT_MILESTONES").is_some_and(|value| value == "1"); + let items: Vec = rows + .iter() + .map(|row| vite_select::SelectItem { + label: vite_str::format!("{} {}", row.name, row.path), + display_name: row.name.clone(), + description: row.path.clone(), + group: None, + }) + .collect(); + let prompt = + format!("Select a package to {command} (\u{2191}/\u{2193}, Enter to run, type to search):"); + let params = vite_select::SelectParams { + items: &items, + query: None, + header: None, + prompt: &prompt, + page_size: 12, + }; + let mut selected_index = 0usize; + let mut stdout = std::io::stdout(); + let result = vite_select::select_list( + &mut stdout, + ¶ms, + vite_select::Mode::Interactive { selected_index: &mut selected_index }, + |state| { + use std::io::Write as _; + if !emit_milestones { + return; + } + let milestone = + vite_str::format!("package-select:{}:{}", state.query, state.selected_index); + let bytes = pty_terminal_test_client::encoded_milestone(&milestone); + let mut out = std::io::stdout(); + let _ = out.write_all(&bytes); + let _ = out.flush(); + }, + ) + .map_err(Error::Anyhow)?; + Ok(match result { + vite_select::SelectResult::Selected => Some(selected_index), + vite_select::SelectResult::Cancelled => None, + }) +} + +/// Pure predicate for the vp-script interception: would `resolve_app_target` +/// do anything other than run in `cwd`? Never prints and never runs the +/// picker. Slightly over-approximates (an empty workspace reports true), in +/// which case the script merely spawns the real binary, which then behaves +/// identically to a direct invocation. +pub(super) fn needs_elicitation( + subcommand: &SynthesizableSubcommand, + cwd: &AbsolutePathBuf, +) -> bool { + classify(subcommand, cwd).is_some() +} + +/// Why a bare app command needs target elicitation. +enum Elicitation { + /// The invocation root's config explicitly declares `defaultPackage` + /// (with this value — possibly invalid, which the resolver reports). + DefaultPackage(vite_static_config::FieldValue), + /// Bare app command at a real workspace root: picker/listing territory. + WorkspaceRoot(vite_workspace::WorkspaceRoot), +} + +/// The RFC's resolution order, written once for both entry points: bare app +/// command, then `defaultPackage` at the invocation root, then the workspace +/// root itself. `defaultPackage` is a root-pointer concept: it applies where +/// the invocation directory is its own root (a workspace root, a standalone +/// package, or a framework directory with no package.json ancestry); below a +/// workspace root the current directory already identifies the target, so a +/// member's own config must not redirect. +fn classify( + subcommand: &SynthesizableSubcommand, + cwd: &AbsolutePathBuf, +) -> Option<(&'static str, Elicitation)> { + let (command, args) = app_command_parts(subcommand)?; + if !is_bare(command, args) { + return None; + } + let workspace = vite_workspace::find_workspace_root(cwd); + let at_invocation_root = + workspace.as_ref().map_or(true, |(_, rel_from_root)| rel_from_root.as_str().is_empty()); + if at_invocation_root + && let Some(value) = + vite_static_config::resolve_static_config(cwd).get_declared("defaultPackage") + { + return Some((command, Elicitation::DefaultPackage(value))); + } + // The picker/listing needs workspace metadata; anything unresolvable + // keeps today's behavior (the caller surfaces its own workspace errors). + let Ok((workspace_root, rel_from_root)) = workspace else { + return None; + }; + if !rel_from_root.as_str().is_empty() + || matches!(workspace_root.workspace_file, WorkspaceFile::NonWorkspacePackage(_)) + { + return None; + } + // A runnable workspace root runs in place, TTY or not: the invocation + // already has its configured target, and repos whose root is the app or + // library (e.g. a single package with a settings-only pnpm-workspace.yaml) + // ran this way before elicitation existed. Eliciting only when the root + // is not a plausible target is what keeps this feature purely additive. + if looks_runnable(&workspace_root.path.to_absolute_path_buf(), command, true) { + return None; + } + Some((command, Elicitation::WorkspaceRoot(workspace_root))) +} + +pub(super) fn resolve_app_target( + subcommand: &SynthesizableSubcommand, + cwd: &AbsolutePathBuf, +) -> Result { + let Some((command, elicitation)) = classify(subcommand, cwd) else { + return Ok(AppTarget::CurrentDir); + }; + let workspace_root = match elicitation { + Elicitation::DefaultPackage(value) => { + return Ok(resolve_default_package(command, cwd, value)); + } + Elicitation::WorkspaceRoot(workspace_root) => workspace_root, + }; + + let graph = + vite_workspace::load_package_graph(&workspace_root).map_err(|e| Error::Anyhow(e.into()))?; + let mut rows: Vec = graph + .node_weights() + .filter(|info| { + // The root is never a row: when it looks runnable, classify + // already ran the command in place instead of eliciting. + !info.path.as_str().is_empty() + }) + .map(|info| { + let absolute = info.absolute_path.to_absolute_path_buf(); + PackageRow { + name: info.package_json.name.clone(), + path: vite_str::Str::from(info.path.as_str()), + runnable: looks_runnable(&absolute, command, false), + absolute, + } + }) + .collect(); + if rows.is_empty() { + return Ok(AppTarget::CurrentDir); + } + rows.sort_by(|a, b| (!a.runnable, a.path.as_str()).cmp(&(!b.runnable, b.path.as_str()))); + + // In an interactive terminal, pick the target: exactly one likely-runnable + // package (rows are sorted runnable first) auto-selects without a menu; + // otherwise the fuzzy picker runs. + if vite_shared::is_interactive_terminal() { + let single_runnable = rows[0].runnable && rows.get(1).is_none_or(|row| !row.runnable); + let picked = if single_runnable { Some(0) } else { run_package_picker(command, &rows)? }; + let Some(index) = picked else { + return Ok(AppTarget::Exit(ExitStatus(130))); + }; + let row = &rows[index]; + // Deliberately stdout via println!: these lines belong to the + // command's own output stream, like the tool output that follows. + println!("Selected package: {} ({})", row.name, row.path); + println!("Tip: run this directly with `vp -C {} {command}`", row.path); + return Ok(AppTarget::Dir(row.absolute.clone())); + } + + output::error(&format!("`vp {command}` at the workspace root needs a target package.")); + output::raw_stderr(""); + output::raw_stderr(" Packages in this workspace:"); + let name_width = rows.iter().map(|row| row.name.len()).max().unwrap_or(0); + for row in &rows { + output::raw_stderr(&format!(" {:>(); + assert!(is_bare("dev", &to_args(&[]))); + assert!(is_bare("dev", &to_args(&["--watch"]))); + assert!(is_bare("build", &to_args(&["-w", "--minify"]))); + // A positional target disables elicitation. + assert!(!is_bare("dev", &to_args(&["apps/web"]))); + // Like cac, any non-boolean flag consumes a following non-flag token + // as its value — required and optional values alike. + assert!(is_bare("dev", &to_args(&["--port", "3000"]))); + assert!(is_bare("dev", &to_args(&["--host", "0.0.0.0"]))); + assert!(is_bare("dev", &to_args(&["--open", "/foo"]))); + assert!(is_bare("build", &to_args(&["--mode", "production", "--minify"]))); + assert!(is_bare("build", &to_args(&["--port=3000"]))); + assert!(is_bare("pack", &to_args(&["--env-file", ".env"]))); + assert!(is_bare("pack", &to_args(&["--env.FOO=bar", "--minify"]))); + // A token after a boolean flag is a positional; the tables are + // command-specific (--minify is optional-value for Vite build, + // boolean for pack). + assert!(!is_bare("build", &to_args(&["--watch", "apps/web"]))); + assert!(!is_bare("pack", &to_args(&["--minify", "src/index.ts"]))); + assert!(!is_bare("pack", &to_args(&["--env.FOO", "bar", "src/cli.ts"]))); + assert!(is_bare("build", &to_args(&["--minify", "esbuild"]))); + // pack workspace selectors define their own target set, in both the + // spaced and inline-value forms. + assert!(!is_bare("pack", &to_args(&["-W"]))); + assert!(!is_bare("pack", &to_args(&["--workspace", "packages/a"]))); + assert!(!is_bare("pack", &to_args(&["-F", "ui"]))); + assert!(!is_bare("pack", &to_args(&["--filter=ui"]))); + assert!(!is_bare("pack", &to_args(&["--workspace=packages/a"]))); + assert!(!is_bare("pack", &to_args(&["--root", "packages/lib"]))); + assert!(!is_bare("pack", &to_args(&["--root=packages/lib"]))); + // `--` terminates options; a token after it is an explicit positional. + assert!(!is_bare("build", &to_args(&["--", "apps/web"]))); + assert!(!is_bare("pack", &to_args(&["--minify", "--", "src/index.ts"]))); + assert!(is_bare("build", &to_args(&["--"]))); + // Help/version requests go to the underlying tool, never elicitation. + assert!(!is_bare("dev", &to_args(&["--help"]))); + assert!(!is_bare("dev", &to_args(&["-h"]))); + assert!(!is_bare("build", &to_args(&["--watch", "--version"]))); + // Vite and tsdown are cac-based and use `-v` for version. + assert!(!is_bare("build", &to_args(&["-v"]))); + } + + #[test] + fn only_app_commands_elicit() { + for (subcommand, expected) in [ + (SynthesizableSubcommand::Dev { args: vec![] }, Some("dev")), + (SynthesizableSubcommand::Build { args: vec![] }, Some("build")), + (SynthesizableSubcommand::Preview { args: vec![] }, Some("preview")), + (SynthesizableSubcommand::Pack { args: vec![] }, Some("pack")), + (SynthesizableSubcommand::Lint { args: vec![] }, None), + (SynthesizableSubcommand::Test { args: vec![] }, None), + ] { + assert_eq!(app_command_parts(&subcommand).map(|(name, _)| name), expected); + } + } +} diff --git a/packages/cli/binding/src/cli/execution.rs b/packages/cli/binding/src/cli/execution.rs index 5a5811b23d..9e32e411d5 100644 --- a/packages/cli/binding/src/cli/execution.rs +++ b/packages/cli/binding/src/cli/execution.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, ffi::OsStr, io::IsTerminal, process::Stdio, sync::Arc}; +use std::{borrow::Cow, ffi::OsStr, process::Stdio, sync::Arc}; use rustc_hash::FxHashMap; use vite_error::Error; @@ -127,7 +127,7 @@ pub(crate) async fn resolve_and_capture_output( resolve_and_build_command(resolver, subcommand, resolved_vite_config, envs, cwd).await?; cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); - if force_color_if_terminal && std::io::stdout().is_terminal() { + if force_color_if_terminal && vite_shared::is_stdout_terminal() { cmd.env("FORCE_COLOR", "1"); } diff --git a/packages/cli/binding/src/cli/handler.rs b/packages/cli/binding/src/cli/handler.rs index 518305ac1a..bfd07e49b6 100644 --- a/packages/cli/binding/src/cli/handler.rs +++ b/packages/cli/binding/src/cli/handler.rs @@ -44,6 +44,12 @@ impl CommandHandler for VitePlusCommandHandler { if program != "vp" && program != "vpr" { return Ok(HandledCommand::Verbatim); } + // A leading global `-C` runs verbatim: CLIArgs has no global flags, and + // the spawned vp/vpr binary applies the directory change (and target + // elicitation) exactly like a direct invocation. + if command.args.first().is_some_and(|arg| arg.starts_with("-C")) { + return Ok(HandledCommand::Verbatim); + } // "vpr " is shorthand for "vp run ", so prepend "run" for parsing. let is_vpr = program == "vpr"; let cli_args = match CLIArgs::try_parse_from( @@ -72,6 +78,14 @@ impl CommandHandler for VitePlusCommandHandler { ))) } CLIArgs::Synthesizable(subcmd) => { + // Bare app commands in scripts get the same workspace-root + // target elicitation as direct invocations: spawn the real + // binary, which elicits (defaultPackage note, listing + + // exit 1) identically instead of silently running the root. + let cwd = command.cwd.to_absolute_path_buf(); + if super::app_target::needs_elicitation(&subcmd, &cwd) { + return Ok(HandledCommand::Verbatim); + } let resolved = self.resolver.resolve(subcmd, None, &command.envs).await?; Ok(HandledCommand::Synthesized(resolved.into_synthetic_plan_request())) } diff --git a/packages/cli/binding/src/cli/help.rs b/packages/cli/binding/src/cli/help.rs index c0941c429b..1c9fa5b0c5 100644 --- a/packages/cli/binding/src/cli/help.rs +++ b/packages/cli/binding/src/cli/help.rs @@ -36,6 +36,13 @@ fn is_vitest_help_flag(arg: &str) -> bool { matches!(arg, "-h" | "--help") } +/// Help/version flags of the forwarded app tools (Vite and tsdown are +/// cac-based: `-v, --version`; vp's own clap surface uses `-V`). Requests for +/// these must always reach the tool, never target elicitation. +pub(super) fn is_app_tool_help_or_version_flag(arg: &str) -> bool { + matches!(arg, "-h" | "--help" | "-v" | "-V" | "--version") +} + fn is_vitest_watch_flag(arg: &str) -> bool { matches!(arg, "-w" | "--watch") } @@ -201,6 +208,7 @@ pub(super) fn print_help() { {bold}install{reset} Install all dependencies, or add packages if package names are provided Options: + -C Run as if vp was started in instead of the current working directory -h, --help Print help" ); } diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index 5033a24339..b1dbddad20 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -3,6 +3,7 @@ //! This module contains all the CLI-related code. //! It handles argument parsing, command dispatching, and orchestration of the task execution. +mod app_target; mod execution; mod handler; mod help; @@ -46,6 +47,17 @@ async fn execute_direct_subcommand( cwd: &AbsolutePathBuf, options: Option, ) -> Result { + // A bare app command at a workspace root resolves its target first + // (defaultPackage, package listing); the command then runs as if invoked + // in the resolved directory (rfcs/cwd-flag.md). + let target = app_target::resolve_app_target(&subcommand, cwd)?; + let retargeted = matches!(&target, app_target::AppTarget::Dir(_)); + let cwd = match &target { + app_target::AppTarget::Exit(status) => return Ok(*status), + app_target::AppTarget::Dir(dir) => dir, + app_target::AppTarget::CurrentDir => cwd, + }; + let (workspace_root, _) = vite_workspace::find_workspace_root(cwd)?; let workspace_path: Arc = workspace_root.path.into(); @@ -55,11 +67,19 @@ async fn execute_direct_subcommand( SubcommandResolver::new(Arc::clone(&workspace_path)) }; - let envs: Arc, Arc>> = Arc::new( - std::env::vars_os() + let envs: Arc, Arc>> = Arc::new({ + let mut envs: FxHashMap, Arc> = std::env::vars_os() .map(|(k, v)| (Arc::from(k.as_os_str()), Arc::from(v.as_os_str()))) - .collect(), - ); + .collect(); + // When elicitation retargeted the command, the tool runs with the + // target as its working directory: keep the POSIX PWD consistent, + // like a real `cd`. Untargeted runs keep the caller's PWD verbatim + // (it may legitimately differ from cwd through shell symlinks). + if cfg!(unix) && retargeted { + envs.insert(Arc::from(OsStr::new("PWD")), Arc::from(cwd.as_path().as_os_str())); + } + envs + }); let envs = envs_with_explicit_package_manager_path(cwd, envs).await?; let status = match subcommand { diff --git a/packages/cli/snap-tests/command-helper/snap.txt b/packages/cli/snap-tests/command-helper/snap.txt index 80a8ba3a14..4138af9d2d 100644 --- a/packages/cli/snap-tests/command-helper/snap.txt +++ b/packages/cli/snap-tests/command-helper/snap.txt @@ -22,6 +22,7 @@ Package Manager Commands: install Install all dependencies, or add packages if package names are provided Options: + -C Run as if vp was started in instead of the current working directory -h, --help Print help > vp pack -h # pack help message diff --git a/packages/cli/snap-tests/command-vp-alias/snap.txt b/packages/cli/snap-tests/command-vp-alias/snap.txt index 946aaf4ec1..a23e0883b3 100644 --- a/packages/cli/snap-tests/command-vp-alias/snap.txt +++ b/packages/cli/snap-tests/command-vp-alias/snap.txt @@ -22,6 +22,7 @@ Package Manager Commands: install Install all dependencies, or add packages if package names are provided Options: + -C Run as if vp was started in instead of the current working directory -h, --help Print help > vp run -h # vp run should show help diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index ab86d07e83..f7ee7d07a1 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -38,6 +38,49 @@ function getErrorMessage(err: unknown): string { // Parse command line arguments let args = process.argv.slice(2); +// Global `-C ` flag: run as if vp was started in . The global Rust +// CLI parses this itself and spawns bin.js with the target cwd already set; +// this branch covers direct local-bin invocations (`pnpm exec vp -C ...`). +// Accepts `-C dir`, `-Cdir`, and `-C=dir`, matching the clap grammar. +if (args[0]?.startsWith('-C')) { + const inline = args[0].length > 2; + const dir = inline ? args[0].slice(args[0][2] === '=' ? 3 : 2) : args[1]; + if (!dir) { + errorMsg('-C requires a directory argument'); + process.exit(1); + } + const target = path.resolve(dir); + // chdir is the single validation point: a pre-check stat can itself throw + // (EACCES on a parent, ELOOP), while chdir reports every failure mode + // through one catchable path. + try { + process.chdir(target); + } catch (err) { + const code = typeof err === 'object' && err !== null && 'code' in err ? err.code : undefined; + if (code === 'ENOENT' || code === 'ENOTDIR') { + errorMsg(`directory not found: ${dir}`); + } else { + errorMsg(`cannot change directory to ${dir}: ${getErrorMessage(err)}`); + } + process.exit(1); + } + if (process.platform !== 'win32') { + // Keep the POSIX PWD in sync, like a real `cd`. + process.env.PWD = target; + } + args = args.slice(inline ? 1 : 2); + process.argv = process.argv.slice(0, 2).concat(args); +} + +// `vpr` is shorthand for `vp run`: the bin/vpr shim imports this file +// unchanged (argv0 tells them apart, like the Rust shim dispatch), and the +// rewrite happens here, after -C consumption, so `vpr -C ` +// orders itself correctly by construction. +if (path.basename(process.argv[1] ?? '') === 'vpr') { + args = ['run', ...args]; + process.argv = process.argv.slice(0, 2).concat(args); +} + // Transform `vp help [command]` into `vp [command] --help` if (args[0] === 'help' && args[1]) { args = [args[1], '--help', ...args.slice(2)]; diff --git a/packages/cli/src/define-config.ts b/packages/cli/src/define-config.ts index 9b3065b723..e67bc84f76 100644 --- a/packages/cli/src/define-config.ts +++ b/packages/cli/src/define-config.ts @@ -53,6 +53,15 @@ declare module '@voidzero-dev/vite-plus-core' { pack?: PackUserConfig | PackUserConfig[]; + /** + * Default target directory for `vp dev` / `build` / `preview` / `pack` + * when invoked bare in the directory containing this config (an implicit + * `vp -C `). Relative to the config file's directory. Must be a + * static string literal: vp reads it without executing the config, so it + * also works at roots without a vite-plus install. + */ + defaultPackage?: string; + run?: RunConfig; staged?: StagedConfig; diff --git a/rfcs/cwd-flag.md b/rfcs/cwd-flag.md new file mode 100644 index 0000000000..686fb065ab --- /dev/null +++ b/rfcs/cwd-flag.md @@ -0,0 +1,391 @@ +# RFC: Global `-C` Flag for Working-Directory Switching + +## Summary + +Add a global `-C ` flag to vp, then use it to fix the app-command experience in monorepos. Everything is additive and backward compatible: + +1. **`-C ` global flag** (the feature): for every vp command, `vp -C ` behaves exactly like `cd && vp `, following the `git -C` / `make -C` convention: the first-class "run there" form vp currently lacks. Positional semantics stay untouched: `vp dev ` keeps upstream Vite semantics (`root` only), and `vp pack` positionals stay tsdown entries. +2. **App-command UX built on `-C`**: running `vp dev` / `vp build` / `vp preview` / `vp pack` bare at a workspace root elicits the missing target instead of silently running against the root: an interactive fuzzy package picker in a TTY, a `defaultPackage` config for repos with one blessed target, and a clear listing plus exit 1 when non-interactive. All three are defined as an implicit `-C `. + +In the common flows users never type `-C`: bare `vp dev` at the root goes through the picker or `defaultPackage`, and inside a package it runs there as today. `-C` is the explicit, teachable form underneath. The commands stay singular: `vp dev` still starts exactly one Vite dev server in one directory. Running a task across many packages at once remains the job of `vp run` (`-r`, `--filter`). + +## Motivation + +### Current Pain Points + +**1. vp has no first-class "run this command in that directory" form.** + +For `dev`/`build`/`preview`, the positional is forwarded verbatim to Vite's `[root]`, which re-bases config lookup and `.env` loading, but `process.cwd()` of the Vite process stays at the invocation directory. Any cwd-relative read in a config or plugin diverges even though `root` points at the right app: + +```ts +// apps/admin/vite.config.ts +const cert = fs.readFileSync(path.resolve('certs/dev.pem')); // cwd-relative +``` + +``` +$ cd apps/admin && vp dev # cwd = apps/admin, cert found + + VITE+ v0.2.2 + + ➜ Local: http://localhost:5173/ + +$ vp dev apps/admin # root is right, cwd is still the repo root +failed to load config from /acme/apps/admin/vite.config.ts +error when starting dev server: +Error: ENOENT: no such file or directory, open '/acme/certs/dev.pem' +``` + +For `pack`, directories do not work at all: the positional means entry files/globs (`packages/cli/src/pack-bin.ts`) and config always resolves from `process.cwd()`: + +``` +$ vp pack packages/ui +ℹ entry: packages/ui +ℹ Build start +error: Build failed with 1 error: + +[UNRESOLVED_ENTRY] Cannot resolve entry module packages/ui. +``` + +So the only form that works reliably, uniformly, for every command, is `cd && vp `, and vp offers no flag equivalent of it. + +**2. At a monorepo root, the app commands are silently wrong.** + +The workspace root usually has no app, but `vp dev` happily starts a server pointed at it: + +``` +$ vp dev + + VITE+ v0.2.2 + + ➜ Local: http://localhost:5173/ # opens to a 404, no index.html here +``` + +Nothing errors, nothing guides the user toward the right invocation, and the server exposes the whole repository tree. Fixing this requires eliciting a target, and eliciting a target requires a well-defined primitive to expand to. `-C` is that primitive. + +All of the failures above are reproducible with `vite-plus@0.2.2`: https://github.com/why-reproductions-are-required/vite-plus-monorepo-app-commands-repro + +## Proposed UX + +Example workspace used throughout: + +``` +acme/ +├── pnpm-workspace.yaml +├── vite.config.ts +├── apps/web (Vite app) +├── apps/admin (Vite app) +├── packages/ui (library) +└── packages/utils (library) +``` + +### 1. `-C`: run any vp command in another directory + +These do the same thing, byte for byte: + +```bash +vp -C apps/admin dev +cd apps/admin && vp dev +``` + +It is not limited to the app commands: + +```bash +vp -C apps/web test +vp -C apps/web run build +``` + +Args after the subcommand pass through unchanged: + +``` +$ vp -C apps/admin dev --port 4000 + + VITE+ v0.2.2 + + ➜ Local: http://localhost:4000/ +``` + +And `-C` gives pack its missing directory form: + +``` +$ vp -C packages/ui pack +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs 0.10 kB │ gzip: 0.11 kB +✔ Build complete in 9ms +``` + +`vp dev apps/admin` (the positional) is untouched: `root` is set, cwd is not, so the pain point 1 pitfall remains on that form (see Decisions). + +### 2. `vp dev` at the workspace root (interactive terminal) + +Same look and keybindings as the `vp run` task selector, listing packages instead of tasks: + +``` +$ vp dev +Select a package to dev (↑/↓, Enter to run, type to search): + + › web apps/web + admin apps/admin + ui packages/ui + utils packages/utils +``` + +Typing filters fuzzily, with the query shown inline: + +``` +Select a package to dev (↑/↓, Enter to run, type to search): adm + + › admin apps/admin +``` + +Enter confirms, prints the teaching hint once, then runs as an implicit `-C`: + +``` +Selected package: admin (apps/admin) +Tip: run this directly with `vp -C apps/admin dev` + + VITE+ v0.2.2 + + ➜ Local: http://localhost:5173/ + ➜ Network: use --host to expose +``` + +Escape clears the search, Ctrl+C cancels with exit code 130 and runs nothing (matching the task picker). `vp build`, `vp preview`, and `vp pack` at the root look the same, with `Select a package to build` / `preview` / `pack`. + +### 3. Non-interactive at the root (CI, piped output, scripts) + +No picker can appear, so the command fails fast with the same information the picker would have shown: + +``` +$ vp build +✗ `vp build` at the workspace root needs a target package. + + Packages in this workspace: + web apps/web + admin apps/admin + ui packages/ui + utils packages/utils + + Pass a directory: vp -C apps/web build + Or run every package's build script: vp run -r build + +$ echo $? +1 +``` + +### 4. With `defaultPackage` configured + +The motivating repo shape is a framework monorepo where the Vite app lives in a subdirectory of a repo that is not a JS workspace at all, for example a Laravel, Rails, or Go server with a `frontend/` directory: + +``` +shop/ +├── app/ (PHP / Ruby / Go) +├── routes/ +├── composer.json +├── vite.config.ts (root config below) +└── frontend/ (the Vite app) +``` + +There is no `pnpm-workspace.yaml` or `workspaces` field to enumerate, so the picker cannot serve this shape. `defaultPackage` can: + +```ts +// vp reads this key via static extraction and never executes this file, so +// the missing vite-plus install at this root is fine. Vite never loads this +// config either; at this root it is purely a pointer for vp. +export default { + defaultPackage: './frontend', +}; +``` + +Bare app commands at the root now behave as `vp -C ./frontend `, with one line of output so it never feels magical: + +``` +$ vp dev +vp dev: using ./frontend (defaultPackage) + + VITE+ v0.2.2 + + ➜ Local: http://localhost:5173/ +``` + +An explicit `-C` still wins: `vp -C apps/admin dev` ignores `defaultPackage`. + +### 5. Inside a sub-package: nothing changes + +``` +$ cd apps/web +$ vp dev + + VITE+ v0.2.2 + + ➜ Local: http://localhost:5173/ +``` + +No picker ever appears below the root. + +## Command Syntax + +``` +vp [-C ] [args...] +``` + +### The `-C ` global flag + +- A vp-global flag, parsed before the subcommand like `git -C` / `make -C`, never forwarded to the underlying tool. It works with every vp command. +- Semantics: run the command exactly as if invoked in ``. The directory is resolved against the invocation cwd; a missing directory errors with `directory not found`. +- The name follows pnpm: its global `-C ` is documented as "Run as if pnpm was started in `` instead of the current working directory", which is this flag's semantics verbatim. Short form only in v1 (git style); pnpm's `--dir` long alias can be added compatibly later if wanted. +- Because it sits before the subcommand, it cannot collide with Vite or tsdown flags, present or future. The subcommands themselves gain zero flags. + +### Positionals and forwarded args: unchanged + +Everything after the subcommand is forwarded verbatim, exactly as today. `vp dev ` keeps upstream Vite semantics (positional = `root` option), `vp pack` positionals stay tsdown entries, and relative option values keep resolving against the process cwd (the invocation directory, or `` under `-C`). There is no directory-vs-entry disambiguation anywhere. + +Rejected alternatives: repurposing the app-command positional to mean "run there" (breaks Vite CLI parity; see Decisions), a per-command picker-forcing flag, and `-F`-style name filters (`-F` already has other meanings on `pack` and `run`/`exec`). + +## Behavior + +### Target directory resolution + +An app command invocation is **bare** when it has no `-C` and no positional target (no Vite `[root]`, no pack entries). The classification mirrors the tools' own cac parsing: a non-flag token following any non-boolean flag is that flag's value — required and optional values alike (`--port 3000`, `--host 0.0.0.0`) — because the tool itself would never treat it as a positional; only a token no flag consumes is a positional target, and any positional disables elicitation. The boolean-flag tables come from the shipped `--help` of each tool and are command-specific (`--minify` is optional-value for Vite build but boolean for pack). pack's target selectors (`-W`/`--workspace`, `-F`/`--filter`, `--root`) already define their own target and always disable elicitation. For `vp dev` / `build` / `preview` / `pack`, the target directory is resolved in this order: + +1. **`-C `**: run there. Never triggers the picker. +2. **Positional target present**: forward as today, upstream semantics, vp does not interfere. +3. **`defaultPackage`**, when bare in the directory containing the root config (a workspace root, or the root of a non-workspace repo): implicit `-C`, print a one-line note. +4. **Interactive picker**, when bare at the workspace root in an interactive TTY (and not CI): pick, print hint, run as implicit `-C`. +5. **Non-interactive and bare at the workspace root**: print the package list and the `-C` hint, exit 1. +6. **Anywhere else**: current behavior, run in the current directory. + +"Workspace root" means the current directory's package is the workspace root package, as determined by `vite_workspace::find_workspace_root` (already called on every invocation in `packages/cli/binding/src/cli/mod.rs`). + +### Equivalence invariant + +For every vp command: + +``` +vp -C [args...] === cd && vp [args...] +``` + +The child's spawn cwd is ``, so config lookup, `.env` loading, `process.cwd()` reads in configs and plugins, and relative CLI args all behave as if the user had `cd`'d. The POSIX `PWD` environment variable is refreshed too (by both entry points and for elicitation-retargeted tools), so `process.env.PWD` readers match the `cd` form. Both entry points apply `-C` by changing their own process cwd at startup, before any argument normalization, picker, or command logic runs, which is indistinguishable from having been started in ``: the global binary consumes the flag first thing in `main` (so command aliases, the no-command picker, and in-process helpers that read the process cwd are all covered), and the local `vp` bin does the same before dispatch. + +The global binary also resolves the local `vite-plus` install from ``, matching `cd` exactly; through the package's own `vp` bin the executing CLI is already chosen, so there the invariant assumes a single Vite+ version per workspace (the supported monorepo model). The implicit `-C` forms (picker, auto-select, `defaultPackage`) run inside the already-chosen CLI under the same assumption: the spawned tool gets the target's cwd and `PWD`, but the executing CLI and its runtime were resolved at the invocation directory. Within one workspace the two coincide; a `defaultPackage` target carrying its own Vite+ install or runtime pin is resolved by the invoking CLI today (full re-delegation is tracked as follow-up work). + +### Entry points and version assumption + +- `-C` is parsed by both the global binary and the local bin; the picker and `defaultPackage` live in the local CLI's NAPI binding (`execute_direct_subcommand`), which every entry point executes. Bare `vp dev` at a root is primarily a global-CLI experience, but a root-level `"dev": "vp dev"` script flows through the same logic. +- Pre-1.0, both the global CLI and any local install are assumed to ship this feature; no version negotiation with older CLIs is specified. In the non-workspace shape the root has no local install, so the bundled CLI executes end to end, which is equivalent under this assumption. + +### Picker contents + +- One row per workspace package: name plus relative path. Nothing is filtered out; likely-runnable packages (rules below) rank first, then by path, so apps surface at the top while everything stays searchable. +- Fuzzy search over name and path via `vite_select::fuzzy_match`, paging identical to the task picker. +- A runnable workspace root never elicits at all: the command runs in place, TTY or not, exactly as before this RFC. The invocation already has its configured target — a root app, or a single package whose `pnpm-workspace.yaml` only carries settings (catalogs, `minimumReleaseAge`) — and eliciting only when the root is not a plausible target is what keeps the feature purely additive. The root needs a stronger runnable signal than member packages: an `index.html` for `dev`/`build`/`preview` (a shared root config for lint/fmt/tasks is the normal monorepo setup and does not make the root an app), and the usual explicit-`pack`-or-default-entry rule for `pack`. +- With exactly one likely-runnable package, the picker auto-selects it, printing only the `Selected package:` line and the tip. + +### The likely-runnable heuristic + +Used only for ranking and single-candidate auto-select, never to hide a package: a misjudged package still appears in the picker and listing, just lower. Judged per package directory from file existence and static config extraction; nothing is executed, and parent directories never count. + +| Command | A package is likely runnable when | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `dev` / `build` / `preview` | its directory directly contains one of Vite's config file names (`vite.config.{js,mjs,ts,cjs,mts,cts}`, the exact list Vite probes), **or** an `index.html` at the package root (Vite's default app entry) | +| `pack` | its `vite.config.*` explicitly declares a `pack` block (read via static extraction; neither a config without `pack` nor one that merely might contain it behind a spread counts), **or** `src/index.ts` exists (tsdown's only default entry) | + +Both file-based signals are upstream defaults, not vp inventions: `index.html` at the project root is Vite's entry point ([index.html and Project Root](https://vite.dev/guide/#index-html-and-project-root)), the config file names are the list Vite resolves ([Configuring Vite](https://vite.dev/config/), mirrored by `vite_static_config::CONFIG_FILE_NAMES` with the upstream source link), and `src/index.ts` is tsdown's default entry when none is configured ([tsdown Entry](https://tsdown.dev/options/entry); `src/features/entry.ts` in tsdown resolves exactly this one path). + +"Exactly one likely-runnable package" means: after sorting rows runnable-first, the first row is runnable and the second is not. Auto-select additionally requires an interactive terminal. + +Accepted trade-offs, tolerable because the signal never hides anything and a wrong auto-select is immediately visible (the `Selected package:` line, with the `Tip:` line showing the explicit `-C` form): + +- A library whose `vite.config.*` exists only for Vitest or lint settings ranks as runnable for `dev`/`build`/`preview`. A refinement could demote configs whose only top-level keys are tool blocks, via the same static extraction; deferred until it bites in practice. +- An app whose `index.html` lives outside the package root (custom Vite `root`) or whose config is inherited from a parent directory is not ranked first, and never auto-selects. + +### `defaultPackage` config + +```ts +export default defineConfig({ + // Relative to the config file's directory. Used by vp dev/build/preview/pack + // when invoked bare next to this config: an implicit -C. + defaultPackage: './frontend', +}); +``` + +- Type: `string`, a single directory. A per-command map can come later if real demand appears. +- Consulted when a bare app command runs in the directory containing the root config: a workspace root, or a non-workspace repo root. The non-workspace shape has no package list, so `defaultPackage` is the only mechanism that covers it. An explicit `-C` always wins. +- A missing directory errors: `defaultPackage points to a missing directory: ./frontend`. +- Read via static extraction (`vite_static_config` + the loader in `packages/cli/binding/src/cli/handler.rs`), like `run` config. At a non-workspace root there is no install to execute the config, so the file must work unexecuted: a plain default-export object with a static string value. +- Only an explicitly declared `defaultPackage` changes behavior. A declared but non-static value (e.g. `process.env.DIR`) errors; a config that is unanalyzable or hides fields behind a spread is treated as not declaring the key and falls through to the picker or current-dir resolution, so an exotic config can never break unrelated bare commands. +- Consulted only at the invocation root (a workspace root, a standalone package root, or a directory with no `package.json` ancestry). Below a workspace root the current directory already identifies the target, so a member package's own config never redirects. + +## Decisions + +### Vite CLI parity preserved; `-C` carries the `cd` semantics + +The core tension: parity with `vite ` (positional sets `root` only, cwd untouched) and parity with `cd && vp dev` cannot both hold on the same positional. An earlier draft repurposed the positional and accepted a permanent divergence from the upstream CLI. This RFC keeps the positional fully Vite-compatible and puts the `cd` semantics on a new, explicitly named channel instead. Nothing existing changes meaning, pack needs no directory-vs-entry heuristic, and the primitive generalizes to every vp command instead of four. + +The accepted cost: two ways to pass a directory with different semantics. Mitigation: users rarely type either (bare `vp dev` plus picker or `defaultPackage` covers the common flows), and every hint, error, and doc teaches only `-C`. + +Mechanism: `process.chdir()` in the CLI process was rejected as a global mutation that leaks into everything sharing the process. vp is a launcher: the NAPI binding always spawns the tool as a fresh child (`packages/cli/binding/src/cli/execution.rs`), so the child's spawn cwd is free to set, with no upstream change. + +### Root-only interactivity + +Below the root the cwd already identifies the project, so prompting would be noise. At the root the command is ambiguous and silently wrong today; that is where a prompt earns its keep. Unlike bare `vp run`'s informational listing, the app commands exit 1 when non-interactive, because building or serving the wrong directory is worse than failing loudly. + +### Elicitation scope + +`-C` is global and works with every command. The elicitation behaviors (picker, `defaultPackage`, root error) apply only to the single-target app commands, because only they are ambiguous at the root. Tree-scoped commands (`test`, `lint`, `fmt`, `check`) mean "the whole repo" there, which is their desired behavior. Workspace-state commands (`install`, `add`, `outdated`, ...) have the root as their natural home. Orchestrators (`run`, `exec`) own their selection models and remain the way to run one task across many packages. A future command joins the elicitation set exactly when its subject is one package directory. + +## Implementation Architecture + +All changes live in the Rust layers; no upstream Vite or tsdown changes are required. + +- `crates/vite_global_cli/src/cli.rs`: parse the global `-C `; resolve the local install from `` and delegate with `` as the effective cwd. +- `packages/cli/binding/src/cli/types.rs` / `mod.rs`: parse `-C` on the local bin path; in `execute_direct_subcommand`, add the bare-invocation resolution order (workspace-root detection already happens here). +- `packages/cli/binding/src/cli/execution.rs`: spawn the child with cwd set to the target directory. +- Picker: reuse `vite_select` and `vite_workspace`, both already dependencies via the `vite_task` crates. +- `defaultPackage`: extend the `VitePlusConfigLoader` static extraction the same way `run` config is loaded, and add `defaultPackage?: string` to `packages/cli/src/define-config.ts`. +- `packages/cli/src/pack-bin.ts` needs no change: positional handling is untouched and `-C` never reaches it. +- Docs: a `-C` entry in the global CLI docs, `docs/guide/monorepo.md` "App Commands", and a `docs/config/` page for the new key. + +## Compatibility + +Every existing invocation is unchanged. The only behavior change is the bare app command at a workspace root, which goes from "silently serve or build the root" to picker / config / clear error. A runnable root stays available as a picker entry, and `defaultPackage: '.'` restores the old behavior unconditionally. + +## Snap Tests + +Non-interactive branches are covered by snap tests: + +- `vp -C build` / `vp -C pack` / `vp -C run `, plus `-C` with a missing directory. +- Parity regression: `vp dev ` still forwards the positional as Vite `root` with cwd untouched. +- Bare app commands at a workspace root without a TTY: package listing and exit code. +- `defaultPackage`: happy path and missing-directory error. +- Equivalence checks: `vp -C build` and `cd && vp build` produce the same output in a fixture whose config reads `process.cwd()`. + +The interactive picker gets pty snapshot coverage in the `vite_task` repo style (`task_select` fixtures) if the picker lands near `vite_select`, or manual verification via tmux-driven interactive runs otherwise. + +## Open Questions + +1. Does ranking plus search suffice, or is outright filtering of non-runnable packages ever wanted? +2. Add a `VP_DEFAULT_PACKAGE` env override later? Env companions are an established pattern (`NX_DEFAULT_PROJECT`); deferred from v1. +3. Should `vp test` join the elicitation set? Probably not: Vitest already has first-class `projects` semantics at the root (`-C` works with it regardless). +4. Exact non-interactive gate: the `vp run` picker's TTY check plus the `CI` check used by the global command picker? +5. Should `vp dev ` print a one-line tip pointing at `vp -C dev`, or would that be noise on a fully supported upstream form? + +## Appendix: Naming Survey for `defaultPackage` + +How comparable tools name "the member a root-level command targets when none is specified": + +| Tool | Field | Notes | +| -------------------------- | ------------------------------------ | ---------------------------------------------------- | +| Ionic CLI | `defaultProject` | active; root config with a `projects` map | +| Nx | `defaultProject` | deprecated in favor of `NX_DEFAULT_PROJECT` env var | +| Angular CLI | `defaultProject` | deprecated in favor of cwd inference | +| Cargo | `workspace.default-members` | plural: root `cargo build` builds all listed members | +| Salesforce DX | `default: true` on the member | marker pattern; needs member enumeration | +| Vercel / Netlify / Amplify | `rootDirectory` / `base` / `appRoot` | per-app deploy config, not a default among many | +| GitHub Actions | `defaults.run.working-directory` | names the mechanism (cwd) | + +The pattern is `default` plus the tool's own noun for the unit: Angular, Nx, and Ionic say "project", Cargo says "members", Salesforce says "package directories". vp's noun is "package" (the picker, `vp run` docs, `vite_workspace`, pnpm vocabulary), hence `defaultPackage`. + +Rejected: `defaultProject` (collides with Vitest `test.projects`, and the picker says "package"), `defaultWorkspace` ("workspace" means the whole monorepo in vp/pnpm vocabulary), `defaultMembers` (plural, implies running in many packages; meaningless without a workspace), `appRoot`/`rootDirectory`/`base` (collide with Vite's `root`/`base` options), member markers (need enumeration, impossible without workspace metadata). The Angular and Nx deprecations do not transfer: cwd inference is built into the resolution order, and per-environment flexibility is open question 2. + +The `-C` scheme does not change this conclusion. Tools with `-C`-style flags (git, make, tar, ninja, terraform, pnpm, yarn, bun) ship the flag with no config-file default at all, and tools that do have a directory config name it after the mechanism precisely because it applies to everything they run (just's `set working-directory`, GitHub Actions' `defaults.run.working-directory`, per-task `cwd` in vp's own `run.tasks`). `defaultPackage` is neither: it selects a member, only for the app commands, only when bare at the root. A mechanism name like `defaultCwd` or `defaultDir` would promise vp-wide effect it does not have; the member-selection name matches its member-selection scope.