diff --git a/CLAUDE.md b/CLAUDE.md index 993281ce..17b4bd30 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,16 +78,36 @@ vp run # run task in current package vp run # # run task in specific package vp run -r # run task in all packages (recursive) vp run -t # run task in current package + transitive deps -vp run --extra --args # pass extra args to the task command +vp run -- --extra --args # pass extra args to the task command +vp run # interactive task selector (fuzzy search) # Built-in commands (run tools from node_modules/.bin) vp test [args...] # run vitest vp lint [args...] # run oxlint -# Flags --r, --recursive # run across all packages --t, --transitive # run in current package and its dependencies +# Cache management +vp cache clean # remove the cache database + +# Package selection flags +-r, --recursive # select all packages in the workspace +-t, --transitive # select current package + transitive deps +-w, --workspace-root # select the workspace root package +-F, --filter # pnpm-style filter (repeatable, see below) + +# Run flags --ignore-depends-on # skip explicit dependsOn dependencies +-v, --verbose # show full detailed summary after execution +--cache # force caching on for all tasks and scripts +--no-cache # force caching off for all tasks and scripts +--last-details # show detailed summary of the last run + +# Filter patterns (pnpm-style) +-F # select by package name +-F '@scope/*' # select by glob pattern +-F ./ # select packages under a directory +-F '...' # select package and its dependencies +-F '...' # select package and its dependents +-F '!' # exclude packages matching pattern ``` ## Key Architecture @@ -105,16 +125,30 @@ Tasks are defined in `vite-task.json`: ```json { + "cache": true | false | { "scripts": bool, "tasks": bool }, "tasks": { "test": { "command": "vitest run", - "dependsOn": ["build", "lint"], - "cache": true + "cwd": "relative/path", + "dependsOn": ["build", "package#task"], + "cache": true, + "envs": ["NODE_ENV"], + "passThroughEnvs": ["CI"], + "inputs": ["src/**", "!dist/**", { "auto": true }] } } } ``` +- `cache` (root): workspace-wide cache toggle. Default: `{ "scripts": false, "tasks": true }` +- `command`: shell command to run (falls back to package.json script if omitted) +- `cwd`: working directory relative to the package root +- `dependsOn`: explicit task dependencies (`taskName` or `package#task`) +- `cache` (task): enable/disable caching for this task (default: `true`) +- `envs`: env var names to fingerprint and pass to the task +- `passThroughEnvs`: env var names to pass without fingerprinting +- `inputs`: files for cache fingerprinting (globs, `{ "auto": true }`, negation patterns) + ## Task Dependencies 1. **Explicit**: Defined via `dependsOn` in `vite-task.json` (skip with `--ignore-depends-on`) diff --git a/Cargo.lock b/Cargo.lock index c60596b7..cc0b85be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3869,7 +3869,6 @@ dependencies = [ "bstr", "clap", "derive_more", - "diff-struct", "fspy", "futures-util", "nix 0.30.1", diff --git a/a.js b/a.js deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/vite_task/Cargo.toml b/crates/vite_task/Cargo.toml index 183e32d1..8d33be3c 100644 --- a/crates/vite_task/Cargo.toml +++ b/crates/vite_task/Cargo.toml @@ -18,7 +18,6 @@ bincode = { workspace = true, features = ["derive"] } bstr = { workspace = true } clap = { workspace = true, features = ["derive"] } derive_more = { workspace = true, features = ["from"] } -diff-struct = { workspace = true } fspy = { workspace = true } futures-util = { workspace = true } once_cell = { workspace = true } diff --git a/crates/vite_task/docs/boolean-flags.md b/crates/vite_task/docs/boolean-flags.md index 6b75801d..2c1062e1 100644 --- a/crates/vite_task/docs/boolean-flags.md +++ b/crates/vite_task/docs/boolean-flags.md @@ -1,80 +1,54 @@ -# Boolean Flags in vite-plus +# Boolean Flags in Vite Task -This document describes how boolean flags work in vite-plus commands. - -## Negation Pattern - -All boolean flags in vite-plus support a negation pattern using the `--no-` prefix. When a `--no-*` flag is used, it explicitly sets the corresponding boolean option to `false`. +This document describes how boolean flags work in `vp` commands. ## Available Boolean Flags -### Global Flags - -- `--debug` / `--no-debug` - Enable or disable cache debugging output - - Short form: `-d` (only for positive form) - ### Run Command Flags -- `--recursive` / `--no-recursive` - Enable or disable recursive task execution across all packages - - Short form: `-r` (only for positive form) - -- `--parallel` / `--no-parallel` - Enable or disable parallel task execution - - Short form: `-p` (only for positive form) +- `--recursive` / `-r` — Run task in all packages in the workspace +- `--transitive` / `-t` — Run task in the current package and its transitive dependencies +- `--workspace-root` / `-w` — Run task in the workspace root package +- `--ignore-depends-on` — Skip explicit `dependsOn` dependencies +- `--verbose` / `-v` — Show full detailed summary after execution +- `--cache` / `--no-cache` — Force caching on or off for all tasks and scripts -- `--sequential` / `--no-sequential` - Enable or disable sequential task execution - - Short form: `-s` (only for positive form) +### Negation Pattern -- `--topological` / `--no-topological` - Enable or disable topological ordering based on package dependencies - - Short form: `-t` (only for positive form) - -## Behavior - -### Conflicts - -The positive and negative forms of a flag are mutually exclusive. You cannot use both `--flag` and `--no-flag` in the same command: +The `--cache` flag supports a `--no-cache` negation form. When `--no-cache` is used, caching is explicitly disabled for all tasks in that run: ```bash -# This will result in an error -vp run --recursive --no-recursive build -``` +# Force caching off +vp run build --no-cache -### Precedence - -When only the negative form is used, it takes precedence and explicitly sets the value to `false`: - -```bash -# Explicitly disable topological ordering -vp run build -r --no-topological +# Force caching on (even for scripts that default to uncached) +vp run build --cache ``` -### Default Values - -The negative flags are particularly useful for overriding default behaviors: - -- `--recursive` with `--no-topological`: By default, recursive runs enable topological ordering. Use `--no-topological` to disable it: - ```bash - # Recursive run WITHOUT topological ordering - vp run build -r --no-topological - ``` +The positive and negative forms are mutually exclusive — you cannot use both `--cache` and `--no-cache` in the same command. ## Examples ```bash -# Run with debugging disabled (useful if debug is enabled by default in config) -vp --no-debug build +# Recursive build (all packages in dependency order) +vp run build -r + +# Current package + transitive dependencies +vp run build -t -# Recursive build without topological ordering -vp run build --recursive --no-topological +# Run in workspace root +vp run build -w -# Explicitly disable parallel execution -vp run build --no-parallel +# Skip explicit dependsOn edges +vp run build --ignore-depends-on -# Run tests sequentially, not in parallel -vp run test --no-parallel +# Verbose output +vp run build -v + +# Force caching off for this run +vp run build --no-cache ``` ## Implementation Details -The `--no-*` flags use clap's `conflicts_with` attribute to ensure they cannot be used together with their positive counterparts. When processing flags, vite-plus uses a `resolve_bool_flag` function that gives precedence to the negative form when present. - -This pattern provides a consistent and intuitive way to explicitly disable features that might be enabled by default or through configuration files. +The flags use clap's argument parsing. The `--cache`/`--no-cache` pair uses clap's `conflicts_with` attribute to ensure they cannot be used together. diff --git a/crates/vite_task/docs/rfc-cache-fingerprint-ignore-patterns.md b/crates/vite_task/docs/rfc-cache-fingerprint-ignore-patterns.md deleted file mode 100644 index dc7b5434..00000000 --- a/crates/vite_task/docs/rfc-cache-fingerprint-ignore-patterns.md +++ /dev/null @@ -1,491 +0,0 @@ -# RFC: Vite+ Cache Fingerprint Ignore Patterns - -## Summary - -Add support for glob-based ignore patterns to the cache fingerprint calculation, allowing tasks to exclude specific files/directories from triggering cache invalidation while still including important files within ignored directories. - -## Motivation - -Current cache fingerprint behavior tracks all files accessed during task execution. This causes unnecessary cache invalidation in scenarios like: - -1. **Package installation tasks**: The `node_modules` directory changes frequently, but only `package.json` files within it are relevant for cache validation -2. **Build output directories**: Generated files in `dist/` or `.next/` that should not invalidate the cache -3. **Large dependency directories**: When only specific files within large directories matter for reproducibility - -### Example Use Case - -For an `install` task that runs `pnpm install`: - -- Changes to `node_modules/**/*/index.js` should NOT invalidate the cache -- Changes to `node_modules/**/*/package.json` SHOULD invalidate the cache -- This allows cache hits when dependencies remain the same, even if their internal implementation files have different timestamps or minor variations - -## Proposed Solution - -### Configuration Schema - -Extend `TaskConfig` in `vite-task.json` to support a new optional field `fingerprintIgnores`: - -```json -{ - "tasks": { - "my-task": { - "command": "echo bar", - "cacheable": true, - "fingerprintIgnores": [ - "node_modules/**/*", - "!node_modules/**/*/package.json" - ] - } - } -} -``` - -### Ignore Pattern Syntax - -The ignore patterns follow standard glob syntax with gitignore-style semantics: - -1. **Basic patterns**: - - `node_modules/**/*` - ignore all files under node_modules - - `dist/` - ignore the dist directory - - `*.log` - ignore all log files - -2. **Negation patterns** (prefixed with `!`): - - `!node_modules/**/*/package.json` - include package.json files even though node_modules is ignored - - `!important.log` - include important.log even though *.log is ignored - -3. **Pattern evaluation order**: - - Patterns are evaluated in order - - Later patterns override earlier ones - - Negation patterns can "un-ignore" files matched by earlier patterns - - Last match wins semantics - -### Implementation Details - -#### 1. Configuration Schema Changes - -**File**: `crates/vite_task/src/config/mod.rs` - -```rust -pub struct TaskConfig { - // ... - - // New field - #[serde(default)] - pub(crate) fingerprint_ignores: Option>, -} -``` - -#### 2. CommandFingerprint Schema Changes - -**File**: `crates/vite_task/src/config/mod.rs` - -Add `fingerprint_ignores` to `CommandFingerprint` to ensure cache invalidation when ignore patterns change: - -```rust -pub struct CommandFingerprint { - pub cwd: RelativePathBuf, - pub command: TaskCommand, - pub envs_without_pass_through: BTreeMap, - pub pass_through_envs: BTreeSet, - - // New field - pub fingerprint_ignores: Option>, -} -``` - -**Why this is needed**: Including `fingerprint_ignores` in `CommandFingerprint` ensures that when ignore patterns change, the cache is invalidated. This prevents incorrect cache hits when the set of tracked files changes. - -**Example scenario**: - -- First run with `fingerprintIgnores: ["node_modules/**/*"]` → tracks only non-node_modules files -- Change config to `fingerprintIgnores: []` → should track ALL files -- Without this field in CommandFingerprint → cache would incorrectly HIT -- With this field → cache correctly MISSES, re-creates fingerprint with all files - -#### 3. Fingerprint Creation Changes - -**File**: `crates/vite_task/src/fingerprint.rs` - -Modify `PostRunFingerprint::create()` to filter paths based on ignore patterns: - -```rust -impl PostRunFingerprint { - pub fn create( - executed_task: &ExecutedTask, - fs: &impl FileSystem, - base_dir: &AbsolutePath, - fingerprint_ignores: Option<&[Str]>, // New parameter - ) -> Result { - let ignore_matcher = fingerprint_ignores - .filter(|patterns| !patterns.is_empty()) - .map(GlobPatternSet::new) - .transpose()?; - - let inputs = executed_task - .path_reads - .par_iter() - .filter(|(path, _)| { - if let Some(ref matcher) = ignore_matcher { - !matcher.is_match(path) - } else { - true - } - }) - .flat_map(|(path, path_read)| { - Some((|| { - let path_fingerprint = - fs.fingerprint_path(&base_dir.join(path).into(), *path_read)?; - Ok((path.clone(), path_fingerprint)) - })()) - }) - .collect::, Error>>()?; - Ok(Self { inputs }) - } -} -``` - -#### 4. Task Resolution Integration - -**File**: `crates/vite_task/src/config/task_command.rs` - -Update `resolve_command()` to include `fingerprint_ignores` in the fingerprint: - -```rust -impl ResolvedTaskConfig { - pub(crate) fn resolve_command(...) -> Result { - // ... - Ok(ResolvedTaskCommand { - fingerprint: CommandFingerprint { - cwd, - command, - envs_without_pass_through: task_envs.envs_without_pass_through.into_iter().collect(), - pass_through_envs: self.config.pass_through_envs.iter().cloned().collect(), - fingerprint_ignores: self.config.fingerprint_ignores.clone(), // Pass through - }, - all_envs: task_envs.all_envs, - }) - } -} -``` - -#### 5. Cache Update Integration - -**File**: `crates/vite_task/src/cache.rs` - -Update `CommandCacheValue::create()` to pass ignore patterns: - -```rust -impl CommandCacheValue { - pub fn create( - executed_task: ExecutedTask, - fs: &impl FileSystem, - base_dir: &AbsolutePath, - fingerprint_ignores: Option<&[Str]>, // New parameter - ) -> Result { - let post_run_fingerprint = PostRunFingerprint::create( - &executed_task, - fs, - base_dir, - fingerprint_ignores, - )?; - Ok(Self { - post_run_fingerprint, - std_outputs: executed_task.std_outputs, - duration: executed_task.duration, - }) - } -} -``` - -#### 6. Execution Flow Integration - -**File**: `crates/vite_task/src/schedule.rs` - -Update cache creation to pass `fingerprint_ignores` from the task config: - -```rust -if !skip_cache && exit_status.success() { - let cached_task = CommandCacheValue::create( - executed_task, - fs, - base_dir, - task.resolved_config.config.fingerprint_ignores.as_deref(), - )?; - cache.update(&task, cached_task).await?; -} -``` - -### Performance Considerations - -1. **Pattern compilation**: Glob patterns compiled once per fingerprint creation (lazy) -2. **Filtering overhead**: Path filtering happens during fingerprint creation (only when caching) -3. **Memory impact**: - - `fingerprint_ignores` stored in `CommandFingerprint` (Vec) - - Compiled `GlobPatternSet` created only when needed, not cached -4. **Parallel processing**: Existing parallel iteration over paths is preserved -5. **Cache key size**: Minimal increase (~100 bytes for typical ignore patterns) - -### Edge Cases - -1. **Empty ignore list**: No filtering applied (backward compatible) - - `None` → no filtering - - `Some([])` → no filtering (empty array treated same as None) - -2. **Conflicting patterns**: Later patterns take precedence (last-match-wins) - -3. **Invalid glob syntax**: Return error during fingerprint creation - - Detected early when PostRunFingerprint is created - - Task execution completes, but cache save fails with clear error - -4. **Absolute paths in patterns**: Treated as relative to package directory - -5. **Directory vs file patterns**: Both supported via glob syntax - -6. **Config changes**: Changing `fingerprint_ignores` invalidates cache - - Patterns are part of `CommandFingerprint` - - Different patterns → different cache key - - Ensures correct file tracking - -## Alternative Designs Considered - -### Alternative 1: `inputs` field extension - -Extend the existing `inputs` field to support ignore patterns: - -```json -{ - "inputs": { - "include": ["src/**/*"], - "exclude": ["src/**/*.test.js"] - } -} -``` - -**Rejected because**: - -- The `inputs` field currently uses a different mechanism (pre-execution declaration) -- This feature is about post-execution fingerprint filtering -- Mixing the two concepts would be confusing - -### Alternative 2: Separate `fingerprintExcludes` field - -Only support exclude patterns (no negation): - -```json -{ - "fingerprintExcludes": ["node_modules/**/*"] -} -``` - -**Rejected because**: - -- Cannot express "ignore everything except X" patterns -- Less flexible for complex scenarios -- Gitignore-style syntax is more familiar to developers - -### Alternative 3: Include/Exclude separate fields - -```json -{ - "fingerprintExcludes": ["node_modules/**/*"], - "fingerprintIncludes": ["node_modules/**/*/package.json"] -} -``` - -**Rejected because**: - -- More verbose -- Less clear precedence rules -- Gitignore-style is a proven pattern - -## Migration Path - -### Backward Compatibility - -This feature is fully backward compatible: - -- Existing task configurations work unchanged -- Default value for `fingerprintIgnores` is `None` (when omitted) -- No behavior changes when field is absent or `null` -- Empty array `[]` is treated the same as `None` (no filtering) - -## Testing Strategy - -### Unit Tests - -**File**: `crates/vite_task/src/fingerprint.rs` (10 tests added) - -1. **PostRunFingerprint::create() tests** (8 tests): - - `test_postrun_fingerprint_no_ignores` - Verify None case includes all paths - - `test_postrun_fingerprint_empty_ignores` - Verify empty array includes all paths - - `test_postrun_fingerprint_ignore_node_modules` - Basic ignore pattern - - `test_postrun_fingerprint_negation_pattern` - Negation support for package.json - - `test_postrun_fingerprint_multiple_ignore_patterns` - Multiple patterns - - `test_postrun_fingerprint_wildcard_patterns` - File extension wildcards - - `test_postrun_fingerprint_complex_negation` - Nested negation patterns - - `test_postrun_fingerprint_invalid_pattern` - Error handling for bad syntax - -2. **CommandFingerprint tests** (2 tests): - - `test_command_fingerprint_with_fingerprint_ignores` - Verify cache invalidation when ignores change - - `test_command_fingerprint_ignores_order_matters` - Verify pattern order affects cache key - -3. **vite_glob tests** (existing): - - Pattern matching already tested in `vite_glob` crate - - Negation pattern precedence - - Last-match-wins semantics - -### Integration Tests - -**Snap-test**: `packages/cli/snap-tests/fingerprint-ignore-test/` - -Test fixture structure: - -``` -fingerprint-ignore-test/ - package.json - vite-task.json # with fingerprintIgnores config - steps.json # test commands - snap.txt # expected output snapshot -``` - -Test scenario validates: - -1. **First run** → Cache miss (initial execution) -2. **Second run** → Cache hit (no changes) -3. **Modify `node_modules/pkg-a/index.js`** → Cache hit (ignored by pattern) -4. **Modify `dist/bundle.js`** → Cache hit (ignored by pattern) -5. **Modify `node_modules/pkg-a/package.json`** → Cache miss (NOT ignored due to negation) - -This validates the complete feature including: - -- Ignore patterns filter correctly -- Negation patterns work -- Cache invalidation happens at the right times -- Config changes invalidate cache - -## Documentation Requirements - -### User Documentation - -Add to task configuration docs: - -````markdown -### fingerprintIgnores - -Type: `string[]` -Default: `[]` - -Glob patterns to exclude files from cache fingerprint calculation. -Patterns starting with `!` are negation patterns that override earlier excludes. - -Example: - -```json -{ - "tasks": { - "install": { - "command": "pnpm install", - "cacheable": true, - "fingerprintIgnores": [ - "node_modules/**/*", - "!node_modules/**/*/package.json" - ] - } - } -} -``` -```` - -This configuration ignores all files in `node_modules` except `package.json` -files, which are still tracked for cache validation. - -```` -### Examples Documentation - -Add common patterns: - -1. **Package installation**: - ```json - "fingerprintIgnores": [ - "node_modules/**/*", - "!node_modules/**/*/package.json", - "!node_modules/.pnpm/lock.yaml" - ] -```` - -2. **Build outputs**: - ```json - "fingerprintIgnores": [ - "dist/**/*", - ".next/**/*", - "build/**/*" - ] - ``` - -3. **Temporary files**: - ```json - "fingerprintIgnores": [ - "**/*.log", - "**/.DS_Store", - "**/tmp/**" - ] - ``` - -## Implementation Status - -✅ **IMPLEMENTED** - All functionality complete and tested - -### Summary of Changes - -1. **Schema Changes** - Added `fingerprint_ignores: Option>` to: - - `TaskConfig` (config/mod.rs:51) - User-facing configuration - - `CommandFingerprint` (config/mod.rs:272) - Cache key component - -2. **Logic Updates** - Fingerprint creation and validation: - - `PostRunFingerprint::create()` filters paths (fingerprint.rs:85-118) - - `CommandCacheValue::create()` passes patterns (cache.rs:29-42) - - `ResolvedTaskConfig::resolve_command()` includes in fingerprint (task_command.rs:99-113) - - `schedule.rs` execution flow integration (schedule.rs:236-242) - -3. **Testing** - Comprehensive coverage: - - 8 unit tests for `PostRunFingerprint::create()` with filtering - - 2 unit tests for `CommandFingerprint` with ignore patterns - - 1 snap-test for end-to-end validation - - **All 71 tests pass** ✅ - -4. **Documentation**: - - Complete RFC with implementation details - - Test fixtures with examples - - Inline code documentation explaining rationale - -### Key Design Decisions - -1. **Option type**: `Option>` provides true optional semantics -2. **Include in CommandFingerprint**: Ensures cache invalidation on config changes -3. **Leverage vite_glob**: Reuses existing, battle-tested pattern matcher -4. **Filter at creation time**: Paths filtered when creating PostRunFingerprint -5. **Order preservation**: Vec maintains pattern order (last-match-wins semantics) - -### Files Modified - -- `crates/vite_task/src/config/mod.rs` (+13 lines) -- `crates/vite_task/src/config/task_command.rs` (+2 lines) -- `crates/vite_task/src/fingerprint.rs` (+397 lines including tests) -- `crates/vite_task/src/cache.rs` (+2 lines) -- `crates/vite_task/src/execute.rs` (+4 lines) -- `crates/vite_task/src/schedule.rs` (+4 lines) -- `packages/cli/snap-tests/fingerprint-ignore-test/` (new fixture) - -## Conclusion - -This feature successfully adds glob-based ignore patterns to cache fingerprint calculation: - -- ✅ Solves real caching problems (especially for install tasks) -- ✅ Uses familiar gitignore-style syntax -- ✅ Fully backward compatible -- ✅ Minimal performance impact -- ✅ Complete test coverage -- ✅ Production-ready implementation - -The implementation leverages the proven `vite_glob` crate and integrates cleanly with existing fingerprint and cache systems. diff --git a/crates/vite_task/docs/task-cache.md b/crates/vite_task/docs/task-cache.md index b2c450c0..e378e8bb 100644 --- a/crates/vite_task/docs/task-cache.md +++ b/crates/vite_task/docs/task-cache.md @@ -1,6 +1,6 @@ # Task Cache -Vite-plus implements a sophisticated caching system to avoid re-running tasks when their inputs haven't changed. This document describes the architecture, design decisions, and implementation details of the task cache system. +Vite Task implements a caching system to avoid re-running tasks when their inputs haven't changed. This document describes the architecture, design decisions, and implementation details of the task cache system. ## Overview @@ -8,10 +8,10 @@ The task cache system enables: - **Incremental builds**: Only run tasks when inputs have changed - **Shared caching**: Multiple tasks with identical commands can share cache entries -- **Individual task run caching**: Tasks with different arguments get separate cache entries - **Content-based hashing**: Cache keys based on actual content, not timestamps - **Output replay**: Cached stdout/stderr are replayed exactly as originally produced -- **Two-tier caching**: Command-level cache shared across tasks, with task-run associations +- **Two-tier caching**: Cache entries shared across tasks, with task-run associations +- **Configurable inputs**: Control which files are tracked for cache invalidation ### Shared caching @@ -27,17 +27,17 @@ For tasks defined as below: } ``` -the task cache system is able to hit the same cache for `test` task and for the first subcommand in `build` task: +the task cache system is able to hit the same cache for the `test` task and for the first subcommand in the `build` task: 1. user runs `vp run build` -> no cache hit. run `echo $foo` and create cache 2. user runs `vp run test` 1. `echo $foo` -> **hit cache created in step 1 and replay** - 2. `echo $bar` -> no cache hit. run `echo test` and create cache + 2. `echo $bar` -> no cache hit. run `echo $bar` and create cache 3. user changes env `$foo` 4. user runs `vp run test` 1. `echo $foo` 1. the cache system should be able to **locate the cache that was created in step 1 and hit in step 2.1** - 2. compare the command fingerprint and report cache miss because `$foo` is changed. + 2. compare the spawn fingerprint and report cache miss because `$foo` is changed. 3. re-run and replace the cache with a new one. 2. `echo $bar` -> hit cache created in step 2.2 and replay 5. user runs `vp run build`: **hit the cache created in step 4.1.3 and replay**. @@ -56,9 +56,8 @@ the task cache system is able to hit the same cache for `test` task and for the │ ▼ │ │ 2. Cache Key Generation │ │ ────────────────────── │ -│ • Command fingerprint (includes cwd) │ -│ • Task arguments │ -│ • Environment variables │ +│ • Spawn fingerprint (cwd, program, args, envs) │ +│ • Input configuration │ │ │ │ │ ▼ │ │ 3. Cache Lookup (SQLite) │ @@ -68,11 +67,11 @@ the task cache system is able to hit the same cache for `test` task and for the │ └────────┬────────┴─────────┬────────────┘──────────────────┬───────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ -│ 4a. Validate Fingerprint 4b. Execute Task ◀───── 4c. Report what change | -│ ──────────────────────── ──────────────── causes the miss │ -│ • Config match? • Run command │ -│ • Inputs unchanged? • Monitor files (fspy) │ -│ • Command same? • Capture stdout/stderr │ +│ 4a. Validate Fingerprint 4b. Execute Task ◀───── 4c. Report what changed │ +│ ──────────────────────── ──────────────── │ +│ • Inputs unchanged? • Run command │ +│ • Spawn config same? • Monitor files (fspy) │ +│ • Capture stdout/stderr │ │ │ │ │ │ ▼ ▼ │ │ 5a. Replay Outputs 5b. Store in Cache │ @@ -86,29 +85,38 @@ the task cache system is able to hit the same cache for `test` task and for the ## Cache Key Components -### 1. Command Cache Key Structure +### 1. Cache Entry Key -The command cache key uniquely identifies a command execution context: +The cache entry key uniquely identifies a command execution context: ```rust -pub struct CommandCacheKey { - pub command_fingerprint: CommandFingerprint, // Execution context - pub args: Arc<[Str]>, // CLI arguments +pub struct CacheEntryKey { + pub spawn_fingerprint: SpawnFingerprint, + pub input_config: ResolvedInputConfig, } ``` -The command fingerprint captures the complete execution context: +### 2. Spawn Fingerprint + +The spawn fingerprint captures the complete execution context: ```rust -pub struct CommandFingerprint { - pub cwd: RelativePathBuf, // Working directory, relative to workspace root - pub command: TaskCommand, // Shell script or parsed command - pub envs_without_pass_through: HashMap, // Environment variables (excludes pass-through) +pub struct SpawnFingerprint { + pub cwd: RelativePathBuf, + pub command_fingerprint: CommandFingerprint, + pub fingerprinted_envs: BTreeMap, + pub pass_through_envs: BTreeSet, + pub fingerprint_ignores: Option>, +} + +enum CommandFingerprint { + Program { program_fingerprint: ProgramFingerprint, args: Vec }, + ShellScript { script: Str, extra_args: Vec }, } -pub enum TaskCommand { - ShellScript(Str), // Raw shell script - Parsed(TaskParsedCommand), // Parsed command with program and args +enum ProgramFingerprint { + OutsideWorkspace { program_name: Str }, + InsideWorkspace { relative_path: RelativePathBuf }, } ``` @@ -117,10 +125,11 @@ This ensures cache invalidation when: - Working directory changes (package location changes) - Command or arguments change - Declared environment variables differ (pass-through envs don't affect cache) +- Program location changes (inside/outside workspace) -### 2. Environment Variable Impact on Cache +### 3. Environment Variable Impact on Cache -The `envs_without_pass_through` field is crucial for cache correctness: +The `fingerprinted_envs` field is crucial for cache correctness: - Only includes envs explicitly declared in the task's `envs` array - Does NOT include pass-through envs (PATH, CI, etc.) @@ -133,51 +142,40 @@ When a task runs: 3. If a declared env changes value, cache will miss 4. If a pass-through env changes, cache will still hit -For built-in tasks (lint, build, test): +The `pass_through_envs` field stores env names (not values) — if the set of pass-through env names changes, the cache invalidates, but value changes don't. -- The resolver provides envs which become part of the fingerprint -- If resolver provides different envs between runs, cache breaks -- Each built-in task type must have unique task name to avoid cache collision +### 4. Execution Cache Key -### 3. Task Fingerprinting - -The complete task fingerprint includes input files tracked during execution: +The execution cache key associates a task identity with its cache entry: ```rust -pub struct TaskFingerprint { - pub resolved_config: ResolvedTaskConfig, // Task configuration - pub command_fingerprint: CommandFingerprint, // Command execution context - pub inputs: HashMap, // Input file states +pub enum ExecutionCacheKey { + UserTask { + task_name: Str, + and_item_index: Option, + extra_args: Arc<[Str]>, + package_path: RelativePathBuf, + }, + ExecAPI(Str), } ``` -### 4. Task ID Structure +### 5. Cache Entry Value -The task ID uniquely identifies a task: +The cached execution result: ```rust -pub struct TaskId { - /// The name in `vite-task.json`, or the name of the `package.json` script containing this task. - /// See [`terminologies.md`](./terminologies.md) for details - pub task_group_name: Str, - - /// The path of the package containing this task, relative to the monorepo root. - /// We don't use package names as they can be the same for different packages. - pub package_dir: RelativePathBuf, - - /// The index of the subcommand in a parsed command (`echo A && echo B`). - /// None if the task is the last command. - pub subcommand_index: Option, +pub struct CacheEntryValue { + pub post_run_fingerprint: PostRunFingerprint, + pub std_outputs: Arc<[StdOutput]>, + pub duration: Duration, + pub globbed_inputs: Option, } ``` -### 5. (`CommandCacheKey`, `TaskId`) Relationship - -The cache system maintains (`CommandCacheKey`, `TaskId`) relationship in order to locate the previous cache of the same task. This is a one-to-many relationship. +### 6. Input File Tracking -#### Input File Tracking - -Vite-plus uses `fspy` to monitor file system access during task execution: +Vite Task uses `fspy` to monitor file system access during task execution: ``` ┌──────────────────────────────────────────────────────────────┐ @@ -206,36 +204,57 @@ Vite-plus uses `fspy` to monitor file system access during task execution: │ enum PathFingerprint { │ │ NotFound, // File doesn't exist │ │ FileContentHash(u64), // xxHash3 of content │ -│ Folder(Option), // Directory listing │ -| } ▲ │ -│ │ | -| This value is `None` when fspy reports that the task is | -| opening a folder but not reading its entries. This can | -| happen when the opened folder is used as a dirfd for | -| `openat(2)`. In such case, the folder's entries don't need | -| to be fingerprinted. | -| Folders with empty entries fingerprinted are represented as | -| `Folder(Some(empty hashmap))`. | +│ Folder(Option), // Directory listing │ +│ } ▲ │ +│ │ │ +│ This value is `None` when fspy reports that the task is │ +│ opening a folder but not reading its entries. This can │ +│ happen when the opened folder is used as a dirfd for │ +│ `openat(2)`. In such case, the folder's entries don't need │ +│ to be fingerprinted. │ +│ Folders with empty entries fingerprinted are represented as │ +│ `Folder(Some(empty hashmap))`. │ │ │ └──────────────────────────────────────────────────────────────┘ ``` -### 6. Fingerprint Validation +### 7. Inputs Configuration + +The `inputs` field in `vite-task.json` controls which files are tracked for cache fingerprinting: + +```json +{ + "tasks": { + "build": { + "inputs": ["src/**", "!dist/**", { "auto": true }] + } + } +} +``` + +- **Omitted** (default): `[{auto: true}]` — automatically tracks which files the task reads via `fspy` +- **`[]`** (empty array): disables file tracking entirely +- **Glob patterns** (e.g. `"src/**"`): select specific files +- **`{auto: true}`**: enables automatic file tracking +- **Negative patterns** (e.g. `"!dist/**"`): exclude matched files + +See [inputs.md](../../../docs/inputs.md) for full details. + +### 8. Fingerprint Validation When a cache entry exists, the fingerprint is validated to detect changes: ```rust -pub enum CacheMiss { - NotFound, // No cache entry exists - FingerprintMismatch { // Cache exists but invalid - reason: FingerprintMismatchReason, - }, +pub enum FingerprintMismatch { + SpawnFingerprint { old: SpawnFingerprint, new: SpawnFingerprint }, + InputConfig, + InputChanged { kind: InputChangeKind, path: RelativePathBuf }, } -pub enum FingerprintMismatchReason { - ConfigChanged, // Task configuration changed - CommandChanged, // Command fingerprint differs - InputsChanged, // Input files modified +pub enum InputChangeKind { + ContentModified, + Added, + Removed, } ``` @@ -243,7 +262,7 @@ pub enum FingerprintMismatchReason { ### Storage Backend -Vite-plus uses SQLite with WAL (Write-Ahead Logging) mode for cache storage: +Vite Task uses SQLite with WAL (Write-Ahead Logging) mode for cache storage: ```rust // Database initialization @@ -255,34 +274,22 @@ conn.pragma_update(None, "synchronous", "NORMAL")?; // Balance speed/safety ### Database Schema ```sql --- Simple key-value store for commands cache -CREATE TABLE commands ( - key BLOB PRIMARY KEY, -- Serialized CommandsCacheKey - value BLOB -- Serialized CachedTask +-- Cache entries keyed by spawn fingerprint + input config +CREATE TABLE cache_entries ( + key BLOB PRIMARY KEY, -- Serialized CacheEntryKey + value BLOB -- Serialized CacheEntryValue ); --- One-to-many relationships between commands and tasks -CREATE TABLE commands_tasks ( - command_key BLOB, -- Serialized CommandsCacheKey - task_id BLOB -- Serialized TaskId +-- Maps task identity to its cache entry key +CREATE TABLE task_fingerprints ( + key BLOB PRIMARY KEY, -- Serialized ExecutionCacheKey + value BLOB -- Serialized CacheEntryKey ); ``` ### Serialization -Cache entries are serialized using `bincode` for efficient storage: - -```rust -pub struct CachedTask { - pub fingerprint: TaskFingerprint, // Complete task state - pub std_outputs: Arc<[StdOutput]>, // Captured outputs -} - -pub struct StdOutput { - pub kind: OutputKind, // StdOut or StdErr - pub content: MaybeString, // Binary or UTF-8 content -} -``` +Cache entries are serialized using `bincode` for efficient storage. ## Cache Operations @@ -295,42 +302,32 @@ pub struct StdOutput { │ │ │ 1. Generate Cache Keys │ │ ────────────────────── │ -│ TaskRunKey { │ -│ task_id: TaskId { ... }, │ -│ args: ["--production"] │ +│ CacheEntryKey { │ +│ spawn_fingerprint: SpawnFingerprint { ... }, │ +│ input_config: ResolvedInputConfig { ... }, │ │ } │ -│ CommandFingerprint { │ -│ cwd: "packages/app", │ -│ command: Parsed(...), │ -│ envs_without_pass_through: {...}, │ -│ pass_through_envs: {...} │ +│ ExecutionCacheKey::UserTask { │ +│ task_name: "build", │ +│ package_path: "packages/app", │ +│ ... │ │ } │ │ │ │ │ ▼ │ -│ 2. Query Command Cache │ -│ ────────────────────── │ -│ SELECT value FROM command_cache WHERE key = command_fp │ -│ │ │ -│ ▼ │ -│ 3. Deserialize CommandCacheValue │ -│ ───────────────────────────── │ -│ CommandCacheValue { │ -│ post_run_fingerprint: PostRunFingerprint { ... }, │ -│ std_outputs: [StdOutput, ...] │ -│ } │ +│ 2. Query Cache │ +│ ────────────── │ +│ SELECT value FROM cache_entries WHERE key = ? │ │ │ │ │ ▼ │ -│ 4. Validate Post-Run Fingerprint │ +│ 3. Validate Post-Run Fingerprint │ │ ───────────────────────────────── │ │ • Check input file hashes │ -│ • Detect file content changes │ +│ • Detect file content changes, additions, removals │ │ │ │ │ ▼ │ -│ 5. Replay Outputs & Update Association │ -│ ────────────────────────────────────── │ +│ 4. Replay Outputs │ +│ ───────────────── │ │ • Write to stdout/stderr │ │ • Preserve original order │ -│ • Update taskrun_to_command mapping │ │ │ └──────────────────────────────────────────────────────────────┘ ``` @@ -356,20 +353,19 @@ pub struct StdOutput { │ • Record file system access patterns │ │ │ │ │ ▼ │ -│ 3. Create CommandCacheValue │ -│ ────────────────────────── │ -│ CommandCacheValue { │ -│ post_run_fingerprint: generated_fingerprint, │ -│ std_outputs: captured_outputs │ +│ 3. Create CacheEntryValue │ +│ ──────────────────────────── │ +│ CacheEntryValue { │ +│ post_run_fingerprint, │ +│ std_outputs, │ +│ duration, │ +│ globbed_inputs, │ │ } │ │ │ │ │ ▼ │ -│ 4. Store in Database Tables │ -│ ─────────────────────────── │ -│ INSERT OR REPLACE INTO command_cache │ -│ VALUES (command_fingerprint, cache_value) │ -│ INSERT OR REPLACE INTO taskrun_to_command │ -│ VALUES (task_run_key, command_fingerprint) │ +│ 4. Store in Database │ +│ ──────────────────── │ +│ INSERT/UPDATE cache_entries + task_fingerprints │ │ │ └──────────────────────────────────────────────────────────────┘ ``` @@ -381,172 +377,68 @@ pub struct StdOutput { Cache entries are automatically invalidated when: 1. **Command changes**: Different command, arguments, or working directory -2. **Package location changes**: Working directory (`cwd`) in command fingerprint changes +2. **Package location changes**: Working directory (`cwd`) in spawn fingerprint changes 3. **Environment changes**: Modified declared environment variables (pass-through values don't affect cache) 4. **Pass-through config changes**: Pass-through environment names added/removed from configuration 5. **Input files change**: Content hash differs (detected via xxHash3) 6. **File structure changes**: Files added, removed, or type changed -7. **Built-in task location**: Built-in tasks run from different directories get separate caches - -### Fingerprint Mismatch Detection +7. **Input config changes**: The `inputs` configuration itself changes -```rust -// Two-level fingerprint validation during cache lookup -pub async fn try_hit( - &self, - task: &ResolvedTask, - fs: &impl FileSystem, - base_dir: &AbsolutePath, -) -> Result, Error> { - let task_run_key = TaskRunKey { task_id: task.id(), args: task.args.clone() }; - let command_fingerprint = &task.resolved_command.fingerprint; - - if let Some(cache_value) = self.get_command_cache_by_command_fingerprint(command_fingerprint).await? { - // Command fingerprint matches, validate post-run fingerprint - if let Some(post_run_mismatch) = cache_value.post_run_fingerprint.validate(fs, base_dir)? { - Ok(Err(CacheMiss::FingerprintMismatch( - FingerprintMismatch::PostRunFingerprintMismatch(post_run_mismatch), - ))) - } else { - // Cache hit, update association - self.upsert_taskrun_to_command(&task_run_key, command_fingerprint).await?; - Ok(Ok(cache_value)) - } - } else if let Some(old_command_fp) = self.get_command_fingerprint_by_task_run_key(&task_run_key).await? { - // Task run exists but command fingerprint changed - Ok(Err(CacheMiss::FingerprintMismatch( - FingerprintMismatch::CommandFingerprintMismatch( - command_fingerprint.diff(&old_command_fp), - ), - ))) - } else { - // No cache found - Ok(Err(CacheMiss::NotFound)) - } -} -``` +## Configuration -## Performance Optimizations +### Cache Location -### 1. Fast Hashing with xxHash3 +The cache database is stored at `node_modules/.vite/task-cache` in the workspace root. -Vite-plus uses xxHash3 for file content hashing, providing excellent performance: +### Global Cache Control -```rust -use xxhash_rust::xxh3::xxh3_64; +The root `vite-task.json` can configure caching for the entire workspace: -pub fn hash_file_content(content: &[u8]) -> u64 { - xxh3_64(content) // ~10GB/s on modern CPUs +```json +{ + "cache": true, + "tasks": { ... } } ``` -### 2. File System Monitoring - -Instead of scanning all possible input files, `fspy` monitors actual file access: - -``` -┌──────────────────────────────────────────────────────────────┐ -│ Efficient File Tracking │ -├──────────────────────────────────────────────────────────────┤ -│ │ -│ Traditional Approach: │ -│ ──────────────────── │ -│ Scan all src/**/*.ts files → Hash everything │ -│ Problem: Hashes files never accessed │ -│ │ -│ Vite-plus Approach: │ -│ ────────────────── │ -│ Monitor with fspy → Hash only accessed files │ -│ Benefit: Minimal work, accurate dependencies │ -│ │ -└──────────────────────────────────────────────────────────────┘ -``` - -### 3. SQLite Optimizations - -```rust -// WAL mode for better concurrency -conn.pragma_update(None, "journal_mode", "WAL")?; - -// Balanced durability for performance -conn.pragma_update(None, "synchronous", "NORMAL")?; - -// Prepared statements for efficiency -let mut stmt = conn.prepare_cached( - "SELECT value FROM tasks WHERE key = ?" -)?; -``` - -### 4. Binary Serialization - -Using `bincode` for compact, fast serialization: - -```rust -// Efficient binary encoding -let key_bytes = bincode::encode_to_vec(&cache_key, config)?; -let value_bytes = bincode::encode_to_vec(&cached_task, config)?; - -// Direct storage without text conversion -stmt.execute(params![key_bytes, value_bytes])?; -``` - -## Configuration - -### Cache Location - -The cache location can be configured via environment variable: - -```bash -# Custom cache location -VITE_CACHE_PATH=/tmp/vite-cache vp run build - -# Default: node_modules/.vite/task-cache in workspace root -vp run build -``` +- `true` — enables caching for both scripts and tasks +- `false` — disables all caching +- `{ "scripts": false, "tasks": true }` — default; tasks are cached but package.json scripts are not +- `{ "scripts": true, "tasks": true }` — cache everything ### Task-Level Cache Control -Tasks can be marked as cacheable in `vite-task.json`: +Individual tasks can enable or disable caching: ```json { "tasks": { "build": { "command": "tsc && rollup -c", - "cacheable": true, - "dependsOn": ["^build"] + "cache": true, + "dependsOn": ["lint"] }, "deploy": { "command": "deploy-script.sh", - "cacheable": false // Never cache deployment tasks - }, - "test": { - "command": "jest", - "cacheable": true + "cache": false } } } ``` -### Cache Behavior +### CLI Cache Override -- **Default**: Tasks are cacheable unless explicitly disabled -- **Compound commands**: Each subcommand cached independently -- **Dependencies**: Cache considers task dependencies +The `--cache` and `--no-cache` flags override all cache configuration for a single run: + +```bash +vp run build --no-cache # force cache off +vp run build --cache # force cache on (even for scripts) +``` ## Output Capture and Replay ### Output Capture During Execution -```rust -pub struct StdOutput { - pub kind: OutputKind, // StdOut or StdErr - pub content: MaybeString, // Binary-safe content -} - -pub struct MaybeString(Vec); -``` - Outputs are captured exactly as produced: - Preserves order of stdout/stderr interleaving @@ -583,99 +475,35 @@ When a task hits cache, outputs are replayed exactly: └──────────────────────────────────────────────────────────────┘ ``` -## Implementation Examples - -### Example: Task Run Key and Command Fingerprint - -```rust -// Task: app#build --production -TaskRunKey { - task_id: TaskId { - task_group_id: TaskGroupId { - task_group_name: "build".into(), - is_builtin: false, - config_path: RelativePathBuf::from("packages/app"), - }, - subcommand_index: None, - }, - args: vec!["--production"].into(), -} - -CommandFingerprint { - cwd: RelativePathBuf::from("packages/app"), - command: TaskCommand::ShellScript("tsc && rollup -c".into()), - envs_without_pass_through: btreemap! { - "NODE_ENV".into() => "production".into() - }, - pass_through_envs: btreeset! { "PATH".into(), "HOME".into() }, -} -``` - -### Example: Synthetic Task Cache Key - -```rust -// Synthetic task (e.g., "vp lint" in a task script) -TaskRunKey { - task_id: TaskId { - task_group_id: TaskGroupId { - task_group_name: "lint".into(), - is_builtin: true, - config_path: RelativePathBuf::from("packages/frontend"), // Current working directory - }, - subcommand_index: None, - }, - args: vec![].into(), -} +## Performance Optimizations -CommandFingerprint { - cwd: RelativePathBuf::from("packages/frontend"), - command: TaskCommand::Parsed(TaskParsedCommand { - program: "/usr/local/bin/oxlint".into(), - args: vec![".".into()].into(), - envs: HashMap::new(), - }), - envs_without_pass_through: BTreeMap::new(), - pass_through_envs: btreeset! { "PATH".into() }, -} -``` +### Fast Hashing with xxHash3 -## Debugging Cache Behavior +Vite Task uses xxHash3 for file content hashing, providing excellent performance (~10GB/s on modern CPUs). -### Environment Variables +### File System Monitoring -```bash -# Enable debug logging -VITE_LOG=debug vp run build +Instead of scanning all possible input files, `fspy` monitors actual file access: -# Show cache operations -VITE_LOG=trace vp run build ``` +Traditional Approach: + Scan all src/**/*.ts files → Hash everything + Problem: Hashes files never accessed -### Debug Output Examples - -``` -[DEBUG] Cache lookup for app#build -[DEBUG] Cache key: TaskCacheKey { command_fingerprint: ..., args: ... } -[DEBUG] Cache hit! Validating fingerprint... -[DEBUG] Fingerprint mismatch: InputsChanged -[DEBUG] File src/index.ts changed (hash: 0x1234... → 0x5678...) -[DEBUG] Cache miss, executing task +Vite Task Approach: + Monitor with fspy → Hash only accessed files + Benefit: Minimal work, accurate dependencies ``` -### Common Cache Miss Reasons +### SQLite Optimizations -1. **NotFound**: No cache entry exists (first run or after cache clear) -2. **CommandFingerprintMismatch**: Command, args, environment variables, or pass-through config changed -3. **PostRunFingerprintMismatch**: Source files modified or file structure changed +- WAL mode for better concurrency +- Balanced durability for performance +- Prepared/cached statements for efficiency -#### Detailed Cache Miss Messages +### Binary Serialization -From the test cases, cache miss messages include: - -- `Cache miss: foo.txt content changed` - Input file content changed -- `Cache miss: Command fingerprint changed: CommandFingerprintDiff { ... }` - Command changed -- Pass-through env config change: `pass_through_envs: BTreeSetDiff { added: {}, removed: {"MY_ENV2"} }` -- Environment value change: `envs_without_pass_through: HashMapDiff { altered: {"FOO": Some("1")}, removed: {} }` +Using `bincode` for compact, fast serialization with direct storage without text conversion. ## Best Practices @@ -684,15 +512,6 @@ From the test cases, cache miss messages include: Ensure commands produce identical outputs for identical inputs: ```json -// ❌ Bad: Non-deterministic output -{ - "tasks": { - "build": { - "command": "echo Built at $(date) && tsc" - } - } -} - // ✅ Good: Deterministic output { "tasks": { @@ -703,236 +522,59 @@ Ensure commands produce identical outputs for identical inputs: } ``` -### 2. Shared Caching Across Tasks - -Tasks with identical commands automatically share cache entries: +### 2. Disable Cache for Side Effects ```json { - "scripts": { - "script1": "cat foo.txt", - "script2": "cat foo.txt" + "tasks": { + "deploy": { + "command": "deploy-to-production.sh", + "cache": false + } } } ``` -Behavior: - -1. `vp run script1` creates command cache for `cat foo.txt` -2. `vp run script2` hits the same command cache (shared) -3. If `foo.txt` changes, both tasks will see cache miss on next run -4. Cache update from either task benefits the other - -### 3. Individual Caching for Different Arguments - -Tasks with different arguments get separate cache entries: - -```bash -# These create separate caches -vp run echo -- a # TaskRunKey with args: ["a"] -vp run echo -- b # TaskRunKey with args: ["b"] -``` - -### 4. Compound Commands for Granular Caching - -Leverage compound commands for per-subcommand caching: +### 3. Use `inputs` for Precise Cache Control ```json { - "scripts": { - "build": "tsc && rollup -c && terser dist/bundle.js" + "tasks": { + "build": { + "inputs": ["src/**", "tsconfig.json", "!src/**/*.test.ts"] + } } } ``` -Benefit: Each `&&` separated command is cached independently. If only terser config changes, TypeScript and rollup will hit cache. - -### 5. Disable Cache for Side Effects +### 4. Compound Commands for Granular Caching ```json { - "tasks": { - "deploy": { - "command": "deploy-to-production.sh", - "cacheable": false // Always run fresh - }, - "notify": { - "command": "slack-webhook.sh", - "cacheable": false // Side effect: sends notification - } + "scripts": { + "build": "tsc && rollup -c && terser dist/bundle.js" } } ``` -### 6. File Access Patterns - -The cache system automatically tracks accessed files: - -```typescript -// This file access is automatically tracked -import config from './config.json'; - -// Dynamic imports are also tracked -const module = await import(`./locales/${lang}.json`); - -// File system operations are monitored -const data = fs.readFileSync('data.txt'); -``` - -No need to manually specify inputs - fspy captures actual dependencies. - -## Cache Sharing Examples - -### Example 1: Shared Command Cache - -```bash -# Initial run creates command cache -> vp run script1 -Cache not found -bar - -# Different task, same command - hits shared cache -> vp run script2 -Cache hit, replaying -bar - -# File change invalidates shared cache -> echo baz > foo.txt -> vp run script2 -Cache miss: foo.txt content changed -baz - -# Original task benefits from updated cache -> vp run script1 -Cache hit, replaying -baz -``` - -### Example 2: Individual Caching by Arguments - -```bash -# Different args create separate caches -> vp run echo -- a -Cache not found -a - -> vp run echo -- b -Cache not found -b - -# Each argument combination has its own cache -> vp run echo -- a -Cache hit, replaying -a - -> vp run echo -- b -Cache hit, replaying -b -``` - -### Example 3: Task Caching by Working Directory - -```bash -# Different directories create separate caches for tasks -> cd folder1 && vp run lint -Cache not found -Found 0 warnings and 0 errors. - -> cd folder2 && vp run lint -Cache not found # Different cwd = different cache -Found 0 warnings and 0 errors. - -# Each directory maintains its own cache -> cd folder1 && vp run lint -Cache hit, replaying -Found 0 warnings and 0 errors. -``` +Each `&&` separated command is cached independently. If only terser config changes, TypeScript and rollup will hit cache. ## Implementation Reference ### Core Cache Components ``` -┌──────────────────────────────────────────────────────────────┐ -│ Cache System Architecture │ -├──────────────────────────────────────────────────────────────┤ -│ │ -│ crates/vite_task/src/ │ -│ ├── cache.rs # Two-tier cache storage system │ -│ │ ├── CommandCacheValue # Cached execution results │ -│ │ ├── TaskRunKey # Task run identification │ -│ │ ├── TaskCache # Main cache interface │ -│ │ └── try_hit() # Two-level cache lookup │ -│ │ │ -│ ├── fingerprint.rs # Post-run fingerprint generation │ -│ │ ├── PostRunFingerprint # Input file states │ -│ │ ├── PathFingerprint # File/directory state │ -│ │ └── PostRunFingerprintMismatch # Validation results │ -│ │ │ -│ ├── config/mod.rs # Command fingerprint generation │ -│ │ └── CommandFingerprint # Command execution context │ -│ │ │ -│ ├── execute.rs # Task execution with caching │ -│ │ ├── execute_with_cache() # Main execution flow │ -│ │ ├── monitor_files() # fspy integration │ -│ │ └── capture_outputs() # Output collection │ -│ │ │ -│ └── schedule.rs # Task scheduling and cache lookup │ -│ └── schedule_tasks() # Cache-aware task execution │ -│ │ -└──────────────────────────────────────────────────────────────┘ -``` - -### Key Algorithms - -#### Task Run Key Generation - -```rust -// Generate task run key for cache lookup -impl TaskCache { - pub async fn try_hit(&self, task: &ResolvedTask) -> Result<...> { - let task_run_key = TaskRunKey { - task_id: task.id(), - args: task.args.clone(), - }; - let command_fingerprint = &task.resolved_command.fingerprint; - // ... two-tier lookup logic - } -} -``` - -#### Post-Run Fingerprint Validation +crates/vite_task/src/session/ +├── cache/ +│ ├── mod.rs # ExecutionCache, CacheEntryKey/Value, FingerprintMismatch +│ └── fingerprint.rs # SpawnFingerprint, CommandFingerprint, ProgramFingerprint +├── execute/ +│ ├── mod.rs # execute_spawn, SpawnOutcome +│ └── spawn.rs # spawn_with_tracking, fspy integration +└── reporter/ + └── mod.rs # Reporter traits for cache hit/miss display -```rust -// Validates cached post-run fingerprint against current file system state -impl PostRunFingerprint { - pub fn validate( - &self, - fs: &impl FileSystem, - base_dir: &AbsolutePath, - ) -> Result, Error> { - let input_mismatch = self.inputs.par_iter().find_map_any(|(input_path, cached_fp)| { - let full_path = base_dir.join(input_path); - let current_fp = PathFingerprint::create(&full_path, fs); - if cached_fp != ¤t_fp { - Some(PostRunFingerprintMismatch::InputContentChanged { - path: input_path.clone(), - }) - } else { - None - } - }); - Ok(input_mismatch) - } -} +crates/vite_task_plan/src/ +├── cache_metadata.rs # ExecutionCacheKey, CacheMetadata +└── plan.rs # Planning logic ``` - -### Performance Characteristics - -- **Cache key generation**: ~1μs per task -- **File hashing**: ~10GB/s with xxHash3 -- **Database operations**: <1ms for typical queries -- **Fingerprint validation**: ~10μs per task -- **Output replay**: Near-zero overhead - -The cache system adds minimal overhead while providing significant speedups for unchanged tasks, making incremental builds in large monorepos extremely efficient. diff --git a/crates/vite_task/docs/task-query.md b/crates/vite_task/docs/task-query.md index ce70fc13..9fc7f867 100644 --- a/crates/vite_task/docs/task-query.md +++ b/crates/vite_task/docs/task-query.md @@ -56,13 +56,14 @@ Stage 1: Which packages? Stage 2: Which tasks? The CLI flags determine which packages participate: -| Command | What it selects | -| ---------------------------------- | --------------------------------------------- | -| `vp run build` | Just the current package | -| `vp run -r build` | All packages | -| `vp run -t build` | Current package + its transitive dependencies | -| `vp run --filter app... build` | `app` + its transitive dependencies | -| `vp run --filter '!core' -r build` | All packages except `core` | +| Command | What it selects | +| ---------------------------- | --------------------------------------------- | +| `vp run build` | Just the current package | +| `vp run -r build` | All packages | +| `vp run -t build` | Current package + its transitive dependencies | +| `vp run -w build` | The workspace root package | +| `vp run -F app... build` | `app` + its transitive dependencies | +| `vp run -F '!core' -r build` | All packages except `core` | The result is a **package subgraph** — the selected packages plus all the dependency edges between them. This subgraph is a subset of the full package graph. diff --git a/crates/vite_task/docs/terminologies.md b/crates/vite_task/docs/terminologies.md index 39dd574e..dd634e0b 100644 --- a/crates/vite_task/docs/terminologies.md +++ b/crates/vite_task/docs/terminologies.md @@ -13,9 +13,10 @@ ``` ```jsonc -// task.json +// vite-task.json { - "lint": { + "tasks": { + "lint": { "command": "echo lint" } } diff --git a/crates/vite_task/docs/vite-install.md b/crates/vite_task/docs/vite-install.md deleted file mode 100644 index b64ad4fa..00000000 --- a/crates/vite_task/docs/vite-install.md +++ /dev/null @@ -1,361 +0,0 @@ -# `vite install` - -## Overview - -`vite install` will automatically select the most suitable packageManager based on the current directory configuration, then run its install command, and generate a new fingerprint. - -Currently only pnpm, yarn, and npm are supported. More package managers like bun, [vlt](https://docs.vlt.sh/cli/commands/install), lerna*,* etc. will be considered in the future. - -> Assume the current running environment has at least `node` installed - -## Detect workspace root directory - -User maybe run `vite install` in a workspace subdirectory, we need to relocate to the workspace root directory to run. - -### monorepo - -Search for workspace information from the current directory. If not found, search in the parent directory, all the way up to the operating system root directory. - -Workspace root detection priority: - -- `pnpm-workspace.yaml` file exists -- `package.json` file exists and `workspaces` field exists on it - -If no workspace identification information is found, it indicates that the current project is not a monorepo and should be treated as a normal repo. - -Examples - -/path/to/pnpm-worksapce.yaml - -```yaml -packages: - - 'packages/*' - - 'apps/*' - - '!**/test/**' -``` - -/path/to/package.json - -```json -{ - "name": "my-monorepo", - "version": "1.0.0", - "private": true, - "workspaces": [ - "packages/*" - ] -} -``` - -### normal repo - -Starting from the current directory, recursively search upwards to find the directory containing the `package.json` file as the workspace root; if not found, use the current directory as the workspace root. - -## Detect package manager - -Package manager detection priority: - -- `packageManager` field in `package.json` , got `{pm}@{version}` -- `devEngines.packageManager` field in `package.json` (WIP) -- `pnpm-lock.yaml` or `pnpm-workspace.yaml` file exists ⇒ `pnpm@latest` -- `yarn.lock` or `.yarnrc.yml` file exists ⇒ `yarn@latest` -- `package-lock.json` exists ⇒ `npm@latest` -- `pnpmfile.cjs` exists ⇒ `pnpm@latest` -- `yarn.config.cjs` exists ⇒ `yarn@latest`, support in yarn 2.0+ -- List pnpm/yarn/npm to let user choice and recommend user to use `pnpm@latest` by default - -If `packageManager` filed not exists on package.json, it will auto filled correctly after install run. - -## Install package manager - -If the detected package manager does not exist, we will download and use it locally. - -We will install the package manager in a local directory without polluting the user's global installation. - -Also need to [make shims](https://github.com/nodejs/corepack/blob/main/mkshims.ts) like corepack does. - -## Run `pm install` and collect fingerprint - -When running `pm install`, it will use the same approach as vite task to collect fingerprints. Since the install process generates a huge node_modules, specific special filtering will be applied. - -The ignored paths are as follows: - -- All paths under the node_modules directory, but will retain the first-level filename list of node_modules as a fingerprint -- Paths that are not within the `cwd` scope - -## Manual execution - -If the user want their modifications to be cleaned up by a fresh install, they should manually run `vite install`. It will ignore cache checks and re-execute `pm install`, generating a new fingerprint. - -## Auto execution - -The `vite install` task will be auto-executed or quickly skipped at the beginning of any other vite+ command, eliminating the need for the user to run it manually in most cases. - -### No replay when cache hit - -The auto-executed `vite install` task will not replay stdout after hitting the cache, to avoid interfering with the real execution of the command. - -## Architecture - -### Install Execution Flow - -``` -┌──────────────────────────────────────────────────────────────┐ -│ Install Execution Flow │ -├──────────────────────────────────────────────────────────────┤ -│ │ -│ 1. Install Request │ -│ ────────────────── │ -│ vite install [args] │ -│ │ │ -│ ▼ │ -│ 2. Workspace Detection │ -│ ────────────────────── │ -│ • Check pnpm-workspace.yaml │ -│ • Check package.json#workspaces │ -│ • Search upward for package.json │ -│ │ │ -│ ▼ │ -│ 3. Package Manager Detection │ -│ ──────────────────────────── │ -│ ┌──────────────────────┬────────────────┐ │ -│ │ packageManager field │ Lock Files │ │ -│ └──────────┬───────────┴────────┬───────┘ │ -│ │ │ │ -│ ▼ ▼ │ -│ 4a. Parse pm@version 4b. Infer from locks │ -│ ──────────────────── ──────────────────── │ -│ • pnpm@8.15.0 • pnpm-lock.yaml → pnpm │ -│ • yarn@4.0.0 • yarn.lock → yarn │ -│ • npm@10.0.0 • package-lock.json → npm │ -│ │ │ │ -│ └──────────┬──────────┘ │ -│ │ │ -│ ▼ │ -│ 5. Package Manager Installation │ -│ ──────────────────────────────── │ -│ • Check local cache for pm@version │ -│ • Download if missing (with retry) │ -│ • Extract and create shims │ -│ • Update PATH environment │ -│ │ │ -│ ▼ │ -│ 6. Execute Install Command │ -│ ────────────────────────── │ -│ • Run: {pm} install [args] │ -│ • Monitor with fspy │ -│ • Capture stdout/stderr │ -│ │ │ -│ ▼ │ -│ 7. Fingerprint Collection │ -│ ───────────────────────── │ -│ • Hash package.json │ -│ • Hash lock file │ -│ • List node_modules/* (names only) │ -│ │ │ -│ ▼ │ -│ 8. Cache Storage │ -│ ──────────────── │ -│ • Save fingerprint │ -│ • Store outputs │ -│ • Update packageManager field │ -│ │ -└──────────────────────────────────────────────────────────────┘ -``` - -### Package Manager Resolution - -``` -┌──────────────────────────────────────────────────────────────┐ -│ Package Manager Resolution │ -├──────────────────────────────────────────────────────────────┤ -│ │ -│ Priority Order: │ -│ ────────────── │ -│ 1. package.json#packageManager │ -│ 2. pnpm-workspace.yaml exists │ -│ 3. Lock file detection │ -│ 4. User selection (interactive) │ -│ │ │ -│ ▼ │ -│ Detection Flow: │ -│ ────────────── │ -│ │ -│ package.json │ -│ │ │ -│ ├─► packageManager: "pnpm@8.15.0" │ -│ │ └─► Use exact version │ -│ │ │ -│ └─► No packageManager field │ -│ │ │ -│ ├─► pnpm-workspace.yaml exists? │ -│ │ └─► pnpm@latest │ -│ │ │ -│ ├─► pnpm-lock.yaml exists? │ -│ │ └─► pnpm@latest │ -│ │ │ -│ ├─► yarn.lock exists? │ -│ │ └─► yarn@latest │ -│ │ │ -│ ├─► package-lock.json exists? │ -│ │ └─► npm@latest │ -│ │ │ -│ └─► No indicators found │ -│ │ │ -│ ├─► CI environment? │ -│ │ └─► Auto-select pnpm │ -│ │ │ -│ ├─► Non-TTY? │ -│ │ └─► Auto-select pnpm │ -│ │ │ -│ └─► Interactive menu │ -│ └─► User selects │ -│ │ -└──────────────────────────────────────────────────────────────┘ -``` - -### Package Manager Download & Setup - -``` -┌──────────────────────────────────────────────────────────────┐ -│ Package Manager Download & Setup │ -├──────────────────────────────────────────────────────────────┤ -│ │ -│ 1. Cache Check │ -│ ───────────── │ -│ $CACHE_DIR/vite/package_manager/{pm}/{version}/ │ -│ │ │ -│ ├─► Exists? → Use cached │ -│ │ │ -│ └─► Missing? → Download │ -│ │ │ -│ ▼ │ -│ 2. Download with Retry │ -│ ────────────────────── │ -│ GET https://registry.npmjs.org/{package}/-/ │ -│ {package}-{version}.tgz │ -│ │ │ -│ ├─► Success → Extract │ -│ │ │ -│ └─► Failed → Retry with backoff │ -│ • 1st retry: wait 1s │ -│ • 2nd retry: wait 2s │ -│ • 3rd retry: wait 4s │ -│ │ │ -│ ▼ │ -│ 3. Extract & Setup │ -│ ───────────────── │ -│ • Extract tgz to temp dir │ -│ • Rename package/ to {pm}/ │ -│ • Atomic move to cache dir │ -│ │ │ -│ ▼ │ -│ 4. Create Shims │ -│ ────────────── │ -│ For each binary: │ -│ • Create Unix shell script (.sh) │ -│ • Create Windows batch (.cmd) │ -│ • Create PowerShell script (.ps1) │ -│ │ │ -│ ▼ │ -│ Shim Structure: │ -│ ────────────── │ -│ bin/ │ -│ ├── pnpm → ../pnpm.cjs │ -│ ├── pnpm.cmd → ..\pnpm.cjs │ -│ ├── pnpm.ps1 → ../pnpm.cjs │ -│ ├── pnpx → ../pnpx.cjs │ -│ ├── pnpx.cmd → ..\pnpx.cjs │ -│ └── pnpx.ps1 → ../pnpx.cjs │ -│ │ -└──────────────────────────────────────────────────────────────┘ -``` - -### Fingerprint Generation - -``` -┌──────────────────────────────────────────────────────────────┐ -│ Fingerprint Generation │ -├──────────────────────────────────────────────────────────────┤ -│ │ -│ Install Fingerprint Components: │ -│ ─────────────────────────────── │ -│ │ -│ 1. Configuration Files │ -│ ────────────────────── │ -│ • package.json content hash │ -│ • Lock file content hash │ -│ • .npmrc/.yarnrc/.pnpmfile (if exists) │ -│ │ │ -│ ▼ │ -│ 2. Node Modules Structure │ -│ ───────────────────────── │ -│ Special handling for node_modules: │ -│ • Ignore file contents │ -│ • Record first-level directory names only │ -│ • Example fingerprint: │ -│ node_modules/ │ -│ ├── @types (recorded) │ -│ ├── typescript (recorded) │ -│ └── vite (recorded) │ -│ │ │ -│ ▼ │ -│ 3. Environment Context │ -│ ───────────────────── │ -│ • NODE_ENV value │ -│ • Package manager version │ -│ • Install command arguments │ -│ │ │ -│ ▼ │ -│ Fingerprint Hash: │ -│ ──────────────── │ -│ xxHash3({ │ -│ config_files + node_modules_list + env_context │ -│ }) → 0xABCDEF123456789 │ -│ │ -└──────────────────────────────────────────────────────────────┘ -``` - -### Cache Hit/Miss Behavior - -``` -┌──────────────────────────────────────────────────────────────┐ -│ Cache Hit/Miss Behavior │ -├──────────────────────────────────────────────────────────────┤ -│ │ -│ Auto-execution Mode: │ -│ ─────────────────── │ -│ │ -│ Cache Lookup │ -│ │ │ -│ ├─► Cache Hit │ -│ │ • Skip execution │ -│ │ • No output replay │ -│ │ • Continue to next task │ -│ │ │ -│ └─► Cache Miss │ -│ • Execute install │ -│ • Capture outputs │ -│ • Generate fingerprint │ -│ │ -│ Manual Execution Mode (`vite install`): │ -│ ──────────────────────────────── │ -│ │ -│ Skip Cache │ -│ │ │ -│ └─► Always Execute │ -│ • Run install command │ -│ • Generate fingerprint │ -│ │ -│ Fingerprint Validation: │ -│ ────────────────────── │ -│ │ -│ Compare Current vs Cached: │ -│ • package.json changed? → Cache miss │ -│ • Lock file changed? → Cache miss │ -│ • node_modules/* changed? → Cache miss │ -│ • Install args different? → Cache miss │ -│ • All match? → Cache hit │ -│ │ -└──────────────────────────────────────────────────────────────┘ -``` diff --git a/crates/vite_task/docs/vite-run.md b/crates/vite_task/docs/vite-run.md deleted file mode 100644 index 2db416a1..00000000 --- a/crates/vite_task/docs/vite-run.md +++ /dev/null @@ -1,360 +0,0 @@ -# `vp run` - -Vite-plus run has two modes: - -1. Implicit mode is running `vp` without `run` command, it will run the task in the current package, it supposed to replace the `pnpm/yarn run` command. -2. Explicit mode is running `vp run` with run command, like `vp run vite-plus#build`. - -## Implicit mode - -> ⚠️ **Note: Implicit mode is currently incomplete and under development.** Several implementation details need to be resolved: -> -> - Conflict resolution when user-defined tasks have the same name as built-in commands (build, test, lint) -> - Precedence rules for command resolution -> - Handling of edge cases with task arguments and flags -> -> This documentation describes the current behavior and should be updated when the Implicit Mode RFC is done. - -With implicit mode, `vp` will run the task in the current package. It can't accept more than on task. The first argument will be treated as the task name; the args following the command will be treated as the task args and bypass to the task. - -Given the following `package.json` file: - -```json -{ - "scripts": { - "build": "vite build", - "lint": "oxlint" - } -} -``` - -This command equivalent to `vite build`: - -```bash -vp build -``` - -This command equivalent to `vite build --mode production`: - -```bash -vp build --mode production -``` - -This command equivalent to `oxlint packages/cli/binding/index.js`: - -```bash -vp lint packages/cli/binding/index.js -``` - -If the command contains `#`, the command will be treated as a task in the workspace subpackage. - -This command equivalent to `vp run cli#build`: - -```bash -vp cli#build -``` - -## Explicit mode - -With explicit mode, `vp run` will run the task in the workspace subpackage. It can accept more than one task. The arguments will be treated as the task names; the args following the `--` will be treated as the task args and bypass to the task. - -### Default behavior - -The `vp run` command will run the scoped tasks in dependency order. - -Task names without `#` will be resolved in the current package (the package containing the nearest package.json file from the current working directory). For example: - -- `vp run build` - runs the build task in the current package -- `vp run test lint` - Throw `OnlyOneTaskRequest` error -- `vp run @other/package#build` - runs the build task in @other/package - -Behaviors when the package root and/or the workspace root is not found: - -| package root | workspace root | behaviour | -| ------------ | -------------- | ------------------------------------------------------------------------------------------------------------- | -| found | not found | No possible: workspace root always fallbacks to package root. | -| not found | found | `vp run build` should throw an `CurrentPackageNotFound` error. `vp run @other/package#build` is still allowed | -| not found | not found | Throw an error on any `vp run`. The workspace root must always exist as it's where we store the cache. | - -### `--recursive,-r` - -With the `--recursive,-r` flag, the `vp run` command will run the tasks in all monorepo packages. - -The task name should't contain `#` with the `--recursive,-r` flag. If any task name contains `#`, it would cause an `RecursiveRunWithScope` error. - -### `--topological,-t` / `--no-topological` - -The `--topological` flag controls whether implicit dependencies based on package dependencies are included in the task graph. This flag affects how tasks are ordered and executed: - -- **With `--topological` (or `-t`)**: When package A depends on package B, and both have a task with the same name (e.g., "build"), then A's task will automatically depend on B's task. This ensures that dependencies are built before dependents. -- **With `--no-topological`**: Explicitly disables topological ordering. Only explicit task dependencies defined in `vite-task.json` files are honored. Package dependencies do not create implicit task dependencies. - -Default behavior: - -- When used with `--recursive`, topological is **enabled by default** (can be disabled with `--no-topological`) -- When used without `--recursive`, topological is **disabled by default** (can be enabled with `--topological` or `-t`) - -Examples: - -```bash -# Recursive build with topological ordering (default) -vp run build -r - -# Recursive build WITHOUT topological ordering -vp run build -r --no-topological - -# Single package with topological ordering enabled -vp run app#build -t - -# Multiple packages without topological ordering (default) -vp run app#build web#build -``` - -Note: `--topological` and `--no-topological` are mutually exclusive and cannot be used together. See [boolean-flags.md](./boolean-flags.md) for more information about boolean flag patterns. - -## Task Dependencies - -Vite-plus supports two types of task dependencies: - -### 1. Explicit Dependencies (Always Applied) - -Tasks can declare explicit dependencies in `vite-task.json` files using the `dependsOn` field: - -```json -{ - "tasks": { - "lint": { - "command": "eslint src", - "cacheable": true, - "dependsOn": ["build", "core#build"] - }, - "deploy": { - "command": "deploy-script --prod", - "cacheable": false, - "dependsOn": ["test", "build", "utils#lint"] - } - } -} -``` - -These explicit dependencies are always honored regardless of the `--topological` flag. - -### 2. Implicit Dependencies (Topological Mode Only) - -When `--topological` is enabled, vite-plus automatically creates dependencies between tasks with the same name based on package dependencies: - -- If package `app` depends on package `utils` in `package.json` -- And both packages have a `build` script -- Then `app#build` will automatically depend on `utils#build` - -This works transitively - if `app` depends on `utils`, and `utils` depends on `core`, then `app#build` will depend on both `utils#build` and `core#build`. - -## Execution Order - -Tasks are executed in topological order based on their dependencies: - -1. Tasks with no dependencies run first -2. Tasks only run after all their dependencies have completed successfully -3. Independent tasks may run in parallel when `--parallel` is used - -### Example Execution Flow - -Given the following monorepo structure with topological ordering enabled: - -``` -Package Dependencies: - app → utils, ui - ui → utils - utils → (none) - -Task Dependencies (explicit): - app#build → app#lint - utils#build → utils#test -``` - -The execution flow for `vp run build -r --topological`: - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Task Resolution │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ Step 1: Collect all build tasks │ -│ ──────────────────────────────────────── │ -│ • app#build │ -│ • ui#build │ -│ • utils#build │ -│ │ -│ Step 2: Add explicit dependencies │ -│ ───────────────────────────────────── │ -│ • app#build depends on app#lint │ -│ • utils#build depends on utils#test │ -│ │ -│ Step 3: Add implicit dependencies (--topological) │ -│ ────────────────────────────────────────────────── │ -│ • app#build depends on utils#build (app→utils) │ -│ • app#build depends on ui#build (app→ui) │ -│ • ui#build depends on utils#build (ui→utils) │ -│ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Execution Order │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ Wave 1: No dependencies │ -│ ┌──────────────┐ │ -│ │ utils#test │ │ -│ │ app#lint │ (can run in parallel with --parallel) │ -│ └──────────────┘ │ -│ │ │ -│ ▼ │ -│ Wave 2: Dependencies from Wave 1 │ -│ ┌──────────────┐ │ -│ │ utils#build │ │ -│ └──────────────┘ │ -│ │ │ -│ ▼ │ -│ Wave 3: Dependencies from Wave 2 │ -│ ┌──────────────┐ │ -│ │ ui#build │ │ -│ └──────────────┘ │ -│ │ │ -│ ▼ │ -│ Wave 4: Final dependencies │ -│ ┌──────────────┐ │ -│ │ app#build │ │ -│ └──────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Compound Commands - -When a script contains `&&` operators, it's split into subtasks that execute sequentially: - -```json -{ - "scripts": { - "build": "tsc && rollup -c && echo 'Build complete'" - } -} -``` - -This creates three subtasks: - -- `package#build` (subcommand 0): `tsc` -- `package#build` (subcommand 1): `rollup -c` -- `package#build`: `echo 'Build complete'` - -Cross-package dependencies connect to the first subtask, and the last subtask is considered the completion of the task. - -## Package Graph Construction - -Vite-plus builds a package graph to understand the relationships between packages in your monorepo: - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Workspace Root │ -│ (pnpm-workspace.yaml) │ -└─────────────────┬───────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ 1. Package Discovery │ -│ │ -│ packages/ packages/ packages/ │ -│ ├── app/ ├── utils/ └── nameless/ │ -│ │ └── package.json │ └── package.json └── package.json │ -│ │ name: │ name: (no name) │ -│ │ "app" │ "utils" │ -└─────────────────┬───────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 2. Dependency Resolution │ -│ │ -│ app ─────depends─on────▶ utils │ -│ ↓ ↑ │ -│ └──────depends─on────▶ nameless │ -│ │ -│ Note: Nameless packages can be referenced via paths │ -└─────────────────┬───────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 3. Task Graph Construction │ -│ │ -│ app#build ──────────▶ utils#build │ -│ ↓ ↓ │ -│ app#test utils#test │ -│ │ -│ build ◀──── test (nameless package internal deps) │ -└─────────────────┬───────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 4. Execution Planning │ -│ │ -│ Execution Order: utils#build → app#build → parallel(tests) │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 1. Package Discovery - -The package graph builder starts by discovering all packages in the workspace: - -- Reads the workspace configuration (`pnpm-workspace.yaml`, `yarn workspaces`, or `npm workspaces`) -- Resolves glob patterns to find all package directories -- Loads `package.json` from each package directory -- Creates a node in the graph for each package - -### 2. Dependency Resolution - -For each package, the builder analyzes its dependencies: - -- Examines `dependencies`, `devDependencies`, and `peerDependencies` in `package.json` -- Identifies workspace dependencies (marked with `workspace:*` protocol) -- Creates edges in the graph between packages based on these dependencies -- Validates that all referenced workspace packages exist - -### 3. Task Graph Construction - -Once the package graph is built, vite-plus constructs a task graph: - -- Loads tasks from `vite-task.json` files in each package -- Loads scripts from `package.json` files -- Resolves explicit task dependencies (from `dependsOn` fields) -- When `--topological` is enabled, adds implicit dependencies based on package relationships -- Validates that all task dependencies can be resolved - -### 4. Execution Planning - -The final step creates an execution plan: - -- Performs topological sorting of the task graph -- Identifies tasks that can run in parallel -- Detects circular dependencies and reports errors -- Determines the optimal execution order - -## Task Request Matching Rules - -Task requests are in form of `task_name` or `pkg#task_name`. They occur in two places: - -- one or multiple parameters following after `vp run`. -- items in `dependsOn`. - -How task requests work: - -- `build` in `vp run build` matches task `build` in the current package determined by cwd. -- `build` in `dependsOn: ["build"]` matches task `build` in the package where the config file is. -- `app#build` matches task `build` in package `app`. -- `app#build` raises an error if there are multiple packages named `app`. -- Task requests with multiple `#` are invalid. -- Nameless packages are handled consistently with the rules above. They are not special cases. This means: - - `#build` is valid and matches task `build` in the nameless package. - - `#build` raises an error if there are multiple nameless packages. - - `build` does not match task `build` in the nameless package. - -While task requests with multiple `#` are invalid, packages with `#` in their names are valid. For example, a package named `pkg#special` can have a task named `build`. It can be referenced by executing `vp run build` under the folder of package `pkg#special`. diff --git a/crates/vite_task/src/session/cache/fingerprint.rs b/crates/vite_task/src/session/cache/fingerprint.rs deleted file mode 100644 index c691ce59..00000000 --- a/crates/vite_task/src/session/cache/fingerprint.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; - -use bincode::{Decode, Encode}; -use diff::Diff; -use serde::{Deserialize, Serialize}; -use vite_path::RelativePathBuf; -use vite_str::Str; - -/// Fingerprint for command execution that affects caching. -/// -/// # Environment Variable Impact on Cache -/// -/// The `envs_without_pass_through` field is crucial for cache correctness: -/// - Only includes envs explicitly declared in the task's `envs` array -/// - Does NOT include pass-through envs (PATH, CI, etc.) -/// - These envs become part of the cache key -/// -/// When a task runs: -/// 1. All envs (including pass-through) are available to the process -/// 2. Only declared envs affect the cache key -/// 3. If a declared env changes value, cache will miss -/// 4. If a pass-through env changes, cache will still hit -/// -/// For built-in tasks (lint, build, etc): -/// - The resolver provides envs which become part of the fingerprint -/// - If resolver provides different envs between runs, cache breaks -/// - Each built-in task type must have unique task name to avoid cache collision -/// -/// # Fingerprint Ignores Impact on Cache -/// -/// The `fingerprint_ignores` field controls which files are tracked in `PostRunFingerprint`: -/// - Changes to this config must invalidate the cache -/// - Vec maintains insertion order (pattern order matters for last-match-wins semantics) -/// - Even though ignore patterns only affect `PostRunFingerprint`, the config itself is part of the cache key -#[derive(Encode, Decode, Debug, Serialize, Deserialize, PartialEq, Eq, Diff, Clone)] -#[diff(attr(#[derive(Debug)]))] -pub struct SpawnFingerprint { - pub cwd: RelativePathBuf, - pub command_fingerprint: CommandFingerprint, - /// Environment variables that affect caching (excludes pass-through envs) - pub fingerprinted_envs: BTreeMap, // using BTreeMap to have a stable order in cache db - - /// even though value changes to `pass_through_envs` shouldn't invalidate the cache, - /// The names should still be fingerprinted so that the cache can be invalidated if the `pass_through_envs` config changes - pub pass_through_envs: BTreeSet, // using BTreeSet to have a stable order in cache db - - /// Glob patterns for fingerprint filtering. Order matters (last match wins). - /// Changes to this config invalidate the cache to ensure correct fingerprint tracking. - pub fingerprint_ignores: Option>, -} - -/// The program fingerprint used in `SpawnFingerprint` -#[derive(Encode, Decode, Debug, Serialize, Deserialize, PartialEq, Eq, Diff, Clone)] -#[diff(attr(#[derive(Debug)]))] -enum ProgramFingerprint { - /// If the program is outside the workspace, fingerprint by its name only (like `node`, `npm`, etc) - OutsideWorkspace { program_name: Str }, - - /// If the program is inside the workspace, fingerprint by its path relative to the workspace root - InsideWorkspace { relative_path: RelativePathBuf }, -} - -#[derive(Encode, Decode, Debug, Serialize, Deserialize, PartialEq, Eq, Diff, Clone)] -#[diff(attr(#[derive(Debug)]))] -enum CommandFingerprint { - /// A program with args to be executed directly - Program { program_fingerprint: ProgramFingerprint, args: Vec }, - /// A script to be executed by os shell (sh or cmd) - ShellScript { script: Str, extra_args: Vec }, -} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/README.md b/crates/vite_task_bin/tests/e2e_snapshots/README.md index 0863859f..27f592b5 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/README.md +++ b/crates/vite_task_bin/tests/e2e_snapshots/README.md @@ -1,6 +1,6 @@ # E2E Snapshot Tests -End-to-end tests that execute the `vite` binary and verify its output. +End-to-end tests that execute the `vp` binary and verify its output. ## When to add tests here diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test/README.md b/crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test/README.md deleted file mode 100644 index c7c173fc..00000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Fingerprint Ignore Test Fixture - -This fixture demonstrates the `fingerprintIgnores` feature for cache fingerprint calculation. - -## Task Configuration - -The `create-files` task in `vite-task.json` uses the following ignore patterns: - -```json -{ - "fingerprintIgnores": [ - "node_modules/**/*", - "!node_modules/**/package.json", - "dist/**/*" - ] -} -``` - -## Behavior - -With these ignore patterns: - -1. **`node_modules/**/*`** - Ignores all files under `node_modules/` -2. **`!node_modules/**/package.json`** - BUT keeps `package.json` files (negation pattern) -3. **`dist/**/*`** - Ignores all files under `dist/` - -### Cache Behavior - -- ✅ Cache **WILL BE INVALIDATED** when `node_modules/pkg-a/package.json` changes -- ❌ Cache **WILL NOT BE INVALIDATED** when `node_modules/pkg-a/index.js` changes -- ❌ Cache **WILL NOT BE INVALIDATED** when `dist/bundle.js` changes - -This allows caching package installation tasks where only dependency manifests (package.json) matter for cache validation, not the actual implementation files. - -## Example Usage - -```bash -# First run - task executes -vp run create-files - -# Second run - cache hit (all files tracked in fingerprint remain the same) -vp run create-files - -# Modify node_modules/pkg-a/index.js -echo 'modified' > node_modules/pkg-a/index.js - -# Third run - still cache hit (index.js is ignored) -vp run create-files - -# Modify node_modules/pkg-a/package.json -echo '{"name":"pkg-a","version":"2.0.0"}' > node_modules/pkg-a/package.json - -# Fourth run - cache miss (package.json is NOT ignored due to negation pattern) -vp run create-files -``` diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test/package.json deleted file mode 100644 index 625043d9..00000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "@test/fingerprint-ignore", - "version": "1.0.0" -} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test/snapshots/task graph.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test/snapshots/task graph.snap deleted file mode 100644 index 9b1922dc..00000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test/snapshots/task graph.snap +++ /dev/null @@ -1,41 +0,0 @@ ---- -source: crates/vite_task_plan/tests/plan_snapshots/main.rs -expression: task_graph_json -input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test ---- -[ - { - "key": [ - "/", - "create-files" - ], - "node": { - "task_display": { - "package_name": "@test/fingerprint-ignore", - "task_name": "create-files", - "package_path": "/" - }, - "resolved_config": { - "command": "mkdir -p node_modules/pkg-a && echo '{\"name\":\"pkg-a\"}' > node_modules/pkg-a/package.json && echo 'content' > node_modules/pkg-a/index.js && mkdir -p dist && echo 'output' > dist/bundle.js", - "resolved_options": { - "cwd": "/", - "cache_config": { - "env_config": { - "fingerprinted_envs": [], - "pass_through_envs": [ - "" - ] - }, - "input_config": { - "includes_auto": true, - "positive_globs": [], - "negative_globs": [] - } - } - } - }, - "source": "TaskConfig" - }, - "neighbors": [] - } -] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test/vite-task.json deleted file mode 100644 index f3540d91..00000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/fingerprint-ignore-test/vite-task.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "cache": true, - "tasks": { - "create-files": { - "command": "mkdir -p node_modules/pkg-a && echo '{\"name\":\"pkg-a\"}' > node_modules/pkg-a/package.json && echo 'content' > node_modules/pkg-a/index.js && mkdir -p dist && echo 'output' > dist/bundle.js", - "cache": true - } - } -} diff --git a/package.json b/package.json index 277b9d6a..07146397 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,7 @@ }, "type": "module", "scripts": { - "prepare": "husky", - "hello": "bash -c pwd", - "lint": "vp lint" + "prepare": "husky" }, "devDependencies": { "@types/node": "catalog:",