Skip to content

Commit b8bd487

Browse files
authored
feat(migration): rewrite vitest imports (#350)
Add support for rewriting imports from vite/vitest to @voidzero-dev/vite-plus across an entire project: - Add `ignore` crate dependency for gitignore-aware directory walking - Create `file_walker.rs` module with `find_ts_files()` function to locate TypeScript/JavaScript files - Create `import_rewriter.rs` module with comprehensive import rewriting rules: - `vite` → `@voidzero-dev/vite-plus` - `vite/{name}` → `@voidzero-dev/vite-plus/{name}` - `vitest` → `@voidzero-dev/vite-plus/test` - `vitest/config` → `@voidzero-dev/vite-plus` - `vitest/{name}` → `@voidzero-dev/vite-plus/test/{name}` - `@vitest/browser` → `@voidzero-dev/vite-plus/test/browser` - `@vitest/browser/{name}` → `@voidzero-dev/vite-plus/test/browser/{name}` - `@vitest/browser-playwright` → `@voidzero-dev/vite-plus/test/browser-playwright` - `@vitest/browser-playwright/{name}` → `@voidzero-dev/vite-plus/test/browser-playwright/{name}` - Add `rewrite_imports_in_directory()` function for batch processing all files - Add snap test for vitest migration - Update RFC documentation with detailed import rewriting rules This enables the migration command to automatically rewrite all imports in a project, making it much easier for users to migrate from vite/vitest to vite-plus. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces automated, project-wide import rewriting from `vite`/`vitest` (incl. subpaths and `@vitest/*`) to `@voidzero-dev/vite-plus`. > > - **New**: `find_ts_files()` in `vite_migration/file_walker.rs` using `ignore` to respect `.gitignore`/global/exclude > - **New**: `import_rewriter.rs` with ast-grep rules covering `vite`, `vite/{name}`, `vitest`, `vitest/config`, `vitest/{name}`, and `@vitest/...` → mapped `@voidzero-dev/vite-plus` equivalents > - **API**: Expose `rewrite_imports_in_directory()` from Rust and NAPI (`rewriteImportsInDirectory`), plus `BatchRewriteResult`/`BatchRewriteError` types; add `ignore::Error` to `vite_error::Error` > - **CLI**: Migration now runs project-wide rewriting and logs modified files; removes per-file rewrite path; extends package removal to `@vitest/browser*` > - **Tests/Docs**: Add unit tests for walker/rewriter, update snap tests to assert new output, and expand RFC with detailed rewrite rules > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4653d5e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 317d019 commit b8bd487

26 files changed

Lines changed: 1320 additions & 313 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ fspy = { git = "https://github.com/voidzero-dev/vite-task", rev = "edf07c7eac63a
4848
futures-util = "0.3.31"
4949
hex = "0.4.3"
5050
httpmock = "0.7"
51+
ignore = "0.4"
5152
indoc = "2.0.5"
5253
napi = { version = "3.0.0", default-features = false, features = ["async", "error_anyhow"] }
5354
napi-build = "2"

crates/vite_error/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ anyhow = { workspace = true }
1212
ast-grep-config = { workspace = true }
1313
bincode = { workspace = true }
1414
bstr = { workspace = true }
15+
ignore = { workspace = true }
1516
nix = { workspace = true }
1617
rusqlite = { workspace = true }
1718
semver = { workspace = true }

crates/vite_error/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ pub enum Error {
5454
#[error(transparent)]
5555
WaxWalk(#[from] wax::WalkError),
5656

57+
#[error(transparent)]
58+
IgnoreError(#[from] ignore::Error),
59+
5760
#[error(transparent)]
5861
SerdeYml(#[from] serde_yml::Error),
5962

crates/vite_migration/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ rust-version.workspace = true
1010
ast-grep-config = { workspace = true }
1111
ast-grep-core = { workspace = true }
1212
ast-grep-language = { workspace = true }
13+
ignore = { workspace = true }
1314
serde_json = { workspace = true, features = ["preserve_order"] }
1415
vite_error = { workspace = true }
1516

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
use std::path::{Path, PathBuf};
2+
3+
use ignore::WalkBuilder;
4+
use vite_error::Error;
5+
6+
// TODO: only support esm files for now
7+
/// File extensions to process for import rewriting
8+
const TS_JS_EXTENSIONS: &[&str] = &["ts", "tsx", "mts", "js", "jsx", "mjs"];
9+
10+
/// Result of walking TypeScript/JavaScript files
11+
#[derive(Debug)]
12+
pub struct WalkResult {
13+
/// List of file paths found
14+
pub files: Vec<PathBuf>,
15+
}
16+
17+
/// Find all TypeScript/JavaScript files in a directory, respecting gitignore
18+
///
19+
/// This function walks the directory tree starting from `root` and finds all files
20+
/// with TypeScript or JavaScript extensions (.ts, .tsx, .mts, .cts, .js, .jsx, .mjs, .cjs).
21+
///
22+
/// The walk respects:
23+
/// - `.gitignore` files in the directory tree
24+
/// - Global gitignore configuration
25+
/// - `.git/info/exclude` files
26+
/// - Hidden files and directories are skipped
27+
///
28+
/// # Arguments
29+
///
30+
/// * `root` - The root directory to start searching from
31+
///
32+
/// # Returns
33+
///
34+
/// Returns a `WalkResult` containing the list of found files, or an error if
35+
/// the directory walk fails.
36+
///
37+
/// # Example
38+
///
39+
/// ```ignore
40+
/// use std::path::Path;
41+
/// use vite_migration::find_ts_files;
42+
///
43+
/// let result = find_ts_files(Path::new("./src"))?;
44+
/// for file in result.files {
45+
/// println!("Found: {}", file.display());
46+
/// }
47+
/// ```
48+
pub fn find_ts_files(root: &Path) -> Result<WalkResult, Error> {
49+
let mut files = Vec::new();
50+
51+
let walker = WalkBuilder::new(root)
52+
.hidden(true) // Skip hidden files/dirs
53+
.git_ignore(true) // Respect .gitignore
54+
.git_global(true) // Respect global gitignore
55+
.git_exclude(true) // Respect .git/info/exclude
56+
.require_git(false) // Work even if not a git repo
57+
.build();
58+
59+
for entry in walker {
60+
let entry = entry?;
61+
let path = entry.path();
62+
63+
// Skip directories
64+
if path.is_dir() {
65+
continue;
66+
}
67+
68+
// Check extension
69+
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
70+
if TS_JS_EXTENSIONS.contains(&ext) {
71+
files.push(path.to_path_buf());
72+
}
73+
}
74+
}
75+
76+
Ok(WalkResult { files })
77+
}
78+
79+
#[cfg(test)]
80+
mod tests {
81+
use std::fs;
82+
83+
use tempfile::tempdir;
84+
85+
use super::*;
86+
87+
#[test]
88+
fn test_find_ts_files_basic() {
89+
let temp = tempdir().unwrap();
90+
91+
// Create test files
92+
fs::write(temp.path().join("app.ts"), "").unwrap();
93+
fs::write(temp.path().join("utils.tsx"), "").unwrap();
94+
fs::write(temp.path().join("config.js"), "").unwrap();
95+
fs::write(temp.path().join("readme.md"), "").unwrap();
96+
97+
let result = find_ts_files(temp.path()).unwrap();
98+
99+
// Should find ts, tsx, js but not md
100+
assert_eq!(result.files.len(), 3);
101+
}
102+
103+
#[test]
104+
fn test_find_ts_files_nested() {
105+
let temp = tempdir().unwrap();
106+
107+
// Create nested directory
108+
fs::create_dir(temp.path().join("src")).unwrap();
109+
fs::write(temp.path().join("src/index.ts"), "").unwrap();
110+
fs::write(temp.path().join("src/utils.tsx"), "").unwrap();
111+
112+
// Create deeper nesting
113+
fs::create_dir_all(temp.path().join("src/components")).unwrap();
114+
fs::write(temp.path().join("src/components/Button.tsx"), "").unwrap();
115+
116+
let result = find_ts_files(temp.path()).unwrap();
117+
118+
assert_eq!(result.files.len(), 3);
119+
}
120+
121+
#[test]
122+
fn test_find_ts_files_respects_gitignore() {
123+
let temp = tempdir().unwrap();
124+
125+
// Create test files
126+
fs::write(temp.path().join("app.ts"), "").unwrap();
127+
128+
// Create node_modules (should be ignored via gitignore)
129+
fs::create_dir(temp.path().join("node_modules")).unwrap();
130+
fs::write(temp.path().join("node_modules/pkg.ts"), "").unwrap();
131+
132+
// Create dist (should be ignored via gitignore)
133+
fs::create_dir(temp.path().join("dist")).unwrap();
134+
fs::write(temp.path().join("dist/bundle.js"), "").unwrap();
135+
136+
// Create .gitignore
137+
fs::write(temp.path().join(".gitignore"), "node_modules/\ndist/").unwrap();
138+
139+
let result = find_ts_files(temp.path()).unwrap();
140+
141+
// Should only find app.ts, not node_modules or dist files
142+
assert_eq!(result.files.len(), 1);
143+
assert!(result.files[0].ends_with("app.ts"));
144+
}
145+
146+
#[test]
147+
fn test_find_ts_files_all_extensions() {
148+
let temp = tempdir().unwrap();
149+
150+
// Create files with all supported extensions
151+
fs::write(temp.path().join("a.ts"), "").unwrap();
152+
fs::write(temp.path().join("b.tsx"), "").unwrap();
153+
fs::write(temp.path().join("c.mts"), "").unwrap();
154+
fs::write(temp.path().join("d.cts"), "").unwrap();
155+
fs::write(temp.path().join("e.js"), "").unwrap();
156+
fs::write(temp.path().join("f.jsx"), "").unwrap();
157+
fs::write(temp.path().join("g.mjs"), "").unwrap();
158+
fs::write(temp.path().join("h.cjs"), "").unwrap();
159+
160+
// Create non-matching files
161+
fs::write(temp.path().join("i.json"), "").unwrap();
162+
fs::write(temp.path().join("j.css"), "").unwrap();
163+
fs::write(temp.path().join("k.html"), "").unwrap();
164+
165+
let result = find_ts_files(temp.path()).unwrap();
166+
167+
assert_eq!(result.files.len(), 6);
168+
}
169+
170+
#[test]
171+
fn test_find_ts_files_empty_directory() {
172+
let temp = tempdir().unwrap();
173+
174+
let result = find_ts_files(temp.path()).unwrap();
175+
176+
assert!(result.files.is_empty());
177+
}
178+
179+
#[test]
180+
fn test_find_ts_files_skips_hidden() {
181+
let temp = tempdir().unwrap();
182+
183+
// Create visible file
184+
fs::write(temp.path().join("visible.ts"), "").unwrap();
185+
186+
// Create hidden directory with ts file
187+
fs::create_dir(temp.path().join(".hidden")).unwrap();
188+
fs::write(temp.path().join(".hidden/secret.ts"), "").unwrap();
189+
190+
let result = find_ts_files(temp.path()).unwrap();
191+
192+
// Should only find visible.ts
193+
assert_eq!(result.files.len(), 1);
194+
assert!(result.files[0].ends_with("visible.ts"));
195+
}
196+
}

0 commit comments

Comments
 (0)