Skip to content

Commit 0131832

Browse files
authored
feat: migration config to vite.config.ts (#320)
This PR adds functionality to migrate standalone configuration files (`.oxlintrc`, `.oxfmtrc`) into the unified vite.config.ts format. The implementation: - Adds TypeScript language support to ast-grep for parsing config files - Creates AST-based rules for merging JSON configs into vite.config.ts - Handles various config formats including object literals, function callbacks, and plain exports - Properly converts JSON to TypeScript object literals with correct formatting - Exposes a new `mergeJsonConfig` function in the binding API - Updates the migration process to automatically merge configs when detected The implementation handles edge cases like trailing commas, function callbacks, and different export styles to ensure reliable migration. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds AST-driven migration that merges .oxlintrc/.oxfmtrc into vite.config, rewrites vite/vitest imports to vite-plus, exposes merge/rewrite APIs in bindings, and updates CLI with version checks and new workflow with tests. > > - **Migration Engine (Rust `vite_migration`)**: > - Add TypeScript parsing support via `ast-grep` features. > - Implement `merge_json_config` and `rewrite_import` with AST rules to: > - Merge `.oxlintrc`/`.oxfmtrc` into `defineConfig` (supports object, function callbacks, plain exports, return vars, satisfies). > - Rewrite `import ... from 'vite'` and `'vitest/config'` to `'@voidzero-dev/vite-plus'`. > - Include reusable rule files: `rules/oxlint-*.yaml`, `rules/rewrite-import.yaml`. > - **Bindings (NAPI + JS)**: > - Expose `mergeJsonConfig` and `rewriteImport` in `packages/cli/binding` (TS defs and JS exports). > - **CLI Migration Flow (`packages/global/src/migration`)**: > - Detect `vite.config.*`/`vitest.config.*` and rewrite imports. > - Auto-create `vite.config.ts` when missing and merge oxlint/oxfmt configs; remove old files. > - Enforce minimum versions (`vite>=7`, `vitest>=4`) with clear errors; upgrade Yarn <4.10 when needed. > - Apply package manager overrides/resolutions and update scripts; handle monorepos and root workspace files. > - **Detection & Utilities**: > - Add package metadata/version detection; config file detection extended to Vitest. > - **Tests & Docs**: > - Add snapshot tests covering TS/JS, monorepo (pnpm/yarn), auto-create config, and unsupported versions. > - Update RFC with import rewrite and merged config examples. > - **Deps**: > - Add `tree-sitter-typescript` via `ast-grep-language` feature; add `tempfile` for tests. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3d3af8c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 5f8edc2 commit 0131832

88 files changed

Lines changed: 3311 additions & 141 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ anyhow = "1.0.98"
3939
# Safe to migrate back to the official ast-grep release once PR #2359 is merged and a new version is published.
4040
ast-grep-config = { git = "https://github.com/fengmk2/ast-grep.git", rev = "2f4c6924438a72e136485f0e14cd136b2e17d8d3" }
4141
ast-grep-core = { git = "https://github.com/fengmk2/ast-grep.git", rev = "2f4c6924438a72e136485f0e14cd136b2e17d8d3" }
42-
ast-grep-language = { git = "https://github.com/fengmk2/ast-grep.git", rev = "2f4c6924438a72e136485f0e14cd136b2e17d8d3", default-features = false, features = ["lang-bash"] }
42+
ast-grep-language = { git = "https://github.com/fengmk2/ast-grep.git", rev = "2f4c6924438a72e136485f0e14cd136b2e17d8d3", default-features = false, features = ["lang-bash", "lang-typescript"] }
4343
backon = "1.3.0"
4444
bincode = "2.0.1"
4545
bstr = { version = "1.12.0", default-features = false, features = ["alloc", "std"] }

crates/vite_migration/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,8 @@ ast-grep-language = { workspace = true }
1313
serde_json = { workspace = true, features = ["preserve_order"] }
1414
vite_error = { workspace = true }
1515

16+
[dev-dependencies]
17+
tempfile = { workspace = true }
18+
1619
[lints]
1720
workspace = true
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use ast_grep_config::{GlobalRules, RuleConfig, from_yaml_string};
2+
use ast_grep_core::replacer::Replacer;
3+
use ast_grep_language::{LanguageExt, SupportLang};
4+
use vite_error::Error;
5+
6+
/// Apply ast-grep rules to content and return the transformed content
7+
///
8+
/// This is the core transformation function that:
9+
/// 1. Parses the rule YAML
10+
/// 2. Applies each rule to find matches
11+
/// 3. Replaces matches from back to front to maintain correct positions
12+
///
13+
/// # Arguments
14+
///
15+
/// * `content` - The source content to transform
16+
/// * `rule_yaml` - The ast-grep rules in YAML format
17+
///
18+
/// # Returns
19+
///
20+
/// A tuple of (transformed_content, was_updated)
21+
pub(crate) fn apply_rules(content: &str, rule_yaml: &str) -> Result<(String, bool), Error> {
22+
let rules = load_rules(rule_yaml)?;
23+
let result = apply_loaded_rules(content, &rules);
24+
let updated = result != content;
25+
Ok((result, updated))
26+
}
27+
28+
/// Load ast-grep rules from YAML string
29+
pub(crate) fn load_rules(yaml: &str) -> Result<Vec<RuleConfig<SupportLang>>, Error> {
30+
let globals = GlobalRules::default();
31+
let rules: Vec<RuleConfig<SupportLang>> = from_yaml_string::<SupportLang>(yaml, &globals)?;
32+
Ok(rules)
33+
}
34+
35+
/// Apply pre-loaded ast-grep rules to content
36+
///
37+
/// This is useful when you need to apply the same rules multiple times
38+
/// (e.g., processing multiple scripts in a loop).
39+
///
40+
/// # Arguments
41+
///
42+
/// * `content` - The source content to transform
43+
/// * `rules` - Pre-loaded ast-grep rules
44+
///
45+
/// # Returns
46+
///
47+
/// The transformed content (always returns a new string, even if unchanged)
48+
pub(crate) fn apply_loaded_rules(content: &str, rules: &[RuleConfig<SupportLang>]) -> String {
49+
let mut current = content.to_string();
50+
51+
for rule in rules {
52+
// Parse current content with the rule's language
53+
let grep = rule.language.ast_grep(&current);
54+
let root = grep.root();
55+
56+
let matcher = &rule.matcher;
57+
58+
// Get the fixer if available (rules without fix are pure lint, skip them)
59+
let fixers = match rule.get_fixer() {
60+
Ok(f) if !f.is_empty() => f,
61+
_ => continue,
62+
};
63+
64+
// Collect all matches and their replacements
65+
let mut replacements = Vec::new();
66+
for node in root.find_all(matcher) {
67+
let range = node.range();
68+
let replacement_bytes = fixers[0].generate_replacement(&node);
69+
let replacement_str = String::from_utf8_lossy(&replacement_bytes).to_string();
70+
replacements.push((range.start, range.end, replacement_str));
71+
}
72+
73+
// Replace from back to front to maintain correct positions
74+
replacements.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start));
75+
76+
for (start, end, replacement) in replacements {
77+
current.replace_range(start..end, &replacement);
78+
}
79+
}
80+
81+
current
82+
}

