diff --git a/.gitignore b/.gitignore index 411255e24..3e48e505d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules # Fixture node_modules that stand in for real npm installs in snapshot tests. # `**` covers nested monorepo layouts (e.g. packages/foo/node_modules/.bin). !crates/vt_plan/tests/plan_snapshots/fixtures/*/**/node_modules +!crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/**/node_modules dist .claude/settings.local.json *.tsbuildinfo diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f1d90be..4fd869e4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Fixed** A cache hit restores the symbolic links a task's `output` globs match as links, instead of dropping them, so a replayed output that links a directory, such as a bundler's `node_modules/` link, is complete ([#885](https://github.com/voidzero-dev/vite-task/pull/885)). - **Fixed** On Linux and macOS, programs using automatic file-access tracking no longer abort when they call `execve`, `execv`, `execl`, `execle`, or `posix_spawn` with an empty path; the call fails with `ENOENT`, as it does without tracking. On Linux, `execveat` with an empty path and without `AT_EMPTY_PATH` now also fails with `ENOENT`, instead of trying to run what its directory descriptor refers to ([#896](https://github.com/voidzero-dev/vite-task/pull/896)). - **Perf** `vp run` starts faster in workspaces whose tasks share cache `input` and `output` patterns: each distinct pattern is now parsed once instead of once for every task that uses it. In a 138-package workspace, this saves about 2 seconds of CPU on every run ([#887](https://github.com/voidzero-dev/vite-task/pull/887), [#890](https://github.com/voidzero-dev/vite-task/pull/890)). - **Perf** When tasks miss the cache, `vp run` no longer waits for the disk after saving each task's result, so the run finishes sooner. If the computer loses power or the operating system crashes during `vp run`, the cache entries that the run saved can be lost, and those tasks run again next time instead of hitting the cache ([#871](https://github.com/voidzero-dev/vite-task/pull/871)). diff --git a/crates/vt/src/session/cache/archive.rs b/crates/vt/src/session/cache/archive.rs index dc110e29c..c4c0fe6e6 100644 --- a/crates/vt/src/session/cache/archive.rs +++ b/crates/vt/src/session/cache/archive.rs @@ -7,10 +7,13 @@ use std::{ use vt_path::{AbsolutePath, RelativePathBuf}; -/// Create a tar.zst archive from workspace-relative output file paths. +/// Create a tar.zst archive from workspace-relative output paths. /// -/// Files that no longer exist are silently skipped (the task may delete -/// temporary files during execution). +/// A regular file is stored with its contents. A symbolic link is stored as a +/// link with its target, never followed, so a link to a directory or a +/// dangling link round-trips as the task left it. Paths that no longer exist +/// are silently skipped (the task may delete temporary files during +/// execution). /// /// # Errors /// @@ -28,7 +31,7 @@ pub fn create_output_archive( let abs_path = workspace_root.join(rel_path); // Skip files that no longer exist (task may delete temp files between // glob walk and archiving). Any other error is propagated. - let metadata = match std::fs::metadata(abs_path.as_path()) { + let metadata = match std::fs::symlink_metadata(abs_path.as_path()) { Ok(m) => m, Err(err) if err.kind() == io::ErrorKind::NotFound => continue, Err(err) => return Err(err.into()), @@ -39,6 +42,13 @@ pub fn create_output_archive( header.set_metadata(&metadata); header.set_cksum(); builder.append_data(&mut header, rel_path.as_str(), &mut file)?; + } else if metadata.file_type().is_symlink() { + let target = std::fs::read_link(abs_path.as_path())?; + let mut header = tar::Header::new_gnu(); + header.set_metadata(&metadata); + header.set_entry_type(tar::EntryType::Symlink); + header.set_size(0); + builder.append_link(&mut header, rel_path.as_str(), target)?; } } @@ -46,7 +56,8 @@ pub fn create_output_archive( Ok(()) } -/// Extract a tar.zst archive, restoring files relative to workspace root. +/// Extract a tar.zst archive, restoring files and symbolic links relative to +/// workspace root. /// /// Parent directories are created automatically. Existing files are overwritten. /// @@ -61,7 +72,43 @@ pub fn extract_output_archive( let decoder = zstd::Decoder::new(file)?; let mut archive = tar::Archive::new(decoder); + #[cfg(not(windows))] archive.unpack(workspace_root.as_path())?; + #[cfg(windows)] + unpack_with_directory_links(&mut archive, workspace_root)?; + Ok(()) +} + +/// [`tar::Archive::unpack`], except that a restored link whose target is a +/// directory becomes a directory link: `tar` creates every link with +/// `symlink_file` on Windows, and Windows doesn't follow a file link to a +/// directory. Links are fixed up once every entry is unpacked, so their targets +/// exist when they are checked. +#[cfg(windows)] +fn unpack_with_directory_links( + archive: &mut tar::Archive, + workspace_root: &AbsolutePath, +) -> anyhow::Result<()> { + // As `tar::Archive::unpack` does: the canonical root is a verbatim (`\\?\`) + // path, which lifts Windows' path length limit. + let root = std::fs::canonicalize(workspace_root.as_path()) + .unwrap_or_else(|_| workspace_root.as_path().to_path_buf()); + let mut links = Vec::new(); + for entry in archive.entries()? { + let mut entry = entry?; + let is_link = entry.header().entry_type().is_symlink(); + let link = root.join(entry.path()?); + if entry.unpack_in(&root)? && is_link { + links.push(link); + } + } + for link in links { + let target = std::fs::read_link(&link)?; + if link.parent().is_some_and(|parent| parent.join(&target).is_dir()) { + std::fs::remove_file(&link)?; + std::os::windows::fs::symlink_dir(&target, &link)?; + } + } Ok(()) } @@ -84,6 +131,8 @@ pub fn check_output_archive(reader: impl Read) -> io::Result<()> { #[cfg(test)] mod tests { + use std::fs; + use vt_path::AbsolutePathBuf; use super::*; @@ -103,4 +152,76 @@ mod tests { assert!(check_output_archive(corrupt).is_err()); } } + + fn symlink(target: &str, link: &AbsolutePath, directory: bool) { + #[cfg(unix)] + { + let _ = directory; + std::os::unix::fs::symlink(target, link.as_path()).unwrap(); + } + #[cfg(windows)] + if directory { + std::os::windows::fs::symlink_dir(target, link.as_path()).unwrap(); + } else { + std::os::windows::fs::symlink_file(target, link.as_path()).unwrap(); + } + } + + // Unix only: Windows doesn't resolve a relative link target with `/` + // separators, such as `pkg/index.js`, which is how `tar` stores targets. + #[cfg(unix)] + #[test] + fn symlinks_round_trip_as_links() { + let tmp = tempfile::TempDir::new().unwrap(); + let dir = AbsolutePathBuf::new(tmp.path().to_path_buf()).unwrap(); + fs::create_dir_all(dir.join("dist/pkg")).unwrap(); + fs::write(dir.join("dist/pkg/index.js"), "export default 42;").unwrap(); + let links = [ + ("file.js", "pkg/index.js", false), + ("dependency", "pkg", true), + ("dangling", "missing", false), + ]; + for (name, target, directory) in links { + symlink(target, &dir.join("dist").join(name), directory); + } + let files = ["dist/pkg/index.js", "dist/file.js", "dist/dependency", "dist/dangling"] + .map(|path| RelativePathBuf::new(path).unwrap()); + let archive_path = dir.join("output.tar.zst"); + create_output_archive(&dir, &files, &archive_path).unwrap(); + + fs::remove_dir_all(dir.join("dist")).unwrap(); + extract_output_archive(&dir, &archive_path).unwrap(); + for (name, target, _) in links { + let restored = fs::read_link(dir.join("dist").join(name)).unwrap(); + assert_eq!( + RelativePathBuf::new(restored).unwrap(), + RelativePathBuf::new(target).unwrap() + ); + } + assert_eq!(fs::read_to_string(dir.join("dist/file.js")).unwrap(), "export default 42;"); + assert_eq!( + fs::read_to_string(dir.join("dist/dependency/index.js")).unwrap(), + "export default 42;" + ); + assert!(!dir.join("dist/dangling").as_path().exists()); + } + + #[test] + fn restore_rejects_writing_through_a_link_outside_the_workspace() { + let tmp = tempfile::TempDir::new().unwrap(); + let dir = AbsolutePathBuf::new(tmp.path().to_path_buf()).unwrap(); + let outside = tempfile::TempDir::new().unwrap(); + let sentinel = outside.path().join("sentinel.txt"); + fs::write(&sentinel, "archived").unwrap(); + symlink(outside.path().to_str().unwrap(), &dir.join("link"), true); + let files = ["link", "link/sentinel.txt"].map(|path| RelativePathBuf::new(path).unwrap()); + let archive_path = dir.join("output.tar.zst"); + create_output_archive(&dir, &files, &archive_path).unwrap(); + fs::write(&sentinel, "must remain unchanged").unwrap(); + + let restored = dir.join("restored"); + fs::create_dir_all(&restored).unwrap(); + assert!(extract_output_archive(&restored, &archive_path).is_err()); + assert_eq!(fs::read_to_string(&sentinel).unwrap(), "must remain unchanged"); + } } diff --git a/crates/vt/src/session/cache/mod.rs b/crates/vt/src/session/cache/mod.rs index ab6c806f9..30536eb9d 100644 --- a/crates/vt/src/session/cache/mod.rs +++ b/crates/vt/src/session/cache/mod.rs @@ -276,7 +276,7 @@ pub fn split_path(path: &str) -> (Option<&str>, &str) { /// its own cache warm across branch switches, and a cache from a different /// version is simply ignored (it lives in a directory this build never looks /// at) rather than aborting the run. Bumping the version starts a fresh cache. -const CACHE_SCHEMA_VERSION: u32 = 18; +const CACHE_SCHEMA_VERSION: u32 = 19; /// Name of the per-version subdirectory (e.g. `v14`) under the task-cache /// directory that holds the database and output archives for the current diff --git a/crates/vt/src/session/execute/glob.rs b/crates/vt/src/session/execute/glob.rs index 19983639c..f25f9fac5 100644 --- a/crates/vt/src/session/execute/glob.rs +++ b/crates/vt/src/session/execute/glob.rs @@ -15,7 +15,7 @@ use wax::{ }; /// Walk positive globs (rooted at `root`), filtering with negative globs, and -/// call `for_each_file` with the absolute path of each file entry. Stripping +/// call `for_each_file` with the absolute path and type of each file or symlink. Stripping /// the root prefix (if needed) is the caller's responsibility. /// /// Walk errors for non-existent directories are skipped gracefully. Returning @@ -25,7 +25,7 @@ fn walk_glob_files( root: &AbsolutePath, positive_globs: &std::collections::BTreeSet, negative_globs: &std::collections::BTreeSet, - mut for_each_file: impl FnMut(AbsolutePathBuf) -> anyhow::Result>, + mut for_each_file: impl FnMut(AbsolutePathBuf, std::fs::FileType) -> anyhow::Result>, ) -> anyhow::Result<()> { if positive_globs.is_empty() { return Ok(()); @@ -52,13 +52,14 @@ fn walk_glob_files( return Err(io_err.into()); } }; - if !entry.file_type().is_file() { + let file_type = entry.file_type(); + if !file_type.is_file() && !file_type.is_symlink() { continue; } let Some(absolute) = AbsolutePathBuf::new(entry.path().to_path_buf()) else { continue; // wax can hand back non-absolute paths in some cases }; - if for_each_file(absolute)?.is_break() { + if for_each_file(absolute, file_type)?.is_break() { return Ok(()); } } @@ -77,7 +78,10 @@ pub fn compute_globbed_inputs( negative_globs: &std::collections::BTreeSet, ) -> anyhow::Result> { let mut result = BTreeMap::new(); - walk_glob_files(root, positive_globs, negative_globs, |absolute| { + walk_glob_files(root, positive_globs, negative_globs, |absolute, file_type| { + if !file_type.is_file() { + return Ok(ControlFlow::Continue(())); + } let Some(relative) = strip_root(root, &absolute)? else { return Ok(ControlFlow::Continue(())); // outside root }; @@ -98,7 +102,7 @@ pub fn compute_globbed_inputs( Ok(result) } -/// Collect file paths matching positive globs, filtered by negative globs. +/// Collect file and symlink paths matching positive globs, filtered by negative globs. /// /// Like [`compute_globbed_inputs`] but only collects paths (no hashing). /// Used for determining which output files to archive. @@ -108,7 +112,7 @@ pub fn collect_glob_paths( negative_globs: &std::collections::BTreeSet, ) -> anyhow::Result> { let mut result = Vec::new(); - walk_glob_files(root, positive_globs, negative_globs, |absolute| { + walk_glob_files(root, positive_globs, negative_globs, |absolute, _| { if let Some(relative) = strip_root(root, &absolute)? { result.push(relative); } @@ -431,4 +435,51 @@ mod tests { // BTreeMap naturally deduplicates by key assert_eq!(result.len(), 3); } + + fn symlink(target: &str, link: &AbsolutePath, directory: bool) { + #[cfg(unix)] + { + let _ = directory; + std::os::unix::fs::symlink(target, link.as_path()).unwrap(); + } + #[cfg(windows)] + if directory { + std::os::windows::fs::symlink_dir(target, link.as_path()).unwrap(); + } else { + std::os::windows::fs::symlink_file(target, link.as_path()).unwrap(); + } + } + + #[test] + fn output_paths_include_symlinks_without_traversing_them() { + let temp_dir = TempDir::new().unwrap(); + let workspace = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + fs::create_dir_all(workspace.join("dist")).unwrap(); + fs::create_dir_all(workspace.join("outside")).unwrap(); + fs::write(workspace.join("outside/index.js"), "export default 42;").unwrap(); + fs::write(workspace.join("dist/regular.js"), "export default 7;").unwrap(); + for (name, target, directory) in [ + ("file.js", "../outside/index.js", false), + ("dependency", "../outside", true), + ("dangling", "missing", false), + ("excluded", "../outside", true), + ] { + symlink(target, &workspace.join("dist").join(name), directory); + } + let positive: std::collections::BTreeSet = std::iter::once("dist/**".into()).collect(); + let negative: std::collections::BTreeSet = + std::iter::once("dist/excluded".into()).collect(); + + let outputs = collect_glob_paths(&workspace, &positive, &negative).unwrap(); + assert_eq!( + outputs.iter().map(|path| path.as_str()).collect::>(), + ["dist/dangling", "dist/dependency", "dist/file.js", "dist/regular.js"] + ); + // Inputs are unchanged: a link is not a globbed input. + let inputs = compute_globbed_inputs(&workspace, &positive, &negative).unwrap(); + assert_eq!( + inputs.keys().map(|path| path.as_str()).collect::>(), + ["dist/regular.js"] + ); + } } diff --git a/crates/vt_bin/src/vtt/main.rs b/crates/vt_bin/src/vtt/main.rs index e77c719df..bd5cff2e4 100644 --- a/crates/vt_bin/src/vtt/main.rs +++ b/crates/vt_bin/src/vtt/main.rs @@ -28,6 +28,8 @@ mod small_dev_shm; mod stalled_remote_cache; mod stat_file; mod stat_many; +#[cfg(unix)] +mod symlink; mod touch_file; mod write_file; @@ -36,7 +38,7 @@ fn main() { if args.len() < 2 { eprintln!("Usage: vtt [args...]"); eprintln!( - "Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, grep-file, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, small_dev_shm, stalled-remote-cache, stat-file, stat-many, touch-file, write-file" + "Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, grep-file, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, small_dev_shm, stalled-remote-cache, stat-file, stat-many, symlink, touch-file, write-file" ); std::process::exit(1); } @@ -78,6 +80,10 @@ fn main() { Ok(()) } "stat-many" => stat_many::run(&args[2..]), + #[cfg(unix)] + "symlink" => symlink::run(&args[2..]), + #[cfg(not(unix))] + "symlink" => Err("vtt symlink is only supported on Unix".into()), "touch-file" => touch_file::run(&args[2..]), "write-file" => write_file::run(&args[2..]), other => { diff --git a/crates/vt_bin/src/vtt/symlink.rs b/crates/vt_bin/src/vtt/symlink.rs new file mode 100644 index 000000000..2ab960572 --- /dev/null +++ b/crates/vt_bin/src/vtt/symlink.rs @@ -0,0 +1,8 @@ +/// Create a symbolic link at `` that points to ``, like `ln -s`. +pub fn run(args: &[String]) -> Result<(), Box> { + let [target, link] = args else { + return Err("Usage: vtt symlink ".into()); + }; + std::os::unix::fs::symlink(target, link)?; + Ok(()) +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/check.cjs b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/check.cjs new file mode 100644 index 000000000..a451f9d75 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/check.cjs @@ -0,0 +1,6 @@ +// Prints the `tslib` version each traced package resolves to in the server output. +const modules = './.output/server/node_modules/'; +for (const name of ['dep-a', 'dep-b']) { + console.log(`${name}: tslib ${require(modules + name)()}`); +} +console.log(`hoisted: tslib ${require(modules + 'tslib').version}`); diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/nitro.config.ts b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/nitro.config.ts new file mode 100644 index 000000000..dbc80b94e --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/nitro.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'nitro'; + +export default defineConfig({ + serverDir: './', + // Trace these packages instead of bundling them. They depend on different + // versions of `tslib`, so nf3 stores each version under `node_modules/.nf3` + // and links to it. + traceDeps: ['dep-a', 'dep-b'], + // Keep the build output stable for the snapshot: no date-dependent setting, + // no build log (`CONSOLA_LEVEL=0` in the task command quiets nf3's), and no + // bundler warnings, which Nitro prints itself. + compatibilityDate: '2026-10-29', + logLevel: 0, + rolldownConfig: { onwarn() {} }, +}); diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-a/index.js b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-a/index.js new file mode 100644 index 000000000..3d5236f35 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-a/index.js @@ -0,0 +1 @@ +module.exports = () => require('tslib').version; diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-a/node_modules/tslib/package.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-a/node_modules/tslib/package.json new file mode 100644 index 000000000..a9ad8f11d --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-a/node_modules/tslib/package.json @@ -0,0 +1,5 @@ +{ + "name": "tslib", + "version": "1.14.1", + "main": "tslib.js" +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-a/node_modules/tslib/tslib.js b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-a/node_modules/tslib/tslib.js new file mode 100644 index 000000000..2c8ca2498 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-a/node_modules/tslib/tslib.js @@ -0,0 +1 @@ +exports.version = '1.14.1'; diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-a/package.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-a/package.json new file mode 100644 index 000000000..c95b9ddfe --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-a/package.json @@ -0,0 +1,8 @@ +{ + "name": "dep-a", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "tslib": "1.14.1" + } +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-b/index.js b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-b/index.js new file mode 100644 index 000000000..3d5236f35 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-b/index.js @@ -0,0 +1 @@ +module.exports = () => require('tslib').version; diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-b/node_modules/tslib/package.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-b/node_modules/tslib/package.json new file mode 100644 index 000000000..f95741e46 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-b/node_modules/tslib/package.json @@ -0,0 +1,5 @@ +{ + "name": "tslib", + "version": "2.8.1", + "main": "tslib.js" +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-b/node_modules/tslib/tslib.js b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-b/node_modules/tslib/tslib.js new file mode 100644 index 000000000..a93be49c9 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-b/node_modules/tslib/tslib.js @@ -0,0 +1 @@ +exports.version = '2.8.1'; diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-b/package.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-b/package.json new file mode 100644 index 000000000..7e415db1e --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/node_modules/dep-b/package.json @@ -0,0 +1,8 @@ +{ + "name": "dep-b", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "tslib": "2.8.1" + } +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/package.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/package.json new file mode 100644 index 000000000..acba63e82 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/package.json @@ -0,0 +1,5 @@ +{ + "name": "nitro-build-cache-fixture", + "private": true, + "type": "module" +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/routes/index.ts b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/routes/index.ts new file mode 100644 index 000000000..5817249a6 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/routes/index.ts @@ -0,0 +1,4 @@ +import depA from 'dep-a'; +import depB from 'dep-b'; + +export default () => ({ depA: depA(), depB: depB() }); diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/snapshots.toml new file mode 100644 index 000000000..a5b9528c2 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/snapshots.toml @@ -0,0 +1,32 @@ +[[e2e]] +name = "nitro_build_restores_traced_dependency_links" +comment = """ +A real Nitro server build traces `dep-a` and `dep-b`, which depend on different versions of `tslib` (small stand-ins committed in the fixture's `node_modules`). nf3 writes each version under `.output/server/node_modules/.nf3/tslib@`, links `node_modules/tslib` to the hoisted one, and links each package's `node_modules/tslib` to the one it needs. A cache hit restores those links, so each package still resolves its own version from the restored output. On Windows, nf3 creates junctions, whose targets are absolute paths. +""" +ignore = true +steps = [ + { argv = [ + "vt", + "run", + "build", + ], comment = "first run — cache miss, Nitro builds .output/" }, + { argv = [ + "node", + "check.cjs", + ], comment = "each package resolves its own tslib through nf3's links" }, + { argv = [ + "vtt", + "rm", + "-rf", + ".output", + ], comment = "delete .output/ to prove the restore is real" }, + { argv = [ + "vt", + "run", + "build", + ], comment = "second run — cache hit, restores .output/ with its links" }, + { argv = [ + "node", + "check.cjs", + ], comment = "the restored output resolves the same versions" }, +] diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/snapshots/nitro_build_restores_traced_dependency_links.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/snapshots/nitro_build_restores_traced_dependency_links.md new file mode 100644 index 000000000..58a96b2c0 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/snapshots/nitro_build_restores_traced_dependency_links.md @@ -0,0 +1,49 @@ +# nitro_build_restores_traced_dependency_links + +A real Nitro server build traces `dep-a` and `dep-b`, which depend on different versions of `tslib` (small stand-ins committed in the fixture's `node_modules`). nf3 writes each version under `.output/server/node_modules/.nf3/tslib@`, links `node_modules/tslib` to the hoisted one, and links each package's `node_modules/tslib` to the one it needs. A cache hit restores those links, so each package still resolves its own version from the restored output. On Windows, nf3 creates junctions, whose targets are absolute paths. + +## `vt run build` + +first run — cache miss, Nitro builds .output/ + +``` +$ CONSOLA_LEVEL=0 nitro build +``` + +## `node check.cjs` + +each package resolves its own tslib through nf3's links + +``` +dep-a: tslib 1.14.1 +dep-b: tslib 2.8.1 +hoisted: tslib 2.8.1 +``` + +## `vtt rm -rf .output` + +delete .output/ to prove the restore is real + +``` +``` + +## `vt run build` + +second run — cache hit, restores .output/ with its links + +``` +$ CONSOLA_LEVEL=0 nitro build ◉ cache hit, replaying + +--- +vt run: cache hit. +``` + +## `node check.cjs` + +the restored output resolves the same versions + +``` +dep-a: tslib 1.14.1 +dep-b: tslib 2.8.1 +hoisted: tslib 2.8.1 +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/vite-task.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/vite-task.json new file mode 100644 index 000000000..f53f3a9da --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/nitro_build_cache/vite-task.json @@ -0,0 +1,11 @@ +{ + "tasks": { + "build": { + "command": "CONSOLA_LEVEL=0 nitro build", + "cache": { + "input": ["routes/**", "nitro.config.ts"], + "output": [".output/**"] + } + } + } +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots.toml index 8ba3fa56a..fd54f3af1 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots.toml +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots.toml @@ -155,3 +155,43 @@ steps = [ "dist/skip.txt", ], comment = "skip.txt is NOT restored — it was excluded by the negative glob" }, ] + +[[e2e]] +name = "output_globs___symlinks_restored_on_cache_hit" +comment = """ +A symbolic link matched by an output glob is archived as a link, so a cache hit restores it. Restoring also replaces a link that is already there. Unix only: on Windows, `tar` restores the target `store/pkg` with its `/` separator, which Windows doesn't resolve, and can't replace the directory link already there. +""" +cfg = "unix" +steps = [ + { argv = [ + "vt", + "run", + "build-with-symlink", + ], comment = "first run — cache miss, writes dist/store/pkg/index.js and links dist/pkg to store/pkg" }, + { argv = [ + "vtt", + "rm", + "-rf", + "dist", + ], comment = "delete dist/ to prove the restore is real" }, + { argv = [ + "vt", + "run", + "build-with-symlink", + ], comment = "second run — cache hit, restores the file and the link" }, + { argv = [ + "vtt", + "print-file", + "dist/pkg/index.js", + ], comment = "the file is reachable through the restored link" }, + { argv = [ + "vt", + "run", + "build-with-symlink", + ], comment = "third run — cache hit, restores over the link the second run left" }, + { argv = [ + "vtt", + "print-file", + "dist/pkg/index.js", + ], comment = "the file is still reachable through the link" }, +] diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_globs___symlinks_restored_on_cache_hit.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_globs___symlinks_restored_on_cache_hit.md new file mode 100644 index 000000000..a73f2c8b7 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/output_cache_test/snapshots/output_globs___symlinks_restored_on_cache_hit.md @@ -0,0 +1,65 @@ +# output_globs___symlinks_restored_on_cache_hit + +A symbolic link matched by an output glob is archived as a link, so a cache hit restores it. Restoring also replaces a link that is already there. Unix only: on Windows, `tar` restores the target `store/pkg` with its `/` separator, which Windows doesn't resolve, and can't replace the directory link already there. + +## `vt run build-with-symlink` + +first run — cache miss, writes dist/store/pkg/index.js and links dist/pkg to store/pkg + +``` +$ vtt write-file dist/store/pkg/index.js built + +$ vtt symlink store/pkg dist/pkg + +--- +vt run: 0/2 cache hit (0%), 2/2 successful. (Run `vt run --last-details` for full details) +``` + +## `vtt rm -rf dist` + +delete dist/ to prove the restore is real + +``` +``` + +## `vt run build-with-symlink` + +second run — cache hit, restores the file and the link + +``` +$ vtt write-file dist/store/pkg/index.js built ◉ cache hit, replaying + +$ vtt symlink store/pkg dist/pkg ◉ cache hit, replaying + +--- +vt run: 2/2 cache hit (100%), 2/2 successful. (Run `vt run --last-details` for full details) +``` + +## `vtt print-file dist/pkg/index.js` + +the file is reachable through the restored link + +``` +built +``` + +## `vt run build-with-symlink` + +third run — cache hit, restores over the link the second run left + +``` +$ vtt write-file dist/store/pkg/index.js built ◉ cache hit, replaying + +$ vtt symlink store/pkg dist/pkg ◉ cache hit, replaying + +--- +vt run: 2/2 cache hit (100%), 2/2 successful. (Run `vt run --last-details` for full details) +``` + +## `vtt print-file dist/pkg/index.js` + +the file is still reachable through the link + +``` +built +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/output_cache_test/vite-task.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/output_cache_test/vite-task.json index c63d43105..d5e02b946 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/output_cache_test/vite-task.json +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/output_cache_test/vite-task.json @@ -7,6 +7,13 @@ "output": ["dist/**"] } }, + "build-with-symlink": { + "command": "vtt write-file dist/store/pkg/index.js built && vtt symlink store/pkg dist/pkg", + "cache": { + "input": ["src/**"], + "output": ["dist/**"] + } + }, "build-with-negative": { "command": "vtt write-file dist/keep.txt keep && vtt write-file dist/skip.txt skip", "cache": { diff --git a/packages/tools/package.json b/packages/tools/package.json index 851acbfee..d78218a74 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -23,6 +23,7 @@ "cross-env": "catalog:", "cross-spawn": "catalog:", "deno": "catalog:", + "nitro": "catalog:", "oxfmt": "catalog:", "oxlint": "catalog:", "oxlint-tsgolint": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c327f1b92..440dce426 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ catalogs: deno: specifier: 2.9.6 version: 2.9.6 + nitro: + specifier: 3.0.260903-beta + version: 3.0.260903-beta oxfmt: specifier: 0.73.0 version: 0.73.0 @@ -136,6 +139,9 @@ importers: deno: specifier: 'catalog:' version: 2.9.6 + nitro: + specifier: 'catalog:' + version: 3.0.260903-beta oxfmt: specifier: 'catalog:' version: 0.73.0(vite-plus@1.1.0(@types/node@25.0.3)(@vitest/browser-playwright@5.0.3)(@vitest/ui@5.0.3)(playwright@1.63.0)(typescript@7.1.0-dev.20261001.1)) @@ -1462,6 +1468,10 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1474,6 +1484,17 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crossws@0.4.17: + resolution: {integrity: sha512-YG2kyfafNXKmu7e6k64yrj6whrw7m5sBIsOSXOlJ/72X3TQWRQuw8DhvXFDcBk8HF77PnTSygnm7xpYCTnFpow==} + peerDependencies: + srvx: '>=0.11.5' + peerDependenciesMeta: + srvx: + optional: true + + db0@0.4.2: + resolution: {integrity: sha512-OCBz+HiE8NgfQdlvvNrzdE+4RhmoAV3QnkxOhLU40bQH/N1msRwmPBvVyXmCUDmjhDl1+t4CFcSCccmi5jf+VQ==} + deno@2.9.6: resolution: {integrity: sha512-cr16GxYiUeH1juNvFTMOxt99V5QRVmANfy5UsZvE5hjC6P/o4Op/MBbRhLeBOL8WJpV6tr9hb+Kjb1MW0nEoVg==} hasBin: true @@ -1489,6 +1510,10 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + env-runner@0.2.3: + resolution: {integrity: sha512-S8EFBKyznRwPVwayK187cYjvnpgC92xy+q0BIF++V965pfjMVVRbQ/HoTYGA/1EHmZ7E68k6Heq+4WnvVhOL5w==} + hasBin: true + es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} @@ -1499,6 +1524,9 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + exsolve@1.2.0: + resolution: {integrity: sha512-h1pQPR9XoeSMdntRNCjyTznaume4UpfXrULBnlkZ2CUM/G5eSa+pBByGldO5uR8dxbPC+OuJXsPPNf+JZ81ADg==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1519,6 +1547,25 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + h3@2.0.8: + resolution: {integrity: sha512-ENuKlF9kC6E6PrvY1PUyE0qkrnnPTIt2/v48uyEEfTVfgoiqBCE0uwwf++2XrWwT1gxjQBDLQ8PAsmZVniKvTw==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + crossws: ^0.4.12 + ocache: '>=0.3.0' + peerDependenciesMeta: + crossws: + optional: true + ocache: + optional: true + + hookable@6.1.2: + resolution: {integrity: sha512-+abwxtiEA52GCVIsQqut3S/uKTbUwYIp4Pe/vv+6py5XiXBCqMZHg6pA6Y5qhgLSEys0/cYuPbO0z1QFj5ZCmg==} + + httpxy@0.5.6: + resolution: {integrity: sha512-iqc3nOxvNb5FxbhJozt8wNmHgjlmk4zzba512jMEXVwHCEBCbGAj49eGfy5PI8ts3/LFSFq8siiSNqwfY1AlfQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1615,6 +1662,14 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nf3@0.3.24: + resolution: {integrity: sha512-HxLK4bo+5jNsEETZp4w3tJblHOA9MCBY14IN9nZJJV8JDxt9yNIYxuBuLMjTAR5GFa3HL61+8VQDUrXv3/C8fw==} + + nitro@3.0.260903-beta: + resolution: {integrity: sha512-54gANPi62O8rfMvepiJUVuIzEIinYfpeHbebywlLxvVTgIYXDwSvpaU9Id+0sJOBjBx0wC1/CVYXJkH7SY+l0g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + node-forge@1.4.0: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} @@ -1623,6 +1678,9 @@ packages: resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} engines: {node: '>=12.20.0'} + ocache@0.3.1: + resolution: {integrity: sha512-qeBrhQ0ly6tgXsakgNwLx6diRUppJZIcV5OjVGa5yp9HzztutkaGDDXJqVMo1fKWz5fNDtTp3iPU2fuhHf0+Lg==} + oxfmt@0.72.0: resolution: {integrity: sha512-16OQeL0uhPZbfmT9S9WWM0Nv9+BmVi8Ra1uTYaSKj/qSCs6FB30KOhz9JoNYw6thT+K5bVxsk/tSEOu497i34g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1740,6 +1798,13 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rou3@0.9.2: + resolution: {integrity: sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==} + + rou3@1.0.0: + resolution: {integrity: sha512-ySeaQkSrua+ut9g+65I7FHqOFBaY8tV0FmccXItoq0q21se32Q3u4YiW7/RqXSosWgYJaaKwRHXy3BST9PeCNQ==} + engines: {node: '>=20.19.0'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1756,6 +1821,11 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + srvx@1.0.8: + resolution: {integrity: sha512-ngFS/J6St4PJfHRHGjr78xkPPtbyXtdMOLNE4x/XZunEK3OhV7aGUWMHf9WTUEy6wm1U0W4okkd82ZFFWRVQTw==} + engines: {node: '>=20.16.0'} + hasBin: true + std-env@4.3.0: resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} @@ -1796,6 +1866,12 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + unstorage@2.0.0-beta.0: + resolution: {integrity: sha512-VeTY+zQ3ozrNtIFBRQRuSwanHdpuRH+4t/ZzchZIVQO8/oKq2xl7fJ9c+z76tWJDW/BEXSEuzpHOxqoAl6B64A==} + vite-plus@1.1.0: resolution: {integrity: sha512-khkRlY70qqbBV0QmF9QKXSrhxIRYftb1H05AJMowMoq7iouchmAoXYeReZ8pKKMOadqrWVxFa3S9BA2m7w59iA==} engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} @@ -2786,6 +2862,8 @@ snapshots: commander@9.5.0: {} + consola@3.4.2: {} + convert-source-map@2.0.0: {} cross-env@10.1.0: @@ -2799,6 +2877,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crossws@0.4.17(srvx@1.0.8): + optionalDependencies: + srvx: 1.0.8 + + db0@0.4.2: {} + deno@2.9.6: optionalDependencies: '@deno/darwin-arm64': 2.9.6 @@ -2814,6 +2898,13 @@ snapshots: dom-accessibility-api@0.5.16: {} + env-runner@0.2.3: + dependencies: + crossws: 0.4.17(srvx@1.0.8) + exsolve: 1.2.0 + httpxy: 0.5.6 + srvx: 1.0.8 + es-module-lexer@2.3.2: {} estree-walker@3.0.3: @@ -2822,6 +2913,8 @@ snapshots: expect-type@1.4.0: {} + exsolve@1.2.0: {} + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 @@ -2833,6 +2926,18 @@ snapshots: fsevents@2.3.3: optional: true + h3@2.0.8(crossws@0.4.17(srvx@1.0.8))(ocache@0.3.1): + dependencies: + rou3: 1.0.0 + srvx: 1.0.8 + optionalDependencies: + crossws: 0.4.17(srvx@1.0.8) + ocache: 0.3.1 + + hookable@6.1.2: {} + + httpxy@0.5.6: {} + isexe@2.0.0: {} js-tokens@4.0.0: {} @@ -2896,10 +3001,30 @@ snapshots: nanoid@3.3.18: {} + nf3@0.3.24: {} + + nitro@3.0.260903-beta: + dependencies: + consola: 3.4.2 + crossws: 0.4.17(srvx@1.0.8) + db0: 0.4.2 + env-runner: 0.2.3 + h3: 2.0.8(crossws@0.4.17(srvx@1.0.8))(ocache@0.3.1) + hookable: 6.1.2 + nf3: 0.3.24 + ocache: 0.3.1 + rolldown: 1.2.11 + rou3: 0.9.2 + srvx: 1.0.8 + unenv: 2.0.0-rc.24 + unstorage: 2.0.0-beta.0 + node-forge@1.4.0: {} obug@2.2.1: {} + ocache@0.3.1: {} + oxfmt@0.72.0(vite-plus@1.1.0(@types/node@25.0.3)(@vitest/browser-playwright@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@25.0.3)(typescript@6.0.3))(playwright@1.63.0)(vitest@5.0.3))(@vitest/ui@5.0.3(vitest@5.0.3))(playwright@1.63.0)(typescript@6.0.3)): dependencies: tinypool: 2.2.0 @@ -3124,6 +3249,10 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.11 '@rolldown/binding-win32-x64-msvc': 1.2.11 + rou3@0.9.2: {} + + rou3@1.0.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -3138,6 +3267,8 @@ snapshots: source-map-js@1.2.1: {} + srvx@1.0.8: {} + std-env@4.3.0: {} tinybench@6.2.0: {} @@ -3169,6 +3300,12 @@ snapshots: undici-types@7.16.0: {} + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + unstorage@2.0.0-beta.0: {} + vite-plus@1.1.0(@types/node@25.0.3)(@vitest/browser-playwright@5.0.3(@voidzero-dev/vite-plus-core@1.1.0(@types/node@25.0.3)(typescript@6.0.3))(playwright@1.63.0)(vitest@5.0.3))(@vitest/ui@5.0.3(vitest@5.0.3))(playwright@1.63.0)(typescript@6.0.3): dependencies: '@oxc-project/types': 0.153.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6c7f35006..641226416 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -29,6 +29,7 @@ catalog: deno: 2.9.6 husky: ^9.1.7 lint-staged: ^17.0.0 + nitro: 3.0.260903-beta oxfmt: 0.73.0 oxlint: ^1.55.0 oxlint-tsgolint: ^7.0.0