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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ futures-util = "0.3.31"
insta = "1.44.3"
libc = "0.2.172"
memmap2 = "0.9.7"
monostate = "1.0.2"
nix = { version = "0.30.1", features = ["dir"] }
ntapi = "0.4.1"
once_cell = "1.19"
os_str_bytes = "7.1.1"
ouroboros = "0.18.5"
owo-colors = "4.1.0"
Expand Down
1 change: 1 addition & 0 deletions crates/vite_task/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ derive_more = { workspace = true, features = ["from"] }
diff-struct = { workspace = true }
fspy = { workspace = true }
futures-util = { workspace = true }
once_cell = { workspace = true }
owo-colors = { workspace = true }
petgraph = { workspace = true }
rayon = { workspace = true }
Expand Down
26 changes: 23 additions & 3 deletions crates/vite_task/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,37 @@ use vite_str::Str;
use vite_task_graph::{TaskSpecifier, query::TaskQueryKind};
use vite_task_plan::plan_request::{PlanOptions, PlanRequest, QueryPlanRequest};

/// Represents the CLI arguments handled by vite-task, including both built-in and custom subcommands.
/// Represents the CLI arguments handled by vite-task, including both built-in (like run) and custom subcommands (like lint).
#[derive(Debug)]
pub struct TaskCLIArgs<CustomSubcommand: Subcommand> {
pub(crate) original: Arc<[Str]>,
pub(crate) parsed: ParsedTaskCLIArgs<CustomSubcommand>,
}

impl<CustomSubcommand: Subcommand> TaskCLIArgs<CustomSubcommand> {
/// Inspect the custom subcommand (like lint/install). Returns `None` if it's built-in subcommand
/// The caller should not use this method to actually handle the custom subcommand. Instead, it should
/// private TaskSynthesizer to Session so that vite-task can handle custom subcommands consistently from
/// both direct CLI invocations and invocations in task scripts.
///
/// This method is provided only to make it possible for the caller to behave differently BEFORE and AFTER the session.
/// For example, vite+ needs this method to skip auto-install when the custom subcommand is already `install`.
pub fn custom_subcommand(&self) -> Option<&CustomSubcommand> {
match &self.parsed {
ParsedTaskCLIArgs::BuiltIn(_) => None,
ParsedTaskCLIArgs::Custom(custom) => Some(custom),
}
}
}

/// Represents the overall CLI arguments, containing three kinds of subcommands:
/// 1. Built-in subcommands handled by vite-task (like run)
/// 2. Custom subcommands handled by vite-task with the help of TaskSyntheizer (like lint)
/// 3. Custom subcommands not handled by vite-task (like vite+ commands without cache)
pub enum CLIArgs<CustomSubcommand: Subcommand, NonTaskSubcommand: Subcommand> {
/// vite-task's own built-in subcommands
/// Subcommands handled by vite task, including built-in (like run) and custom (like lint)
Task(TaskCLIArgs<CustomSubcommand>),
/// custom subcommands provided by vite+
/// Custom subcommands not handled by vite task (like vite+ commands without cache)
NonTask(NonTaskSubcommand),
}

Expand Down
8 changes: 7 additions & 1 deletion crates/vite_task/src/session/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

pub mod display;

use std::{fmt::Display, io::Write, sync::Arc, time::Duration};
use std::{fmt::Display, fs::File, io::Write, sync::Arc, time::Duration};

use bincode::{Decode, Encode, decode_from_slice, encode_to_vec};
// Re-export display functions for convenience
Expand Down Expand Up @@ -71,6 +71,11 @@ impl ExecutionCache {
tracing::info!("Creating task cache directory at {:?}", path);
std::fs::create_dir_all(path)?;

// Use file lock to prevent race conditions when multiple processes initialize the database
let lock_path = path.join("db_open.lock");
let lock_file = File::create(lock_path.as_path())?;
lock_file.lock()?;

let db_path = path.join("cache.db");
let conn = Connection::open(db_path.as_path())?;
conn.execute_batch("PRAGMA journal_mode=WAL;")?;
Expand Down Expand Up @@ -101,6 +106,7 @@ impl ExecutionCache {
}
}
}
// Lock is released when lock_file is dropped
Ok(Self { conn: Mutex::new(conn) })
}