crates/vite_migration/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
mod ast_grep;
12
mod package;
3+
mod vite_config;
24

35
pub use package::rewrite_scripts;
6+
pub use vite_config::{MergeResult, RewriteResult, merge_json_config, rewrite_import};

crates/vite_migration/src/package.rs

Lines changed: 8 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
use ast_grep_config::{GlobalRules, RuleConfig, from_yaml_string};
2-
use ast_grep_core::replacer::Replacer;
3-
use ast_grep_language::{LanguageExt, SupportLang};
1+
use ast_grep_config::RuleConfig;
2+
use ast_grep_language::SupportLang;
43
use serde_json::{Map, Value};
54
use vite_error::Error;
65

7-
/// load script rules from yaml file
8-
fn load_ast_grep_rules(yaml: &str) -> Result<Vec<RuleConfig<SupportLang>>, Error> {
9-
let globals = GlobalRules::default();
10-
let rules: Vec<RuleConfig<SupportLang>> = from_yaml_string::<SupportLang>(&yaml, &globals)?;
11-
Ok(rules)
12-
}
6+
use crate::ast_grep;
137

148
// Marker to replace "cross-env " before ast-grep processing
159
// Using a fake env var assignment that won't match our rules
@@ -22,57 +16,23 @@ fn rewrite_script(script: &str, rules: &[RuleConfig<SupportLang>]) -> String {
2216
let has_cross_env = script.contains(CROSS_ENV_REPLACEMENT);
2317

2418
// Step 1: Replace "cross-env " with marker so ast-grep can see the actual commands
25-
let mut current = if has_cross_env {
19+
let preprocessed = if has_cross_env {
2620
script.replace(CROSS_ENV_REPLACEMENT, CROSS_ENV_MARKER)
2721
} else {
2822
script.to_string()
2923
};
3024

3125
// Step 2: Process with ast-grep
32-
for rule in rules {
33-
// only handle bash rules
34-
if rule.language != SupportLang::Bash {
35-
continue;
36-
}
37-
38-
// parse current script with corresponding language
39-
let grep = rule.language.ast_grep(&current);
40-
let root = grep.root();
41-
42-
// this matcher is the AST matcher generated by deserializing the YAML rule
43-
let matcher = &rule.matcher;
44-
45-
// rules may not have fix (pure lint), skip here
46-
let fixers = match rule.get_fixer() {
47-
Ok(f) if !f.is_empty() => f,
48-
_ => continue,
49-
};
50-
51-
// collect all matches and their replacements
52-
let mut replacements = Vec::new();
53-
for node in root.find_all(matcher) {
54-
let range = node.range();
55-
let replacement_bytes = fixers[0].generate_replacement(&node);
56-
let replacement_str = String::from_utf8_lossy(&replacement_bytes).to_string();
57-
replacements.push((range.start, range.end, replacement_str));
58-
}
59-
60-
// Replace from back to front
61-
replacements.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start));
62-
63-
for (start, end, replacement) in replacements {
64-
current.replace_range(start..end, &replacement);
65-
}
66-
}
26+
let result = ast_grep::apply_loaded_rules(&preprocessed, rules);
6727

