diff --git a/Cargo.lock b/Cargo.lock index 8f31ba8352..14d2185491 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,71 @@ dependencies = [ "serde_json", ] +[[package]] +name = "ast-grep-config" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6f43c2380274c5d91ef92e344cff3f1c296b7691de5c1b9079577b8ce699e0" +dependencies = [ + "ast-grep-core", + "bit-set 0.8.0", + "globset", + "regex", + "schemars", + "serde", + "serde_yaml", + "thiserror 2.0.17", +] + +[[package]] +name = "ast-grep-core" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fc205cee4cba9c13bc3c4351e75b312ed99ab0a144198dc5bc85a17a03a91a" +dependencies = [ + "bit-set 0.8.0", + "regex", + "thiserror 2.0.17", + "tree-sitter", +] + +[[package]] +name = "ast-grep-language" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b1d1a97874e9d332f7fec312fbc9da25ec80c50e2aa33c9aa1a4d84c0e061e" +dependencies = [ + "ast-grep-core", + "ignore", + "serde", + "tree-sitter", + "tree-sitter-bash", + "tree-sitter-c", + "tree-sitter-c-sharp", + "tree-sitter-cpp", + "tree-sitter-css", + "tree-sitter-elixir", + "tree-sitter-go", + "tree-sitter-haskell", + "tree-sitter-hcl", + "tree-sitter-html", + "tree-sitter-java", + "tree-sitter-javascript", + "tree-sitter-json", + "tree-sitter-kotlin-sg", + "tree-sitter-lua", + "tree-sitter-nix", + "tree-sitter-php", + "tree-sitter-python", + "tree-sitter-ruby", + "tree-sitter-rust", + "tree-sitter-scala", + "tree-sitter-solidity", + "tree-sitter-swift", + "tree-sitter-typescript", + "tree-sitter-yaml", +] + [[package]] name = "async-attributes" version = "1.1.2" @@ -381,7 +446,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", ] [[package]] @@ -390,6 +464,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -1058,6 +1138,12 @@ version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.15.0" @@ -1520,6 +1606,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "gloo-timers" version = "0.3.0" @@ -1908,6 +2007,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indenter" version = "0.3.4" @@ -2032,7 +2147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas", - "bit-set", + "bit-set 0.5.3", "ena", "itertools 0.11.0", "lalrpop-util", @@ -3237,6 +3352,31 @@ dependencies = [ "windows-sys 0.61.1", ] +[[package]] +name = "schemars" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301858a4023d78debd2353c7426dc486001bddc91ae31a76fb1f55132f7e2633" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.106", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3331,6 +3471,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "serde_json" version = "1.0.145" @@ -3367,6 +3518,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serde_yml" version = "0.0.12" @@ -3566,6 +3730,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + [[package]] name = "string_cache" version = "0.8.9" @@ -3965,6 +4135,276 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tree-sitter" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-bash" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871b0606e667e98a1237ebdc1b0d7056e0aebfdc3141d12b399865d4cb6ed8a6" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-c" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3aad8f0129083a59fe8596157552d2bb7148c492d44c21558d68ca1c722707" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-c-sharp" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67f06accca7b45351758663b8215089e643d53bd9a660ce0349314263737fcb0" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-cpp" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-css" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cbc5e18f29a2c6d6435891f42569525cf95435a3e01c2f1947abcde178686f" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-elixir" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45d444647b4fd53d8fd32474c1b8bedc1baa22669ce3a78d083e365fa9a2d3f" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-go" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8560a4d2f835cc0d4d2c2e03cbd0dde2f6114b43bc491164238d333e28b16ea" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-haskell" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "977c51e504548cba13fc27cb5a2edab2124cf6716a1934915d07ab99523b05a4" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-hcl" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7b2cc3d7121553b84309fab9d11b3ff3d420403eef9ae50f9fd1cd9d9cf012" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-html" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261b708e5d92061ede329babaaa427b819329a9d427a1d710abb0f67bbef63ee" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-java" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa6cbcdc8c679b214e616fd3300da67da0e492e066df01bcf5a5921a71e90d6" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-javascript" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68204f2abc0627a90bdf06e605f5c470aa26fdcb2081ea553a04bdad756693f5" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-json" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86a5d6b3ea17e06e7a34aabeadd68f5866c0d0f9359155d432095f8b751865e4" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-kotlin-sg" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0e175b7530765d1e36ad234a7acaa8b2a3316153f239d724376c7ee5e8d8e98" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8" + +[[package]] +name = "tree-sitter-lua" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb9adf0965fec58e7660cbb3a059dbb12ebeec9459e6dcbae3db004739641e" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-nix" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4952a9733f3a98f6683a0ccd1035d84ab7a52f7e84eeed58548d86765ad92de3" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-php" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c17c3ab69052c5eeaa7ff5cd972dd1bc25d1b97ee779fec391ad3b5df5592" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-python" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-ruby" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-rust" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b18034c684a2420722be8b2a91c9c44f2546b631c039edf575ccba8c61be1" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-scala" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7516aeb3d1f40ede8e3045b163e86993b3434514dd06c34c0b75e782d9a0b251" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-solidity" +version = "1.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eacf8875b70879f0cb670c60b233ad0b68752d9e1474e6c3ef168eea8a90b25" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-swift" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef216011c3e3df4fa864736f347cb8d509b1066cf0c8549fb1fd81ac9832e59" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-yaml" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c223db85f05e34794f065454843b0668ebc15d240ada63e2b5939f43ce7c97" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -4004,6 +4444,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -4120,6 +4566,7 @@ dependencies = [ "vite_command", "vite_error", "vite_install", + "vite_migration", "vite_path", "vite_str", "vite_task", @@ -4145,6 +4592,7 @@ name = "vite_error" version = "0.0.0" dependencies = [ "anyhow", + "ast-grep-config", "bincode", "bstr", "nix 0.30.1", @@ -4202,6 +4650,18 @@ dependencies = [ "vite_workspace", ] +[[package]] +name = "vite_migration" +version = "0.0.0" +dependencies = [ + "ast-grep-config", + "ast-grep-core", + "ast-grep-language", + "serde_json", + "tokio", + "vite_error", +] + [[package]] name = "vite_path" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 0b851f8fa6..682b1ab28f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ vite_command = { path = "crates/vite_command" } vite_error = { path = "crates/vite_error" } vite_glob = { git = "https://github.com/voidzero-dev/vite-task", rev = "edf07c7eac63aa24fe07ab4ff1039f682e48d6c3" } vite_install = { path = "crates/vite_install" } +vite_migration = { path = "crates/vite_migration" } vite_path = { git = "https://github.com/voidzero-dev/vite-task", rev = "edf07c7eac63aa24fe07ab4ff1039f682e48d6c3" } vite_str = { git = "https://github.com/voidzero-dev/vite-task", rev = "edf07c7eac63aa24fe07ab4ff1039f682e48d6c3" } vite_task = { git = "https://github.com/voidzero-dev/vite-task", rev = "edf07c7eac63aa24fe07ab4ff1039f682e48d6c3" } @@ -76,6 +77,9 @@ vite_workspace = { git = "https://github.com/voidzero-dev/vite-task", rev = "edf wax = "0.6.0" which = "8.0.0" +ast-grep-config = "0.40.0" +ast-grep-core = "0.40.0" +ast-grep-language = "0.40.0" napi = { version = "3.0.0", default-features = false, features = ["async", "error_anyhow"] } napi-build = "2" napi-derive = { version = "3.0.0", default-features = false, features = ["type-def", "strict"] } diff --git a/crates/vite_error/Cargo.toml b/crates/vite_error/Cargo.toml index 5e8ab60dca..418b5b01df 100644 --- a/crates/vite_error/Cargo.toml +++ b/crates/vite_error/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true [dependencies] anyhow = { workspace = true } +ast-grep-config = { workspace = true } bincode = { workspace = true } bstr = { workspace = true } nix = { workspace = true } diff --git a/crates/vite_error/src/lib.rs b/crates/vite_error/src/lib.rs index bc00a2a345..9f9b943e34 100644 --- a/crates/vite_error/src/lib.rs +++ b/crates/vite_error/src/lib.rs @@ -125,6 +125,9 @@ pub enum Error { #[error("Cannot find binary path for command '{0}'")] CannotFindBinaryPath(Str), + #[error(transparent)] + AstGrepConfigError(#[from] ast_grep_config::RuleConfigError), + #[error(transparent)] Anyhow(#[from] anyhow::Error), } diff --git a/crates/vite_migration/Cargo.toml b/crates/vite_migration/Cargo.toml new file mode 100644 index 0000000000..ae43a1ef14 --- /dev/null +++ b/crates/vite_migration/Cargo.toml @@ -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 diff --git a/crates/vite_migration/src/lib.rs b/crates/vite_migration/src/lib.rs new file mode 100644 index 0000000000..cb398a6712 --- /dev/null +++ b/crates/vite_migration/src/lib.rs @@ -0,0 +1,3 @@ +mod package; + +pub use package::rewrite_package_json_scripts; diff --git a/crates/vite_migration/src/package.rs b/crates/vite_migration/src/package.rs new file mode 100644 index 0000000000..3745143a75 --- /dev/null +++ b/crates/vite_migration/src/package.rs @@ -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>, Error> { + let yaml = fs::read_to_string(yaml_path).await?; + let globals = GlobalRules::default(); + let rules: Vec> = from_yaml_string::(&yaml, &globals)?; + Ok(rules) +} + +/// rewrite a single script command string using rules +fn rewrite_script(script: &str, rules: &[RuleConfig]) -> 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); + } + } + + 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 { + 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 = 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)); + } + } + } + } + + 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) +} + +#[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> = + from_yaml_string::(&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" + ); + } +} diff --git a/packages/cli/binding/Cargo.toml b/packages/cli/binding/Cargo.toml index e3a368c40e..a3120b3fd0 100644 --- a/packages/cli/binding/Cargo.toml +++ b/packages/cli/binding/Cargo.toml @@ -22,6 +22,7 @@ tracing-subscriber = { workspace = true } vite_command = { workspace = true } vite_error = { workspace = true } vite_install = { workspace = true } +vite_migration = { workspace = true } vite_path = { workspace = true } vite_str = { workspace = true } vite_task = { workspace = true } diff --git a/packages/cli/binding/index.d.ts b/packages/cli/binding/index.d.ts index ccd9ff725f..333cb0fcd4 100644 --- a/packages/cli/binding/index.d.ts +++ b/packages/cli/binding/index.d.ts @@ -135,6 +135,27 @@ export interface PathAccess { readDir: boolean } +/** + * 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}`); + * ``` + */ +export declare function rewritePackageJsonScripts(packageJsonPath: string, rulesYamlPath: string): Promise + /** * Main entry point for the CLI, called from JavaScript. * diff --git a/packages/cli/binding/index.js b/packages/cli/binding/index.js index 72e8808bd2..377fc7ccf7 100644 --- a/packages/cli/binding/index.js +++ b/packages/cli/binding/index.js @@ -575,8 +575,9 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { detectWorkspace, downloadPackageManager, run, runCommand } = nativeBinding +const { detectWorkspace, downloadPackageManager, rewritePackageJsonScripts, run, runCommand } = nativeBinding export { detectWorkspace } export { downloadPackageManager } +export { rewritePackageJsonScripts } export { run } export { runCommand } diff --git a/packages/cli/binding/src/lib.rs b/packages/cli/binding/src/lib.rs index bc66691f30..0dc1a3cb5d 100644 --- a/packages/cli/binding/src/lib.rs +++ b/packages/cli/binding/src/lib.rs @@ -15,6 +15,7 @@ mod cli; mod commands; +mod migration; mod package_manager; mod utils; @@ -29,7 +30,10 @@ use vite_path::current_dir; use vite_task::ResolveCommandResult; use crate::cli::{Args, CliOptions as ViteTaskCliOptions, Commands}; -pub use crate::package_manager::{detect_workspace, download_package_manager}; +pub use crate::{ + migration::rewrite_package_json_scripts, + package_manager::{detect_workspace, download_package_manager}, +}; /// Module initialization - sets up tracing for debugging #[napi_derive::module_init] diff --git a/packages/cli/binding/src/migration.rs b/packages/cli/binding/src/migration.rs new file mode 100644 index 0000000000..9e452aeb03 --- /dev/null +++ b/packages/cli/binding/src/migration.rs @@ -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}`); +/// ``` +#[napi] +pub async fn rewrite_package_json_scripts( + package_json_path: String, + rules_yaml_path: String, +) -> Result { + 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) +} diff --git a/packages/global/package.json b/packages/global/package.json index 87e4aa673e..0a7d007ba4 100644 --- a/packages/global/package.json +++ b/packages/global/package.json @@ -15,7 +15,8 @@ "files": [ "bin", "dist", - "templates" + "templates", + "rules" ], "scripts": { "build": "rolldown -c rolldown.config.ts", diff --git a/packages/global/rules/package-json-scripts.yml b/packages/global/rules/package-json-scripts.yml new file mode 100644 index 0000000000..7ef0588e06 --- /dev/null +++ b/packages/global/rules/package-json-scripts.yml @@ -0,0 +1,118 @@ +# 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 + +# oxfmt => vite fmt +--- +id: replace-oxfmt-alone +language: bash +rule: + kind: command + has: + kind: command_name + regex: '^oxfmt$' + not: + has: + kind: word + field: argument +fix: vite fmt + +# oxfmt [OPTIONS] => vite fmt [OPTIONS] +--- +id: replace-oxfmt-with-args +language: bash +rule: + pattern: oxfmt $$$ARGS +fix: vite fmt $$$ARGS + +# vitest => vite test +--- +id: replace-vitest-alone +language: bash +rule: + kind: command + has: + kind: command_name + regex: '^vitest$' + not: + has: + kind: word + field: argument +fix: vite test + +# vitest [OPTIONS] => vite test [OPTIONS] +--- +id: replace-vitest-with-args +language: bash +rule: + pattern: vitest $$$ARGS +fix: vite test $$$ARGS + +# tsdown => vite lib +--- +id: replace-tsdown-alone +language: bash +rule: + kind: command + has: + kind: command_name + regex: '^tsdown$' + not: + has: + kind: word + field: argument +fix: vite lib + +# tsdown [OPTIONS] => vite lib [OPTIONS] +--- +id: replace-tsdown-with-args +language: bash +rule: + pattern: tsdown $$$ARGS +fix: vite lib $$$ARGS diff --git a/packages/global/src/gen/__tests__/__snapshots__/migration.spec.ts.snap b/packages/global/src/gen/__tests__/__snapshots__/migration.spec.ts.snap new file mode 100644 index 0000000000..856d910997 --- /dev/null +++ b/packages/global/src/gen/__tests__/__snapshots__/migration.spec.ts.snap @@ -0,0 +1,31 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`migratePackageJson > should migrate package.json scripts 1`] = ` +{ + "build": "pnpm install &&vite build -r && vite run build --watch && vite lib && tsc || exit 1", + "dev": "vite dev", + "dev_analyze": "vite dev --analyze", + "dev_debug": "vite dev --debug", + "dev_help": "vite dev --help && vite dev -h", + "dev_host": "vite dev --host 0.0.0.0", + "dev_open": "vite dev --open", + "dev_port": "vite dev --port 3000", + "dev_profile": "vite dev --profile", + "dev_stats": "vite dev --stats", + "dev_trace": "vite dev --trace", + "dev_verbose": "vite dev --verbose", + "fmt": "vite fmt", + "fmt_config": "vite fmt --config .oxfmt.json", + "lib": "vite lib", + "lib_watch": "vite lib --watch", + "lint": "vite lint", + "lint_config": "vite lint --config .oxlint.json", + "lint_type_aware": "vite lint --type-aware", + "optimize": "vite optimize", + "preview": "vite preview", + "ready": "vite lint --fix --type-aware && vite test run && vite lib && vite fmt --fix", + "ready_new": "vite install && vite fmt && vite lint --type-aware && vite test -r && vite build -r", + "test": "vite test", + "test_run": "vite test run && vite test --ui", +} +`; diff --git a/packages/global/src/gen/__tests__/migration.spec.ts b/packages/global/src/gen/__tests__/migration.spec.ts new file mode 100644 index 0000000000..4f4edadf1c --- /dev/null +++ b/packages/global/src/gen/__tests__/migration.spec.ts @@ -0,0 +1,54 @@ +import fs from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { migratePackageJson } from '../migration.ts'; + +describe('migratePackageJson', () => { + it('should migrate package.json scripts', async () => { + const tempDir = await fs.mkdtemp(path.join(tmpdir(), 'vite-plus-test-')); + await fs.writeFile( + path.join(tempDir, 'package.json'), + JSON.stringify( + { + scripts: { + test: 'vitest', + test_run: 'vitest run && vitest --ui', + lint: 'oxlint', + lint_config: 'oxlint --config .oxlint.json', + lint_type_aware: 'oxlint --type-aware', + fmt: 'oxfmt', + fmt_config: 'oxfmt --config .oxfmt.json', + lib: 'tsdown', + lib_watch: 'tsdown --watch', + preview: 'vite preview', + optimize: 'vite optimize', + build: 'pnpm install &&vite build -r && vite run build --watch && tsdown && tsc || exit 1', + dev: 'vite', + dev_help: 'vite --help && vite -h', + dev_port: 'vite --port 3000', + dev_host: 'vite --host 0.0.0.0', + dev_open: 'vite --open', + dev_verbose: 'vite --verbose', + dev_debug: 'vite --debug', + dev_trace: 'vite --trace', + dev_profile: 'vite --profile', + dev_stats: 'vite --stats', + dev_analyze: 'vite --analyze', + ready: 'oxlint --fix --type-aware && vitest run && tsdown && oxfmt --fix', + ready_new: 'vite install && vite fmt && vite lint --type-aware && vite test -r && vite build -r', + }, + }, + null, + 2, + ), + ); + const updated = await migratePackageJson(path.join(tempDir, 'package.json')); + const scripts = JSON.parse(await fs.readFile(path.join(tempDir, 'package.json'), 'utf-8')).scripts; + await fs.rm(tempDir, { recursive: true }); + expect(updated).toBe(true); + expect(scripts).toMatchSnapshot(); + }); +}); diff --git a/packages/global/src/gen/migration.ts b/packages/global/src/gen/migration.ts index 542b1331a1..5a8fdc5266 100644 --- a/packages/global/src/gen/migration.ts +++ b/packages/global/src/gen/migration.ts @@ -4,9 +4,11 @@ import path from 'node:path'; import * as prompts from '@clack/prompts'; import colors from 'picocolors'; +import { rewritePackageJsonScripts } from '@voidzero-dev/vite-plus/binding'; import type { WorkspaceInfo } from './types.ts'; -import { editJsonFile, editOrCreateFile, readJsonFile, templatesDir } from './utils.ts'; +import { editJsonFile, editOrCreateFile, pkgRoot, readJsonFile, templatesDir } from './utils.ts'; +const rulesDir = path.join(pkgRoot, 'rules'); const { gray } = colors; const viteTools = ['vite', 'vitest', 'oxlint', 'oxfmt', 'tsdown']; @@ -33,9 +35,15 @@ export function detectStandaloneViteTools(projectDir: string, cwd: string): stri return detected; } +export async function migratePackageJson(jsonFile: string): Promise { + const rulesYamlPath = path.join(rulesDir, 'package-json-scripts.yml'); + return await rewritePackageJsonScripts(jsonFile, rulesYamlPath); +} + // Migrate standalone vite tools to vite-plus -export function migrateToVitePlus(projectDir: string, cwd: string, isMonorepo: boolean): void { - const packageJsonPath = path.join(cwd, projectDir, 'package.json'); +export async function migrateToVitePlus(projectDir: string, cwd: string, isMonorepo: boolean): Promise { + const packageJsonFile = path.join(projectDir, 'package.json'); + const packageJsonPath = path.join(cwd, packageJsonFile); editJsonFile(packageJsonPath, (pkg) => { // Track where vite was originally located (dependencies or devDependencies) const viteInDependencies = !!pkg.dependencies?.['vite']; @@ -79,31 +87,13 @@ export function migrateToVitePlus(projectDir: string, cwd: string, isMonorepo: b }); } - // Update scripts if needed - // TODO: use ast-grep to update scripts - if (pkg.scripts) { - // Update common script patterns - if (pkg.scripts.dev === 'vite') { - pkg.scripts.dev = 'vite dev'; - } - if (pkg.scripts.dev === 'tsdown --watch') { - pkg.scripts.dev = 'vite lib --watch'; - } - if (pkg.scripts.build === 'tsdown') { - pkg.scripts.build = 'vite lib'; - } - if (pkg.scripts.test === 'vitest' || pkg.scripts.test === 'vitest run') { - pkg.scripts.test = 'vite test'; - } - if (pkg.scripts.lint === 'oxlint') { - pkg.scripts.lint = 'vite lint'; - } - if (pkg.scripts.format === 'oxfmt') { - pkg.scripts.format = 'vite fmt'; - } - } return pkg; }); + + const updated = await migratePackageJson(packageJsonPath); + if (updated) { + prompts.log.info(` ${gray('•')} Updated ${packageJsonFile} scripts`); + } } // Perform auto-migration with prompts and feedback @@ -116,7 +106,7 @@ export async function performAutoMigration( return; // No migration needed } - migrateToVitePlus(projectDir, workspaceInfo.rootDir, workspaceInfo.isMonorepo); + await migrateToVitePlus(projectDir, workspaceInfo.rootDir, workspaceInfo.isMonorepo); prompts.log.success(`Migrated to vite-plus ${gray('✓')}`); prompts.log.info(` ${gray('•')} Removed: ${standaloneTools.join(', ')}`); prompts.log.info( diff --git a/packages/global/src/gen/templates/monorepo.ts b/packages/global/src/gen/templates/monorepo.ts index 2fa222380a..32205dfbc2 100644 --- a/packages/global/src/gen/templates/monorepo.ts +++ b/packages/global/src/gen/templates/monorepo.ts @@ -144,7 +144,7 @@ export async function executeMonorepoTemplate( const appPackageName = workspaceInfo.monorepoScope ? `${workspaceInfo.monorepoScope}/website` : 'website'; setPackageName(path.join(fullPath, appDir), appPackageName); // Perform auto-migration on the created app - migrateToVitePlus( + await migrateToVitePlus( appDir, fullPath, // The monorepo directory true, // Always in monorepo context @@ -172,7 +172,7 @@ export async function executeMonorepoTemplate( const libraryPackageName = workspaceInfo.monorepoScope ? `${workspaceInfo.monorepoScope}/utils` : 'utils'; setPackageName(path.join(fullPath, libraryDir), libraryPackageName); // Perform auto-migration on the created library - migrateToVitePlus( + await migrateToVitePlus( libraryDir, fullPath, // The monorepo directory true, // Always in monorepo context