Expand Down
24 changes: 19 additions & 5 deletions crates/vite_task/src/session/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub mod spawn;
use std::sync::Arc;

use futures_util::FutureExt;
use petgraph::{algo::toposort, graph::DiGraph};
use petgraph::{algo::toposort, stable_graph::StableGraph};
use vite_path::AbsolutePath;
use vite_task_plan::{
ExecutionItemKind, ExecutionPlan, LeafExecutionKind, SpawnExecution, TaskExecution,
Expand Down Expand Up @@ -46,9 +46,9 @@ impl ExecutionContext<'_> {
) -> Result<(), ExecutionAborted> {
match item_kind {
ExecutionItemKind::Expanded(graph) => {
// clone for reversing edges and removing nodes
let mut graph: DiGraph<&TaskExecution, (), ExecutionIx> =
graph.map(|_, task_execution| task_execution, |_, ()| ());
// Use StableGraph to preserve node indices during removal
let mut graph: StableGraph<&TaskExecution, (), _, ExecutionIx> =
graph.map(|_, task_execution| task_execution, |_, ()| ()).into();

// To be consistent with the package graph in vite_package_manager and the dependency graph definition in Wikipedia
// https://en.wikipedia.org/wiki/Dependency_graph, we construct the graph with edges from dependents to dependencies
Expand Down Expand Up @@ -339,10 +339,24 @@ impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> {
plan: ExecutionPlan,
mut reporter: Box<dyn Reporter>,
) -> Result<(), ExitStatus> {
// Lazily initialize the cache on first execution
let cache = match self.cache() {
Ok(cache) => cache,
Err(err) => {
reporter.handle_event(ExecutionEvent {
execution_id: ExecutionId::zero(),
kind: ExecutionEventKind::Error {
message: format!("Failed to initialize cache: {err}"),
},
});
return Err(ExitStatus(1));
}
};

let mut execution_context = ExecutionContext {
event_handler: &mut *reporter,
current_execution_id: ExecutionId::zero(),
cache: &self.cache,
cache,
cache_base_path: &self.workspace_path,
};

Expand Down
24 changes: 13 additions & 11 deletions crates/vite_task/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use cache::ExecutionCache;
pub use cache::{CacheMiss, FingerprintMismatch};
use clap::{Parser, Subcommand};
pub use event::ExecutionEvent;
use once_cell::sync::OnceCell;
pub use reporter::{LabeledReporter, Reporter};
use vite_path::{AbsolutePath, AbsolutePathBuf};
use vite_str::Str;
Expand Down Expand Up @@ -142,7 +143,10 @@ pub struct Session<'a, CustomSubcommand> {

plan_request_parser: PlanRequestParser<'a, CustomSubcommand>,

cache: ExecutionCache,
/// Cache is lazily initialized to avoid SQLite race conditions when multiple
/// processes (e.g., parallel `vite lib` commands) start simultaneously.
cache: OnceCell<ExecutionCache>,
cache_path: AbsolutePathBuf,
}

fn get_cache_path_of_workspace(workspace_root: &AbsolutePath) -> AbsolutePathBuf {
Expand Down Expand Up @@ -181,13 +185,7 @@ impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> {
let workspace_node_modules_bin = workspace_root.path.join("node_modules").join(".bin");
prepend_path_env(&mut envs, &workspace_node_modules_bin)?;

if !cache_path.as_path().exists()
&& let Some(cache_dir) = cache_path.as_path().parent()
{
tracing::info!("Creating task cache directory at {}", cache_dir.display());
std::fs::create_dir_all(cache_dir)?;
}
let cache = ExecutionCache::load_from_path(cache_path)?;
// Cache is lazily initialized on first access to avoid SQLite race conditions
Ok(Self {
workspace_path: Arc::clone(&workspace_root.path),
lazy_task_graph: LazyTaskGraph::Uninitialized {
Expand All @@ -197,12 +195,16 @@ impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> {
envs: Arc::new(envs),
cwd,
plan_request_parser: PlanRequestParser { task_synthesizer: callbacks.task_synthesizer },
cache,
cache: OnceCell::new(),
cache_path,
})
}

pub fn cache(&self) -> &ExecutionCache {
&self.cache
/// Lazily initializes and returns the execution cache.
/// The cache is only created when first accessed to avoid SQLite race conditions
/// when multiple processes start simultaneously.
pub fn cache(&self) -> anyhow::Result<&ExecutionCache> {
self.cache.get_or_try_init(|| ExecutionCache::load_from_path(self.cache_path.clone()))
}

pub fn workspace_path(&self) -> Arc<AbsolutePath> {
Expand Down
2 changes: 1 addition & 1 deletion crates/vite_task_bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ path = "src/main.rs"
anyhow = { workspace = true }
async-trait = { workspace = true }
clap = { workspace = true, features = ["derive"] }
monostate = "1.0.2"
monostate = { workspace = true }
tokio = { workspace = true, features = ["full"] }
vite_path = { workspace = true }
vite_str = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"scripts": {
"lint": "vite run a#lint"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "a",
"scripts": {
"lint": "vite lint"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
packages:
- 'packages/*'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[[plan]]
name = "synthetic-in-subpackage"
args = ["run", "lint"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
source: crates/vite_task_bin/tests/test_snapshots/main.rs
expression: "&plan_json"
input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage
---
{
"root_node": {
"Expanded": [
{
"key": [
"<workspace>/",
"lint"
],
"node": {
"task_display": {
"package_name": "",
"task_name": "lint",
"package_path": "<workspace>/"
},
"items": [
{
"execution_item_display": {
"task_display": {
"package_name": "",
"task_name": "lint",
"package_path": "<workspace>/"
},
"command": "vite run a#lint",
"and_item_index": null,
"cwd": "<workspace>/"
},
"kind": {
"Expanded": [
{
"key": [
"<workspace>/packages/a",
"lint"
],
"node": {
"task_display": {
"package_name": "a",
"task_name": "lint",
"package_path": "<workspace>/packages/a"
},
"items": [
{
"execution_item_display": {
"task_display": {
"package_name": "a",
"task_name": "lint",
"package_path": "<workspace>/packages/a"
},
"command": "vite lint",
"and_item_index": null,
"cwd": "<workspace>/packages/a"
},
"kind": {
"Leaf": {
"Spawn": {
"cache_metadata": {
"spawn_fingerprint": {
"cwd": "packages/a",
"program_fingerprint": {
"OutsideWorkspace": {
"program_name": "oxlint"
}
},
"args": [],
"env_fingerprints": {
"fingerprinted_envs": {},
"pass_through_env_config": [
"<default pass-through envs>"
]
},
"fingerprint_ignores": null
},
"execution_cache_key": {
"kind": {
"UserTask": {
"task_name": "lint",
"and_item_index": 0,
"extra_args": []
}
},
"origin_path": "packages/a"
}
},
"spawn_command": {
"program_path": "<manifest_dir>/test_bins/node_modules/.bin/oxlint",
"args": [],
"all_envs": {
"NO_COLOR": "1",
"PATH": "<workspace>/packages/a/node_modules/.bin:<workspace>/node_modules/.bin:<manifest_dir>/test_bins/node_modules/.bin"
},
"cwd": "<workspace>/packages/a"
}
}
}
}
}
]
},
"neighbors": []
}
]
}
}
]
},
"neighbors": []
}
]
}
}
Loading