Skip to content

Commit db0eaf1

Browse files
authored
fix(plan): use task's cwd for synthetic requests instead of context cwd (#90)
# Lazy Cache Initialization and Improved Subpackage Task Execution This PR makes two key improvements: 1. Adds lazy initialization of the SQLite cache to prevent race conditions when multiple processes start simultaneously. The cache is now created only when first accessed, using a file lock to ensure thread safety. 2. Fixes synthetic command execution in subpackages by ensuring commands use the task's resolved working directory rather than the original invocation context. This ensures tools like oxlint run in the correct package directory when invoked through a synthetic command. Additional changes: - Switched from `DiGraph` to `StableGraph` to preserve node indices during removal - Added test fixtures for synthetic commands in subpackages - Improved documentation for CLI argument handling - Added `custom_subcommand()` method to inspect custom subcommands before session creation
1 parent b0067a9 commit db0eaf1

File tree

17 files changed

+262
-27
lines changed

17 files changed

+262
-27
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ futures-util = "0.3.31"
7676
insta = "1.44.3"
7777
libc = "0.2.172"
7878
memmap2 = "0.9.7"
79+
monostate = "1.0.2"
7980
nix = { version = "0.30.1", features = ["dir"] }
8081
ntapi = "0.4.1"
82+
once_cell = "1.19"
8183
os_str_bytes = "7.1.1"
8284
ouroboros = "0.18.5"
8385
owo-colors = "4.1.0"

crates/vite_task/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ derive_more = { workspace = true, features = ["from"] }
2121
diff-struct = { workspace = true }
2222
fspy = { workspace = true }
2323
futures-util = { workspace = true }
24+
once_cell = { workspace = true }
2425
owo-colors = { workspace = true }
2526
petgraph = { workspace = true }
2627
rayon = { workspace = true }

crates/vite_task/src/cli/mod.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,37 @@ use vite_str::Str;
66
use vite_task_graph::{TaskSpecifier, query::TaskQueryKind};
77
use vite_task_plan::plan_request::{PlanOptions, PlanRequest, QueryPlanRequest};
88

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

16+
impl<CustomSubcommand: Subcommand> TaskCLIArgs<CustomSubcommand> {
17+
/// Inspect the custom subcommand (like lint/install). Returns `None` if it's built-in subcommand
18+
/// The caller should not use this method to actually handle the custom subcommand. Instead, it should
19+
/// private TaskSynthesizer to Session so that vite-task can handle custom subcommands consistently from
20+
/// both direct CLI invocations and invocations in task scripts.
21+
///
22+
/// This method is provided only to make it possible for the caller to behave differently BEFORE and AFTER the session.
23+
/// For example, vite+ needs this method to skip auto-install when the custom subcommand is already `install`.
24+
pub fn custom_subcommand(&self) -> Option<&CustomSubcommand> {
25+
match &self.parsed {
26+
ParsedTaskCLIArgs::BuiltIn(_) => None,
27+
ParsedTaskCLIArgs::Custom(custom) => Some(custom),
28+
}
29+
}
30+
}
31+
32+
/// Represents the overall CLI arguments, containing three kinds of subcommands:
33+
/// 1. Built-in subcommands handled by vite-task (like run)
34+
/// 2. Custom subcommands handled by vite-task with the help of TaskSyntheizer (like lint)
35+
/// 3. Custom subcommands not handled by vite-task (like vite+ commands without cache)
1636
pub enum CLIArgs<CustomSubcommand: Subcommand, NonTaskSubcommand: Subcommand> {
17-
/// vite-task's own built-in subcommands
37+
/// Subcommands handled by vite task, including built-in (like run) and custom (like lint)
1838
Task(TaskCLIArgs<CustomSubcommand>),
19-
/// custom subcommands provided by vite+
39+
/// Custom subcommands not handled by vite task (like vite+ commands without cache)
2040
NonTask(NonTaskSubcommand),
2141
}
2242

crates/vite_task/src/session/cache/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
pub mod display;
44

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

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

