Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
464 changes: 462 additions & 2 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,17 @@ 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" }
vite_workspace = { git = "https://github.com/voidzero-dev/vite-task", rev = "edf07c7eac63aa24fe07ab4ff1039f682e48d6c3" }
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"] }
Expand Down
1 change: 1 addition & 0 deletions crates/vite_error/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
3 changes: 3 additions & 0 deletions crates/vite_error/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
21 changes: 21 additions & 0 deletions crates/vite_migration/Cargo.toml
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
3 changes: 3 additions & 0 deletions crates/vite_migration/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod package;

pub use package::rewrite_package_json_scripts;
233 changes: 233 additions & 0 deletions crates/vite_migration/src/package.rs
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(&current);
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<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));
}
}
}
}

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<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"
);
}
}
1 change: 1 addition & 0 deletions packages/cli/binding/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/binding/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>

/**
* Main entry point for the CLI, called from JavaScript.
*
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/binding/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
6 changes: 5 additions & 1 deletion packages/cli/binding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

mod cli;
mod commands;
mod migration;
mod package_manager;
mod utils;

Expand All @@ -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]
Expand Down
35 changes: 35 additions & 0 deletions packages/cli/binding/src/migration.rs
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}`);
/// ```
#[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)
}
Loading