-
Notifications
You must be signed in to change notification settings - Fork 161
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| mod package; | ||
|
|
||
| pub use package::rewrite_package_json_scripts; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.
Show resolved
Hide resolved
|
||
| 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.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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.
Show resolved
Hide resolved
|
||
| #[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" | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.
Show resolved
Hide resolved
|
||
| #[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) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.