Skip to content

Commit da1d4d3

Browse files
committed
feat: add vite_migration crate
1 parent c1cc51b commit da1d4d3

18 files changed

Lines changed: 1015 additions & 34 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,17 @@ vite_command = { path = "crates/vite_command" }
6969
vite_error = { path = "crates/vite_error" }
7070
vite_glob = { git = "https://github.com/voidzero-dev/vite-task", rev = "edf07c7eac63aa24fe07ab4ff1039f682e48d6c3" }
7171
vite_install = { path = "crates/vite_install" }
72+
vite_migration = { path = "crates/vite_migration" }
7273
vite_path = { git = "https://github.com/voidzero-dev/vite-task", rev = "edf07c7eac63aa24fe07ab4ff1039f682e48d6c3" }
7374
vite_str = { git = "https://github.com/voidzero-dev/vite-task", rev = "edf07c7eac63aa24fe07ab4ff1039f682e48d6c3" }
7475
vite_task = { git = "https://github.com/voidzero-dev/vite-task", rev = "edf07c7eac63aa24fe07ab4ff1039f682e48d6c3" }
7576
vite_workspace = { git = "https://github.com/voidzero-dev/vite-task", rev = "edf07c7eac63aa24fe07ab4ff1039f682e48d6c3" }
7677
wax = "0.6.0"
7778
which = "8.0.0"
7879

80+
ast-grep-config = "0.40.0"
81+
ast-grep-core = "0.40.0"
82+
ast-grep-language = "0.40.0"
7983
napi = { version = "3.0.0", default-features = false, features = ["async", "error_anyhow"] }
8084
napi-build = "2"
8185
napi-derive = { version = "3.0.0", default-features = false, features = ["type-def", "strict"] }

crates/vite_error/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ rust-version.workspace = true
99

1010
[dependencies]
1111
anyhow = { workspace = true }
12+
ast-grep-config = { workspace = true }
1213
bincode = { workspace = true }
1314
bstr = { workspace = true }
1415
nix = { workspace = true }

