diff --git a/crates/vite_task_bin/src/vtt/cp.rs b/crates/vite_task_bin/src/vtt/cp.rs index 5e53dc83c..36ed0a934 100644 --- a/crates/vite_task_bin/src/vtt/cp.rs +++ b/crates/vite_task_bin/src/vtt/cp.rs @@ -1,7 +1,38 @@ pub fn run(args: &[String]) -> Result<(), Box> { - if args.len() != 2 { - return Err("Usage: vtt cp ".into()); + let (recursive, paths) = match args { + [flag, src, dst] if flag == "-r" => (true, [src.as_str(), dst.as_str()]), + [src, dst] => (false, [src.as_str(), dst.as_str()]), + _ => return Err("Usage: vtt cp [-r] ".into()), + }; + + let src = std::path::Path::new(paths[0]); + let dst = std::path::Path::new(paths[1]); + if src.is_dir() { + if !recursive { + return Err("copying a directory requires -r".into()); + } + copy_dir_recursive(src, dst)?; + } else { + std::fs::copy(src, dst)?; + } + Ok(()) +} + +fn copy_dir_recursive( + src: &std::path::Path, + dst: &std::path::Path, +) -> Result<(), Box> { + std::fs::create_dir_all(dst)?; + for entry in std::fs::read_dir(src)? { + let entry = entry?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + let file_type = entry.file_type()?; + if file_type.is_dir() { + copy_dir_recursive(&src_path, &dst_path)?; + } else if file_type.is_file() { + std::fs::copy(&src_path, &dst_path)?; + } } - std::fs::copy(&args[0], &args[1])?; Ok(()) } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/index.html b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/index.html new file mode 100644 index 000000000..74acba4fa --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/index.html @@ -0,0 +1,2 @@ +
+ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/package.json new file mode 100644 index 000000000..4fac7bfad --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/package.json @@ -0,0 +1,5 @@ +{ + "name": "vite-build-cache-portable-fixture", + "private": true, + "type": "module" +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/src/main.js b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/src/main.js new file mode 100644 index 000000000..79dcec2cd --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/src/main.js @@ -0,0 +1 @@ +console.log('plain vite build'); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/vite-task.json new file mode 100644 index 000000000..a93517a9e --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/vite-task.json @@ -0,0 +1,9 @@ +{ + "tasks": { + "build": { + // TODO: --configLoader runner works around a Vite config-loader bug that makes this cache non-portable. + "command": "vite build --configLoader runner", + "cache": true + } + } +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/vite.config.js b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/vite.config.js new file mode 100644 index 000000000..f5e879584 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_clone/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + logLevel: 'silent', + build: { + rollupOptions: { + output: { + entryFileNames: 'assets/index.js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name][extname]', + }, + }, + }, +}); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/index.html b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/index.html new file mode 100644 index 000000000..74acba4fa --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/index.html @@ -0,0 +1,2 @@ +
+ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/package.json new file mode 100644 index 000000000..4fac7bfad --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/package.json @@ -0,0 +1,5 @@ +{ + "name": "vite-build-cache-portable-fixture", + "private": true, + "type": "module" +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/src/main.js b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/src/main.js new file mode 100644 index 000000000..79dcec2cd --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/src/main.js @@ -0,0 +1 @@ +console.log('plain vite build'); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/vite-task.json new file mode 100644 index 000000000..a93517a9e --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/vite-task.json @@ -0,0 +1,9 @@ +{ + "tasks": { + "build": { + // TODO: --configLoader runner works around a Vite config-loader bug that makes this cache non-portable. + "command": "vite build --configLoader runner", + "cache": true + } + } +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/vite.config.js b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/vite.config.js new file mode 100644 index 000000000..f5e879584 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/portable_origin/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + logLevel: 'silent', + build: { + rollupOptions: { + output: { + entryFileNames: 'assets/index.js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name][extname]', + }, + }, + }, +}); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots.toml index eddcfc821..4248aa961 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots.toml @@ -100,6 +100,44 @@ steps = [ ], comment = "cache-bravo label is now in the bundle" }, ] +[[e2e]] +name = "vite_build_cache_is_portable_across_workspace_roots" +comment = """ +Two identical plain Vite workspaces run from different absolute roots. The test copies the origin root's default task cache directory into the clone root to simulate uploading and downloading cache data; the clone should hit that copied cache and restore build outputs. +""" +ignore = true +steps = [ + { cwd = "portable_origin", argv = [ + "vt", + "run", + "--cache", + "build", + ], comment = "origin workspace root: cache miss populates its default cache" }, + { cwd = "portable_origin", argv = [ + "vtt", + "stat-file", + "dist/assets/index.js", + ], comment = "origin emitted the non-hashed build asset" }, + { argv = [ + "vtt", + "cp", + "-r", + "portable_origin/node_modules/.vite/task-cache", + "portable_clone/node_modules/.vite/task-cache", + ], comment = "copy-paste the cache directory to simulate upload/download" }, + { cwd = "portable_clone", argv = [ + "vt", + "run", + "--cache", + "build", + ], comment = "clone workspace root: cache hit from the origin root" }, + { cwd = "portable_clone", argv = [ + "vtt", + "stat-file", + "dist/assets/index.js", + ], comment = "clone restored the non-hashed asset from the portable cache archive" }, +] + [[e2e]] name = "vite_node_env_change_invalidates_cache" comment = """ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_build_cache_is_portable_across_workspace_roots.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_build_cache_is_portable_across_workspace_roots.md new file mode 100644 index 000000000..39afb0b99 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_build_cache_is_portable_across_workspace_roots.md @@ -0,0 +1,45 @@ +# vite_build_cache_is_portable_across_workspace_roots + +Two identical plain Vite workspaces run from different absolute roots. The test copies the origin root's default task cache directory into the clone root to simulate uploading and downloading cache data; the clone should hit that copied cache and restore build outputs. + +## `cd portable_origin && vt run --cache build` + +origin workspace root: cache miss populates its default cache + +``` +$ vite build --configLoader runner +``` + +## `cd portable_origin && vtt stat-file dist/assets/index.js` + +origin emitted the non-hashed build asset + +``` +dist/assets/index.js: exists +``` + +## `vtt cp -r portable_origin/node_modules/.vite/task-cache portable_clone/node_modules/.vite/task-cache` + +copy-paste the cache directory to simulate upload/download + +``` +``` + +## `cd portable_clone && vt run --cache build` + +clone workspace root: cache hit from the origin root + +``` +$ vite build --configLoader runner ◉ cache hit, replaying + +--- +vt run: cache hit. +``` + +## `cd portable_clone && vtt stat-file dist/assets/index.js` + +clone restored the non-hashed asset from the portable cache archive + +``` +dist/assets/index.js: exists +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/main.rs b/crates/vite_task_bin/tests/e2e_snapshots/main.rs index 45518cad8..0fc54c43f 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/main.rs +++ b/crates/vite_task_bin/tests/e2e_snapshots/main.rs @@ -38,6 +38,10 @@ enum Step { #[serde(deny_unknown_fields)] struct StepConfig { argv: Vec1, + /// Optional working directory for this step, relative to the staged fixture root. + /// Defaults to the case-level `cwd`. + #[serde(default)] + cwd: Option, /// Appended as `# comment` in the snapshot display line. #[serde(default)] comment: Option, @@ -61,11 +65,11 @@ impl Step { } } - /// Shell-escaped command line including any env-var prefix, without the - /// comment (e.g. `MY_ENV=1 vt run test`). The comment is surfaced - /// separately by [`Self::comment`]. + /// Shell-escaped command line including any env-var prefix and non-default + /// cwd, without the comment (e.g. `cd packages/a && MY_ENV=1 vt run test`). + /// The comment is surfaced separately by [`Self::comment`]. #[expect(clippy::disallowed_types, reason = "String required by join/format")] - fn display_command_line(&self) -> String { + fn display_command_line(&self, default_cwd: &RelativePathBuf) -> String { let argv_str = self .argv() .iter() @@ -80,7 +84,7 @@ impl Step { .collect::>() .join(" "); - match self { + let command = match self { Self::Simple(_) => argv_str, Self::Detailed(config) => { let mut parts = String::new(); @@ -90,6 +94,19 @@ impl Step { parts.push_str(&argv_str); parts } + }; + + let cwd = self.cwd().unwrap_or(default_cwd); + if cwd == default_cwd { + command + } else { + let cwd = if cwd.as_str().is_empty() { "." } else { cwd.as_str() }; + let mut display = String::new(); + display.push_str("cd "); + display.push_str(shell_escape::escape(cwd.into()).as_ref()); + display.push_str(" && "); + display.push_str(&command); + display } } @@ -114,6 +131,13 @@ impl Step { } } + const fn cwd(&self) -> Option<&RelativePathBuf> { + match self { + Self::Detailed(config) => config.cwd.as_ref(), + Self::Simple(_) => None, + } + } + const fn formatted_snapshot(&self) -> bool { match self { Self::Detailed(config) => config.formatted_snapshot, @@ -247,11 +271,8 @@ enum TerminationState { } /// Substitutes sentinels in step env values with values only known at -/// test-run time. Currently supports ``, which -/// expands to the path of the `preload_test_lib` cdylib built via the -/// artifact dependency (Linux only — the sentinel is only used by the -/// `preload_test_lib`-gated e2e fixture). Keeps the raw sentinel in the -/// snapshot's displayed command line, so snapshots stay machine-independent. +/// test-run time. Keeps the raw sentinel in the snapshot's displayed command +/// line, so snapshots stay machine-independent. fn resolve_env_placeholder(raw: &str) -> std::borrow::Cow<'_, OsStr> { if raw == "" { let path = env::var_os("CARGO_CDYLIB_FILE_PRELOAD_TEST_LIB").unwrap_or_else(|| { @@ -395,7 +416,7 @@ fn run_case( } { for step in &e2e.steps { - let step_display = step.display_command_line(); + let step_display = step.display_command_line(&e2e.cwd); let step_comment = step.comment().map(str::to_owned); let argv = step.argv(); @@ -455,7 +476,8 @@ fn run_case( let resolved = resolve_env_placeholder(v.as_str()); cmd.env(k.as_str(), AsRef::::as_ref(&resolved)); } - cmd.cwd(e2e_stage_path.join(&e2e.cwd).as_path()); + let step_cwd = step.cwd().unwrap_or(&e2e.cwd); + cmd.cwd(e2e_stage_path.join(step_cwd).as_path()); let terminal = TestTerminal::spawn(SCREEN_SIZE, cmd).unwrap(); let mut killer = terminal.child_handle.clone();