6828
// Step 3: Replace marker back with "cross-env " (only if we replaced it)
69-
if has_cross_env { current.replace(CROSS_ENV_MARKER, CROSS_ENV_REPLACEMENT) } else { current }
29+
if has_cross_env { result.replace(CROSS_ENV_MARKER, CROSS_ENV_REPLACEMENT) } else { result }
7030
}
7131

7232
/// rewrite scripts json content using rules from rules_yaml
7333
pub fn rewrite_scripts(scripts_json: &str, rules_yaml: &str) -> Result<Option<String>, Error> {
7434
let mut scripts: Map<String, Value> = serde_json::from_str(scripts_json)?;
75-
let rules = load_ast_grep_rules(rules_yaml)?;
35+
let rules = ast_grep::load_rules(rules_yaml)?;
7636

7737
let mut updated = false;
7838
// get scripts field (object)
@@ -156,9 +116,7 @@ fix: vite test
156116

157117
#[test]
158118
fn test_rewrite_script() {
159-
let globals = GlobalRules::default();
160-
let rules: Vec<RuleConfig<SupportLang>> =
161-
from_yaml_string::<SupportLang>(&RULES_YAML, &globals).unwrap();
119+
let rules = ast_grep::load_rules(RULES_YAML).unwrap();
162120
// vite commands
163121
assert_eq!(rewrite_script("vite", &rules), "vite dev");
164122
assert_eq!(rewrite_script("vite dev", &rules), "vite dev");

0 commit comments

Comments
 (0)