From 6ee27ab551fc33a1ac67b2cee427126893935e33 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Fri, 9 Oct 2026 14:02:05 +0800 Subject: [PATCH] fix(cache): leave noisy fields of pnpm's .modules.yaml out of its fingerprint pnpm rewrites `node_modules/.modules.yaml` with a new `prunedAt` on fresh installs, and records machine-specific paths in it. Vite reads it on every `resolveConfig`, so cached `vite build` and `vitest` tasks missed after every reinstall and never hit across machines. The manifest is now parsed (JSON, falling back to YAML for pnpm < 10.29) and hashed as canonical JSON without `prunedAt`, `storeDir`, `virtualStoreDir`, and `linkedSkills`. Content in neither format is hashed as-is. Redactions live in `session/execute/redact/`, one module per file. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + Cargo.lock | 1 + crates/vt/Cargo.toml | 1 + crates/vt/src/session/execute/fingerprint.rs | 2 +- crates/vt/src/session/execute/glob.rs | 2 +- crates/vt/src/session/execute/hash.rs | 89 +++++++++- crates/vt/src/session/execute/mod.rs | 1 + crates/vt/src/session/execute/redact/mod.rs | 32 ++++ .../execute/redact/pnpm_modules_manifest.rs | 155 ++++++++++++++++++ .../pnpm_modules_manifest/modules.json | 7 + .../pnpm_modules_manifest/package.json | 4 + .../pnpm_modules_manifest/snapshots.toml | 69 ++++++++ .../pnpm_modules_manifest_unstable_fields.md | 96 +++++++++++ .../pnpm_modules_manifest/vite-task.json | 8 + 14 files changed, 465 insertions(+), 3 deletions(-) create mode 100644 crates/vt/src/session/execute/redact/mod.rs create mode 100644 crates/vt/src/session/execute/redact/pnpm_modules_manifest.rs create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/modules.json create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/package.json create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/snapshots.toml create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/snapshots/pnpm_modules_manifest_unstable_fields.md create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/vite-task.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 150be0285..c6393b452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Fixed** Reinstalling dependencies with pnpm no longer causes cache misses in tasks that run Vite or Vitest ([#823](https://github.com/voidzero-dev/vite-task/pull/823)). - **Added** Vite+ now builds and runs on platforms without file-access tracking support, such as FreeBSD. On those platforms, automatic input and output tracking is unavailable, so tasks that rely on it report that tracking is unsupported and must configure their `input` and `output` manually to be cached ([#829](https://github.com/voidzero-dev/vite-task/pull/829)). - **Changed** The `vp run` summary now reads like `3/4 cache hit (75%), 9/10 successful, 2s saved.` The cache hit count leaves out tasks with caching disabled, and the count of successful tasks out of all tasks replaces the count of failed tasks. When no task has caching enabled, the summary shows only the successful count. In `vp run --verbose` and `vp run --last-details`, the cache hit rate also leaves out tasks with caching disabled, and says `no task has caching enabled` when there are none ([#821](https://github.com/voidzero-dev/vite-task/pull/821)). - **Perf** On Linux, tasks with automatic file-access tracking start faster ([#817](https://github.com/voidzero-dev/vite-task/pull/817)). diff --git a/Cargo.lock b/Cargo.lock index 11749ca95..d18f72928 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4934,6 +4934,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", + "serde_norway", "supports-color 3.0.2", "tar", "tempfile", diff --git a/crates/vt/Cargo.toml b/crates/vt/Cargo.toml index 1be50ccaf..d11458bb1 100644 --- a/crates/vt/Cargo.toml +++ b/crates/vt/Cargo.toml @@ -30,6 +30,7 @@ rusqlite = { workspace = true, features = ["bundled"] } rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive", "rc"] } serde_json = { workspace = true } +serde_norway = { workspace = true } supports-color = { workspace = true } thiserror = { workspace = true } tar = { workspace = true } diff --git a/crates/vt/src/session/execute/fingerprint.rs b/crates/vt/src/session/execute/fingerprint.rs index 8b9f64980..32f4cb880 100644 --- a/crates/vt/src/session/execute/fingerprint.rs +++ b/crates/vt/src/session/execute/fingerprint.rs @@ -417,7 +417,7 @@ pub fn fingerprint_path( return process_directory(std_path, path_read); } } - Ok(PathFingerprint::FileContentHash(super::hash::hash_content(reader)?)) + Ok(PathFingerprint::FileContentHash(super::hash::hash_file_content(path, reader)?)) } /// Process a directory on Windows using `std::fs::read_dir` diff --git a/crates/vt/src/session/execute/glob.rs b/crates/vt/src/session/execute/glob.rs index cf5287193..23808b422 100644 --- a/crates/vt/src/session/execute/glob.rs +++ b/crates/vt/src/session/execute/glob.rs @@ -130,7 +130,7 @@ fn strip_root( } fn hash_file_content(path: &AbsolutePath) -> io::Result { - super::hash::hash_content(io::BufReader::new(File::open(path.as_path())?)) + super::hash::hash_file_content(path, io::BufReader::new(File::open(path.as_path())?)) } #[cfg(test)] diff --git a/crates/vt/src/session/execute/hash.rs b/crates/vt/src/session/execute/hash.rs index 0bbfb0fac..bfde5c66f 100644 --- a/crates/vt/src/session/execute/hash.rs +++ b/crates/vt/src/session/execute/hash.rs @@ -1,8 +1,13 @@ use std::{hash::Hasher as _, io}; +use twox_hash::XxHash3_64; +use vt_path::AbsolutePath; + +use super::redact::{UnexpectedFormat, pnpm_modules_manifest}; + /// Hash content using 8 KiB buffered `xxHash3_64`. pub(super) fn hash_content(mut stream: impl io::Read) -> io::Result { - let mut hasher = twox_hash::XxHash3_64::default(); + let mut hasher = XxHash3_64::default(); let mut buf = [0u8; 8192]; loop { let n = stream.read(&mut buf)?; @@ -13,3 +18,85 @@ pub(super) fn hash_content(mut stream: impl io::Read) -> io::Result { } Ok(hasher.finish()) } + +/// Hash the content of the file at `path`, read from `stream`. Files with +/// noise in their content are hashed without it; see [`super::redact`]. +pub(super) fn hash_file_content(path: &AbsolutePath, stream: impl io::Read) -> io::Result { + if pnpm_modules_manifest::matches(path) { + return hash_redacted(stream, pnpm_modules_manifest::redact); + } + hash_content(stream) +} + +/// Hash what `redact` writes for the content of `stream`, or the raw content +/// when it is not in the format `redact` expects. +fn hash_redacted( + mut stream: impl io::Read, + redact: impl FnOnce(&[u8], &mut HashWriter) -> Result<(), UnexpectedFormat>, +) -> io::Result { + let mut content = Vec::new(); + stream.read_to_end(&mut content)?; + let mut hasher = HashWriter::default(); + match redact(&content, &mut hasher) { + Ok(()) => Ok(hasher.finish()), + Err(UnexpectedFormat) => hash_content(content.as_slice()), + } +} + +/// Hashes the bytes written to it with `xxHash3_64`. +#[derive(Default)] +struct HashWriter(XxHash3_64); + +impl HashWriter { + fn finish(&self) -> u64 { + self.0.finish() + } +} + +impl io::Write for HashWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.write(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const MANIFEST: &[u8] = + br#"{ "packageManager": "pnpm@11.24.0", "prunedAt": "Thu, 08 Oct 2026 08:18:18 GMT" }"#; + const MANIFEST_REINSTALLED: &[u8] = + br#"{ "packageManager": "pnpm@11.24.0", "prunedAt": "Fri, 09 Oct 2026 03:13:58 GMT" }"#; + + fn hash_at(relative_path: &str, content: &[u8]) -> u64 { + let path = vt_path::current_dir().unwrap().join(relative_path); + hash_file_content(&path, content).unwrap() + } + + #[test] + fn redacted_file_hashes_without_noise() { + assert_eq!( + hash_at("node_modules/.modules.yaml", MANIFEST), + hash_at("node_modules/.modules.yaml", MANIFEST_REINSTALLED) + ); + } + + #[test] + fn unparsable_redacted_file_hashes_raw_content() { + let content = b"{ \"prunedAt\": "; + assert_eq!( + hash_at("node_modules/.modules.yaml", content), + hash_content(&content[..]).unwrap() + ); + } + + #[test] + fn other_files_hash_raw_content() { + assert_eq!(hash_at(".modules.yaml", MANIFEST), hash_content(MANIFEST).unwrap()); + } +} diff --git a/crates/vt/src/session/execute/mod.rs b/crates/vt/src/session/execute/mod.rs index 13a49f26b..ad51c4a61 100644 --- a/crates/vt/src/session/execute/mod.rs +++ b/crates/vt/src/session/execute/mod.rs @@ -3,6 +3,7 @@ pub mod fingerprint; pub mod glob; mod hash; pub mod pipe; +mod redact; mod scheduler; pub mod spawn; #[cfg(fspy)] diff --git a/crates/vt/src/session/execute/redact/mod.rs b/crates/vt/src/session/execute/redact/mod.rs new file mode 100644 index 000000000..79eb04bcd --- /dev/null +++ b/crates/vt/src/session/execute/redact/mod.rs @@ -0,0 +1,32 @@ +//! Files whose content carries noise — timestamps, machine-specific paths — +//! that would otherwise cause cache misses without any meaningful change. +//! +//! To redact another file, add a module here with a `matches` condition and a +//! `redact` action, and call them from +//! [`hash_file_content`](super::hash::hash_file_content). + +pub(super) mod pnpm_modules_manifest; + +use std::io; + +use serde_json::Value; + +/// The content is not in the format a redaction expects, so the raw content is +/// hashed instead. +#[derive(Debug, PartialEq, Eq)] +pub struct UnexpectedFormat; + +/// Writes `value` as JSON with every object's keys sorted, so the output does +/// not depend on the file's key order. +/// +/// Serializing a JSON value fails only when `out` does, which the in-memory +/// writers used here never do. +fn write_canonical_json( + mut value: Value, + out: &mut W, +) -> Result<(), UnexpectedFormat> { + // serde_json's `preserve_order` feature is enabled in this workspace, so + // objects keep the file's key order until sorted. + value.sort_all_objects(); + serde_json::to_writer(out, &value).map_err(|_| UnexpectedFormat) +} diff --git a/crates/vt/src/session/execute/redact/pnpm_modules_manifest.rs b/crates/vt/src/session/execute/redact/pnpm_modules_manifest.rs new file mode 100644 index 000000000..3c96d5081 --- /dev/null +++ b/crates/vt/src/session/execute/redact/pnpm_modules_manifest.rs @@ -0,0 +1,155 @@ +//! pnpm's `node_modules/.modules.yaml`, which records how `node_modules` was +//! installed. + +use std::io; + +use serde_json::Value; +use vt_path::AbsolutePath; + +use super::{UnexpectedFormat, write_canonical_json}; + +/// Fields that vary with time or with the machine rather than with what is +/// installed: +/// +/// - `prunedAt`: when pnpm last pruned the virtual store. +/// - `storeDir`: absolute path of pnpm's store. +/// - `virtualStoreDir`: absolute on Windows, and inside pnpm's store with the +/// global virtual store. +/// - `linkedSkills`: absolute for skill entries outside the workspace. +const UNSTABLE_FIELDS: [&str; 4] = ["prunedAt", "storeDir", "virtualStoreDir", "linkedSkills"]; + +/// Whether `path` is pnpm's modules manifest. Matches whole path components, +/// with either separator on Windows. +pub fn matches(path: &AbsolutePath) -> bool { + path.ends_with("node_modules/.modules.yaml") +} + +/// Writes the manifest without [`UNSTABLE_FIELDS`] to `out`. Fails when +/// `content` is neither a JSON nor a YAML object. +pub fn redact(content: &[u8], out: &mut W) -> Result<(), UnexpectedFormat> { + let mut manifest = parse_json_or_yaml(content)?; + let fields = manifest.as_object_mut().ok_or(UnexpectedFormat)?; + for field in UNSTABLE_FIELDS { + fields.remove(field); + } + write_canonical_json(manifest, out) +} + +/// pnpm 10.29 and later write JSON; earlier versions write YAML. JSON is tried +/// first because it parses faster, and because the YAML parser rejects keys +/// longer than 1024 characters, which long dependency paths exceed. +fn parse_json_or_yaml(content: &[u8]) -> Result { + serde_json::from_slice(content) + .or_else(|_| serde_norway::from_slice(content)) + .map_err(|_| UnexpectedFormat) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + fn manifest() -> Value { + json!({ + "hoistedDependencies": { + "debug@4.4.0": { "debug": "private" }, + "ms@2.1.3": { "ms": "private" }, + }, + "packageManager": "pnpm@11.24.0", + "prunedAt": "Thu, 08 Oct 2026 08:18:18 GMT", + "storeDir": "/Users/alice/Library/pnpm/store/v11", + "virtualStoreDir": ".pnpm", + "linkedSkills": [".claude/skills/a"], + }) + } + + fn redact_manifest(content: &[u8]) -> Result, UnexpectedFormat> { + let mut out = Vec::new(); + redact(content, &mut out)?; + Ok(out) + } + + fn redact_manifest_value(manifest: &Value) -> Vec { + redact_manifest(&serde_json::to_vec_pretty(manifest).unwrap()).unwrap() + } + + #[test] + fn matches_only_inside_node_modules() { + let cwd = vt_path::current_dir().unwrap(); + assert!(matches(&cwd.join("node_modules/.modules.yaml"))); + assert!(matches(&cwd.join("packages/a/node_modules/.modules.yaml"))); + assert!(!matches(&cwd.join(".modules.yaml"))); + assert!(!matches(&cwd.join("node_modules/.modules.yaml.bak"))); + #[cfg(windows)] + assert!(matches(&cwd.join(r"packages\a\node_modules\.modules.yaml"))); + } + + #[test] + fn unstable_fields_do_not_change_redacted_manifest() { + let mut changed = manifest(); + changed["prunedAt"] = json!("Fri, 09 Oct 2026 03:13:58 GMT"); + changed["storeDir"] = json!("/tmp/store/v11"); + changed["virtualStoreDir"] = json!("../../tmp/store/v11/links"); + changed["linkedSkills"] = json!(["/opt/skills/a"]); + assert_eq!(redact_manifest_value(&manifest()), redact_manifest_value(&changed)); + } + + #[test] + fn other_fields_change_redacted_manifest() { + let mut changed = manifest(); + changed["packageManager"] = json!("pnpm@11.25.0"); + assert_ne!(redact_manifest_value(&manifest()), redact_manifest_value(&changed)); + } + + #[test] + fn key_order_does_not_change_redacted_manifest() { + let reordered = json!({ + "linkedSkills": [".claude/skills/a"], + "virtualStoreDir": ".pnpm", + "storeDir": "/Users/alice/Library/pnpm/store/v11", + "prunedAt": "Thu, 08 Oct 2026 08:18:18 GMT", + "packageManager": "pnpm@11.24.0", + "hoistedDependencies": { + "ms@2.1.3": { "ms": "private" }, + "debug@4.4.0": { "debug": "private" }, + }, + }); + assert_ne!( + serde_json::to_vec(&manifest()).unwrap(), + serde_json::to_vec(&reordered).unwrap() + ); + assert_eq!(redact_manifest_value(&manifest()), redact_manifest_value(&reordered)); + } + + #[test] + fn yaml_manifest_redacts_like_json_manifest() { + let yaml = b"hoistedDependencies: + debug@4.4.0: + debug: private + ms@2.1.3: + ms: private +linkedSkills: + - /opt/skills/a +packageManager: pnpm@11.24.0 +prunedAt: Fri, 09 Oct 2026 03:13:58 GMT +storeDir: /tmp/store/v11 +virtualStoreDir: ../../tmp/store/v11/links +"; + assert_eq!(redact_manifest(yaml).unwrap(), redact_manifest_value(&manifest())); + } + + #[test] + fn json_manifest_with_long_keys_is_redacted() { + let mut manifest = manifest(); + manifest["hoistedDependencies"]["a".repeat(2000)] = json!({ "a": "private" }); + let mut changed = manifest.clone(); + changed["prunedAt"] = json!("Fri, 09 Oct 2026 03:13:58 GMT"); + assert_eq!(redact_manifest_value(&manifest), redact_manifest_value(&changed)); + } + + #[test] + fn unparsable_manifest_is_not_redacted() { + assert_eq!(redact_manifest(b"{ \"prunedAt\": "), Err(UnexpectedFormat)); + } +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/modules.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/modules.json new file mode 100644 index 000000000..c015da05a --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/modules.json @@ -0,0 +1,7 @@ +{ + "packageManager": "pnpm@11.24.0", + "prunedAt": "Thu, 08 Oct 2026 08:18:18 GMT", + "storeDir": "/home/alice/.local/share/pnpm/store/v11", + "virtualStoreDir": ".pnpm", + "linkedSkills": [".claude/skills/a"] +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/package.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/package.json new file mode 100644 index 000000000..23bdd0303 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/package.json @@ -0,0 +1,4 @@ +{ + "name": "pnpm-modules-manifest", + "private": true +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/snapshots.toml new file mode 100644 index 000000000..0e52316e1 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/snapshots.toml @@ -0,0 +1,69 @@ +[[e2e]] +name = "pnpm_modules_manifest_unstable_fields" +comment = """ +Fields of pnpm's `node_modules/.modules.yaml` that vary with time or with the machine are left out of its fingerprint, so changing them keeps the cache hit. Changing any other field is a cache miss. +""" +steps = [ + [ + "vtt", + "mkdir", + "-p", + "node_modules", + ], + [ + "vtt", + "cp", + "modules.json", + "node_modules/.modules.yaml", + ], + { argv = [ + "vt", + "run", + "read-manifest", + ], comment = "cache miss" }, + { argv = [ + "vtt", + "replace-file-content", + "node_modules/.modules.yaml", + "Thu, 08 Oct 2026 08:18:18 GMT", + "Fri, 09 Oct 2026 03:13:58 GMT", + ], comment = "reinstall: new prunedAt" }, + { argv = [ + "vtt", + "replace-file-content", + "node_modules/.modules.yaml", + "/home/alice/.local/share/pnpm/store/v11", + "/home/bob/.local/share/pnpm/store/v11", + ], comment = "another machine: new storeDir" }, + { argv = [ + "vtt", + "replace-file-content", + "node_modules/.modules.yaml", + "\".pnpm\"", + "\"../../home/bob/.local/share/pnpm/store/v11/links\"", + ], comment = "global virtual store: new virtualStoreDir" }, + { argv = [ + "vtt", + "replace-file-content", + "node_modules/.modules.yaml", + "\".claude/skills/a\"", + "\"/home/bob/skills/a\"", + ], comment = "skill outside the workspace: new linkedSkills" }, + { argv = [ + "vt", + "run", + "read-manifest", + ], comment = "cache hit" }, + { argv = [ + "vtt", + "replace-file-content", + "node_modules/.modules.yaml", + "pnpm@11.24.0", + "pnpm@11.25.0", + ], comment = "new packageManager" }, + { argv = [ + "vt", + "run", + "read-manifest", + ], comment = "cache miss: 'node_modules/.modules.yaml' modified" }, +] diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/snapshots/pnpm_modules_manifest_unstable_fields.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/snapshots/pnpm_modules_manifest_unstable_fields.md new file mode 100644 index 000000000..7613c7021 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/snapshots/pnpm_modules_manifest_unstable_fields.md @@ -0,0 +1,96 @@ +# pnpm_modules_manifest_unstable_fields + +Fields of pnpm's `node_modules/.modules.yaml` that vary with time or with the machine are left out of its fingerprint, so changing them keeps the cache hit. Changing any other field is a cache miss. + +## `vtt mkdir -p node_modules` + +``` +``` + +## `vtt cp modules.json node_modules/.modules.yaml` + +``` +``` + +## `vt run read-manifest` + +cache miss + +``` +$ vtt print-file node_modules/.modules.yaml +{ + "packageManager": "pnpm@11.24.0", + "prunedAt": "Thu, 08 Oct 2026 08:18:18 GMT", + "storeDir": "/home/alice/.local/share/pnpm/store/v11", + "virtualStoreDir": ".pnpm", + "linkedSkills": [".claude/skills/a"] +} +``` + +## `vtt replace-file-content node_modules/.modules.yaml 'Thu, 08 Oct 2026 08:18:18 GMT' 'Fri, 09 Oct 2026 03:13:58 GMT'` + +reinstall: new prunedAt + +``` +``` + +## `vtt replace-file-content node_modules/.modules.yaml /home/alice/.local/share/pnpm/store/v11 /home/bob/.local/share/pnpm/store/v11` + +another machine: new storeDir + +``` +``` + +## `vtt replace-file-content node_modules/.modules.yaml '".pnpm"' '"../../home/bob/.local/share/pnpm/store/v11/links"'` + +global virtual store: new virtualStoreDir + +``` +``` + +## `vtt replace-file-content node_modules/.modules.yaml '".claude/skills/a"' '"/home/bob/skills/a"'` + +skill outside the workspace: new linkedSkills + +``` +``` + +## `vt run read-manifest` + +cache hit + +``` +$ vtt print-file node_modules/.modules.yaml ◉ cache hit, replaying +{ + "packageManager": "pnpm@11.24.0", + "prunedAt": "Thu, 08 Oct 2026 08:18:18 GMT", + "storeDir": "/home/alice/.local/share/pnpm/store/v11", + "virtualStoreDir": ".pnpm", + "linkedSkills": [".claude/skills/a"] +} + +--- +vt run: cache hit. +``` + +## `vtt replace-file-content node_modules/.modules.yaml pnpm@11.24.0 pnpm@11.25.0` + +new packageManager + +``` +``` + +## `vt run read-manifest` + +cache miss: 'node_modules/.modules.yaml' modified + +``` +$ vtt print-file node_modules/.modules.yaml ○ cache miss: 'node_modules/.modules.yaml' modified, executing +{ + "packageManager": "pnpm@11.25.0", + "prunedAt": "Fri, 09 Oct 2026 03:13:58 GMT", + "storeDir": "/home/bob/.local/share/pnpm/store/v11", + "virtualStoreDir": "../../home/bob/.local/share/pnpm/store/v11/links", + "linkedSkills": ["/home/bob/skills/a"] +} +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/vite-task.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/vite-task.json new file mode 100644 index 000000000..78679f4ee --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_modules_manifest/vite-task.json @@ -0,0 +1,8 @@ +{ + "tasks": { + "read-manifest": { + "command": "vtt print-file node_modules/.modules.yaml", + "cache": true + } + } +}