crates/vite_error/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ pub enum Error {
125125
#[error("Cannot find binary path for command '{0}'")]
126126
CannotFindBinaryPath(Str),
127127

128+
#[error(transparent)]
129+
AstGrepConfigError(#[from] ast_grep_config::RuleConfigError),
130+
128131
#[error(transparent)]
129132
Anyhow(#[from] anyhow::Error),
130133
}

crates/vite_migration/Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "vite_migration"
3+
version = "0.0.0"
4+
authors.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
rust-version.workspace = true
8+
9+
[dependencies]
10+
ast-grep-config = { workspace = true }
11+
ast-grep-core = { workspace = true }
12+
ast-grep-language = { workspace = true }
13+
serde_json = { workspace = true, features = ["preserve_order"] }
14+
tokio = { workspace = true, features = ["fs"] }
15+
vite_error = { workspace = true }
16+
17+
[dev-dependencies]
18+
tokio = { workspace = true, features = ["macros", "rt"] }
19+
20+
[lints]
21+
workspace = true

crates/vite_migration/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mod package;
2+
3+
pub use package::rewrite_package_json_scripts;
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
use std::path::Path;
2+
3+
use ast_grep_config::{GlobalRules, RuleConfig, from_yaml_string};
4+
use ast_grep_core::replacer::Replacer;
5+
use ast_grep_language::{LanguageExt, SupportLang};
6+
use serde_json::Value;
7+
use tokio::fs;
8+
use vite_error::Error;
9+
10+
/// load script rules from yaml file
11+
async fn load_ast_grep_rules(yaml_path: &Path) -> Result<Vec<RuleConfig<SupportLang>>, Error> {
12+
let yaml = fs::read_to_string(yaml_path).await?;
13+
let globals = GlobalRules::default();
14+
let rules: Vec<RuleConfig<SupportLang>> = from_yaml_string::<SupportLang>(&yaml, &globals)?;
15+
Ok(rules)
16+
}
17+
18+
/// rewrite a single script command string using rules
19+
fn rewrite_script(script: &str, rules: &[RuleConfig<SupportLang>]) -> String {
20+
// current stores the current script text, and update it when the rule matches
21+
let mut current = script.to_string();
22+
23+
for rule in rules {
24+
// only handle bash rules
25+
if rule.language != SupportLang::Bash {
26+
continue;
27+
}
28+
29+
// parse current script with corresponding language
30+
let grep = rule.language.ast_grep(&current);
31+
let root = grep.root();
32+
33+
// this matcher is the AST matcher generated by deserializing the YAML rule
34+
let matcher = &rule.matcher;
35+
36+
// rules may not have fix (pure lint), skip here
37+
let fixers = match rule.get_fixer() {
38+
Ok(f) if !f.is_empty() => f,
39+
_ => continue,
40+
};
41+
42+
// collect all matches and their replacements
43+
let mut replacements = Vec::new();
44+
for node in root.find_all(matcher) {
45+
let range = node.range();
46+
let replacement_bytes = fixers[0].generate_replacement(&node);
47+
let replacement_str = String::from_utf8_lossy(&replacement_bytes).to_string();
48+
replacements.push((range.start, range.end, replacement_str));
49+
}
50+
51+
// Replace from back to front
52+
replacements.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start));
53+
54+
for (start, end, replacement) in replacements {
55+
current.replace_range(start..end, &replacement);
56+
}
57+
}
58+
59+
current
60+
}
61+
62+
/// rewrite scripts in package.json using rules from rules_yaml_path
63+
pub async fn rewrite_package_json_scripts(
64+
package_json_path: &Path,
65+
rules_yaml_path: &Path,
66+
) -> Result<bool, Error> {
67+
let content = fs::read_to_string(package_json_path).await?;
68+
let mut json: Value = serde_json::from_str(&content)?;
69+
let rules = load_ast_grep_rules(rules_yaml_path).await?;
70+
71+
let mut updated = false;
72+
// get scripts field (object)
73+
if let Some(scripts) = json.get_mut("scripts").and_then(Value::as_object_mut) {
74+
let keys: Vec<String> = scripts.keys().cloned().collect();
75+
for key in keys {
76+
if let Some(Value::String(script)) = scripts.get(&key) {
77+
let new_script = rewrite_script(script, &rules);
78+
if new_script != *script {
79+
updated = true;
80+
scripts.insert(key.clone(), Value::String(new_script));
81+
}
82+
}
83+
}
84+
}
85+
86+
if updated {
87+
// write back to file
88+
let new_content = serde_json::to_string_pretty(&json)?;
89+
fs::write(package_json_path, new_content).await?;
90+
}
91+
92+
Ok(updated)
93+
}
94+
95+
#[cfg(test)]
96+
mod tests {
97+
use super::*;
98+
99+
#[test]
100+
fn test_rewrite_script() {
101+
let yaml = r#"
102+
# vite => vite dev
103+
---
104+
id: replace-vite-alone
105+
language: bash
106+
rule:
107+
kind: command
108+
has:
109+
kind: command_name
110+
regex: '^vite$'
111+
not:
112+
has:
113+
kind: word
114+
field: argument
115+
fix: vite dev
116+
117+
# vite [OPTIONS] => vite dev [OPTIONS]
118+
---
119+
id: replace-vite-with-args
120+
language: bash
121+
severity: info
122+
rule:
123+
pattern: vite $$$ARGS
124+
not:
125+
# ignore non-flag arguments
126+
regex: 'vite\s+[^-]'
127+
fix: vite dev $$$ARGS
128+
129+
# oxlint => vite lint
130+
---
131+
id: replace-oxlint-alone
132+
language: bash
133+
rule:
134+
kind: command
135+
has:
136+
kind: command_name
137+
regex: '^oxlint$'
138+
not:
139+
has:
140+
kind: word
141+
field: argument
142+
fix: vite lint
143+
144+
# oxlint [OPTIONS] => vite lint [OPTIONS]
145+
---
146+
id: replace-oxlint-with-args
147+
language: bash
148+
rule:
149+
pattern: oxlint $$$ARGS
150+
fix: vite lint $$$ARGS
151+
"#;
152+
let globals = GlobalRules::default();
153+
let rules: Vec<RuleConfig<SupportLang>> =
154+
from_yaml_string::<SupportLang>(&yaml, &globals).unwrap();
155+
// vite commands
156+
assert_eq!(rewrite_script("vite", &rules), "vite dev");
157+
assert_eq!(rewrite_script("vite dev", &rules), "vite dev");
158+
assert_eq!(rewrite_script("vite i", &rules), "vite i");
159+
assert_eq!(rewrite_script("vite install", &rules), "vite install");
160+
assert_eq!(rewrite_script("vite test", &rules), "vite test");
161+
assert_eq!(rewrite_script("vite lint", &rules), "vite lint");
162+
assert_eq!(rewrite_script("vite fmt", &rules), "vite fmt");
163+
assert_eq!(rewrite_script("vite lib", &rules), "vite lib");
164+
assert_eq!(rewrite_script("vite preview", &rules), "vite preview");
165+
assert_eq!(rewrite_script("vite optimize", &rules), "vite optimize");
166+
assert_eq!(rewrite_script("vite build -r", &rules), "vite build -r");
167+
assert_eq!(rewrite_script("vite --port 3000", &rules), "vite dev --port 3000");
168+
assert_eq!(
169+
rewrite_script("vite --port 3000 --host 0.0.0.0 --open", &rules),
170+
"vite dev --port 3000 --host 0.0.0.0 --open"
171+
);
172+
assert_eq!(
173+
rewrite_script("vite --port 3000 || vite --port 3001", &rules),
174+
"vite dev --port 3000 || vite dev --port 3001"
175+
);
176+
assert_eq!(
177+
rewrite_script("npm run lint && vite --port 3000", &rules),
178+
"npm run lint && vite dev --port 3000"
179+
);
180+
assert_eq!(
181+
rewrite_script("vite --port 3000 && npm run lint", &rules),
182+
"vite dev --port 3000 && npm run lint"
183+
);
184+
assert_eq!(
185+
rewrite_script("vite && tsc --check && vite run -r build", &rules),
186+
"vite dev && tsc --check && vite run -r build"
187+
);
188+
assert_eq!(
189+
rewrite_script("vite && tsc --check && vite run test", &rules),
190+
"vite dev && tsc --check && vite run test"
191+
);
192+
assert_eq!(
193+
rewrite_script("vite && tsc --check && vite test", &rules),
194+
"vite dev && tsc --check && vite test"
195+
);
196+
assert_eq!(
197+
rewrite_script("prettier --write src/** vite", &rules),
198+
"prettier --write src/** vite"
199+
);
200+
// complex examples
201+
assert_eq!(
202+
rewrite_script("if [ -f file.txt ]; then vite; fi", &rules),
203+
"if [ -f file.txt ]; then vite dev; fi"
204+
);
205+
assert_eq!(
206+
rewrite_script("if [ -f file.txt ]; then vite --port 3000; fi", &rules),
207+
"if [ -f file.txt ]; then vite dev --port 3000; fi"
208+
);
209+
assert_eq!(
210+
rewrite_script("if [ -f file.txt ]; then vite --port 3000 && npm run lint; fi", &rules),
211+
"if [ -f file.txt ]; then vite dev --port 3000 && npm run lint; fi"
212+
);
213+
assert_eq!(
214+
rewrite_script(
215+
"if [ -f file.txt ]; then vite dev --port 3000 && npm run lint; fi",
216+
&rules
217+
),
218+
"if [ -f file.txt ]; then vite dev --port 3000 && npm run lint; fi"
219+
);
220+
// oxlint commands
221+
assert_eq!(rewrite_script("oxlint", &rules), "vite lint");
222+
assert_eq!(rewrite_script("oxlint --type-aware", &rules), "vite lint --type-aware");
223+
assert_eq!(
224+
rewrite_script("oxlint --type-aware --config .oxlintrc", &rules),
225+
"vite lint --type-aware --config .oxlintrc"
226+
);
227+
assert_eq!(rewrite_script("oxlint && vite dev", &rules), "vite lint && vite dev");
228+
assert_eq!(
229+
rewrite_script("npm run type-check && oxlint --type-aware", &rules),
230+
"npm run type-check && vite lint --type-aware"
231+
);
232+
}
233+
}

