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** A task specifier like `vp run nope#build`, where no workspace package is named `nope`, now fails with `Package 'nope' not found in the workspace` and exits 1, including from a task's command, instead of running nothing and exiting 0. A short name that auto-completes to several scoped packages, like `app` for `@a/app` and `@b/app`, now fails as ambiguous. `--filter nope` still only warns and exits 0, unless `--fail-if-no-match` is set ([#984](https://github.com/voidzero-dev/vite-task/pull/984)).
- **Added** `vp run -vv` also lists what each cached run depends on, changed or not, and the output files a hit restores: in the detailed summary, under `Unchanged:` and `Outputs:`, and with `--dry-run --json`, as each step's `baseline.fingerprint` and `baseline.outputs`. It shows, for an unexpected hit, whether an edited file is an input at all. `-v` is now a counted flag, so `-vv` is the same as `--verbose --verbose` ([#969](https://github.com/voidzero-dev/vite-task/pull/969)).
- **Added** `vp run --dry-run --json` prints what a run would do as one JSON document for scripts, CI and coding agents: each task the run would run, the tasks it waits for, and each step's program, arguments and directory, with whether it would hit the cache and, if not, every change found. Like `vp run --json`, it's indented in a terminal and on one line when captured, and its `$schema` field points to a JSON Schema that describes each field. A task that runs `vp run` shows that run's own plan as its step. A step that waits on one that would write files first, by running or by restoring outputs, is `unknown`, since its files may change before it starts. `--json` with a task requires `--dry-run` ([#966](https://github.com/voidzero-dev/vite-task/pull/966), [#967](https://github.com/voidzero-dev/vite-task/pull/967), [#968](https://github.com/voidzero-dev/vite-task/pull/968)).
- **Changed** A cache miss now lists every change to the command, the task's cache settings and the envs it depends on, instead of the first: when a task's `input` and `output` settings changed together, or a tracked env var and a file changed together, `vp run --verbose` and `vp run --last-details` show each change on its own line. Without `-v`, files are checked only up to the first changed one; with `-v`, every input of a miss is checked, and every changed file is listed in path order. When the command, a setting or a `cache.env` var changed, only those changes are listed, since there's no cached run with the new ones to compare files with. A summary saved by an earlier version can't be shown by `vp run --last-details` ([#961](https://github.com/voidzero-dev/vite-task/pull/961), [#963](https://github.com/voidzero-dev/vite-task/pull/963)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,17 @@ comment = """
Strict mode also propagates through nesting: a task command that adds `--fail-if-no-match` aborts the outer task when the nested filter selects nothing. The error chain identifies both the nested command and the unmatched filter source.
"""
steps = [["vt", "run", "filter-nonexistent-strict"]]

[[e2e]]
name = "unknown_package_in_task_specifier_errors"
comment = """
A `package#task` specifier names its package explicitly, so a package that doesn't exist is an error rather than an empty `--filter`: the run exits 1, and the error goes to stderr with nothing on stdout.
"""
steps = [["vtt", "pipe-stdout", "--", "vt", "run", "nonexistent#build"]]

[[e2e]]
name = "nested_unknown_package_in_task_specifier_errors"
comment = """
A nested `vt run nonexistent#build` fails the outer task's planning too, unlike the nested `--filter nonexistent` above. The error chain names the nested command and the missing package.
"""
steps = [["vt", "run", "run-unknown-package"]]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# nested_unknown_package_in_task_specifier_errors

A nested `vt run nonexistent#build` fails the outer task's planning too, unlike the nested `--filter nonexistent` above. The error chain names the nested command and the missing package.

## `vt run run-unknown-package`

**Exit code:** 1

```
error: Failed to plan tasks from `vt run nonexistent#build` in task filter-unmatched-test#run-unknown-package
* Package 'nonexistent' not found in the workspace
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# unknown_package_in_task_specifier_errors

A `package#task` specifier names its package explicitly, so a package that doesn't exist is an error rather than an empty `--filter`: the run exits 1, and the error goes to stderr with nothing on stdout.

## `vtt pipe-stdout -- vt run nonexistent#build`

**Exit code:** 1

```
error: Package 'nonexistent' not found in the workspace
--- stdout ---
```
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"filter-nonexistent-strict": {
"command": "vt run --filter nonexistent --fail-if-no-match build",
"cache": false
},
"run-unknown-package": {
"command": "vt run nonexistent#build",
"cache": false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ steps = [
"--dry-run",
"--json",
"nope#build",
], comment = "No package is named `nope`, so, as in a real run, the plan is empty.", redact = { '"\$schema":"[^"]*"' = '"$$schema":"<schema>"' } },
], comment = "No package is named `nope`, so, as in a real run, it's an error and nothing goes to stdout." },
{ argv = [
"vtt",
"pipe-stdout",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ error: Task "typo" not found

## `vtt pipe-stdout -- vt run --dry-run --json nope#build`

No package is named `nope`, so, as in a real run, the plan is empty.
No package is named `nope`, so, as in a real run, it's an error and nothing goes to stdout.

**Exit code:** 1

```
error: Package 'nope' not found in the workspace
--- stdout ---
{"$schema":"<schema>","version":1,"workspaceRoot":"<workspace>","plan":{}}
```

## `vtt pipe-stdout -- vt run --dry-run --json --filter nope build`
Expand Down
4 changes: 2 additions & 2 deletions crates/vt_graph/src/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ impl IndexedTaskGraph<'_> {
///
/// # Errors
///
/// Returns [`PackageQueryResolveError::AmbiguousPackageName`] when an exact
/// package name (from a `pkg#task` specifier) matches multiple packages, and
/// Returns [`PackageQueryResolveError`] when an exact package name (from a
/// `pkg#task` specifier) matches no package or several, and
/// [`TaskGraphLoadError`] when the tasks of a selected package fail to load.
///
/// # Order of operations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,10 @@ args = ["run", "-t", "@test/duplicate#build"]
compact = true
name = "filter_matches_both_duplicates"
args = ["run", "--filter", "@test/duplicate", "build"]

# The short name `duplicate` auto-completes to `@test/duplicate`, which two
# packages share. A `pkg#task` specifier errors instead of matching neither.
[[plan]]
compact = true
name = "ambiguous_short_package_name"
args = ["run", "duplicate#build"]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Package name 'duplicate' is ambiguous; found in multiple locations: <workspace>/packages/pkg-a, <workspace>/packages/pkg-b
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ compact = true
name = "transitive_with_package_specifier"
args = ["run", "-t", "@test/app#build"]

# package#task with a short name auto-completes to the one scoped package.
[[plan]]
compact = true
name = "short_package_specifier"
args = ["run", "app#build"]

# recursive flag = All
[[plan]]
compact = true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// run app#build
{
"packages/app#build": []
}
12 changes: 7 additions & 5 deletions crates/vt_workspace/src/package_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ pub(crate) enum PackageNamePattern {
/// but exactly one `@*/bar` package exists, that package is matched.
/// pnpm ref: <https://github.com/pnpm/pnpm/blob/491a84fb26fa716408bf6bd361680f6a450c61fc/workspace/filter-workspace-packages/src/index.ts#L303-L306>
///
/// When `unique` is true, resolution errors if multiple packages share the
/// name. Set for `pkg#task` CLI specifiers; false for `--filter`.
Exact { name: Str, unique: bool },
/// When `exactly_one` is true, resolution errors unless the name picks
/// exactly one package: none found, several sharing the name, or several
/// scoped candidates. Set for `pkg#task` CLI specifiers; false for
/// `--filter`, where an unmatched name only warns.
Exact { name: Str, exactly_one: bool },

/// Glob pattern (e.g. `@scope/*`, `*-utils`). Iterates all packages.
///
Expand Down Expand Up @@ -404,7 +406,7 @@ impl PackageQueryArgs {
exclude: false,
selector: PackageSelector::Name(PackageNamePattern::Exact {
name,
unique: true,
exactly_one: true,
}),
traversal,
source: None,
Expand Down Expand Up @@ -613,7 +615,7 @@ fn build_name_pattern(name: &str) -> Result<PackageNamePattern, PackageFilterPar
if glob.clone().partition().1.is_some() {
Ok(PackageNamePattern::Glob(Box::new(GlobPattern::new(glob, name.into()))))
} else {
Ok(PackageNamePattern::Exact { name: name.into(), unique: false })
Ok(PackageNamePattern::Exact { name: name.into(), exactly_one: false })
}
}

Expand Down
50 changes: 37 additions & 13 deletions crates/vt_workspace/src/package_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ pub enum PackageQueryResolveError {
package_paths.iter().map(|p| p.as_path().display().to_string()).collect::<Vec<_>>().join(", ")
)]
AmbiguousPackageName { package_name: Str, package_paths: Box<[Arc<AbsolutePath>]> },

#[error("Package '{package_name}' not found in the workspace")]
PackageNotFound { package_name: Str },
}

// ────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -206,7 +209,8 @@ impl IndexedPackageGraph {
/// # Errors
///
/// Returns [`PackageQueryResolveError::AmbiguousPackageName`] when an exact
/// package name (from a `pkg#task` specifier) matches multiple packages.
/// package name (from a `pkg#task` specifier) matches multiple packages, and
/// [`PackageQueryResolveError::PackageNotFound`] when it matches none.
pub fn resolve_query(
&self,
query: &PackageQuery,
Expand Down Expand Up @@ -343,29 +347,24 @@ impl IndexedPackageGraph {
/// For `Exact` names, scoped auto-completion applies
/// (pnpm ref: <https://github.com/pnpm/pnpm/blob/491a84fb26fa716408bf6bd361680f6a450c61fc/workspace/filter-workspace-packages/src/index.ts#L303-L306>):
/// if `"bar"` has no exact match but exactly one `@*/bar` package exists,
/// that package is used instead.
/// that package is used instead. An `exactly_one` name that still matches
/// no package, or several, is an error.
fn match_by_name_pattern(
&self,
pattern: &PackageNamePattern,
out: &mut FxHashSet<PackageNodeIndex>,
) -> Result<(), PackageQueryResolveError> {
match pattern {
PackageNamePattern::Exact { name, unique } => {
PackageNamePattern::Exact { name, exactly_one } => {
if let Some(indices) = self.get_package_indices_by_name(name) {
if *unique && indices.len() > 1 {
return Err(PackageQueryResolveError::AmbiguousPackageName {
package_name: name.clone(),
package_paths: indices
.iter()
.map(|i| Arc::clone(&self.graph[*i].absolute_path))
.collect(),
});
if *exactly_one && indices.len() > 1 {
return Err(self.ambiguous_package_name(name, indices));
}
out.extend(indices.iter().copied());
} else {
// Scoped auto-completion: `"bar"` → `"@scope/bar"` if exactly one match.
let scoped_suffix = vt_str::format!("/{}", name);
let scoped: Vec<_> = self
let mut scoped: Vec<_> = self
.indices_by_name
.iter()
.filter(|(pkg_name, _)| {
Expand All @@ -375,8 +374,18 @@ impl IndexedPackageGraph {
.collect();
if scoped.len() == 1 {
out.insert(scoped[0]);
} else if *exactly_one {
if scoped.is_empty() {
return Err(PackageQueryResolveError::PackageNotFound {
package_name: name.clone(),
});
}
// Sorted so the listed paths don't depend on hash order.
scoped.sort_unstable();
return Err(self.ambiguous_package_name(name, &scoped));
}
// 0 matches: nothing to insert.
// For `--filter`:
// 0 matches: nothing to insert; the caller warns about the filter.
// >1 matches: ambiguous; skip rather than guessing wrong.
}
}
Expand All @@ -393,6 +402,21 @@ impl IndexedPackageGraph {
Ok(())
}

/// The error for a `pkg#task` name that matches several packages.
fn ambiguous_package_name(
&self,
name: &Str,
indices: &[PackageNodeIndex],
) -> PackageQueryResolveError {
PackageQueryResolveError::AmbiguousPackageName {
package_name: name.clone(),
package_paths: indices
.iter()
.map(|i| Arc::clone(&self.graph[*i].absolute_path))
.collect(),
}
}

/// Match packages by a directory pattern, inserting into `out`.
///
/// pnpm ref: <https://github.com/pnpm/pnpm/blob/491a84fb26fa716408bf6bd361680f6a450c61fc/workspace/filter-workspace-packages/src/index.ts#L317-L324>
Expand Down
Loading