Skip to content

Commit 8e06554

Browse files
fengmk2claude
andcommitted
feat(migration): add batch import rewriting with gitignore support
Add file walker module and batch rewrite functionality to process all TypeScript/JavaScript files in a directory while respecting .gitignore rules. - Add `ignore` crate dependency for gitignore-aware directory walking - Create `file_walker.rs` module with `find_ts_files()` function - Add `BatchRewriteResult` struct for tracking modified/unchanged/error files - Add `rewrite_imports_in_directory()` function for batch processing - Export new public API: `find_ts_files`, `WalkResult`, `BatchRewriteResult`, `rewrite_imports_in_directory` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3e9cdec commit 8e06554

8 files changed

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

crates/vite_migration/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
mod ast_grep;
2+
mod file_walker;
23
mod package;
34
mod vite_config;
45

6+
pub use file_walker::{WalkResult, find_ts_files};
57
pub use package::rewrite_scripts;
6-
pub use vite_config::{MergeResult, RewriteResult, merge_json_config, rewrite_import};
8+
pub use vite_config::{
9+
BatchRewriteResult, MergeResult, RewriteResult, merge_json_config, rewrite_import,
10+
rewrite_imports_in_directory,
11+
};

0 commit comments

Comments
 (0)