Skip to content
Open
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

- **Perf** `vp run` starts faster in workspaces whose tasks share cache `input` and `output` patterns: each distinct pattern is now parsed once instead of once for every task that uses it. In a 138-package workspace, this saves about 2 seconds of CPU on every run ([#887](https://github.com/voidzero-dev/vite-task/pull/887)).
- **Perf** When tasks miss the cache, `vp run` no longer waits for the disk after saving each task's result, so the run finishes sooner. If the computer loses power or the operating system crashes during `vp run`, the cache entries that the run saved can be lost, and those tasks run again next time instead of hitting the cache ([#871](https://github.com/voidzero-dev/vite-task/pull/871)).
- **Fixed** Running `vp run` in a package that sits inside a workspace folder but isn't listed in its `pnpm-workspace.yaml` `packages` or `package.json` `workspaces` now runs that package's own tasks, instead of treating the enclosing workspace as its root. When both configurations exist, `pnpm-workspace.yaml` takes precedence ([#859](https://github.com/voidzero-dev/vite-task/pull/859)).
- **Fixed** A mistyped or misplaced setting in a task now gets an error that names it. For example, `cache: { outputs: [...] }` now fails with an `unknown field` error that names `outputs` and lists the accepted settings, instead of `data did not match any variant of untagged enum UserTaskDefinition` ([vite-plus#2635](https://github.com/voidzero-dev/vite-plus/issues/2635), [#815](https://github.com/voidzero-dev/vite-task/pull/815)).
Expand Down
81 changes: 69 additions & 12 deletions crates/vt_graph/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
pub mod user;

use std::{collections::BTreeSet, sync::Arc};
use std::{
collections::BTreeSet,
sync::{Arc, LazyLock, Mutex, PoisonError},
};

use rustc_hash::FxHashSet;
use rustc_hash::{FxHashMap, FxHashSet};
use serde::Serialize;
pub use user::{
AutoTracking, Command, EnabledCacheConfig, GlobWithBase, InputBase, ResolvedGlobalCacheConfig,
Expand Down Expand Up @@ -291,13 +294,9 @@ fn resolve_glob_to_workspace_relative(
pattern
};

let glob = wax::Glob::new(pattern).map_err(|source| ResolveTaskConfigError::InvalidGlob {
pattern: Str::from(pattern),
source: Box::new(source),
})?;
let (invariant_prefix, variant) = glob.partition();
let partitioned = partition_glob(pattern)?;

let joined = package_dir.join(&invariant_prefix).clean();
let joined = package_dir.join(partitioned.invariant_prefix.as_str()).clean();
let stripped = joined.strip_prefix(workspace_root).map_err(|_| {
ResolveTaskConfigError::GlobOutsideWorkspace { pattern: Str::from(pattern) }
})?;
Expand All @@ -309,10 +308,8 @@ fn resolve_glob_to_workspace_relative(

let escaped_prefix = wax::escape(stripped.as_str());

let result = match variant {
Some(variant_glob) if escaped_prefix.is_empty() => {
Str::from(variant_glob.to_string().as_str())
}
let result = match &partitioned.variant {
Some(variant_glob) if escaped_prefix.is_empty() => variant_glob.clone(),
Some(variant_glob) => vt_str::format!("{escaped_prefix}/{variant_glob}"),
None if escaped_prefix.is_empty() => Str::from("**"),
None => Str::from(escaped_prefix.as_ref()),
Expand All @@ -321,6 +318,45 @@ fn resolve_glob_to_workspace_relative(
Ok(result)
}

/// A glob pattern split into its literal prefix and the glob that follows it.
struct PartitionedGlob {
invariant_prefix: Str,
variant: Option<Str>,
}

/// Parse and partition `pattern`, at most once per distinct pattern in the process.
///
/// `wax` compiles a regular expression both when it parses a glob and when it partitions one,
/// and only the partition's text is kept here. The result depends on the pattern alone, while
/// workspaces repeat the same patterns across packages and tasks, so loading a task graph would
/// otherwise compile the same expressions thousands of times.
fn partition_glob(pattern: &str) -> Result<Arc<PartitionedGlob>, ResolveTaskConfigError> {
static PARTITIONED: LazyLock<Mutex<FxHashMap<Str, Arc<PartitionedGlob>>>> =
LazyLock::new(Mutex::default);

if let Some(partitioned) =
PARTITIONED.lock().unwrap_or_else(PoisonError::into_inner).get(pattern)
{
return Ok(Arc::clone(partitioned));
}

let invalid = |source| ResolveTaskConfigError::InvalidGlob {
pattern: Str::from(pattern),
source: Box::new(source),
};
let (invariant_prefix, variant) = wax::Glob::new(pattern).map_err(invalid)?.partition();
let partitioned = Arc::new(PartitionedGlob {
// The prefix is made of the pattern's own literal components, so this is lossless.
invariant_prefix: Str::from(invariant_prefix.to_string_lossy().as_ref()),
variant: variant.map(|glob| Str::from(glob.to_string().as_str())),
});
PARTITIONED
.lock()
.unwrap_or_else(PoisonError::into_inner)
.insert(Str::from(pattern), Arc::clone(&partitioned));
Ok(partitioned)
}

#[derive(Debug, Clone, Serialize)]
pub struct EnvConfig {
/// environment variable names to be fingerprinted and passed to the task, with defaults populated
Expand Down Expand Up @@ -624,6 +660,27 @@ mod tests {
);
}

#[test]
fn test_resolved_input_config_same_pattern_in_two_packages() {
// A pattern is parsed once per process; each package still resolves it against its own
// directory, and the workspace check still applies to each use.
let (pkg, ws) = test_paths();
let other = pkg.parent().unwrap().join("other-pkg");
let user_inputs = vec![UserInputEntry::Glob("src/**".into())];
let own = ResolvedGlobConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap();
let from_other =
ResolvedGlobConfig::from_user_config(Some(&user_inputs), &other, &ws).unwrap();
assert!(own.positive_globs.contains("packages/my-pkg/src/**"), "{:?}", own.positive_globs);
assert!(
from_other.positive_globs.contains("packages/other-pkg/src/**"),
"{:?}",
from_other.positive_globs
);
let user_inputs = vec![UserInputEntry::Glob("../../../outside/**".into())];
assert!(ResolvedGlobConfig::from_user_config(Some(&user_inputs), &pkg, &ws).is_err());
assert!(ResolvedGlobConfig::from_user_config(Some(&user_inputs), &other, &ws).is_err());
}

#[test]
fn test_resolved_input_config_outside_workspace_error() {
let (pkg, ws) = test_paths();
Expand Down