packages/cli/binding/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ tracing-subscriber = { workspace = true }
2222
vite_command = { workspace = true }
2323
vite_error = { workspace = true }
2424
vite_install = { workspace = true }
25+
vite_migration = { workspace = true }
2526
vite_path = { workspace = true }
2627
vite_str = { workspace = true }
2728
vite_task = { workspace = true }

packages/cli/binding/index.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,27 @@ export interface PathAccess {
135135
readDir: boolean
136136
}
137137

138+
/**
139+
* Rewrite package.json scripts using rules from rules_yaml_path
140+
*
141+
* # Arguments
142+
*
143+
* * `package_json_path` - The path to the package.json file
144+
* * `rules_yaml_path` - The path to the ast-grep rules.yaml file
145+
*
146+
* # Returns
147+
*
148+
* * `updated` - Whether the package.json scripts were updated
149+
*
150+
* # Example
151+
*
152+
* ```javascript
153+
* const updated = await rewritePackageJsonScripts("package.json", "rules.yaml");
154+
* console.log(`Updated: ${updated}`);
155+
* ```
156+
*/
157+
export declare function rewritePackageJsonScripts(packageJsonPath: string, rulesYamlPath: string): Promise<boolean>
158+
138159
/**
139160
* Main entry point for the CLI, called from JavaScript.
140161
*

packages/cli/binding/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,8 +575,9 @@ if (!nativeBinding) {
575575
throw new Error(`Failed to load native binding`)
576576
}
577577

578-
const { detectWorkspace, downloadPackageManager, run, runCommand } = nativeBinding
578+
const { detectWorkspace, downloadPackageManager, rewritePackageJsonScripts, run, runCommand } = nativeBinding
579579
export { detectWorkspace }
580580
export { downloadPackageManager }
581+
export { rewritePackageJsonScripts }
581582
export { run }
582583
export { runCommand }

0 commit comments

Comments
 (0)