74+
// Use file lock to prevent race conditions when multiple processes initialize the database
75+
let lock_path = path.join("db_open.lock");
76+
let lock_file = File::create(lock_path.as_path())?;
77+
lock_file.lock()?;
78+
7479
let db_path = path.join("cache.db");
7580
let conn = Connection::open(db_path.as_path())?;
7681
conn.execute_batch("PRAGMA journal_mode=WAL;")?;
@@ -101,6 +106,7 @@ impl ExecutionCache {
101106
}
102107
}
103108
}
109+
// Lock is released when lock_file is dropped
104110
Ok(Self { conn: Mutex::new(conn) })
105111
}
106112

crates/vite_task/src/session/execute/mod.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pub mod spawn;
44
use std::sync::Arc;
55

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

5353
// To be consistent with the package graph in vite_package_manager and the dependency graph definition in Wikipedia
5454
// https://en.wikipedia.org/wiki/Dependency_graph, we construct the graph with edges from dependents to dependencies
@@ -339,10 +339,24 @@ impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> {
339339
plan: ExecutionPlan,
340340
mut reporter: Box<dyn Reporter>,
341341
) -> Result<(), ExitStatus> {
342+
// Lazily initialize the cache on first execution
343+
let cache = match self.cache() {
344+
Ok(cache) => cache,
345+
Err(err) => {
346+
reporter.handle_event(ExecutionEvent {
347+
execution_id: ExecutionId::zero(),
348+
kind: ExecutionEventKind::Error {
349+
message: format!("Failed to initialize cache: {err}"),
350+
},
351+
});
352+
return Err(ExitStatus(1));
353+
}
354+
};
355+
342356
let mut execution_context = ExecutionContext {
343357
event_handler: &mut *reporter,
344358
current_execution_id: ExecutionId::zero(),
345-
cache: &self.cache,
359+
cache,
346360
cache_base_path: &self.workspace_path,
347361
};
348362

crates/vite_task/src/session/mod.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use cache::ExecutionCache;
1010
pub use cache::{CacheMiss, FingerprintMismatch};
1111
use clap::{Parser, Subcommand};
1212
pub use event::ExecutionEvent;
13+
use once_cell::sync::OnceCell;
1314
pub use reporter::{LabeledReporter, Reporter};
1415
use vite_path::{AbsolutePath, AbsolutePathBuf};
1516
use vite_str::Str;
@@ -142,7 +143,10 @@ pub struct Session<'a, CustomSubcommand> {
142143

143144
plan_request_parser: PlanRequestParser<'a, CustomSubcommand>,
144145

145-
cache: ExecutionCache,
146+
/// Cache is lazily initialized to avoid SQLite race conditions when multiple
147+
/// processes (e.g., parallel `vite lib` commands) start simultaneously.
148+
cache: OnceCell<ExecutionCache>,
149+
cache_path: AbsolutePathBuf,
146150
}
147151

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

184-
if !cache_path.as_path().exists()
185-
&& let Some(cache_dir) = cache_path.as_path().parent()
186-
{
187-
tracing::info!("Creating task cache directory at {}", cache_dir.display());
188-
std::fs::create_dir_all(cache_dir)?;
189-
}
190-
let cache = ExecutionCache::load_from_path(cache_path)?;
188+
// Cache is lazily initialized on first access to avoid SQLite race conditions
191189
Ok(Self {
192190
workspace_path: Arc::clone(&workspace_root.path),
193191
lazy_task_graph: LazyTaskGraph::Uninitialized {
@@ -197,12 +195,16 @@ impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> {
197195
envs: Arc::new(envs),
198196
cwd,
199197
plan_request_parser: PlanRequestParser { task_synthesizer: callbacks.task_synthesizer },
200-
cache,
198+
cache: OnceCell::new(),
199+
cache_path,
201200
})
202201
}
203202

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

208210
pub fn workspace_path(&self) -> Arc<AbsolutePath> {

crates/vite_task_bin/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ path = "src/main.rs"
1414
anyhow = { workspace = true }
1515
async-trait = { workspace = true }
1616
clap = { workspace = true, features = ["derive"] }
17-
monostate = "1.0.2"
17+
monostate = { workspace = true }
1818
tokio = { workspace = true, features = ["full"] }
1919
vite_path = { workspace = true }
2020
vite_str = { workspace = true }
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"scripts": {
3+
"lint": "vite run a#lint"
4+
}
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "a",
3+
"scripts": {
4+
"lint": "vite lint"
5+
}
6+
}

0 commit comments

Comments
 (0)