Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 253
feat: add package.json script migration with ast-grep#310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| [package] | ||
| name = "vite_migration" | ||
| version = "0.0.0" | ||
| authors.workspace = true | ||
| edition.workspace = true | ||
| license.workspace = true | ||
| rust-version.workspace = true | ||
| [dependencies] | ||
| ast-grep-config = { workspace = true } | ||
| ast-grep-core = { workspace = true } | ||
| ast-grep-language = { workspace = true } | ||
| serde_json = { workspace = true, features = ["preserve_order"] } | ||
| tokio = { workspace = true, features = ["fs"] } | ||
| vite_error = { workspace = true } | ||
| [dev-dependencies] | ||
| tokio = { workspace = true, features = ["macros", "rt"] } | ||
| [lints] | ||
| workspace = true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| mod package; | ||
| pub use package::rewrite_package_json_scripts; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,233 @@ | ||
| use std::path::Path; | ||
| use ast_grep_config::{GlobalRules, RuleConfig, from_yaml_string}; | ||
| use ast_grep_core::replacer::Replacer; | ||
| use ast_grep_language::{LanguageExt, SupportLang}; | ||
| use serde_json::Value; | ||
| use tokio::fs; | ||
| use vite_error::Error; | ||
| /// load script rules from yaml file | ||
| async fn load_ast_grep_rules(yaml_path: &Path) -> Result<Vec<RuleConfig<SupportLang>>, Error> { | ||
| let yaml = fs::read_to_string(yaml_path).await?; | ||
| let globals = GlobalRules::default(); | ||
| let rules: Vec<RuleConfig<SupportLang>> = from_yaml_string::<SupportLang>(&yaml, &globals)?; | ||
| Ok(rules) | ||
| } | ||
| /// rewrite a single script command string using rules | ||
| fn rewrite_script(script: &str, rules: &[RuleConfig<SupportLang>]) -> String { | ||
| // current stores the current script text, and update it when the rule matches | ||
| let mut current = script.to_string(); | ||
| for rule in rules { | ||
| // only handle bash rules | ||
| if rule.language != SupportLang::Bash { | ||
| continue; | ||
| } | ||
| // parse current script with corresponding language | ||
| let grep = rule.language.ast_grep(¤t); | ||
| let root = grep.root(); | ||
| // this matcher is the AST matcher generated by deserializing the YAML rule | ||
| let matcher = &rule.matcher; | ||
| // rules may not have fix (pure lint), skip here | ||
| let fixers = match rule.get_fixer() { | ||
| Ok(f) if !f.is_empty() => f, | ||
| _ => continue, | ||
| }; | ||
| // collect all matches and their replacements | ||
| let mut replacements = Vec::new(); | ||
| for node in root.find_all(matcher) { | ||
| let range = node.range(); | ||
| let replacement_bytes = fixers[0].generate_replacement(&node); | ||
| let replacement_str = String::from_utf8_lossy(&replacement_bytes).to_string(); | ||
| replacements.push((range.start, range.end, replacement_str)); | ||
| } | ||
| // Replace from back to front | ||
| replacements.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start)); | ||
| for (start, end, replacement) in replacements { | ||
| current.replace_range(start..end, &replacement); | ||
| } | ||
| } | ||
fengmk2 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| current | ||
| } | ||
| /// rewrite scripts in package.json using rules from rules_yaml_path | ||
| pub async fn rewrite_package_json_scripts( | ||
| package_json_path: &Path, | ||
| rules_yaml_path: &Path, | ||
| ) -> Result<bool, Error> { | ||
| let content = fs::read_to_string(package_json_path).await?; | ||
| let mut json: Value = serde_json::from_str(&content)?; | ||
| let rules = load_ast_grep_rules(rules_yaml_path).await?; | ||
| let mut updated = false; | ||
| // get scripts field (object) | ||
| if let Some(scripts) = json.get_mut("scripts").and_then(Value::as_object_mut) { | ||
| let keys: Vec<String> = scripts.keys().cloned().collect(); | ||
| for key in keys { | ||
| if let Some(Value::String(script)) = scripts.get(&key) { | ||
| let new_script = rewrite_script(script, &rules); | ||
| if new_script != *script { | ||
| updated = true; | ||
| scripts.insert(key.clone(), Value::String(new_script)); | ||
fengmk2 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if updated { | ||
| // write back to file | ||
| let new_content = serde_json::to_string_pretty(&json)?; | ||
| fs::write(package_json_path, new_content).await?; | ||
| } | ||
| Ok(updated) | ||
| } | ||
fengmk2 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| #[test] | ||
| fn test_rewrite_script() { | ||
| let yaml = r#" | ||
| # vite => vite dev | ||
| --- | ||
| id: replace-vite-alone | ||
| language: bash | ||
| rule: | ||
| kind: command | ||
| has: | ||
| kind: command_name | ||
| regex: '^vite$' | ||
| not: | ||
| has: | ||
| kind: word | ||
| field: argument | ||
| fix: vite dev | ||
| # vite [OPTIONS] => vite dev [OPTIONS] | ||
| --- | ||
| id: replace-vite-with-args | ||
| language: bash | ||
| severity: info | ||
| rule: | ||
| pattern: vite $$$ARGS | ||
| not: | ||
| # ignore non-flag arguments | ||
| regex: 'vite\s+[^-]' | ||
| fix: vite dev $$$ARGS | ||
| # oxlint => vite lint | ||
| --- | ||
| id: replace-oxlint-alone | ||
| language: bash | ||
| rule: | ||
| kind: command | ||
| has: | ||
| kind: command_name | ||
| regex: '^oxlint$' | ||
| not: | ||
| has: | ||
| kind: word | ||
| field: argument | ||
| fix: vite lint | ||
| # oxlint [OPTIONS] => vite lint [OPTIONS] | ||
| --- | ||
| id: replace-oxlint-with-args | ||
| language: bash | ||
| rule: | ||
| pattern: oxlint $$$ARGS | ||
| fix: vite lint $$$ARGS | ||
| "#; | ||
| let globals = GlobalRules::default(); | ||
| let rules: Vec<RuleConfig<SupportLang>> = | ||
| from_yaml_string::<SupportLang>(&yaml, &globals).unwrap(); | ||
| // vite commands | ||
| assert_eq!(rewrite_script("vite", &rules), "vite dev"); | ||
| assert_eq!(rewrite_script("vite dev", &rules), "vite dev"); | ||
| assert_eq!(rewrite_script("vite i", &rules), "vite i"); | ||
| assert_eq!(rewrite_script("vite install", &rules), "vite install"); | ||
| assert_eq!(rewrite_script("vite test", &rules), "vite test"); | ||
| assert_eq!(rewrite_script("vite lint", &rules), "vite lint"); | ||
| assert_eq!(rewrite_script("vite fmt", &rules), "vite fmt"); | ||
| assert_eq!(rewrite_script("vite lib", &rules), "vite lib"); | ||
| assert_eq!(rewrite_script("vite preview", &rules), "vite preview"); | ||
| assert_eq!(rewrite_script("vite optimize", &rules), "vite optimize"); | ||
| assert_eq!(rewrite_script("vite build -r", &rules), "vite build -r"); | ||
| assert_eq!(rewrite_script("vite --port 3000", &rules), "vite dev --port 3000"); | ||
| assert_eq!( | ||
| rewrite_script("vite --port 3000 --host 0.0.0.0 --open", &rules), | ||
| "vite dev --port 3000 --host 0.0.0.0 --open" | ||
| ); | ||
| assert_eq!( | ||
| rewrite_script("vite --port 3000 || vite --port 3001", &rules), | ||
| "vite dev --port 3000 || vite dev --port 3001" | ||
| ); | ||
| assert_eq!( | ||
| rewrite_script("npm run lint && vite --port 3000", &rules), | ||
| "npm run lint && vite dev --port 3000" | ||
| ); | ||
| assert_eq!( | ||
| rewrite_script("vite --port 3000 && npm run lint", &rules), | ||
| "vite dev --port 3000 && npm run lint" | ||
| ); | ||
| assert_eq!( | ||
| rewrite_script("vite && tsc --check && vite run -r build", &rules), | ||
| "vite dev && tsc --check && vite run -r build" | ||
| ); | ||
| assert_eq!( | ||
| rewrite_script("vite && tsc --check && vite run test", &rules), | ||
| "vite dev && tsc --check && vite run test" | ||
| ); | ||
| assert_eq!( | ||
| rewrite_script("vite && tsc --check && vite test", &rules), | ||
| "vite dev && tsc --check && vite test" | ||
| ); | ||
| assert_eq!( | ||
| rewrite_script("prettier --write src/** vite", &rules), | ||
| "prettier --write src/** vite" | ||
| ); | ||
| // complex examples | ||
| assert_eq!( | ||
| rewrite_script("if [ -f file.txt ]; then vite; fi", &rules), | ||
| "if [ -f file.txt ]; then vite dev; fi" | ||
| ); | ||
| assert_eq!( | ||
| rewrite_script("if [ -f file.txt ]; then vite --port 3000; fi", &rules), | ||
| "if [ -f file.txt ]; then vite dev --port 3000; fi" | ||
| ); | ||
| assert_eq!( | ||
| rewrite_script("if [ -f file.txt ]; then vite --port 3000 && npm run lint; fi", &rules), | ||
| "if [ -f file.txt ]; then vite dev --port 3000 && npm run lint; fi" | ||
| ); | ||
| assert_eq!( | ||
| rewrite_script( | ||
| "if [ -f file.txt ]; then vite dev --port 3000 && npm run lint; fi", | ||
| &rules | ||
| ), | ||
| "if [ -f file.txt ]; then vite dev --port 3000 && npm run lint; fi" | ||
| ); | ||
| // oxlint commands | ||
| assert_eq!(rewrite_script("oxlint", &rules), "vite lint"); | ||
| assert_eq!(rewrite_script("oxlint --type-aware", &rules), "vite lint --type-aware"); | ||
| assert_eq!( | ||
| rewrite_script("oxlint --type-aware --config .oxlintrc", &rules), | ||
| "vite lint --type-aware --config .oxlintrc" | ||
| ); | ||
| assert_eq!(rewrite_script("oxlint && vite dev", &rules), "vite lint && vite dev"); | ||
| assert_eq!( | ||
| rewrite_script("npm run type-check && oxlint --type-aware", &rules), | ||
| "npm run type-check && vite lint --type-aware" | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| use std::path::PathBuf; | ||
| use napi::{anyhow, bindgen_prelude::*}; | ||
| use napi_derive::napi; | ||
| /// Rewrite package.json scripts using rules from rules_yaml_path | ||
| /// | ||
| /// # Arguments | ||
| /// | ||
| /// * `package_json_path` - The path to the package.json file | ||
| /// * `rules_yaml_path` - The path to the ast-grep rules.yaml file | ||
| /// | ||
| /// # Returns | ||
| /// | ||
| /// * `updated` - Whether the package.json scripts were updated | ||
| /// | ||
| /// # Example | ||
| /// | ||
| /// ```javascript | ||
| /// const updated = await rewritePackageJsonScripts("package.json", "rules.yaml"); | ||
| /// console.log(`Updated: ${updated}`); | ||
| /// ``` | ||
fengmk2 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| #[napi] | ||
| pub async fn rewrite_package_json_scripts( | ||
| package_json_path: String, | ||
| rules_yaml_path: String, | ||
| ) -> Result<bool> { | ||
| let package_json_path = PathBuf::from(&package_json_path); | ||
| let rules_yaml_path = PathBuf::from(&rules_yaml_path); | ||
| let updated = | ||
| vite_migration::rewrite_package_json_scripts(&package_json_path, &rules_yaml_path) | ||
| .await | ||
| .map_err(anyhow::Error::from)?; | ||
| Ok(updated) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.