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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

- **Fixed** Windows builds no longer hang on CI when a `node_modules/.bin` `.cmd` shim is routed through PowerShell: the npm/pnpm/yarn `.ps1` wrappers read stdin and block forever on a non-TTY pipe, so the PowerShell rewrite is now skipped when stdin is not an interactive terminal, falling back to the `.cmd` (which never reads stdin) ([#491](https://github.com/voidzero-dev/vite-task/pull/491)).
- **Added** First-party support for caching `vite build` with zero cache config, giving Vite projects correct cache hits out of the box ([vitejs/vite#22453](https://github.com/vitejs/vite/pull/22453)).
- **Added** Support for specifying tasks from dependency packages in `dependsOn`, such as `dependsOn: [{ "task": "build", "from": "dependencies" }]` ([#479](https://github.com/voidzero-dev/vite-task/pull/479)).
- **Added** [`@voidzero-dev/vite-task-client`](https://npmx.dev/package/@voidzero-dev/vite-task-client), allowing tools to report cache information to Vite Task at runtime so users do not need to configure it manually ([#441](https://github.com/voidzero-dev/vite-task/pull/441), [#454](https://github.com/voidzero-dev/vite-task/pull/454), [#449](https://github.com/voidzero-dev/vite-task/pull/449), [#450](https://github.com/voidzero-dev/vite-task/pull/450), [#458](https://github.com/voidzero-dev/vite-task/pull/458), [#431](https://github.com/voidzero-dev/vite-task/pull/431), [#459](https://github.com/voidzero-dev/vite-task/pull/459), [#472](https://github.com/voidzero-dev/vite-task/pull/472)).
Expand Down
31 changes: 29 additions & 2 deletions crates/vite_powershell/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
//! and lets Ctrl+C propagate cleanly.
//!
//! This crate carries only the platform-shared primitives (the
//! `PowerShell` host lookup, the fixed argument prefix, and the
//! sibling-`.ps1` discovery). Higher-level wrappers in
//! `PowerShell` host lookup, the fixed argument prefix, the
//! sibling-`.ps1` discovery, and the stdin-TTY gate). Higher-level wrappers in
//! `vite_task_plan::ps1_shim` (cwd-relative arg rewrite, scoped to
//! `node_modules/.bin`) and `vite_command::ps1_shim` (absolute-path
//! arg rewrite, applied to any `.cmd`) compose these primitives with
Expand Down Expand Up @@ -76,6 +76,25 @@ pub fn find_ps1_sibling(resolved: &AbsolutePath) -> Option<AbsolutePathBuf> {
Some(ps1)
}

/// Cached `stdin.is_terminal()`. The TTY-ness of stdin is fixed for the
/// process lifetime, so the underlying syscall runs at most once per process.
///
/// Gates the `.cmd` -> PowerShell `.ps1` rewrite that both `vite_task_plan`
/// and `vite_command` perform: the npm/pnpm/yarn `.ps1` wrappers read stdin
/// (`$MyInvocation.ExpectingInput` -> `$input | & node ...`) and hang forever
/// on a non-TTY pipe or null, as on CI runners. Without a terminal there is
/// also no Ctrl+C "Terminate batch job (Y/N)?" prompt to corrupt, so callers
/// fall back to the `.cmd` (which never reads stdin) when this returns `false`.
///
/// See <https://github.com/voidzero-dev/vite-plus/issues/1489>.
#[must_use]
pub fn is_stdin_terminal() -> bool {
use std::{io::IsTerminal, sync::LazyLock};

static IS_TTY: LazyLock<bool> = LazyLock::new(|| std::io::stdin().is_terminal());
*IS_TTY
}

#[cfg(test)]
mod tests {
use std::fs;
Expand Down Expand Up @@ -143,4 +162,12 @@ mod tests {
let resolved = abs(root.as_path().join("node"));
assert!(find_ps1_sibling(&resolved).is_none());
}

#[test]
fn is_stdin_terminal_is_idempotent() {
// The value depends on how the test runner wires stdin (non-TTY under
// nextest), so assert the cached result is stable rather than a fixed
// value.
assert_eq!(is_stdin_terminal(), is_stdin_terminal());
}
}
56 changes: 47 additions & 9 deletions crates/vite_task_plan/src/ps1_shim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
//! left alone even if it happens to live under some other `node_modules/.bin`.
//!
//! Cross-platform primitives (`POWERSHELL_PREFIX`, `powershell_host`,
//! `find_ps1_sibling`) live in the `vite_powershell` crate so
//! `vite_command::ps1_shim` can share them.
//! `find_ps1_sibling`, `is_stdin_terminal`) live in the `vite_powershell`
//! crate so `vite_command::ps1_shim` can share them.
//!
//! See <https://github.com/voidzero-dev/vite-plus/issues/1176>.

Expand All @@ -47,7 +47,14 @@ pub fn rewrite_cmd_shim_with_args(
workspace_root: &AbsolutePath,
) -> (Arc<AbsolutePath>, Arc<[Str]>) {
if let Some(host) = vite_powershell::powershell_host()
&& let Some(rewritten) = rewrite_with_host(&resolved, &args, cwd, workspace_root, host)
&& let Some(rewritten) = rewrite_with_host(
&resolved,
&args,
cwd,
workspace_root,
host,
vite_powershell::is_stdin_terminal(),
)
{
return rewritten;
}
Expand All @@ -74,7 +81,15 @@ fn rewrite_with_host(
cwd: &AbsolutePath,
workspace_root: &AbsolutePath,
host: &Arc<AbsolutePath>,
is_interactive: bool,
) -> Option<(Arc<AbsolutePath>, Arc<[Str]>)> {
// Only route through PowerShell when stdin is an interactive terminal. The
// `.ps1` wrappers hang on a non-TTY stdin pipe (CI), and without a terminal
// there is no Ctrl+C prompt to protect against. See
// `vite_powershell::is_stdin_terminal`.
if !is_interactive {
return None;
}
if !is_in_workspace_node_modules_bin(resolved, workspace_root) {
return None;
}
Expand Down Expand Up @@ -162,7 +177,7 @@ mod tests {
let args: Arc<[Str]> = Arc::from(vec![Str::from("--port"), Str::from("3000")]);

let (program, rewritten_args) =
rewrite_with_host(&resolved, &args, &workspace, &workspace, &host)
rewrite_with_host(&resolved, &args, &workspace, &workspace, &host, true)
.expect("should rewrite");

assert_eq!(program.as_path(), host.as_path());
Expand All @@ -182,6 +197,29 @@ mod tests {
);
}

/// Regression for the CI hang: the npm/pnpm/yarn `.ps1` wrappers read stdin
/// and block forever on a non-TTY pipe, so a structurally-valid shim must
/// NOT be rewritten when stdin is not an interactive terminal. The spawn
/// then falls back to the `.cmd` directly, which never reads stdin.
/// See <https://github.com/voidzero-dev/vite-plus/issues/1489>.
#[test]
fn skips_rewrite_when_not_interactive() {
let dir = tempdir().unwrap();
let workspace = abs(dir.path().canonicalize().unwrap());
let bin = bin_dir(workspace.as_path());
fs::write(bin.join("vite.cmd"), "").unwrap();
fs::write(bin.join("vite.ps1"), "").unwrap();

let host = host_arc(&workspace);
let resolved = abs(bin.join("vite.cmd"));
let args: Arc<[Str]> = Arc::from(vec![Str::from("build")]);

assert!(
rewrite_with_host(&resolved, &args, &workspace, &workspace, &host, false).is_none(),
"non-interactive spawns must not be rewritten through PowerShell"
);
}

#[test]
fn rewrites_cmd_to_cwd_relative_ps1_in_hoisted_monorepo_subpackage() {
// Task cwd is `<ws>/packages/foo`; shim lives at hoisted
Expand All @@ -202,7 +240,7 @@ mod tests {
let args: Arc<[Str]> = Arc::from(vec![]);

let (_program, rewritten_args) =
rewrite_with_host(&resolved, &args, &sub_pkg, &workspace, &host)
rewrite_with_host(&resolved, &args, &sub_pkg, &workspace, &host, true)
.expect("should rewrite");

assert_eq!(
Expand All @@ -222,7 +260,7 @@ mod tests {
let resolved = abs(bin.join("vite.cmd"));
let args: Arc<[Str]> = Arc::from(vec![Str::from("build")]);

assert!(rewrite_with_host(&resolved, &args, &workspace, &workspace, &host).is_none());
assert!(rewrite_with_host(&resolved, &args, &workspace, &workspace, &host, true).is_none());
}

#[test]
Expand All @@ -236,7 +274,7 @@ mod tests {
let resolved = abs(workspace.as_path().join("where.cmd"));
let args: Arc<[Str]> = Arc::from(vec![]);

assert!(rewrite_with_host(&resolved, &args, &workspace, &workspace, &host).is_none());
assert!(rewrite_with_host(&resolved, &args, &workspace, &workspace, &host, true).is_none());
}

#[test]
Expand All @@ -251,7 +289,7 @@ mod tests {
let resolved = abs(bin.join("node.exe"));
let args: Arc<[Str]> = Arc::from(vec![Str::from("--version")]);

assert!(rewrite_with_host(&resolved, &args, &workspace, &workspace, &host).is_none());
assert!(rewrite_with_host(&resolved, &args, &workspace, &workspace, &host, true).is_none());
}

#[test]
Expand All @@ -276,6 +314,6 @@ mod tests {
let resolved = abs(global_bin.join("vite.cmd"));
let args: Arc<[Str]> = Arc::from(vec![]);

assert!(rewrite_with_host(&resolved, &args, &workspace, &workspace, &host).is_none());
assert!(rewrite_with_host(&resolved, &args, &workspace, &workspace, &host, true).is_none());
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Windows-only: verifies the `.cmd` → PowerShell rewrite at plan time, and
# that the resulting cache key stays portable regardless of how the user
# navigated to the sub-package task. Both plan cases below target the same
# task; their snapshots should be byte-identical apart from the plan query
# itself, proving the cache key doesn't depend on invocation style.
# Windows-only. The `.cmd` → PowerShell rewrite at plan time is gated on an
# interactive terminal (see `vite_task_plan::ps1_shim::is_stdin_terminal`): the
# `.ps1` wrappers hang on a non-TTY stdin pipe, as on CI. The test runner has no
# TTY on stdin, so the rewrite is skipped here and the plan records the
# `node_modules/.bin` shim directly instead of `powershell -File …vite.ps1`.
# This still verifies the cache key stays portable regardless of how the user
# navigated to the sub-package task: both plan cases below target the same task
# and their snapshots should be byte-identical apart from the plan query itself,
# proving the cache key doesn't depend on invocation style.
platform = "windows"

# Direct invocation: user `cd`'d into the sub-package before running.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,11 @@
"spawn_fingerprint": {
"cwd": "packages/foo",
"program_fingerprint": {
"OutsideWorkspace": {
"program_name": "<powershell>"
"InsideWorkspace": {
"relative_program_path": "packages/foo/node_modules/.bin/vite.cmd"
}
},
"args": [
"-NoProfile",
"-NoLogo",
"-ExecutionPolicy",
"Bypass",
"-File",
"node_modules/.bin/vite.ps1",
"--port",
"3000"
],
Expand Down Expand Up @@ -72,14 +66,8 @@
}
},
"spawn_command": {
"program_path": "<powershell>",
"program_path": "<workspace>/packages/foo/node_modules/.bin/vite",
"args": [
"-NoProfile",
"-NoLogo",
"-ExecutionPolicy",
"Bypass",
"-File",
"node_modules/.bin/vite.ps1",
"--port",
"3000"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,11 @@
"spawn_fingerprint": {
"cwd": "packages/foo",
"program_fingerprint": {
"OutsideWorkspace": {
"program_name": "<powershell>"
"InsideWorkspace": {
"relative_program_path": "packages/foo/node_modules/.bin/vite.cmd"
}
},
"args": [
"-NoProfile",
"-NoLogo",
"-ExecutionPolicy",
"Bypass",
"-File",
"node_modules/.bin/vite.ps1",
"--port",
"3000"
],
Expand Down Expand Up @@ -72,14 +66,8 @@
}
},
"spawn_command": {
"program_path": "<powershell>",
"program_path": "<workspace>/packages/foo/node_modules/.bin/vite",
"args": [
"-NoProfile",
"-NoLogo",
"-ExecutionPolicy",
"Bypass",
"-File",
"node_modules/.bin/vite.ps1",
"--port",
"3000"
],
Expand Down
Loading