From 12351bd3984881b0481e52975ef2341241b54a70 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 26 Nov 2025 11:27:04 +0800 Subject: [PATCH 1/6] feat: merge oxlint config --- Cargo.lock | 11 + Cargo.toml | 2 +- .../vite_migration/rules/oxlint-config.yaml | 115 ++ .../vite_migration/rules/oxlint-simple.yaml | 72 ++ crates/vite_migration/src/lib.rs | 2 + crates/vite_migration/src/vite_config.rs | 1090 +++++++++++++++++ packages/cli/binding/index.d.ts | 44 + packages/cli/binding/index.js | 10 +- packages/cli/binding/src/lib.rs | 2 +- packages/cli/binding/src/migration.rs | 59 + .../.oxfmtrc.json | 7 + .../.oxlintrc.json | 5 + .../package.json | 6 + .../snap.txt | 35 + .../steps.json | 8 + packages/global/src/migration/migrator.ts | 53 +- 16 files changed, 1517 insertions(+), 4 deletions(-) create mode 100644 crates/vite_migration/rules/oxlint-config.yaml create mode 100644 crates/vite_migration/rules/oxlint-simple.yaml create mode 100644 crates/vite_migration/src/vite_config.rs create mode 100644 packages/global/snap-tests/migration-auto-create-vite-config/.oxfmtrc.json create mode 100644 packages/global/snap-tests/migration-auto-create-vite-config/.oxlintrc.json create mode 100644 packages/global/snap-tests/migration-auto-create-vite-config/package.json create mode 100644 packages/global/snap-tests/migration-auto-create-vite-config/snap.txt create mode 100644 packages/global/snap-tests/migration-auto-create-vite-config/steps.json diff --git a/Cargo.lock b/Cargo.lock index 186de225a4..2a84d5ee8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,7 @@ dependencies = [ "serde", "tree-sitter", "tree-sitter-bash", + "tree-sitter-typescript", ] [[package]] @@ -4138,6 +4139,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8" +[[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 = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 1fb3841a4b..57ed9a24be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ anyhow = "1.0.98" # Safe to migrate back to the official ast-grep release once PR #2359 is merged and a new version is published. ast-grep-config = { git = "https://github.com/fengmk2/ast-grep.git", rev = "2f4c6924438a72e136485f0e14cd136b2e17d8d3" } ast-grep-core = { git = "https://github.com/fengmk2/ast-grep.git", rev = "2f4c6924438a72e136485f0e14cd136b2e17d8d3" } -ast-grep-language = { git = "https://github.com/fengmk2/ast-grep.git", rev = "2f4c6924438a72e136485f0e14cd136b2e17d8d3", default-features = false, features = ["lang-bash"] } +ast-grep-language = { git = "https://github.com/fengmk2/ast-grep.git", rev = "2f4c6924438a72e136485f0e14cd136b2e17d8d3", default-features = false, features = ["lang-bash", "lang-typescript"] } backon = "1.3.0" bincode = "2.0.1" bstr = { version = "1.12.0", default-features = false, features = ["alloc", "std"] } diff --git a/crates/vite_migration/rules/oxlint-config.yaml b/crates/vite_migration/rules/oxlint-config.yaml new file mode 100644 index 0000000000..30dc784a27 --- /dev/null +++ b/crates/vite_migration/rules/oxlint-config.yaml @@ -0,0 +1,115 @@ +# ast-grep rules for migrating .oxlintrc configuration to vite.config.ts +# +# These rules merge oxlint configuration from .oxlintrc into the defineConfig +# call in vite.config.ts, following the vite-plus unified configuration approach. +# +# Usage: +# The migration script should: +# 1. Read .oxlintrc JSON content +# 2. Convert it to TypeScript object literal format +# 3. Replace __OXLINT_CONFIG__ placeholder with the actual config +# 4. Run: sg scan -r oxlint-config.yaml vite.config.ts -U +# +# Example workflow: +# +# # Step 1: Create rule file with actual oxlint config +# cat > /tmp/migrate-oxlint.yaml << 'EOF' +# --- +# id: merge-oxlint-config +# language: TypeScript +# rule: +# pattern: | +# defineConfig({ +# $$$CONFIG +# }) +# fix: |- +# defineConfig({ +# $$$CONFIG +# lint: { +# rules: { +# 'no-unused-vars': 'error', +# 'no-console': 'warn', +# }, +# ignorePatterns: ['dist', 'node_modules'], +# }, +# }) +# EOF +# +# # Step 2: Run ast-grep +# sg scan -r /tmp/migrate-oxlint.yaml vite.config.ts -U +# +# # Step 3: Remove old config file +# rm .oxlintrc +# +# For programmatic usage, use the vite_migration Rust crate which provides +# proper JSON to TypeScript conversion. + +# Rule 1: Add lint config to defineConfig with existing properties +# Matches: +# defineConfig({ +# plugins: [...], +# server: {...}, +# }) +# Result: +# defineConfig({ +# plugins: [...], +# server: {...}, +# lint: {...}, +# }) +--- +id: add-lint-config-to-defineconfig +language: TypeScript +severity: info +message: 'Add lint configuration to vite.config.ts' +note: 'Migrating oxlint configuration from .oxlintrc to vite.config.ts' +rule: + pattern: | + defineConfig({ + $$$EXISTING_CONFIG + }) +fix: |- + defineConfig({ + $$$EXISTING_CONFIG + // lint configuration (merged from .oxlintrc) + lint: __OXLINT_CONFIG__, + }) +files: + - '**/vite.config.ts' + - '**/vite.config.mts' + - '**/vite.config.js' + - '**/vite.config.mjs' +ignores: + - '**/node_modules/**' + - '**/dist/**' + +# Rule 2: Detect defineConfig with function callback (warning only) +# This pattern requires manual migration +# Matches: defineConfig((env) => ({ ... })) +--- +id: detect-function-defineconfig +language: TypeScript +severity: warning +message: 'defineConfig uses a function callback - manual migration required' +note: | + The defineConfig uses a function callback pattern which cannot be + automatically migrated. Please manually add the lint configuration: + + export default defineConfig((env) => ({ + ...existingConfig, + lint: { + rules: { + // your rules from .oxlintrc + }, + }, + })); +rule: + # Using explicit arrow function pattern to match callback style + pattern: defineConfig(($PARAMS) => $BODY) +files: + - '**/vite.config.ts' + - '**/vite.config.mts' + - '**/vite.config.js' + - '**/vite.config.mjs' +ignores: + - '**/node_modules/**' + - '**/dist/**' diff --git a/crates/vite_migration/rules/oxlint-simple.yaml b/crates/vite_migration/rules/oxlint-simple.yaml new file mode 100644 index 0000000000..ad9c0764cf --- /dev/null +++ b/crates/vite_migration/rules/oxlint-simple.yaml @@ -0,0 +1,72 @@ +# Simple ast-grep rule for adding lint config to defineConfig +# +# This rule is designed to be used programmatically where the +# __OXLINT_CONFIG__ placeholder is replaced with actual content. +# +# Usage: +# 1. Read .oxlintrc content and convert to TypeScript +# 2. Replace __OXLINT_CONFIG__ in this file with the converted content +# 3. Run: sg scan -r oxlint-simple.yaml vite.config.ts -U +# +# Example: +# # Create a rule with actual config content +# cat > /tmp/oxlint-rule.yaml << 'EOF' +# --- +# id: merge-oxlint-to-vite-config +# language: TypeScript +# rule: +# pattern: | +# defineConfig({ +# $$$CONFIG +# }) +# fix: |- +# defineConfig({ +# $$$CONFIG +# lint: { +# rules: { +# 'no-console': 'warn', +# }, +# }, +# }) +# EOF +# sg scan -r /tmp/oxlint-rule.yaml vite.config.ts -U +# +# Note: This rule handles the common case where defineConfig receives +# an object literal directly. For function callbacks or complex patterns, +# manual migration may be needed. + +--- +id: merge-oxlint-to-vite-config +language: TypeScript +severity: info +message: 'Merge .oxlintrc configuration into vite.config.ts' +note: | + This rule adds the lint configuration from .oxlintrc to the + defineConfig call in vite.config.ts. + + Before: defineConfig({ plugins: [...] }) + After: defineConfig({ plugins: [...], lint: {...} }) + +rule: + # Match defineConfig call with object argument + # Using multiline pattern to correctly capture the content + pattern: | + defineConfig({ + $$$CONFIG + }) + +fix: |- + defineConfig({ + $$$CONFIG + // lint configuration (migrated from .oxlintrc) + lint: __OXLINT_CONFIG__, + }) + +files: + - '**/vite.config.ts' + - '**/vite.config.mts' + - '**/vite.config.js' + - '**/vite.config.mjs' +ignores: + - '**/node_modules/**' + - '**/dist/**' diff --git a/crates/vite_migration/src/lib.rs b/crates/vite_migration/src/lib.rs index 6e54c1f2a7..38260d1724 100644 --- a/crates/vite_migration/src/lib.rs +++ b/crates/vite_migration/src/lib.rs @@ -1,3 +1,5 @@ mod package; +mod vite_config; pub use package::rewrite_scripts; +pub use vite_config::{MergeResult, merge_json_config}; diff --git a/crates/vite_migration/src/vite_config.rs b/crates/vite_migration/src/vite_config.rs new file mode 100644 index 0000000000..37ffa7a4b0 --- /dev/null +++ b/crates/vite_migration/src/vite_config.rs @@ -0,0 +1,1090 @@ +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 vite_error::Error; + +/// Result of merging JSON config into vite config +#[derive(Debug)] +pub struct MergeResult { + /// The updated vite config content + pub content: String, + /// Whether any changes were made + pub updated: bool, + /// Whether the config uses a function callback + pub uses_function_callback: bool, +} + +/// Merge a JSON configuration file into vite.config.ts or vite.config.js +/// +/// This function reads a JSON configuration file and merges it into the vite +/// configuration file by adding a section with the specified key to the config. +/// +/// Note: TypeScript parser is used for both .ts and .js files since TypeScript +/// syntax is a superset of JavaScript. +/// +/// # Arguments +/// +/// * `vite_config_path` - Path to the vite.config.ts or vite.config.js file +/// * `json_config_path` - Path to the JSON config file (e.g., .oxlintrc.json, .oxfmtrc.json) +/// * `config_key` - The key to use in the vite config (e.g., "lint", "format") +/// +/// # Returns +/// +/// Returns a `MergeResult` containing: +/// - `content`: The updated vite config content +/// - `updated`: Whether any changes were made +/// - `uses_function_callback`: Whether the config uses a function callback +/// +/// # Example +/// +/// ```ignore +/// use std::path::Path; +/// use vite_migration::merge_json_config; +/// +/// // Merge oxlint config with "lint" key +/// let result = merge_json_config( +/// Path::new("vite.config.ts"), +/// Path::new(".oxlintrc"), +/// "lint", +/// )?; +/// +/// // Merge oxfmt config with "format" key +/// let result = merge_json_config( +/// Path::new("vite.config.ts"), +/// Path::new(".oxfmtrc.json"), +/// "format", +/// )?; +/// +/// if result.updated { +/// std::fs::write("vite.config.ts", &result.content)?; +/// } +/// ``` +pub fn merge_json_config( + vite_config_path: &Path, + json_config_path: &Path, + config_key: &str, +) -> Result { + // Read the vite config file + let vite_config_content = std::fs::read_to_string(vite_config_path)?; + + // Read and parse the JSON config file + let json_config_content = std::fs::read_to_string(json_config_path)?; + let json_config: Value = serde_json::from_str(&json_config_content)?; + + // Convert JSON to TypeScript object literal + let ts_config = json_to_js_object_literal(&json_config, 0); + + // Merge the config + merge_json_config_content(&vite_config_content, &ts_config, config_key) +} + +/// Merge JSON configuration into vite config content +/// +/// This is the internal function that performs the actual merge using ast-grep. +/// It takes the vite config content and the JSON config as a TypeScript object literal string. +/// +/// # Arguments +/// +/// * `vite_config_content` - The content of the vite.config.ts or vite.config.js file +/// * `ts_config` - The config as a TypeScript object literal string +/// * `config_key` - The key to use in the vite config (e.g., "lint", "format") +/// +/// # Returns +/// +/// Returns a `MergeResult` with the updated content and status flags. +fn merge_json_config_content( + vite_config_content: &str, + ts_config: &str, + config_key: &str, +) -> Result { + // Check if the config uses a function callback (for informational purposes) + let uses_function_callback = check_function_callback(vite_config_content)?; + + // Generate the ast-grep rules with the actual config + let rule_yaml = generate_merge_rule(ts_config, config_key); + + // Load and apply the rules + let globals = GlobalRules::default(); + let rules: Vec> = + from_yaml_string::(&rule_yaml, &globals)?; + + // Apply the transformation + let mut current = vite_config_content.to_string(); + let mut updated = false; + + for rule in &rules { + // Only handle TypeScript rules + if rule.language != SupportLang::TypeScript { + continue; + } + + // Parse current config with TypeScript language + let grep = rule.language.ast_grep(¤t); + let root = grep.root(); + + let matcher = &rule.matcher; + + // Get the fixer if available + 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 to maintain correct positions + replacements.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start)); + + for (start, end, replacement) in replacements { + current.replace_range(start..end, &replacement); + updated = true; + } + } + + Ok(MergeResult { content: current, updated, uses_function_callback }) +} + +/// Check if the vite config uses a function callback pattern +fn check_function_callback(vite_config_content: &str) -> Result { + // Match both sync and async arrow functions + let check_rule = r#" +--- +id: check-function-callback +language: TypeScript +rule: + any: + - pattern: defineConfig(($PARAMS) => $BODY) + - pattern: defineConfig(async ($PARAMS) => $BODY) +"#; + + let globals = GlobalRules::default(); + let rules: Vec> = + from_yaml_string::(check_rule, &globals)?; + + for rule in &rules { + if rule.language != SupportLang::TypeScript { + continue; + } + + let grep = rule.language.ast_grep(vite_config_content); + let root = grep.root(); + let matcher = &rule.matcher; + + if root.find(matcher).is_some() { + return Ok(true); + } + } + + Ok(false) +} + +/// Generate the ast-grep rules YAML for merging JSON config +/// +/// This generates six rules: +/// 1. For object literal: `defineConfig({ ... })` +/// 2. For arrow function with direct return: `defineConfig((env) => ({ ... }))` +/// 3. For return object literal inside defineConfig callback: `return { ... }` +/// 4. For return variable inside defineConfig callback: `return configObj` -> `return { ..., ...configObj }` +/// 5. For plain object export: `export default { ... }` +/// 6. For satisfies pattern: `export default { ... } satisfies Type` +/// +/// The config is placed first to avoid trailing comma issues. +fn generate_merge_rule(ts_config: &str, config_key: &str) -> String { + // Indent the config to match the YAML structure + let indented_config = indent_multiline(ts_config, 4); + + let template = r#"--- +id: merge-json-config-object +language: TypeScript +rule: + pattern: | + defineConfig({ + $$$CONFIG + }) +fix: |- + defineConfig({ + __CONFIG_KEY__: __JSON_CONFIG__, + $$$CONFIG + }) +--- +id: merge-json-config-function +language: TypeScript +rule: + pattern: | + defineConfig(($PARAMS) => ({ + $$$CONFIG + })) +fix: |- + defineConfig(($PARAMS) => ({ + __CONFIG_KEY__: __JSON_CONFIG__, + $$$CONFIG + })) +--- +id: merge-json-config-return +language: TypeScript +rule: + pattern: | + return { + $$$CONFIG + } + inside: + stopBy: end + pattern: defineConfig($$$ARGS) +fix: |- + return { + __CONFIG_KEY__: __JSON_CONFIG__, + $$$CONFIG + } +--- +id: merge-json-config-return-var +language: TypeScript +rule: + pattern: return $VAR + has: + pattern: $VAR + kind: identifier + inside: + stopBy: end + pattern: defineConfig($$$ARGS) +fix: |- + return { + __CONFIG_KEY__: __JSON_CONFIG__, + ...$VAR, + } +--- +id: merge-json-config-plain-export +language: TypeScript +rule: + pattern: | + export default { + $$$CONFIG + } +fix: |- + export default { + __CONFIG_KEY__: __JSON_CONFIG__, + $$$CONFIG + } +--- +id: merge-json-config-satisfies +language: TypeScript +rule: + pattern: | + export default { + $$$CONFIG + } satisfies $TYPE +fix: |- + export default { + __CONFIG_KEY__: __JSON_CONFIG__, + $$$CONFIG + } satisfies $TYPE +"#; + + template.replace("__CONFIG_KEY__", config_key).replace("__JSON_CONFIG__", &indented_config) +} + +/// Indent each line of a multiline string +fn indent_multiline(s: &str, spaces: usize) -> String { + let indent = " ".repeat(spaces); + let lines: Vec<&str> = s.lines().collect(); + + if lines.len() <= 1 { + return s.to_string(); + } + + // First line doesn't get indented (it's on the same line as the key) + // Subsequent lines get the specified indent + lines + .iter() + .enumerate() + .map(|(i, line)| if i == 0 { line.to_string() } else { format!("{indent}{line}") }) + .collect::>() + .join("\n") +} + +/// Convert a JSON value to JavaScript object literal format +/// +/// This function recursively converts JSON values to their JavaScript +/// object literal representation with proper formatting. +fn json_to_js_object_literal(value: &Value, indent: usize) -> String { + match value { + Value::Null => "null".to_string(), + Value::Bool(b) => b.to_string(), + Value::Number(n) => n.to_string(), + Value::String(s) => format!("'{}'", escape_single_quotes(s)), + Value::Array(arr) => { + if arr.is_empty() { + return "[]".to_string(); + } + let items: Vec = + arr.iter().map(|item| json_to_js_object_literal(item, indent + 2)).collect(); + format!("[{}]", items.join(", ")) + } + Value::Object(obj) => { + // Filter out $schema field (used for JSON schema validation, not needed in JS) + let filtered: Vec<_> = obj.iter().filter(|(key, _)| *key != "$schema").collect(); + + if filtered.is_empty() { + return "{}".to_string(); + } + + let spaces = " ".repeat(indent); + let inner_spaces = " ".repeat(indent + 2); + + let props: Vec = filtered + .iter() + .map(|(key, val)| { + let formatted_key = format_object_key(key); + let formatted_value = json_to_js_object_literal(val, indent + 2); + format!("{inner_spaces}{formatted_key}: {formatted_value}") + }) + .collect(); + + format!("{{\n{},\n{spaces}}}", props.join(",\n")) + } + } +} + +/// Format an object key for TypeScript +/// +/// If the key is a valid identifier, return it as-is. +/// Otherwise, wrap it in single quotes. +fn format_object_key(key: &str) -> String { + // Check if the key is a valid JavaScript identifier + if is_valid_identifier(key) { + key.to_string() + } else { + format!("'{}'", escape_single_quotes(key)) + } +} + +/// Check if a string is a valid JavaScript identifier +fn is_valid_identifier(s: &str) -> bool { + if s.is_empty() { + return false; + } + + let mut chars = s.chars(); + + // First character must be a letter, underscore, or dollar sign + match chars.next() { + Some(c) if c.is_ascii_alphabetic() || c == '_' || c == '$' => {} + _ => return false, + } + + // Rest can also include digits + for c in chars { + if !c.is_ascii_alphanumeric() && c != '_' && c != '$' { + return false; + } + } + + // Check against reserved words (basic set) + !matches!( + s, + "break" + | "case" + | "catch" + | "continue" + | "debugger" + | "default" + | "delete" + | "do" + | "else" + | "finally" + | "for" + | "function" + | "if" + | "in" + | "instanceof" + | "new" + | "return" + | "switch" + | "this" + | "throw" + | "try" + | "typeof" + | "var" + | "void" + | "while" + | "with" + | "class" + | "const" + | "enum" + | "export" + | "extends" + | "import" + | "super" + | "implements" + | "interface" + | "let" + | "package" + | "private" + | "protected" + | "public" + | "static" + | "yield" + ) +} + +/// Escape single quotes in a string for TypeScript string literals +fn escape_single_quotes(s: &str) -> String { + s.replace('\\', "\\\\").replace('\'', "\\'") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_json_to_js_object_literal_primitives() { + assert_eq!(json_to_js_object_literal(&Value::Null, 0), "null"); + assert_eq!(json_to_js_object_literal(&Value::Bool(true), 0), "true"); + assert_eq!(json_to_js_object_literal(&Value::Bool(false), 0), "false"); + assert_eq!(json_to_js_object_literal(&serde_json::json!(42), 0), "42"); + assert_eq!(json_to_js_object_literal(&serde_json::json!(3.14), 0), "3.14"); + assert_eq!(json_to_js_object_literal(&serde_json::json!("hello"), 0), "'hello'"); + } + + #[test] + fn test_json_to_js_object_literal_string_escaping() { + assert_eq!(json_to_js_object_literal(&serde_json::json!("it's"), 0), "'it\\'s'"); + assert_eq!(json_to_js_object_literal(&serde_json::json!("a\\b"), 0), "'a\\\\b'"); + } + + #[test] + fn test_json_to_js_object_literal_array() { + assert_eq!(json_to_js_object_literal(&serde_json::json!([]), 0), "[]"); + assert_eq!(json_to_js_object_literal(&serde_json::json!([1, 2, 3]), 0), "[1, 2, 3]"); + assert_eq!(json_to_js_object_literal(&serde_json::json!(["a", "b"]), 0), "['a', 'b']"); + } + + #[test] + fn test_json_to_js_object_literal_object() { + assert_eq!(json_to_js_object_literal(&serde_json::json!({}), 0), "{}"); + + let obj = serde_json::json!({ + "key": "value" + }); + let result = json_to_js_object_literal(&obj, 0); + assert!(result.contains("key: 'value'")); + } + + #[test] + fn test_json_to_js_object_literal_ignores_schema() { + // $schema field should be filtered out + let obj = serde_json::json!({ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "foo": "bar" + }); + let result = json_to_js_object_literal(&obj, 0); + assert!(!result.contains("$schema")); + assert!(result.contains("foo: 'bar'")); + + // Object with only $schema should become empty + let obj = serde_json::json!({ + "$schema": "./schema.json" + }); + assert_eq!(json_to_js_object_literal(&obj, 0), "{}"); + } + + #[test] + fn test_json_to_js_object_literal_complex() { + let config = serde_json::json!({ + "rules": { + "no-unused-vars": "error", + "no-console": "warn" + }, + "ignorePatterns": ["dist", "node_modules"] + }); + + let result = json_to_js_object_literal(&config, 2); + assert!(result.contains("rules:")); + assert!(result.contains("'no-unused-vars': 'error'")); + assert!(result.contains("'no-console': 'warn'")); + assert!(result.contains("ignorePatterns: ['dist', 'node_modules']")); + } + + #[test] + fn test_format_object_key() { + assert_eq!(format_object_key("validKey"), "validKey"); + assert_eq!(format_object_key("_private"), "_private"); + assert_eq!(format_object_key("$special"), "$special"); + assert_eq!(format_object_key("key123"), "key123"); + assert_eq!(format_object_key("no-dashes"), "'no-dashes'"); + assert_eq!(format_object_key("has space"), "'has space'"); + assert_eq!(format_object_key("123start"), "'123start'"); + } + + #[test] + fn test_is_valid_identifier() { + assert!(is_valid_identifier("validName")); + assert!(is_valid_identifier("_private")); + assert!(is_valid_identifier("$jquery")); + assert!(is_valid_identifier("camelCase")); + assert!(is_valid_identifier("PascalCase")); + assert!(is_valid_identifier("name123")); + + assert!(!is_valid_identifier("")); + assert!(!is_valid_identifier("123start")); + assert!(!is_valid_identifier("has-dash")); + assert!(!is_valid_identifier("has space")); + assert!(!is_valid_identifier("class")); // reserved word + assert!(!is_valid_identifier("const")); // reserved word + } + + #[test] + fn test_check_function_callback() { + let simple_config = r#" +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [], +}); +"#; + assert!(!check_function_callback(simple_config).unwrap()); + + let function_config = r#" +import { defineConfig } from 'vite'; + +export default defineConfig((env) => ({ + plugins: [], + server: { + port: env.mode === 'production' ? 8080 : 3000, + }, +})); +"#; + assert!(check_function_callback(function_config).unwrap()); + } + + #[test] + fn test_merge_json_config_content_simple() { + let vite_config = r#"import { defineConfig } from 'vite'; + +export default defineConfig({});"#; + + let oxlint_config = r#"{ + rules: { + 'no-console': 'warn', + }, +}"#; + + let result = merge_json_config_content(vite_config, oxlint_config, "lint").unwrap(); + assert_eq!( + result.content, + r#"import { defineConfig } from 'vite'; + +export default defineConfig({ + lint: { + rules: { + 'no-console': 'warn', + }, + }, + +});"# + ); + assert!(result.updated); + assert!(!result.uses_function_callback); + } + + #[test] + fn test_merge_json_config_content_with_existing_config() { + let vite_config = r#"import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + }, +});"#; + + let oxlint_config = r#"{ + rules: { + 'no-unused-vars': 'error', + }, +}"#; + + let result = merge_json_config_content(vite_config, oxlint_config, "lint").unwrap(); + assert!(result.updated); + assert!(result.content.contains("plugins: [react()]")); + assert!(result.content.contains("port: 3000")); + assert!(result.content.contains("lint:")); + assert!(result.content.contains("'no-unused-vars': 'error'")); + } + + #[test] + fn test_merge_json_config_content_function_callback() { + let vite_config = r#"import { defineConfig } from 'vite'; + +export default defineConfig((env) => ({ + plugins: [], +}));"#; + + let oxlint_config = r#"{ + rules: { + 'no-console': 'warn', + }, +}"#; + + let result = merge_json_config_content(vite_config, oxlint_config, "lint").unwrap(); + assert!(result.uses_function_callback); + // Function callbacks are now supported + assert!(result.updated); + assert!(result.content.contains("lint:")); + assert!(result.content.contains("'no-console': 'warn'")); + // Verify the function callback structure is preserved + assert!(result.content.contains("(env) =>")); + println!("result: {}", result.content); + } + + #[test] + fn test_merge_json_config_content_complex_function_callback() { + let oxlint_config = r#"{ + rules: { + 'no-console': 'warn', + }, +}"#; + // Complex function callback with conditional returns + // https://vite.dev/config/#conditional-config + let vite_config = r#"import { defineConfig } from 'vite'; + +export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => { + if (command === 'serve') { + return { + // dev specific config + } + } else { + // command === 'build' + return { + // build specific config + } + } +});"#; + + let result = merge_json_config_content(vite_config, oxlint_config, "lint").unwrap(); + println!("result: {}", result.content); + // Detected as function callback + assert!(result.uses_function_callback); + // Now can be auto-migrated using return statement matching + assert!(result.updated); + // Both return statements should have lint config added + assert_eq!( + result.content.matches("lint: {").count(), + 2, + "Expected 2 lint configs, one for each return statement" + ); + assert!(result.content.contains("'no-console': 'warn'")); + + // https://vite.dev/config/#using-environment-variables-in-config + let vite_config = r#" +import { defineConfig, loadEnv } from 'vite' + +export default defineConfig(({ mode }) => { + // Load env file based on `mode` in the current working directory. + // Set the third parameter to '' to load all env regardless of the + // `VITE_` prefix. + const env = loadEnv(mode, process.cwd(), '') + return { + define: { + // Provide an explicit app-level constant derived from an env var. + __APP_ENV__: JSON.stringify(env.APP_ENV), + }, + // Example: use an env var to set the dev server port conditionally. + server: { + port: env.APP_PORT ? Number(env.APP_PORT) : 5173, + }, + } +}) +"#; + + let result = merge_json_config_content(vite_config, oxlint_config, "lint").unwrap(); + println!("result: {}", result.content); + // Detected as function callback + assert!(result.uses_function_callback); + // Now can be auto-migrated using return statement matching + assert!(result.updated); + assert!(result.content.contains("'no-console': 'warn'")); + + // https://vite.dev/config/#async-config + let vite_config = r#" +export default defineConfig(async ({ command, mode }) => { + const data = await asyncFunction() + return { + // vite config + } +}) +"#; + + let result = merge_json_config_content(vite_config, oxlint_config, "lint").unwrap(); + println!("result: {}", result.content); + // Detected as function callback + assert!(result.uses_function_callback); + // Now can be auto-migrated using return statement matching + assert!(result.updated); + assert!(result.content.contains("'no-console': 'warn'")); + } + + #[test] + fn test_generate_merge_rule() { + let config = "{ rules: { 'no-console': 'warn' } }"; + + // Test with "lint" key + let rule = generate_merge_rule(config, "lint"); + assert!(rule.contains("id: merge-json-config-object")); + assert!(rule.contains("id: merge-json-config-function")); + assert!(rule.contains("id: merge-json-config-return")); + assert!(rule.contains("id: merge-json-config-return-var")); + assert!(rule.contains("id: merge-json-config-plain-export")); + assert!(rule.contains("id: merge-json-config-satisfies")); + assert!(rule.contains("language: TypeScript")); + assert!(rule.contains("defineConfig")); + assert!(rule.contains("lint:")); + assert!(rule.contains("'no-console': 'warn'")); + assert!(rule.contains("($PARAMS) =>")); + assert!(rule.contains("inside:")); + assert!(rule.contains("defineConfig($$$ARGS)")); + assert!(rule.contains("export default {")); + assert!(rule.contains("...$VAR,")); + + // Test with "format" key + let rule = generate_merge_rule(config, "format"); + assert!(rule.contains("format:")); + assert!(!rule.contains("lint:")); + } + + #[test] + fn test_merge_json_config_content_arrow_wrapper() { + // Arrow function that wraps defineConfig + let vite_config = r#"import { defineConfig } from "vite"; + +export default () => + defineConfig({ + root: "./", + build: { + outDir: "./build/app", + }, + });"#; + + let oxlint_config = r#"{ + rules: { + 'no-console': 'warn', + }, +}"#; + + let result = merge_json_config_content(vite_config, oxlint_config, "lint").unwrap(); + println!("result: {}", result.content); + assert!(result.updated); + assert!(!result.uses_function_callback); + assert!(result.content.contains("lint: {")); + assert!(result.content.contains("'no-console': 'warn'")); + } + + #[test] + fn test_merge_json_config_content_plain_export() { + // Plain object export without defineConfig + // https://vite.dev/config/#config-intellisense + let vite_config = r#"export default { + server: { + port: 5173, + }, +}"#; + + let oxlint_config = r#"{ + rules: { + 'no-console': 'warn', + }, +}"#; + + let result = merge_json_config_content(vite_config, oxlint_config, "lint").unwrap(); + println!("result: {}", result.content); + assert!(result.updated); + assert!(!result.uses_function_callback); + assert!(result.content.contains("lint: {")); + assert!(result.content.contains("'no-console': 'warn'")); + assert!(result.content.contains("server: {")); + + let vite_config = r#" +import type { UserConfig } from 'vite' + +export default { + server: { + port: 5173, + }, +} satisfies UserConfig + "#; + + let result = merge_json_config_content(vite_config, oxlint_config, "lint").unwrap(); + println!("result: {}", result.content); + assert!(result.updated); + assert!(!result.uses_function_callback); + assert!(result.content.contains("lint: {")); + assert!(result.content.contains("'no-console': 'warn'")); + assert!(result.content.contains("server: {")); + } + + #[test] + fn test_merge_json_config_content_return_variable() { + // Return a variable instead of object literal + let vite_config = r#"import { defineConfig, loadEnv } from 'vite' + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + const configObject = { + define: { + __APP_ENV__: JSON.stringify(env.APP_ENV), + }, + server: { + port: env.APP_PORT ? Number(env.APP_PORT) : 5173, + }, + } + + return configObject +})"#; + + let oxlint_config = r#"{ + rules: { + 'no-console': 'warn', + }, +}"#; + + let result = merge_json_config_content(vite_config, oxlint_config, "lint").unwrap(); + assert_eq!( + result.content, + r#"import { defineConfig, loadEnv } from 'vite' + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + const configObject = { + define: { + __APP_ENV__: JSON.stringify(env.APP_ENV), + }, + server: { + port: env.APP_PORT ? Number(env.APP_PORT) : 5173, + }, + } + + return { + lint: { + rules: { + 'no-console': 'warn', + }, + }, + ...configObject, + } +})"# + ); + assert!(result.updated); + assert!(result.uses_function_callback); + } + + #[test] + fn test_merge_json_config_content_with_format_key() { + // Test merge_json_config_content with "format" key (for oxfmt) + let vite_config = r#"import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [], +});"#; + + let format_config = r#"{ + indentWidth: 2, + lineWidth: 100, +}"#; + + let result = merge_json_config_content(vite_config, format_config, "format").unwrap(); + println!("result: {}", result.content); + assert!(result.updated); + assert!(result.content.contains("format: {")); + assert!(result.content.contains("indentWidth: 2")); + assert!(result.content.contains("lineWidth: 100")); + assert!(!result.content.contains("lint:")); + } + + #[test] + fn test_merge_json_config_with_files() { + use std::io::Write; + + // Create temporary files + let temp_dir = std::env::temp_dir().join("vite_migration_test"); + std::fs::create_dir_all(&temp_dir).unwrap(); + + let vite_config_path = temp_dir.join("vite.config.ts"); + let oxlint_config_path = temp_dir.join(".oxlintrc"); + + // Write test vite config + let mut vite_file = std::fs::File::create(&vite_config_path).unwrap(); + writeln!( + vite_file, + r#"import {{ defineConfig }} from 'vite'; + +export default defineConfig({{ + plugins: [], +}});"# + ) + .unwrap(); + + // Write test oxlint config + let mut oxlint_file = std::fs::File::create(&oxlint_config_path).unwrap(); + writeln!( + oxlint_file, + r#"{{ + "rules": {{ + "no-unused-vars": "error", + "no-console": "warn" + }}, + "ignorePatterns": ["dist", "node_modules"] +}}"# + ) + .unwrap(); + + // Run the merge + let result = merge_json_config(&vite_config_path, &oxlint_config_path, "lint").unwrap(); + + // Verify the result + assert_eq!( + result.content, + r#"import { defineConfig } from 'vite'; + +export default defineConfig({ + lint: { + rules: { + 'no-unused-vars': 'error', + 'no-console': 'warn', + }, + ignorePatterns: ['dist', 'node_modules'], + }, + plugins: [], +}); +"# + ); + // Clean up + std::fs::remove_dir_all(&temp_dir).ok(); + } + + #[test] + fn test_full_json_to_js_object_literal_conversion() { + // Test a realistic .oxlintrc config + let oxlint_json = serde_json::json!({ + "rules": { + "no-unused-vars": "error", + "no-console": "warn", + "no-debugger": "error" + }, + "ignorePatterns": ["dist", "node_modules", "*.config.js"], + "plugins": ["react", "typescript"], + "settings": { + "react": { + "version": "detect" + } + } + }); + + let ts_literal = json_to_js_object_literal(&oxlint_json, 0); + + // Verify the conversion + assert_eq!( + ts_literal, + r#"{ + rules: { + 'no-unused-vars': 'error', + 'no-console': 'warn', + 'no-debugger': 'error', + }, + ignorePatterns: ['dist', 'node_modules', '*.config.js'], + plugins: ['react', 'typescript'], + settings: { + react: { + version: 'detect', + }, + }, +}"# + ); + } + + #[test] + fn test_indent_multiline() { + // Single line - no change + assert_eq!(indent_multiline("single line", 4), "single line"); + + // Empty string + assert_eq!(indent_multiline("", 4), ""); + + // Multiple lines + let input = "first\nsecond\nthird"; + let expected = "first\n second\n third"; + assert_eq!(indent_multiline(input, 4), expected); + } + + #[test] + fn test_merge_json_config_content_no_trailing_comma() { + // Config WITHOUT trailing comma - lint is placed first to avoid comma issues + let vite_config = r#"import { defineConfig } from 'vite'; +export default defineConfig({ + plugins: [] +});"#; + + let oxlint_config = r#"{ + rules: { + 'no-console': 'warn', + }, +}"#; + + let result = merge_json_config_content(vite_config, oxlint_config, "lint").unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + "import { defineConfig } from 'vite'; +export default defineConfig({ + lint: { + rules: { + 'no-console': 'warn', + }, + }, + plugins: [] +});" + ); + } + + #[test] + fn test_merge_json_config_content_with_trailing_comma() { + // Config WITH trailing comma - no issues since lint is placed first + let vite_config = r#"import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [], +})"#; + + let oxlint_config = r#"{ + rules: { + 'no-console': 'warn', + }, +}"#; + + let result = merge_json_config_content(vite_config, oxlint_config, "lint").unwrap(); + println!("result: {}", result.content); + assert!(result.updated); + assert_eq!( + result.content, + "import { defineConfig } from 'vite' + +export default defineConfig({ + lint: { + rules: { + 'no-console': 'warn', + }, + }, + plugins: [], +})" + ); + } +} diff --git a/packages/cli/binding/index.d.ts b/packages/cli/binding/index.d.ts index 2ea2cc4c65..f0c9c8cb07 100644 --- a/packages/cli/binding/index.d.ts +++ b/packages/cli/binding/index.d.ts @@ -127,6 +127,50 @@ export interface JsCommandResolvedResult { envs: Record; } +/** + * Merge JSON configuration file into vite config file + * + * This function reads the files from disk and merges the JSON config + * into the vite configuration file. + * + * # Arguments + * + * * `vite_config_path` - Path to the vite.config.ts or vite.config.js file + * * `json_config_path` - Path to the JSON config file (e.g., .oxlintrc, .oxfmtrc) + * * `config_key` - The key to use in the vite config (e.g., "lint", "format") + * + * # Returns + * + * Returns a `MergeJsonConfigResult` containing: + * - `content`: The updated vite config content + * - `updated`: Whether any changes were made + * - `usesFunctionCallback`: Whether the config uses a function callback + * + * # Example + * + * ```javascript + * const result = mergeJsonConfig('vite.config.ts', '.oxlintrc', 'lint'); + * if (result.updated) { + * fs.writeFileSync('vite.config.ts', result.content); + * } + * ``` + */ +export declare function mergeJsonConfig( + viteConfigPath: string, + jsonConfigPath: string, + configKey: string, +): MergeJsonConfigResult; + +/** Result of merging JSON config into vite config */ +export interface MergeJsonConfigResult { + /** The updated vite config content */ + content: string; + /** Whether any changes were made */ + updated: boolean; + /** Whether the config uses a function callback */ + usesFunctionCallback: boolean; +} + /** Access modes for a path. */ export interface PathAccess { /** Whether the path was read */ diff --git a/packages/cli/binding/index.js b/packages/cli/binding/index.js index 57a6c23bbc..b8c66fb09f 100644 --- a/packages/cli/binding/index.js +++ b/packages/cli/binding/index.js @@ -744,9 +744,17 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`); } -const { detectWorkspace, downloadPackageManager, rewriteScripts, run, runCommand } = nativeBinding; +const { + detectWorkspace, + downloadPackageManager, + mergeJsonConfig, + rewriteScripts, + run, + runCommand, +} = nativeBinding; export { detectWorkspace }; export { downloadPackageManager }; +export { mergeJsonConfig }; export { rewriteScripts }; export { run }; export { runCommand }; diff --git a/packages/cli/binding/src/lib.rs b/packages/cli/binding/src/lib.rs index 9d7c100e20..7437ee11a6 100644 --- a/packages/cli/binding/src/lib.rs +++ b/packages/cli/binding/src/lib.rs @@ -31,7 +31,7 @@ use vite_task::ResolveCommandResult; use crate::cli::{Args, CliOptions as ViteTaskCliOptions, Commands}; pub use crate::{ - migration::rewrite_scripts, + migration::{merge_json_config, rewrite_scripts}, package_manager::{detect_workspace, download_package_manager}, }; diff --git a/packages/cli/binding/src/migration.rs b/packages/cli/binding/src/migration.rs index 76792e5499..00e2275cb0 100644 --- a/packages/cli/binding/src/migration.rs +++ b/packages/cli/binding/src/migration.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use napi::{anyhow, bindgen_prelude::*}; use napi_derive::napi; @@ -24,3 +26,60 @@ pub fn rewrite_scripts(scripts_json: String, rules_yaml: String) -> Result Result { + let result = vite_migration::merge_json_config( + Path::new(&vite_config_path), + Path::new(&json_config_path), + &config_key, + ) + .map_err(anyhow::Error::from)?; + + Ok(MergeJsonConfigResult { + content: result.content, + updated: result.updated, + uses_function_callback: result.uses_function_callback, + }) +} diff --git a/packages/global/snap-tests/migration-auto-create-vite-config/.oxfmtrc.json b/packages/global/snap-tests/migration-auto-create-vite-config/.oxfmtrc.json new file mode 100644 index 0000000000..a91f91670b --- /dev/null +++ b/packages/global/snap-tests/migration-auto-create-vite-config/.oxfmtrc.json @@ -0,0 +1,7 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "trailingComma": "es5" +} diff --git a/packages/global/snap-tests/migration-auto-create-vite-config/.oxlintrc.json b/packages/global/snap-tests/migration-auto-create-vite-config/.oxlintrc.json new file mode 100644 index 0000000000..0eb0592cdb --- /dev/null +++ b/packages/global/snap-tests/migration-auto-create-vite-config/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-unused-vars": "error" + } +} diff --git a/packages/global/snap-tests/migration-auto-create-vite-config/package.json b/packages/global/snap-tests/migration-auto-create-vite-config/package.json new file mode 100644 index 0000000000..f8265e66bd --- /dev/null +++ b/packages/global/snap-tests/migration-auto-create-vite-config/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "oxlint": "1", + "oxfmt": "1" + } +} diff --git a/packages/global/snap-tests/migration-auto-create-vite-config/snap.txt b/packages/global/snap-tests/migration-auto-create-vite-config/snap.txt new file mode 100644 index 0000000000..e594499f58 --- /dev/null +++ b/packages/global/snap-tests/migration-auto-create-vite-config/snap.txt @@ -0,0 +1,35 @@ +> vp migration # migration should auto create vite.config.ts and remove oxlintrc and oxfmtrc +┌ Vite+ Migration +│ +● Using default package manager: pnpm +│ +◆ ✅ Merged .oxlintrc.json into vite.config.ts +│ +◆ ✅ Merged .oxfmtrc.json into vite.config.ts +│ +└ ✨ Migration completed! + + +> cat vite.config.ts # check vite.config.ts +import { defineConfig } from '@voidzero-dev/vite-plus'; + +export default defineConfig({ + format: { + printWidth: 100, + tabWidth: 2, + semi: true, + singleQuote: true, + trailingComma: 'es5', + }, + lint: { + rules: { + 'no-unused-vars': 'error', + }, + }, +}); + +> cat .oxlintrc.json && exit 1 || true # check .oxlintrc.json is removed +cat: .oxlintrc.json: No such file or directory + +> cat .oxfmtrc.json && exit 1 || true # check .oxfmtrc.json is removed +cat: .oxfmtrc.json: No such file or directory diff --git a/packages/global/snap-tests/migration-auto-create-vite-config/steps.json b/packages/global/snap-tests/migration-auto-create-vite-config/steps.json new file mode 100644 index 0000000000..72005c8960 --- /dev/null +++ b/packages/global/snap-tests/migration-auto-create-vite-config/steps.json @@ -0,0 +1,8 @@ +{ + "commands": [ + "vp migration # migration should auto create vite.config.ts and remove oxlintrc and oxfmtrc", + "cat vite.config.ts # check vite.config.ts", + "cat .oxlintrc.json && exit 1 || true # check .oxlintrc.json is removed", + "cat .oxfmtrc.json && exit 1 || true # check .oxfmtrc.json is removed" + ] +} diff --git a/packages/global/src/migration/migrator.ts b/packages/global/src/migration/migrator.ts index 39430dbf21..dc2a2be412 100644 --- a/packages/global/src/migration/migrator.ts +++ b/packages/global/src/migration/migrator.ts @@ -1,7 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; -import { rewriteScripts, type DownloadPackageManagerResult } from '@voidzero-dev/vite-plus/binding'; +import * as prompts from '@clack/prompts'; +import { mergeJsonConfig, rewriteScripts, type DownloadPackageManagerResult } from '@voidzero-dev/vite-plus/binding'; import { Scalar, YAMLMap, YAMLSeq } from 'yaml'; import { PackageManager, type WorkspaceInfo } from '../types/index.ts'; @@ -12,6 +13,7 @@ import { rulesDir, type YamlDocument, } from '../utils/index.ts'; +import { detectConfigs } from './detector.ts'; const VITE_PLUS_NAME = '@voidzero-dev/vite-plus'; const VITE_PLUS_VERSION = 'latest'; @@ -88,6 +90,7 @@ export function rewriteStandaloneProject(projectPath: string, workspaceInfo: Wor // set .npmrc to use vite-plus rewriteNpmrc(projectPath); rewriteLintStagedConfigFile(projectPath); + rewriteViteConfigFile(projectPath); // set package manager setPackageManager(projectPath, workspaceInfo.downloadPackageManager); } @@ -116,6 +119,7 @@ export function rewriteMonorepo(workspaceInfo: WorkspaceInfo): void { // set .npmrc to use vite-plus rewriteNpmrc(workspaceInfo.rootDir); rewriteLintStagedConfigFile(workspaceInfo.rootDir); + rewriteViteConfigFile(workspaceInfo.rootDir); // set package manager setPackageManager(workspaceInfo.rootDir, workspaceInfo.downloadPackageManager); } @@ -402,6 +406,53 @@ function rewriteNpmrc(projectPath: string): void { } } +function rewriteViteConfigFile(projectPath: string): void { + const configs = detectConfigs(projectPath); + if (!configs.oxfmtConfig && !configs.oxlintConfig) { + return; + } + if (!configs.viteConfig) { + // create vite.config.ts + configs.viteConfig = path.join(projectPath, 'vite.config.ts'); + fs.writeFileSync( + configs.viteConfig, + `import { defineConfig } from '${VITE_PLUS_NAME}'; + +export default defineConfig({}); +`, + ); + } + if (configs.oxlintConfig) { + // merge oxlint config into vite.config.ts + mergeAndRemoveJsonConfig(projectPath, configs.viteConfig, configs.oxlintConfig, 'lint'); + } + if (configs.oxfmtConfig) { + // merge oxfmt config into vite.config.ts + mergeAndRemoveJsonConfig(projectPath, configs.viteConfig, configs.oxfmtConfig, 'format'); + } +} + +function mergeAndRemoveJsonConfig( + projectPath: string, + viteConfigPath: string, + jsonConfigPath: string, + configKey: string, +): void { + const result = mergeJsonConfig(viteConfigPath, jsonConfigPath, configKey); + const jsonConfigRelativePath = path.relative(projectPath, jsonConfigPath); + const viteConfigRelativePath = path.relative(projectPath, viteConfigPath); + if (result.updated) { + fs.writeFileSync(viteConfigPath, result.content); + fs.unlinkSync(jsonConfigPath); + prompts.log.success(`✅ Merged ${jsonConfigRelativePath} into ${viteConfigRelativePath}`); + } else { + prompts.log.warn(`❌ Failed to merge ${jsonConfigRelativePath} into ${viteConfigRelativePath}`); + prompts.log.info( + `Please complete the merge manually and follow the instructions in the documentation: https://viteplus.dev/config/`, + ); + } +} + function setPackageManager( projectDir: string, downloadPackageManager: DownloadPackageManagerResult, From f99239c8702e7a6fda58ae1d0c372561cde8671e Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 27 Nov 2025 19:59:40 +0800 Subject: [PATCH 2/6] FIXUP --- Cargo.lock | 1 + crates/vite_migration/Cargo.toml | 3 + .../vite_migration/rules/oxlint-config.yaml | 115 ------- .../vite_migration/rules/oxlint-simple.yaml | 72 ----- crates/vite_migration/src/ast_grep.rs | 82 +++++ crates/vite_migration/src/lib.rs | 3 +- crates/vite_migration/src/package.rs | 58 +--- crates/vite_migration/src/vite_config.rs | 295 ++++++++++++++---- package.json | 2 +- packages/cli/binding/index.d.ts | 32 ++ packages/cli/binding/index.js | 2 + packages/cli/binding/src/lib.rs | 2 +- packages/cli/binding/src/migration.rs | 36 +++ .../snap.txt | 22 +- .../steps.json | 6 +- .../migration-lintstagedrc/steps.json | 3 + .../.oxlintrc.json | 5 + .../package.json | 12 + .../migration-merge-vite-config-js/snap.txt | 49 +++ .../migration-merge-vite-config-js/steps.json | 11 + .../vite.config.js | 5 + .../.oxfmtrc.json | 7 + .../.oxlintrc.json | 5 + .../package.json | 25 ++ .../migration-merge-vite-config-ts/snap.txt | 99 ++++++ .../migration-merge-vite-config-ts/steps.json | 13 + .../vite.config.ts | 6 + .../vitest.config.ts | 19 ++ .../migration-monorepo-pnpm/.oxfmtrc.json | 7 + .../migration-monorepo-pnpm/.oxlintrc.json | 5 + .../migration-monorepo-pnpm/package.json | 25 ++ .../packages/app/package.json | 21 ++ .../packages/utils/package.json | 15 + .../pnpm-workspace.yaml | 13 + .../migration-monorepo-pnpm/snap.txt | 140 +++++++++ .../migration-monorepo-pnpm/steps.json | 15 + .../migration-monorepo-pnpm/vite.config.ts | 6 + .../migration-monorepo-yarn4/.oxlintrc.json | 5 + .../migration-monorepo-yarn4/package.json | 28 ++ .../packages/app/package.json | 21 ++ .../packages/utils/package.json | 15 + .../migration-monorepo-yarn4/snap.txt | 117 +++++++ .../migration-monorepo-yarn4/steps.json | 15 + .../migration-monorepo-yarn4/vite.config.ts | 6 + .../package.json | 6 + .../migration-not-supported-npm8.2/snap.txt | 18 ++ .../migration-not-supported-npm8.2/steps.json | 9 + .../package.json | 6 + .../migration-not-supported-pnpm9.4/snap.txt | 18 ++ .../steps.json | 9 + .../package.json | 5 + .../migration-not-supported-vite6/snap.txt | 21 ++ .../migration-not-supported-vite6/steps.json | 13 + .../package.json | 5 + .../migration-not-supported-vitest3/snap.txt | 21 ++ .../steps.json | 13 + packages/global/src/migration/bin.ts | 50 ++- packages/global/src/migration/detector.ts | 45 ++- packages/global/src/migration/migrator.ts | 121 +++++-- rfcs/migration-command.md | 76 ++++- 60 files changed, 1539 insertions(+), 341 deletions(-) delete mode 100644 crates/vite_migration/rules/oxlint-config.yaml delete mode 100644 crates/vite_migration/rules/oxlint-simple.yaml create mode 100644 crates/vite_migration/src/ast_grep.rs create mode 100644 packages/global/snap-tests/migration-merge-vite-config-js/.oxlintrc.json create mode 100644 packages/global/snap-tests/migration-merge-vite-config-js/package.json create mode 100644 packages/global/snap-tests/migration-merge-vite-config-js/snap.txt create mode 100644 packages/global/snap-tests/migration-merge-vite-config-js/steps.json create mode 100644 packages/global/snap-tests/migration-merge-vite-config-js/vite.config.js create mode 100644 packages/global/snap-tests/migration-merge-vite-config-ts/.oxfmtrc.json create mode 100644 packages/global/snap-tests/migration-merge-vite-config-ts/.oxlintrc.json create mode 100644 packages/global/snap-tests/migration-merge-vite-config-ts/package.json create mode 100644 packages/global/snap-tests/migration-merge-vite-config-ts/snap.txt create mode 100644 packages/global/snap-tests/migration-merge-vite-config-ts/steps.json create mode 100644 packages/global/snap-tests/migration-merge-vite-config-ts/vite.config.ts create mode 100644 packages/global/snap-tests/migration-merge-vite-config-ts/vitest.config.ts create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm/.oxfmtrc.json create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm/.oxlintrc.json create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm/package.json create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm/packages/app/package.json create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm/packages/utils/package.json create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm/pnpm-workspace.yaml create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm/snap.txt create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm/steps.json create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm/vite.config.ts create mode 100644 packages/global/snap-tests/migration-monorepo-yarn4/.oxlintrc.json create mode 100644 packages/global/snap-tests/migration-monorepo-yarn4/package.json create mode 100644 packages/global/snap-tests/migration-monorepo-yarn4/packages/app/package.json create mode 100644 packages/global/snap-tests/migration-monorepo-yarn4/packages/utils/package.json create mode 100644 packages/global/snap-tests/migration-monorepo-yarn4/snap.txt create mode 100644 packages/global/snap-tests/migration-monorepo-yarn4/steps.json create mode 100644 packages/global/snap-tests/migration-monorepo-yarn4/vite.config.ts create mode 100644 packages/global/snap-tests/migration-not-supported-npm8.2/package.json create mode 100644 packages/global/snap-tests/migration-not-supported-npm8.2/snap.txt create mode 100644 packages/global/snap-tests/migration-not-supported-npm8.2/steps.json create mode 100644 packages/global/snap-tests/migration-not-supported-pnpm9.4/package.json create mode 100644 packages/global/snap-tests/migration-not-supported-pnpm9.4/snap.txt create mode 100644 packages/global/snap-tests/migration-not-supported-pnpm9.4/steps.json create mode 100644 packages/global/snap-tests/migration-not-supported-vite6/package.json create mode 100644 packages/global/snap-tests/migration-not-supported-vite6/snap.txt create mode 100644 packages/global/snap-tests/migration-not-supported-vite6/steps.json create mode 100644 packages/global/snap-tests/migration-not-supported-vitest3/package.json create mode 100644 packages/global/snap-tests/migration-not-supported-vitest3/snap.txt create mode 100644 packages/global/snap-tests/migration-not-supported-vitest3/steps.json diff --git a/Cargo.lock b/Cargo.lock index 2a84d5ee8e..8ce9fafe70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4402,6 +4402,7 @@ dependencies = [ "ast-grep-core", "ast-grep-language", "serde_json", + "tempfile", "vite_error", ] diff --git a/crates/vite_migration/Cargo.toml b/crates/vite_migration/Cargo.toml index 32fbd9ca35..be5e524c96 100644 --- a/crates/vite_migration/Cargo.toml +++ b/crates/vite_migration/Cargo.toml @@ -13,5 +13,8 @@ ast-grep-language = { workspace = true } serde_json = { workspace = true, features = ["preserve_order"] } vite_error = { workspace = true } +[dev-dependencies] +tempfile = { workspace = true } + [lints] workspace = true diff --git a/crates/vite_migration/rules/oxlint-config.yaml b/crates/vite_migration/rules/oxlint-config.yaml deleted file mode 100644 index 30dc784a27..0000000000 --- a/crates/vite_migration/rules/oxlint-config.yaml +++ /dev/null @@ -1,115 +0,0 @@ -# ast-grep rules for migrating .oxlintrc configuration to vite.config.ts -# -# These rules merge oxlint configuration from .oxlintrc into the defineConfig -# call in vite.config.ts, following the vite-plus unified configuration approach. -# -# Usage: -# The migration script should: -# 1. Read .oxlintrc JSON content -# 2. Convert it to TypeScript object literal format -# 3. Replace __OXLINT_CONFIG__ placeholder with the actual config -# 4. Run: sg scan -r oxlint-config.yaml vite.config.ts -U -# -# Example workflow: -# -# # Step 1: Create rule file with actual oxlint config -# cat > /tmp/migrate-oxlint.yaml << 'EOF' -# --- -# id: merge-oxlint-config -# language: TypeScript -# rule: -# pattern: | -# defineConfig({ -# $$$CONFIG -# }) -# fix: |- -# defineConfig({ -# $$$CONFIG -# lint: { -# rules: { -# 'no-unused-vars': 'error', -# 'no-console': 'warn', -# }, -# ignorePatterns: ['dist', 'node_modules'], -# }, -# }) -# EOF -# -# # Step 2: Run ast-grep -# sg scan -r /tmp/migrate-oxlint.yaml vite.config.ts -U -# -# # Step 3: Remove old config file -# rm .oxlintrc -# -# For programmatic usage, use the vite_migration Rust crate which provides -# proper JSON to TypeScript conversion. - -# Rule 1: Add lint config to defineConfig with existing properties -# Matches: -# defineConfig({ -# plugins: [...], -# server: {...}, -# }) -# Result: -# defineConfig({ -# plugins: [...], -# server: {...}, -# lint: {...}, -# }) ---- -id: add-lint-config-to-defineconfig -language: TypeScript -severity: info -message: 'Add lint configuration to vite.config.ts' -note: 'Migrating oxlint configuration from .oxlintrc to vite.config.ts' -rule: - pattern: | - defineConfig({ - $$$EXISTING_CONFIG - }) -fix: |- - defineConfig({ - $$$EXISTING_CONFIG - // lint configuration (merged from .oxlintrc) - lint: __OXLINT_CONFIG__, - }) -files: - - '**/vite.config.ts' - - '**/vite.config.mts' - - '**/vite.config.js' - - '**/vite.config.mjs' -ignores: - - '**/node_modules/**' - - '**/dist/**' - -# Rule 2: Detect defineConfig with function callback (warning only) -# This pattern requires manual migration -# Matches: defineConfig((env) => ({ ... })) ---- -id: detect-function-defineconfig -language: TypeScript -severity: warning -message: 'defineConfig uses a function callback - manual migration required' -note: | - The defineConfig uses a function callback pattern which cannot be - automatically migrated. Please manually add the lint configuration: - - export default defineConfig((env) => ({ - ...existingConfig, - lint: { - rules: { - // your rules from .oxlintrc - }, - }, - })); -rule: - # Using explicit arrow function pattern to match callback style - pattern: defineConfig(($PARAMS) => $BODY) -files: - - '**/vite.config.ts' - - '**/vite.config.mts' - - '**/vite.config.js' - - '**/vite.config.mjs' -ignores: - - '**/node_modules/**' - - '**/dist/**' diff --git a/crates/vite_migration/rules/oxlint-simple.yaml b/crates/vite_migration/rules/oxlint-simple.yaml deleted file mode 100644 index ad9c0764cf..0000000000 --- a/crates/vite_migration/rules/oxlint-simple.yaml +++ /dev/null @@ -1,72 +0,0 @@ -# Simple ast-grep rule for adding lint config to defineConfig -# -# This rule is designed to be used programmatically where the -# __OXLINT_CONFIG__ placeholder is replaced with actual content. -# -# Usage: -# 1. Read .oxlintrc content and convert to TypeScript -# 2. Replace __OXLINT_CONFIG__ in this file with the converted content -# 3. Run: sg scan -r oxlint-simple.yaml vite.config.ts -U -# -# Example: -# # Create a rule with actual config content -# cat > /tmp/oxlint-rule.yaml << 'EOF' -# --- -# id: merge-oxlint-to-vite-config -# language: TypeScript -# rule: -# pattern: | -# defineConfig({ -# $$$CONFIG -# }) -# fix: |- -# defineConfig({ -# $$$CONFIG -# lint: { -# rules: { -# 'no-console': 'warn', -# }, -# }, -# }) -# EOF -# sg scan -r /tmp/oxlint-rule.yaml vite.config.ts -U -# -# Note: This rule handles the common case where defineConfig receives -# an object literal directly. For function callbacks or complex patterns, -# manual migration may be needed. - ---- -id: merge-oxlint-to-vite-config -language: TypeScript -severity: info -message: 'Merge .oxlintrc configuration into vite.config.ts' -note: | - This rule adds the lint configuration from .oxlintrc to the - defineConfig call in vite.config.ts. - - Before: defineConfig({ plugins: [...] }) - After: defineConfig({ plugins: [...], lint: {...} }) - -rule: - # Match defineConfig call with object argument - # Using multiline pattern to correctly capture the content - pattern: | - defineConfig({ - $$$CONFIG - }) - -fix: |- - defineConfig({ - $$$CONFIG - // lint configuration (migrated from .oxlintrc) - lint: __OXLINT_CONFIG__, - }) - -files: - - '**/vite.config.ts' - - '**/vite.config.mts' - - '**/vite.config.js' - - '**/vite.config.mjs' -ignores: - - '**/node_modules/**' - - '**/dist/**' diff --git a/crates/vite_migration/src/ast_grep.rs b/crates/vite_migration/src/ast_grep.rs new file mode 100644 index 0000000000..748a22d17b --- /dev/null +++ b/crates/vite_migration/src/ast_grep.rs @@ -0,0 +1,82 @@ +use ast_grep_config::{GlobalRules, RuleConfig, from_yaml_string}; +use ast_grep_core::replacer::Replacer; +use ast_grep_language::{LanguageExt, SupportLang}; +use vite_error::Error; + +/// Apply ast-grep rules to content and return the transformed content +/// +/// This is the core transformation function that: +/// 1. Parses the rule YAML +/// 2. Applies each rule to find matches +/// 3. Replaces matches from back to front to maintain correct positions +/// +/// # Arguments +/// +/// * `content` - The source content to transform +/// * `rule_yaml` - The ast-grep rules in YAML format +/// +/// # Returns +/// +/// A tuple of (transformed_content, was_updated) +pub(crate) fn apply_rules(content: &str, rule_yaml: &str) -> Result<(String, bool), Error> { + let rules = load_rules(rule_yaml)?; + let result = apply_loaded_rules(content, &rules); + let updated = result != content; + Ok((result, updated)) +} + +/// Load ast-grep rules from YAML string +pub(crate) fn load_rules(yaml: &str) -> Result>, Error> { + let globals = GlobalRules::default(); + let rules: Vec> = from_yaml_string::(yaml, &globals)?; + Ok(rules) +} + +/// Apply pre-loaded ast-grep rules to content +/// +/// This is useful when you need to apply the same rules multiple times +/// (e.g., processing multiple scripts in a loop). +/// +/// # Arguments +/// +/// * `content` - The source content to transform +/// * `rules` - Pre-loaded ast-grep rules +/// +/// # Returns +/// +/// The transformed content (always returns a new string, even if unchanged) +pub(crate) fn apply_loaded_rules(content: &str, rules: &[RuleConfig]) -> String { + let mut current = content.to_string(); + + for rule in rules { + // Parse current content with the rule's language + let grep = rule.language.ast_grep(¤t); + let root = grep.root(); + + let matcher = &rule.matcher; + + // Get the fixer if available (rules without fix are pure lint, skip them) + 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 to maintain correct positions + replacements.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start)); + + for (start, end, replacement) in replacements { + current.replace_range(start..end, &replacement); + } + } + + current +} diff --git a/crates/vite_migration/src/lib.rs b/crates/vite_migration/src/lib.rs index 38260d1724..499c820e1c 100644 --- a/crates/vite_migration/src/lib.rs +++ b/crates/vite_migration/src/lib.rs @@ -1,5 +1,6 @@ +mod ast_grep; mod package; mod vite_config; pub use package::rewrite_scripts; -pub use vite_config::{MergeResult, merge_json_config}; +pub use vite_config::{MergeResult, RewriteResult, merge_json_config, rewrite_import}; diff --git a/crates/vite_migration/src/package.rs b/crates/vite_migration/src/package.rs index 0240d3d8e9..d71a5472cc 100644 --- a/crates/vite_migration/src/package.rs +++ b/crates/vite_migration/src/package.rs @@ -1,15 +1,9 @@ -use ast_grep_config::{GlobalRules, RuleConfig, from_yaml_string}; -use ast_grep_core::replacer::Replacer; -use ast_grep_language::{LanguageExt, SupportLang}; +use ast_grep_config::RuleConfig; +use ast_grep_language::SupportLang; use serde_json::{Map, Value}; use vite_error::Error; -/// load script rules from yaml file -fn load_ast_grep_rules(yaml: &str) -> Result>, Error> { - let globals = GlobalRules::default(); - let rules: Vec> = from_yaml_string::(&yaml, &globals)?; - Ok(rules) -} +use crate::ast_grep; // Marker to replace "cross-env " before ast-grep processing // Using a fake env var assignment that won't match our rules @@ -22,57 +16,23 @@ fn rewrite_script(script: &str, rules: &[RuleConfig]) -> String { let has_cross_env = script.contains(CROSS_ENV_REPLACEMENT); // Step 1: Replace "cross-env " with marker so ast-grep can see the actual commands - let mut current = if has_cross_env { + let preprocessed = if has_cross_env { script.replace(CROSS_ENV_REPLACEMENT, CROSS_ENV_MARKER) } else { script.to_string() }; // Step 2: Process with ast-grep - 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); - } - } + let result = ast_grep::apply_loaded_rules(&preprocessed, rules); // Step 3: Replace marker back with "cross-env " (only if we replaced it) - if has_cross_env { current.replace(CROSS_ENV_MARKER, CROSS_ENV_REPLACEMENT) } else { current } + if has_cross_env { result.replace(CROSS_ENV_MARKER, CROSS_ENV_REPLACEMENT) } else { result } } /// rewrite scripts json content using rules from rules_yaml pub fn rewrite_scripts(scripts_json: &str, rules_yaml: &str) -> Result, Error> { let mut scripts: Map = serde_json::from_str(scripts_json)?; - let rules = load_ast_grep_rules(rules_yaml)?; + let rules = ast_grep::load_rules(rules_yaml)?; let mut updated = false; // get scripts field (object) @@ -156,9 +116,7 @@ fix: vite test #[test] fn test_rewrite_script() { - let globals = GlobalRules::default(); - let rules: Vec> = - from_yaml_string::(&RULES_YAML, &globals).unwrap(); + let rules = ast_grep::load_rules(RULES_YAML).unwrap(); // vite commands assert_eq!(rewrite_script("vite", &rules), "vite dev"); assert_eq!(rewrite_script("vite dev", &rules), "vite dev"); diff --git a/crates/vite_migration/src/vite_config.rs b/crates/vite_migration/src/vite_config.rs index 37ffa7a4b0..c1ce6b65c7 100644 --- a/crates/vite_migration/src/vite_config.rs +++ b/crates/vite_migration/src/vite_config.rs @@ -1,11 +1,51 @@ 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 vite_error::Error; +use crate::ast_grep; + +/// ast-grep rules for rewriting imports to @voidzero-dev/vite-plus +/// +/// This rewrites: +/// - `import { ... } from 'vite'` → `import { ... } from '@voidzero-dev/vite-plus'` +/// - `import { ... } from 'vitest/config'` → `import { ... } from '@voidzero-dev/vite-plus'` +const REWRITE_IMPORT_RULES: &str = r#"--- +id: rewrite-vitest-config-import +language: TypeScript +rule: + pattern: "'vitest/config'" + inside: + kind: import_statement +fix: "'@voidzero-dev/vite-plus'" +--- +id: rewrite-vitest-config-import-double-quotes +language: TypeScript +rule: + pattern: '"vitest/config"' + inside: + kind: import_statement +fix: '"@voidzero-dev/vite-plus"' +--- +id: rewrite-vite-import +language: TypeScript +rule: + pattern: "'vite'" + inside: + kind: import_statement +fix: "'@voidzero-dev/vite-plus'" +--- +id: rewrite-vite-import-double-quotes +language: TypeScript +rule: + pattern: '"vite"' + inside: + kind: import_statement +fix: '"@voidzero-dev/vite-plus"' +"#; + /// Result of merging JSON config into vite config #[derive(Debug)] pub struct MergeResult { @@ -17,6 +57,15 @@ pub struct MergeResult { pub uses_function_callback: bool, } +/// Result of rewriting imports in vite config +#[derive(Debug)] +pub struct RewriteResult { + /// The updated vite config content + pub content: String, + /// Whether any changes were made + pub updated: bool, +} + /// Merge a JSON configuration file into vite.config.ts or vite.config.js /// /// This function reads a JSON configuration file and merges it into the vite @@ -81,6 +130,48 @@ pub fn merge_json_config( merge_json_config_content(&vite_config_content, &ts_config, config_key) } +/// Rewrite imports in vite config file from 'vite' or 'vitest/config' to '@voidzero-dev/vite-plus' +/// +/// This function reads a vite configuration file and rewrites the import statements +/// to use '@voidzero-dev/vite-plus' instead of 'vite' or 'vitest/config'. +/// +/// # Arguments +/// +/// * `vite_config_path` - Path to the vite.config.ts or vite.config.js file +/// +/// # Returns +/// +/// Returns a `RewriteResult` containing: +/// - `content`: The updated vite config content +/// - `updated`: Whether any changes were made +/// +/// # Example +/// +/// ```ignore +/// use std::path::Path; +/// use vite_migration::rewrite_import; +/// +/// let result = rewrite_import(Path::new("vite.config.ts"))?; +/// if result.updated { +/// std::fs::write("vite.config.ts", &result.content)?; +/// } +/// ``` +pub fn rewrite_import(vite_config_path: &Path) -> Result { + // Read the vite config file + let vite_config_content = std::fs::read_to_string(vite_config_path)?; + + // Rewrite the imports + rewrite_import_content(&vite_config_content) +} + +/// Rewrite imports in vite config content from 'vite' or 'vitest/config' to '@voidzero-dev/vite-plus' +/// +/// This is the internal function that performs the actual rewrite using ast-grep. +fn rewrite_import_content(vite_config_content: &str) -> Result { + let (content, updated) = ast_grep::apply_rules(vite_config_content, REWRITE_IMPORT_RULES)?; + Ok(RewriteResult { content, updated }) +} + /// Merge JSON configuration into vite config content /// /// This is the internal function that performs the actual merge using ast-grep. @@ -106,52 +197,10 @@ fn merge_json_config_content( // Generate the ast-grep rules with the actual config let rule_yaml = generate_merge_rule(ts_config, config_key); - // Load and apply the rules - let globals = GlobalRules::default(); - let rules: Vec> = - from_yaml_string::(&rule_yaml, &globals)?; - // Apply the transformation - let mut current = vite_config_content.to_string(); - let mut updated = false; - - for rule in &rules { - // Only handle TypeScript rules - if rule.language != SupportLang::TypeScript { - continue; - } - - // Parse current config with TypeScript language - let grep = rule.language.ast_grep(¤t); - let root = grep.root(); - - let matcher = &rule.matcher; - - // Get the fixer if available - 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 to maintain correct positions - replacements.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start)); - - for (start, end, replacement) in replacements { - current.replace_range(start..end, &replacement); - updated = true; - } - } + let (content, updated) = ast_grep::apply_rules(vite_config_content, &rule_yaml)?; - Ok(MergeResult { content: current, updated, uses_function_callback }) + Ok(MergeResult { content, updated, uses_function_callback }) } /// Check if the vite config uses a function callback pattern @@ -443,6 +492,10 @@ fn escape_single_quotes(s: &str) -> String { #[cfg(test)] mod tests { + use std::io::Write; + + use tempfile::tempdir; + use super::*; #[test] @@ -912,18 +965,15 @@ export default defineConfig({ #[test] fn test_merge_json_config_with_files() { - use std::io::Write; - - // Create temporary files - let temp_dir = std::env::temp_dir().join("vite_migration_test"); - std::fs::create_dir_all(&temp_dir).unwrap(); + // Create temporary directory (automatically cleaned up when dropped) + let temp_dir = tempdir().unwrap(); - let vite_config_path = temp_dir.join("vite.config.ts"); - let oxlint_config_path = temp_dir.join(".oxlintrc"); + let vite_config_path = temp_dir.path().join("vite.config.ts"); + let oxlint_config_path = temp_dir.path().join(".oxlintrc"); // Write test vite config let mut vite_file = std::fs::File::create(&vite_config_path).unwrap(); - writeln!( + write!( vite_file, r#"import {{ defineConfig }} from 'vite'; @@ -935,7 +985,7 @@ export default defineConfig({{ // Write test oxlint config let mut oxlint_file = std::fs::File::create(&oxlint_config_path).unwrap(); - writeln!( + write!( oxlint_file, r#"{{ "rules": {{ @@ -964,11 +1014,8 @@ export default defineConfig({ ignorePatterns: ['dist', 'node_modules'], }, plugins: [], -}); -"# +});"# ); - // Clean up - std::fs::remove_dir_all(&temp_dir).ok(); } #[test] @@ -1087,4 +1134,136 @@ export default defineConfig({ })" ); } + + #[test] + fn test_rewrite_import_content_vite() { + let vite_config = r#"import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [], +});"#; + + let result = rewrite_import_content(vite_config).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"import { defineConfig } from '@voidzero-dev/vite-plus' + +export default defineConfig({ + plugins: [], +});"# + ); + } + + #[test] + fn test_rewrite_import_content_vite_double_quotes() { + let vite_config = r#"import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [], +});"#; + + let result = rewrite_import_content(vite_config).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"import { defineConfig } from "@voidzero-dev/vite-plus"; + +export default defineConfig({ + plugins: [], +});"# + ); + } + + #[test] + fn test_rewrite_import_content_vitest_config() { + let vite_config = r#"import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + }, +});"#; + + let result = rewrite_import_content(vite_config).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"import { defineConfig } from '@voidzero-dev/vite-plus'; + +export default defineConfig({ + test: { + globals: true, + }, +});"# + ); + } + + #[test] + fn test_rewrite_import_content_multiple_imports() { + let vite_config = r#"import { defineConfig, loadEnv, type UserWorkspaceConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], +});"#; + + let result = rewrite_import_content(vite_config).unwrap(); + assert!(result.updated); + assert_eq!( + result.content, + r#"import { defineConfig, loadEnv, type UserWorkspaceConfig } from '@voidzero-dev/vite-plus'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], +});"# + ); + } + + #[test] + fn test_rewrite_import_content_already_vite_plus() { + let vite_config = r#"import { defineConfig } from '@voidzero-dev/vite-plus'; + +export default defineConfig({ + plugins: [], +});"#; + + let result = rewrite_import_content(vite_config).unwrap(); + assert!(!result.updated); + assert_eq!(result.content, vite_config); + } + + #[test] + fn test_rewrite_import_with_file() { + // Create temporary directory (automatically cleaned up when dropped) + let temp_dir = tempdir().unwrap(); + + let vite_config_path = temp_dir.path().join("vite.config.ts"); + + // Write test vite config + let mut vite_file = std::fs::File::create(&vite_config_path).unwrap(); + write!( + vite_file, + r#"import {{ defineConfig }} from 'vite'; + +export default defineConfig({{ + plugins: [], +}});"# + ) + .unwrap(); + + // Run the rewrite + let result = rewrite_import(&vite_config_path).unwrap(); + + assert!(result.updated); + assert_eq!( + result.content, + r#"import { defineConfig } from '@voidzero-dev/vite-plus'; + +export default defineConfig({ + plugins: [], +});"# + ); + } } diff --git a/package.json b/package.json index 817ab7d269..ec6a09c1bf 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vite-plus-monorepo", "license": "BUSL-1.1", "private": true, - "packageManager": "pnpm@10.23.0", + "packageManager": "pnpm@10.24.0", "engines": { "node": "^20.19.0 || >=22.12.0" }, diff --git a/packages/cli/binding/index.d.ts b/packages/cli/binding/index.d.ts index f0c9c8cb07..63c5e6327f 100644 --- a/packages/cli/binding/index.d.ts +++ b/packages/cli/binding/index.d.ts @@ -181,6 +181,38 @@ export interface PathAccess { readDir: boolean; } +/** + * Rewrite imports in vite config from 'vite' or 'vitest/config' to '@voidzero-dev/vite-plus' + * + * # Arguments + * + * * `vite_config_path` - Path to the vite.config.ts or vite.config.js file + * + * # Returns + * + * Returns a `RewriteResult` containing: + * - `content`: The updated vite config content + * - `updated`: Whether any changes were made + * + * # Example + * + * ```javascript + * const result = rewriteImport('vite.config.ts'); + * if (result.updated) { + * fs.writeFileSync('vite.config.ts', result.content); + * } + * ``` + */ +export declare function rewriteImport(viteConfigPath: string): RewriteResult; + +/** Result of rewriting imports in vite config */ +export interface RewriteResult { + /** The updated vite config content */ + content: string; + /** Whether any changes were made */ + updated: boolean; +} + /** * Rewrite scripts json content using rules from rules_yaml * diff --git a/packages/cli/binding/index.js b/packages/cli/binding/index.js index b8c66fb09f..0d93fe7980 100644 --- a/packages/cli/binding/index.js +++ b/packages/cli/binding/index.js @@ -748,6 +748,7 @@ const { detectWorkspace, downloadPackageManager, mergeJsonConfig, + rewriteImport, rewriteScripts, run, runCommand, @@ -755,6 +756,7 @@ const { export { detectWorkspace }; export { downloadPackageManager }; export { mergeJsonConfig }; +export { rewriteImport }; export { rewriteScripts }; export { run }; export { runCommand }; diff --git a/packages/cli/binding/src/lib.rs b/packages/cli/binding/src/lib.rs index 7437ee11a6..b614451fc3 100644 --- a/packages/cli/binding/src/lib.rs +++ b/packages/cli/binding/src/lib.rs @@ -31,7 +31,7 @@ use vite_task::ResolveCommandResult; use crate::cli::{Args, CliOptions as ViteTaskCliOptions, Commands}; pub use crate::{ - migration::{merge_json_config, rewrite_scripts}, + migration::{merge_json_config, rewrite_import, rewrite_scripts}, package_manager::{detect_workspace, download_package_manager}, }; diff --git a/packages/cli/binding/src/migration.rs b/packages/cli/binding/src/migration.rs index 00e2275cb0..ac95c1f352 100644 --- a/packages/cli/binding/src/migration.rs +++ b/packages/cli/binding/src/migration.rs @@ -83,3 +83,39 @@ pub fn merge_json_config( uses_function_callback: result.uses_function_callback, }) } + +/// Result of rewriting imports in vite config +#[napi(object)] +pub struct RewriteResult { + /// The updated vite config content + pub content: String, + /// Whether any changes were made + pub updated: bool, +} + +/// Rewrite imports in vite config from 'vite' or 'vitest/config' to '@voidzero-dev/vite-plus' +/// +/// # Arguments +/// +/// * `vite_config_path` - Path to the vite.config.ts or vite.config.js file +/// +/// # Returns +/// +/// Returns a `RewriteResult` containing: +/// - `content`: The updated vite config content +/// - `updated`: Whether any changes were made +/// +/// # Example +/// +/// ```javascript +/// const result = rewriteImport('vite.config.ts'); +/// if (result.updated) { +/// fs.writeFileSync('vite.config.ts', result.content); +/// } +/// ``` +#[napi] +pub fn rewrite_import(vite_config_path: String) -> Result { + let result = vite_migration::rewrite_import(Path::new(&vite_config_path)) + .map_err(anyhow::Error::from)?; + Ok(RewriteResult { content: result.content, updated: result.updated }) +} diff --git a/packages/global/snap-tests/migration-auto-create-vite-config/snap.txt b/packages/global/snap-tests/migration-auto-create-vite-config/snap.txt index e594499f58..be863dfd90 100644 --- a/packages/global/snap-tests/migration-auto-create-vite-config/snap.txt +++ b/packages/global/snap-tests/migration-auto-create-vite-config/snap.txt @@ -3,6 +3,12 @@ │ ● Using default package manager: pnpm │ +● pnpm@latest installing... +│ +● pnpm@ installed +│ +◆ ✅ Created vite.config.ts in vite.config.ts +│ ◆ ✅ Merged .oxlintrc.json into vite.config.ts │ ◆ ✅ Merged .oxfmtrc.json into vite.config.ts @@ -14,7 +20,7 @@ import { defineConfig } from '@voidzero-dev/vite-plus'; export default defineConfig({ - format: { + fmt: { printWidth: 100, tabWidth: 2, semi: true, @@ -33,3 +39,17 @@ cat: .oxlintrc.json: No such file or directory > cat .oxfmtrc.json && exit 1 || true # check .oxfmtrc.json is removed cat: .oxfmtrc.json: No such file or directory + +> cat package.json # check package.json +{ + "devDependencies": { + "@voidzero-dev/vite-plus": "latest" + }, + "pnpm": { + "overrides": { + "vite": "npm:@voidzero-dev/vite-plus-core@latest", + "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + } + }, + "packageManager": "pnpm@" +} diff --git a/packages/global/snap-tests/migration-auto-create-vite-config/steps.json b/packages/global/snap-tests/migration-auto-create-vite-config/steps.json index 72005c8960..a5f6122791 100644 --- a/packages/global/snap-tests/migration-auto-create-vite-config/steps.json +++ b/packages/global/snap-tests/migration-auto-create-vite-config/steps.json @@ -1,8 +1,12 @@ { + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, "commands": [ "vp migration # migration should auto create vite.config.ts and remove oxlintrc and oxfmtrc", "cat vite.config.ts # check vite.config.ts", "cat .oxlintrc.json && exit 1 || true # check .oxlintrc.json is removed", - "cat .oxfmtrc.json && exit 1 || true # check .oxfmtrc.json is removed" + "cat .oxfmtrc.json && exit 1 || true # check .oxfmtrc.json is removed", + "cat package.json # check package.json" ] } diff --git a/packages/global/snap-tests/migration-lintstagedrc/steps.json b/packages/global/snap-tests/migration-lintstagedrc/steps.json index 7c3592f75c..a01566d37a 100644 --- a/packages/global/snap-tests/migration-lintstagedrc/steps.json +++ b/packages/global/snap-tests/migration-lintstagedrc/steps.json @@ -1,4 +1,7 @@ { + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, "commands": [ "vp migration -h # migration help message", "vp migration # migration work with lintstagedrc.json", diff --git a/packages/global/snap-tests/migration-merge-vite-config-js/.oxlintrc.json b/packages/global/snap-tests/migration-merge-vite-config-js/.oxlintrc.json new file mode 100644 index 0000000000..0eb0592cdb --- /dev/null +++ b/packages/global/snap-tests/migration-merge-vite-config-js/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-unused-vars": "error" + } +} diff --git a/packages/global/snap-tests/migration-merge-vite-config-js/package.json b/packages/global/snap-tests/migration-merge-vite-config-js/package.json new file mode 100644 index 0000000000..9c5c2257c1 --- /dev/null +++ b/packages/global/snap-tests/migration-merge-vite-config-js/package.json @@ -0,0 +1,12 @@ +{ + "devDependencies": { + "vite": "^7.0.0", + "@vitejs/plugin-react": "^4.2.0", + "oxlint": "1" + }, + "scripts": { + "dev": "vite --port 3000", + "build": "vite build", + "lint": "oxlint" + } +} diff --git a/packages/global/snap-tests/migration-merge-vite-config-js/snap.txt b/packages/global/snap-tests/migration-merge-vite-config-js/snap.txt new file mode 100644 index 0000000000..46c50261ae --- /dev/null +++ b/packages/global/snap-tests/migration-merge-vite-config-js/snap.txt @@ -0,0 +1,49 @@ +> vp migration # migration should merge vite.config.js and remove oxlintrc +┌ Vite+ Migration +│ +● Using default package manager: pnpm +│ +● pnpm@latest installing... +│ +● pnpm@ installed +│ +◆ ✅ Merged .oxlintrc.json into vite.config.js +│ +└ ✨ Migration completed! + + +> cat vite.config.js # check vite.config.js +import react from '@vitejs/plugin-react'; + +export default { + lint: { + rules: { + 'no-unused-vars': 'error', + }, + }, + plugins: [react()], +} + +> cat .oxlintrc.json && exit 1 || true # check .oxlintrc.json is removed +cat: .oxlintrc.json: No such file or directory + +> cat package.json # check package.json +{ + "devDependencies": { + "vite": "npm:@voidzero-dev/vite-plus-core@latest", + "@vitejs/plugin-react": "^4.2.0", + "@voidzero-dev/vite-plus": "latest" + }, + "scripts": { + "dev": "vite dev --port 3000", + "build": "vite build", + "lint": "vite lint" + }, + "pnpm": { + "overrides": { + "vite": "npm:@voidzero-dev/vite-plus-core@latest", + "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + } + }, + "packageManager": "pnpm@" +} diff --git a/packages/global/snap-tests/migration-merge-vite-config-js/steps.json b/packages/global/snap-tests/migration-merge-vite-config-js/steps.json new file mode 100644 index 0000000000..82942b14fb --- /dev/null +++ b/packages/global/snap-tests/migration-merge-vite-config-js/steps.json @@ -0,0 +1,11 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp migration # migration should merge vite.config.js and remove oxlintrc", + "cat vite.config.js # check vite.config.js", + "cat .oxlintrc.json && exit 1 || true # check .oxlintrc.json is removed", + "cat package.json # check package.json" + ] +} diff --git a/packages/global/snap-tests/migration-merge-vite-config-js/vite.config.js b/packages/global/snap-tests/migration-merge-vite-config-js/vite.config.js new file mode 100644 index 0000000000..f06c1c7e30 --- /dev/null +++ b/packages/global/snap-tests/migration-merge-vite-config-js/vite.config.js @@ -0,0 +1,5 @@ +import react from '@vitejs/plugin-react'; + +export default { + plugins: [react()], +}; diff --git a/packages/global/snap-tests/migration-merge-vite-config-ts/.oxfmtrc.json b/packages/global/snap-tests/migration-merge-vite-config-ts/.oxfmtrc.json new file mode 100644 index 0000000000..a91f91670b --- /dev/null +++ b/packages/global/snap-tests/migration-merge-vite-config-ts/.oxfmtrc.json @@ -0,0 +1,7 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "trailingComma": "es5" +} diff --git a/packages/global/snap-tests/migration-merge-vite-config-ts/.oxlintrc.json b/packages/global/snap-tests/migration-merge-vite-config-ts/.oxlintrc.json new file mode 100644 index 0000000000..0eb0592cdb --- /dev/null +++ b/packages/global/snap-tests/migration-merge-vite-config-ts/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-unused-vars": "error" + } +} diff --git a/packages/global/snap-tests/migration-merge-vite-config-ts/package.json b/packages/global/snap-tests/migration-merge-vite-config-ts/package.json new file mode 100644 index 0000000000..c846c00462 --- /dev/null +++ b/packages/global/snap-tests/migration-merge-vite-config-ts/package.json @@ -0,0 +1,25 @@ +{ + "devDependencies": { + "vite": "^7.0.0", + "vitest": "^4.0.0", + "@vitejs/plugin-react": "^4.2.0", + "oxlint": "1", + "oxfmt": "1" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "test:run": "vitest run", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage", + "test:watch": "vitest --watch", + "test": "vitest", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "lint:type-aware": "oxlint --type-aware", + "fmt": "oxfmt", + "fmt:fix": "oxfmt --fix", + "fmt:staged": "oxfmt --staged", + "fmt:staged:fix": "oxfmt --staged --fix" + } +} diff --git a/packages/global/snap-tests/migration-merge-vite-config-ts/snap.txt b/packages/global/snap-tests/migration-merge-vite-config-ts/snap.txt new file mode 100644 index 0000000000..4dafe26a12 --- /dev/null +++ b/packages/global/snap-tests/migration-merge-vite-config-ts/snap.txt @@ -0,0 +1,99 @@ +> vp migration # migration should merge vite.config.ts and remove oxlintrc and oxfmtrc +┌ Vite+ Migration +│ +● Using default package manager: pnpm +│ +● pnpm@latest installing... +│ +● pnpm@ installed +│ +◆ ✅ Rewrote import in vite.config.ts +│ +◆ ✅ Rewrote import in vitest.config.ts +│ +◆ ✅ Merged .oxlintrc.json into vite.config.ts +│ +◆ ✅ Merged .oxfmtrc.json into vite.config.ts +│ +└ ✨ Migration completed! + + +> cat vite.config.ts # check vite.config.ts +import react from '@vitejs/plugin-react'; +import { defineConfig } from '@voidzero-dev/vite-plus'; + +export default defineConfig({ + fmt: { + printWidth: 100, + tabWidth: 2, + semi: true, + singleQuote: true, + trailingComma: 'es5', + }, + lint: { + rules: { + 'no-unused-vars': 'error', + }, + }, + plugins: [react()], +}); + +> cat vitest.config.ts # check vitest.config.ts +import { join } from 'node:path'; + +import { foo } from '@foo/vite-plugin-foo'; +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from '@voidzero-dev/vite-plus'; + +export default defineConfig({ + plugins: [foo()], + test: { + dir: join(import.meta.dirname, 'test'), + browser: { + enabled: true, + provider: playwright(), + headless: true, + screenshotFailures: false, + instances: [{ browser: 'chromium' }], + }, + }, +}); + +> cat .oxlintrc.json && exit 1 || true # check .oxlintrc.json is removed +cat: .oxlintrc.json: No such file or directory + +> cat .oxfmtrc.json && exit 1 || true # check .oxfmtrc.json is removed +cat: .oxfmtrc.json: No such file or directory + +> cat package.json # check package.json +{ + "devDependencies": { + "vite": "npm:@voidzero-dev/vite-plus-core@latest", + "vitest": "npm:@voidzero-dev/vite-plus-test@latest", + "@vitejs/plugin-react": "^4.2.0", + "@voidzero-dev/vite-plus": "latest" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test:run": "vite test run", + "test:ui": "vite test --ui", + "test:coverage": "vite test run --coverage", + "test:watch": "vite test --watch", + "test": "vite test", + "lint": "vite lint", + "lint:fix": "vite lint --fix", + "lint:type-aware": "vite lint --type-aware", + "fmt": "vite fmt", + "fmt:fix": "vite fmt --fix", + "fmt:staged": "vite fmt --staged", + "fmt:staged:fix": "vite fmt --staged --fix" + }, + "pnpm": { + "overrides": { + "vite": "npm:@voidzero-dev/vite-plus-core@latest", + "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + } + }, + "packageManager": "pnpm@" +} diff --git a/packages/global/snap-tests/migration-merge-vite-config-ts/steps.json b/packages/global/snap-tests/migration-merge-vite-config-ts/steps.json new file mode 100644 index 0000000000..6ddeeacf17 --- /dev/null +++ b/packages/global/snap-tests/migration-merge-vite-config-ts/steps.json @@ -0,0 +1,13 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp migration # migration should merge vite.config.ts and remove oxlintrc and oxfmtrc", + "cat vite.config.ts # check vite.config.ts", + "cat vitest.config.ts # check vitest.config.ts", + "cat .oxlintrc.json && exit 1 || true # check .oxlintrc.json is removed", + "cat .oxfmtrc.json && exit 1 || true # check .oxfmtrc.json is removed", + "cat package.json # check package.json" + ] +} diff --git a/packages/global/snap-tests/migration-merge-vite-config-ts/vite.config.ts b/packages/global/snap-tests/migration-merge-vite-config-ts/vite.config.ts new file mode 100644 index 0000000000..fabde1a8f5 --- /dev/null +++ b/packages/global/snap-tests/migration-merge-vite-config-ts/vite.config.ts @@ -0,0 +1,6 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/packages/global/snap-tests/migration-merge-vite-config-ts/vitest.config.ts b/packages/global/snap-tests/migration-merge-vite-config-ts/vitest.config.ts new file mode 100644 index 0000000000..21f3d45dd0 --- /dev/null +++ b/packages/global/snap-tests/migration-merge-vite-config-ts/vitest.config.ts @@ -0,0 +1,19 @@ +import { join } from 'node:path'; + +import { foo } from '@foo/vite-plugin-foo'; +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [foo()], + test: { + dir: join(import.meta.dirname, 'test'), + browser: { + enabled: true, + provider: playwright(), + headless: true, + screenshotFailures: false, + instances: [{ browser: 'chromium' }], + }, + }, +}); diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/.oxfmtrc.json b/packages/global/snap-tests/migration-monorepo-pnpm/.oxfmtrc.json new file mode 100644 index 0000000000..a91f91670b --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/.oxfmtrc.json @@ -0,0 +1,7 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "trailingComma": "es5" +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/.oxlintrc.json b/packages/global/snap-tests/migration-monorepo-pnpm/.oxlintrc.json new file mode 100644 index 0000000000..0eb0592cdb --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-unused-vars": "error" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/package.json b/packages/global/snap-tests/migration-monorepo-pnpm/package.json new file mode 100644 index 0000000000..ca655dd69f --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/package.json @@ -0,0 +1,25 @@ +{ + "name": "migration-monorepo-pnpm", + "version": "1.0.0", + "packageManager": "pnpm@10.18.0", + "dependencies": { + "testnpm2": "1.0.0", + "vite": "catalog:" + }, + "devDependencies": { + "vitest": "catalog:", + "oxlint": "catalog:", + "oxfmt": "catalog:" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test:run": "vitest run", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage", + "test:watch": "vitest --watch", + "test": "vitest", + "lint": "oxlint", + "fmt": "oxfmt" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/packages/app/package.json b/packages/global/snap-tests/migration-monorepo-pnpm/packages/app/package.json new file mode 100644 index 0000000000..78189ebb64 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/packages/app/package.json @@ -0,0 +1,21 @@ +{ + "name": "app", + "dependencies": { + "testnpm2": "catalog:", + "test-vite-plus-install": "1.0.0", + "@vite-plus-test/utils": "workspace:*" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0", + "vite": "catalog:", + "vitest": "catalog:" + }, + "optionalDependencies": { + "test-vite-plus-other-optional": "1.0.0" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test": "vitest" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/packages/utils/package.json b/packages/global/snap-tests/migration-monorepo-pnpm/packages/utils/package.json new file mode 100644 index 0000000000..18577eb8aa --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/packages/utils/package.json @@ -0,0 +1,15 @@ +{ + "name": "@vite-plus-test/utils", + "dependencies": { + "testnpm2": "1.0.0" + }, + "devDependencies": { + "vite": "catalog:", + "vitest": "catalog:" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test": "vitest" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/pnpm-workspace.yaml b/packages/global/snap-tests/migration-monorepo-pnpm/pnpm-workspace.yaml new file mode 100644 index 0000000000..b8b7c1b4f9 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/pnpm-workspace.yaml @@ -0,0 +1,13 @@ +packages: + - packages/* + +catalog: + testnpm2: ^1.0.0 + # test comment here to check if the comment is preserved + vite: ^7.0.0 + vitest: ^4.0.0 + oxlint: ^1.0.0 + oxfmt: ^1.0.0 + oxlint-tsgolint: ^1.0.0 + +minimumReleaseAge: 1440 diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/snap.txt b/packages/global/snap-tests/migration-monorepo-pnpm/snap.txt new file mode 100644 index 0000000000..f4635f86ff --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/snap.txt @@ -0,0 +1,140 @@ +> vp migration # migration should merge vite.config.ts and remove oxlintrc and oxfmtrc +┌ Vite+ Migration +│ +● pnpm@ installing... +│ +● pnpm@ installed +│ +◆ ✅ Rewrote import in vite.config.ts +│ +◆ ✅ Merged .oxlintrc.json into vite.config.ts +│ +◆ ✅ Merged .oxfmtrc.json into vite.config.ts +│ +└ ✨ Migration completed! + + +> cat vite.config.ts # check vite.config.ts +import react from '@vitejs/plugin-react'; +import { defineConfig } from '@voidzero-dev/vite-plus'; + +export default defineConfig({ + fmt: { + printWidth: 100, + tabWidth: 2, + semi: true, + singleQuote: true, + trailingComma: 'es5', + }, + lint: { + rules: { + 'no-unused-vars': 'error', + }, + }, + plugins: [react()], +}); + +> cat .oxlintrc.json && exit 1 || true # check .oxlintrc.json is removed +cat: .oxlintrc.json: No such file or directory + +> cat .oxfmtrc.json && exit 1 || true # check .oxfmtrc.json is removed +cat: .oxfmtrc.json: No such file or directory + +> cat package.json # check package.json +{ + "name": "migration-monorepo-pnpm", + "version": "1.0.0", + "packageManager": "pnpm@", + "dependencies": { + "testnpm2": "1.0.0", + "vite": "catalog:" + }, + "devDependencies": { + "vitest": "catalog:", + "@voidzero-dev/vite-plus": "catalog:" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test:run": "vite test run", + "test:ui": "vite test --ui", + "test:coverage": "vite test run --coverage", + "test:watch": "vite test --watch", + "test": "vite test", + "lint": "vite lint", + "fmt": "vite fmt" + } +} + +> cat pnpm-workspace.yaml # check pnpm-workspace.yaml +packages: + - packages/* + +catalog: + testnpm2: ^1.0.0 + # test comment here to check if the comment is preserved + vite: npm:@voidzero-dev/vite-plus-core@latest + vitest: npm:@voidzero-dev/vite-plus-test@latest + '@voidzero-dev/vite-plus': latest + +minimumReleaseAge: 1440 +overrides: + vite: 'catalog:' + vitest: 'catalog:' +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: '*' + vitest: '*' +minimumReleaseAgeExclude: + - '@voidzero-dev/*' + - oxlint + - '@oxlint/*' + - oxlint-tsgolint + - '@oxlint-tsgolint/*' + - oxfmt + - '@oxfmt/*' + +> cat packages/app/package.json # check app package.json +{ + "name": "app", + "dependencies": { + "testnpm2": "catalog:", + "test-vite-plus-install": "1.0.0", + "@vite-plus-test/utils": "workspace:*" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0", + "vite": "catalog:", + "vitest": "catalog:", + "@voidzero-dev/vite-plus": "catalog:" + }, + "optionalDependencies": { + "test-vite-plus-other-optional": "1.0.0" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test": "vite test" + } +} + +> cat packages/utils/package.json # check utils package.json +{ + "name": "@vite-plus-test/utils", + "dependencies": { + "testnpm2": "1.0.0" + }, + "devDependencies": { + "vite": "catalog:", + "vitest": "catalog:", + "@voidzero-dev/vite-plus": "catalog:" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test": "vite test" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/steps.json b/packages/global/snap-tests/migration-monorepo-pnpm/steps.json new file mode 100644 index 0000000000..3b2a96790e --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/steps.json @@ -0,0 +1,15 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp migration # migration should merge vite.config.ts and remove oxlintrc and oxfmtrc", + "cat vite.config.ts # check vite.config.ts", + "cat .oxlintrc.json && exit 1 || true # check .oxlintrc.json is removed", + "cat .oxfmtrc.json && exit 1 || true # check .oxfmtrc.json is removed", + "cat package.json # check package.json", + "cat pnpm-workspace.yaml # check pnpm-workspace.yaml", + "cat packages/app/package.json # check app package.json", + "cat packages/utils/package.json # check utils package.json" + ] +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/vite.config.ts b/packages/global/snap-tests/migration-monorepo-pnpm/vite.config.ts new file mode 100644 index 0000000000..fabde1a8f5 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/vite.config.ts @@ -0,0 +1,6 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/packages/global/snap-tests/migration-monorepo-yarn4/.oxlintrc.json b/packages/global/snap-tests/migration-monorepo-yarn4/.oxlintrc.json new file mode 100644 index 0000000000..0eb0592cdb --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-yarn4/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-unused-vars": "error" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-yarn4/package.json b/packages/global/snap-tests/migration-monorepo-yarn4/package.json new file mode 100644 index 0000000000..98494d5553 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-yarn4/package.json @@ -0,0 +1,28 @@ +{ + "name": "migration-monorepo-yarn4", + "version": "1.0.0", + "packageManager": "yarn@4.12.0", + "dependencies": { + "testnpm2": "1.0.0", + "vite": "catalog:" + }, + "devDependencies": { + "vitest": "catalog:", + "oxlint": "catalog:", + "oxfmt": "catalog:" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test:run": "vitest run", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage", + "test:watch": "vitest --watch", + "test": "vitest", + "lint": "oxlint", + "fmt": "oxfmt" + }, + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/global/snap-tests/migration-monorepo-yarn4/packages/app/package.json b/packages/global/snap-tests/migration-monorepo-yarn4/packages/app/package.json new file mode 100644 index 0000000000..78189ebb64 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-yarn4/packages/app/package.json @@ -0,0 +1,21 @@ +{ + "name": "app", + "dependencies": { + "testnpm2": "catalog:", + "test-vite-plus-install": "1.0.0", + "@vite-plus-test/utils": "workspace:*" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0", + "vite": "catalog:", + "vitest": "catalog:" + }, + "optionalDependencies": { + "test-vite-plus-other-optional": "1.0.0" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test": "vitest" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-yarn4/packages/utils/package.json b/packages/global/snap-tests/migration-monorepo-yarn4/packages/utils/package.json new file mode 100644 index 0000000000..18577eb8aa --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-yarn4/packages/utils/package.json @@ -0,0 +1,15 @@ +{ + "name": "@vite-plus-test/utils", + "dependencies": { + "testnpm2": "1.0.0" + }, + "devDependencies": { + "vite": "catalog:", + "vitest": "catalog:" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test": "vitest" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-yarn4/snap.txt b/packages/global/snap-tests/migration-monorepo-yarn4/snap.txt new file mode 100644 index 0000000000..a9defb09fa --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-yarn4/snap.txt @@ -0,0 +1,117 @@ +> vp migration # migration should merge vite.config.ts and remove oxlintrc +┌ Vite+ Migration +│ +● yarn@ installing... +│ +● yarn@ installed +│ +◆ ✅ Rewrote import in vite.config.ts +│ +◆ ✅ Merged .oxlintrc.json into vite.config.ts +│ +└ ✨ Migration completed! + + +> cat vite.config.ts # check vite.config.ts +import react from '@vitejs/plugin-react'; +import { defineConfig } from '@voidzero-dev/vite-plus'; + +export default defineConfig({ + lint: { + rules: { + 'no-unused-vars': 'error', + }, + }, + plugins: [react()], +}); + +> cat .oxlintrc.json && exit 1 || true # check .oxlintrc.json is removed +cat: .oxlintrc.json: No such file or directory + +> cat package.json # check package.json +{ + "name": "migration-monorepo-yarn4", + "version": "1.0.0", + "packageManager": "yarn@", + "dependencies": { + "testnpm2": "1.0.0", + "vite": "catalog:" + }, + "devDependencies": { + "vitest": "catalog:", + "@voidzero-dev/vite-plus": "catalog:" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test:run": "vite test run", + "test:ui": "vite test --ui", + "test:coverage": "vite test run --coverage", + "test:watch": "vite test --watch", + "test": "vite test", + "lint": "vite lint", + "fmt": "vite fmt" + }, + "workspaces": [ + "packages/*" + ], + "resolutions": { + "vite": "npm:@voidzero-dev/vite-plus-core@latest", + "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + } +} + +> cat .yarnrc.yml # check .yarnrc.yml +catalog: + vite: npm:@voidzero-dev/vite-plus-core@latest + vitest: npm:@voidzero-dev/vite-plus-test@latest + '@voidzero-dev/vite-plus': latest +npmScopes: + voidzero-dev: + npmRegistryServer: https://npm.pkg.github.com + npmAuthToken: ${GITHUB_TOKEN} + +> cat .npmrc # check .npmrc +@voidzero-dev:registry=https://npm.pkg.github.com/ +//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} +> cat packages/app/package.json # check app package.json +{ + "name": "app", + "dependencies": { + "testnpm2": "catalog:", + "test-vite-plus-install": "1.0.0", + "@vite-plus-test/utils": "workspace:*" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0", + "vite": "catalog:", + "vitest": "catalog:", + "@voidzero-dev/vite-plus": "catalog:" + }, + "optionalDependencies": { + "test-vite-plus-other-optional": "1.0.0" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test": "vite test" + } +} + +> cat packages/utils/package.json # check utils package.json +{ + "name": "@vite-plus-test/utils", + "dependencies": { + "testnpm2": "1.0.0" + }, + "devDependencies": { + "vite": "catalog:", + "vitest": "catalog:", + "@voidzero-dev/vite-plus": "catalog:" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test": "vite test" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-yarn4/steps.json b/packages/global/snap-tests/migration-monorepo-yarn4/steps.json new file mode 100644 index 0000000000..2341db77df --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-yarn4/steps.json @@ -0,0 +1,15 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp migration # migration should merge vite.config.ts and remove oxlintrc", + "cat vite.config.ts # check vite.config.ts", + "cat .oxlintrc.json && exit 1 || true # check .oxlintrc.json is removed", + "cat package.json # check package.json", + "cat .yarnrc.yml # check .yarnrc.yml", + "cat .npmrc # check .npmrc", + "cat packages/app/package.json # check app package.json", + "cat packages/utils/package.json # check utils package.json" + ] +} diff --git a/packages/global/snap-tests/migration-monorepo-yarn4/vite.config.ts b/packages/global/snap-tests/migration-monorepo-yarn4/vite.config.ts new file mode 100644 index 0000000000..fabde1a8f5 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-yarn4/vite.config.ts @@ -0,0 +1,6 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/packages/global/snap-tests/migration-not-supported-npm8.2/package.json b/packages/global/snap-tests/migration-not-supported-npm8.2/package.json new file mode 100644 index 0000000000..7a570a7119 --- /dev/null +++ b/packages/global/snap-tests/migration-not-supported-npm8.2/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "vite": "^7.0.0" + }, + "packageManager": "npm@8.2.0" +} diff --git a/packages/global/snap-tests/migration-not-supported-npm8.2/snap.txt b/packages/global/snap-tests/migration-not-supported-npm8.2/snap.txt new file mode 100644 index 0000000000..6190bbdfe2 --- /dev/null +++ b/packages/global/snap-tests/migration-not-supported-npm8.2/snap.txt @@ -0,0 +1,18 @@ +[1]> vp migration # migration should fail because npm version is not supported +┌ Vite+ Migration +│ +● npm@ installing... +│ +● npm@ installed +│ +■ ❌ npm@ is not supported by migration, please upgrade npm to >=8.3.0 first +└ The project is not supported by migration + + +> cat package.json # check package.json is not updated +{ + "devDependencies": { + "vite": "^7.0.0" + }, + "packageManager": "npm@" +} diff --git a/packages/global/snap-tests/migration-not-supported-npm8.2/steps.json b/packages/global/snap-tests/migration-not-supported-npm8.2/steps.json new file mode 100644 index 0000000000..17d2aeafa9 --- /dev/null +++ b/packages/global/snap-tests/migration-not-supported-npm8.2/steps.json @@ -0,0 +1,9 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp migration # migration should fail because npm version is not supported", + "cat package.json # check package.json is not updated" + ] +} diff --git a/packages/global/snap-tests/migration-not-supported-pnpm9.4/package.json b/packages/global/snap-tests/migration-not-supported-pnpm9.4/package.json new file mode 100644 index 0000000000..6791452785 --- /dev/null +++ b/packages/global/snap-tests/migration-not-supported-pnpm9.4/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "vite": "^7.0.0" + }, + "packageManager": "pnpm@9.4.0" +} diff --git a/packages/global/snap-tests/migration-not-supported-pnpm9.4/snap.txt b/packages/global/snap-tests/migration-not-supported-pnpm9.4/snap.txt new file mode 100644 index 0000000000..27bc0a023c --- /dev/null +++ b/packages/global/snap-tests/migration-not-supported-pnpm9.4/snap.txt @@ -0,0 +1,18 @@ +[1]> vp migration # migration should fail because pnpm version is not supported +┌ Vite+ Migration +│ +● pnpm@ installing... +│ +● pnpm@ installed +│ +■ ❌ pnpm@ is not supported by migration, please upgrade pnpm to >=9.5.0 first +└ The project is not supported by migration + + +> cat package.json # check package.json is not updated +{ + "devDependencies": { + "vite": "^7.0.0" + }, + "packageManager": "pnpm@" +} diff --git a/packages/global/snap-tests/migration-not-supported-pnpm9.4/steps.json b/packages/global/snap-tests/migration-not-supported-pnpm9.4/steps.json new file mode 100644 index 0000000000..e273b3411a --- /dev/null +++ b/packages/global/snap-tests/migration-not-supported-pnpm9.4/steps.json @@ -0,0 +1,9 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp migration # migration should fail because pnpm version is not supported", + "cat package.json # check package.json is not updated" + ] +} diff --git a/packages/global/snap-tests/migration-not-supported-vite6/package.json b/packages/global/snap-tests/migration-not-supported-vite6/package.json new file mode 100644 index 0000000000..60e38ecc36 --- /dev/null +++ b/packages/global/snap-tests/migration-not-supported-vite6/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "vite": "^6.0.0" + } +} diff --git a/packages/global/snap-tests/migration-not-supported-vite6/snap.txt b/packages/global/snap-tests/migration-not-supported-vite6/snap.txt new file mode 100644 index 0000000000..e6a08ea1f7 --- /dev/null +++ b/packages/global/snap-tests/migration-not-supported-vite6/snap.txt @@ -0,0 +1,21 @@ +> vp install # install dependencies first +[1]> vp migration # migration should fail because vite version is not supported +┌ Vite+ Migration +│ +● pnpm@ installing... +│ +● pnpm@ installed +│ +■ ❌ vite version is not supported by migration +│ +● Please upgrade vite to version >=7.0.0 first +└ The project is not supported by migration + + +> cat package.json # check package.json is not updated +{ + "devDependencies": { + "vite": "^6.0.0" + }, + "packageManager": "pnpm@" +} \ No newline at end of file diff --git a/packages/global/snap-tests/migration-not-supported-vite6/steps.json b/packages/global/snap-tests/migration-not-supported-vite6/steps.json new file mode 100644 index 0000000000..b3261ff6ef --- /dev/null +++ b/packages/global/snap-tests/migration-not-supported-vite6/steps.json @@ -0,0 +1,13 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + { + "command": "vp install # install dependencies first", + "ignoreOutput": true + }, + "vp migration # migration should fail because vite version is not supported", + "cat package.json # check package.json is not updated" + ] +} diff --git a/packages/global/snap-tests/migration-not-supported-vitest3/package.json b/packages/global/snap-tests/migration-not-supported-vitest3/package.json new file mode 100644 index 0000000000..4fea60792a --- /dev/null +++ b/packages/global/snap-tests/migration-not-supported-vitest3/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "vitest": "^3.0.0" + } +} diff --git a/packages/global/snap-tests/migration-not-supported-vitest3/snap.txt b/packages/global/snap-tests/migration-not-supported-vitest3/snap.txt new file mode 100644 index 0000000000..c98c634f29 --- /dev/null +++ b/packages/global/snap-tests/migration-not-supported-vitest3/snap.txt @@ -0,0 +1,21 @@ +> vp install # install dependencies first +[1]> vp migration # migration should fail because vitest version is not supported +┌ Vite+ Migration +│ +● pnpm@ installing... +│ +● pnpm@ installed +│ +■ ❌ vitest version is not supported by migration +│ +● Please upgrade vitest to version >=4.0.0 first +└ The project is not supported by migration + + +> cat package.json # check package.json is not updated +{ + "devDependencies": { + "vitest": "^3.0.0" + }, + "packageManager": "pnpm@" +} \ No newline at end of file diff --git a/packages/global/snap-tests/migration-not-supported-vitest3/steps.json b/packages/global/snap-tests/migration-not-supported-vitest3/steps.json new file mode 100644 index 0000000000..4dacda2c99 --- /dev/null +++ b/packages/global/snap-tests/migration-not-supported-vitest3/steps.json @@ -0,0 +1,13 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + { + "command": "vp install # install dependencies first", + "ignoreOutput": true + }, + "vp migration # migration should fail because vitest version is not supported", + "cat package.json # check package.json is not updated" + ] +} diff --git a/packages/global/src/migration/bin.ts b/packages/global/src/migration/bin.ts index 1ab5add3f9..82cd6d7513 100644 --- a/packages/global/src/migration/bin.ts +++ b/packages/global/src/migration/bin.ts @@ -5,7 +5,7 @@ import mri from 'mri'; import colors from 'picocolors'; import semver from 'semver'; -import type { WorkspaceInfo } from '../types/index.ts'; +import { PackageManager, type WorkspaceInfo } from '../types/index.ts'; import { defaultInteractive, detectWorkspace, @@ -13,8 +13,14 @@ import { downloadPackageManager, runViteInstall, upgradeYarn, + cancelAndExit, } from '../utils/index.ts'; -import { rewriteMonorepo, rewriteStandaloneProject } from './migrator.ts'; +import { + checkVitestVersion, + checkViteVersion, + rewriteMonorepo, + rewriteStandaloneProject, +} from './migrator.ts'; const { cyan, green, gray } = colors; @@ -106,20 +112,50 @@ async function main() { downloadPackageManager: downloadResult, }; + // run vite install first to ensure the project is ready + await runViteInstall(workspaceInfo.rootDir, options.interactive); + // check vite and vitest version is supported by migration + const isViteSupported = checkViteVersion(workspaceInfo.rootDir); + const isVitestSupported = checkVitestVersion(workspaceInfo.rootDir); + if (!isViteSupported || !isVitestSupported) { + cancelAndExit('The project is not supported by migration', 1); + } + // support catalog require yarn@>=4.10.0 https://yarnpkg.com/features/catalogs // if `yarn<4.10.0 && yarn>=4.0.0`, upgrade yarn to stable version - if (packageManager === 'yarn' && semver.satisfies(downloadResult.version, '>=4.0.0 <4.10.0')) { - await upgradeYarn(projectPath, options.interactive); + if ( + packageManager === PackageManager.yarn && + semver.satisfies(downloadResult.version, '>=4.0.0 <4.10.0') + ) { + await upgradeYarn(workspaceInfo.rootDir, options.interactive); + } else if ( + packageManager === PackageManager.pnpm && + semver.satisfies(downloadResult.version, '< 9.5.0') + ) { + // required pnpm@>=9.5.0 to support catalog https://pnpm.io/9.x/catalogs + prompts.log.error( + `❌ pnpm@${downloadResult.version} is not supported by migration, please upgrade pnpm to >=9.5.0 first`, + ); + cancelAndExit('The project is not supported by migration', 1); + } else if ( + packageManager === PackageManager.npm && + semver.satisfies(downloadResult.version, '< 8.3.0') + ) { + // required npm@>=8.3.0 to support overrides https://github.com/npm/cli/releases/tag/v8.3.0 + prompts.log.error( + `❌ npm@${downloadResult.version} is not supported by migration, please upgrade npm to >=8.3.0 first`, + ); + cancelAndExit('The project is not supported by migration', 1); } if (workspaceInfo.isMonorepo) { rewriteMonorepo(workspaceInfo); } else { - rewriteStandaloneProject(projectPath, workspaceInfo); + rewriteStandaloneProject(workspaceInfo.rootDir, workspaceInfo); } - await runViteInstall(projectPath, options.interactive); - + // reinstall after migration + await runViteInstall(workspaceInfo.rootDir, options.interactive); prompts.outro(green('✨ Migration completed!')); } diff --git a/packages/global/src/migration/detector.ts b/packages/global/src/migration/detector.ts index 467449912c..606c09b1c5 100644 --- a/packages/global/src/migration/detector.ts +++ b/packages/global/src/migration/detector.ts @@ -1,8 +1,10 @@ import fs from 'node:fs'; +import { createRequire } from 'node:module'; import path from 'node:path'; export interface ConfigFiles { viteConfig?: string; + vitestConfig?: string; oxlintConfig?: string; oxfmtConfig?: string; } @@ -20,18 +22,15 @@ export function detectConfigs(projectPath: string): ConfigFiles { } } - // TODO: Check for vitest.config.* + // Check for vitest.config.* // https://vitest.dev/config/ - // const vitestConfigs = [ - // 'vitest.config.ts', - // 'vitest.config.js', - // ]; - // for (const config of vitestConfigs) { - // if (fs.existsSync(path.join(projectPath, config))) { - // configs.vitestConfig = config; - // break; - // } - // } + const vitestConfigs = ['vitest.config.ts', 'vitest.config.js']; + for (const config of vitestConfigs) { + if (fs.existsSync(path.join(projectPath, config))) { + configs.vitestConfig = config; + break; + } + } // TODO: Check for tsdown.config.* // https://tsdown.dev/options/config-file @@ -75,3 +74,27 @@ export function detectConfigs(projectPath: string): ConfigFiles { return configs; } + +const require = createRequire(import.meta.url); + +interface PackageMetadata { + name: string; + version: string; +} + +export function detectPackageMetadata( + projectPath: string, + packageName: string, +): PackageMetadata | void { + try { + const pkgFilePath = require.resolve(`${packageName}/package.json`, { paths: [projectPath] }); + const pkg = JSON.parse(fs.readFileSync(pkgFilePath, 'utf8')); + return { + name: pkg.name, + version: pkg.version, + }; + } catch { + // ignore MODULE_NOT_FOUND error + return; + } +} diff --git a/packages/global/src/migration/migrator.ts b/packages/global/src/migration/migrator.ts index dc2a2be412..392d8eb199 100644 --- a/packages/global/src/migration/migrator.ts +++ b/packages/global/src/migration/migrator.ts @@ -2,7 +2,13 @@ import fs from 'node:fs'; import path from 'node:path'; import * as prompts from '@clack/prompts'; -import { mergeJsonConfig, rewriteScripts, type DownloadPackageManagerResult } from '@voidzero-dev/vite-plus/binding'; +import { + mergeJsonConfig, + rewriteScripts, + rewriteImport, + type DownloadPackageManagerResult, +} from '@voidzero-dev/vite-plus/binding'; +import semver from 'semver'; import { Scalar, YAMLMap, YAMLSeq } from 'yaml'; import { PackageManager, type WorkspaceInfo } from '../types/index.ts'; @@ -13,7 +19,7 @@ import { rulesDir, type YamlDocument, } from '../utils/index.ts'; -import { detectConfigs } from './detector.ts'; +import { detectConfigs, detectPackageMetadata } from './detector.ts'; const VITE_PLUS_NAME = '@voidzero-dev/vite-plus'; const VITE_PLUS_VERSION = 'latest'; @@ -23,6 +29,39 @@ const OVERRIDE_PACKAGES = { } as const; const REMOVE_PACKAGES = ['oxlint', 'oxlint-tsgolint', 'oxfmt']; +/** + * Check the vite version is supported by migration + * @param projectPath - The path to the project + * @returns true if the vite version is supported by migration + */ +export function checkViteVersion(projectPath: string): boolean { + return checkPackageVersion(projectPath, 'vite', '7.0.0'); +} + +export function checkVitestVersion(projectPath: string): boolean { + return checkPackageVersion(projectPath, 'vitest', '4.0.0'); +} + +/** + * Check the package version is supported by migration + * @param projectPath - The path to the project + * @param name - The name of the package + * @param minVersion - The minimum version of the package + * @returns true if the package version is supported by migration + */ +function checkPackageVersion(projectPath: string, name: string, minVersion: string): boolean { + const metadata = detectPackageMetadata(projectPath, name); + if (!metadata || metadata.name !== name) { + return true; + } + if (semver.satisfies(metadata.version, `<${minVersion}`)) { + prompts.log.error(`❌ ${name} version ${metadata.version} is not supported by migration`); + prompts.log.info(`Please upgrade ${name} to version >=${minVersion} first`); + return false; + } + return true; +} + /** * Rewrite standalone project to add vite-plus dependencies * @param projectPath - The path to the project @@ -129,6 +168,8 @@ export function rewriteMonorepo(workspaceInfo: WorkspaceInfo): void { * @param projectPath - The path to the project */ export function rewriteMonorepoProject(projectPath: string, packageManager: PackageManager): void { + rewriteViteConfigFile(projectPath); + const packageJsonPath = path.join(projectPath, 'package.json'); if (!fs.existsSync(packageJsonPath)) { return; @@ -140,15 +181,25 @@ export function rewriteMonorepoProject(projectPath: string, packageManager: Pack scripts?: Record; }>(packageJsonPath, (pkg) => { const isNpm = packageManager === PackageManager.npm; + let needVitePlus = false; for (const [key, value] of Object.entries(OVERRIDE_PACKAGES)) { const version = isNpm ? value : 'catalog:'; if (pkg.devDependencies?.[key]) { pkg.devDependencies[key] = version; + needVitePlus = true; } if (pkg.dependencies?.[key]) { pkg.dependencies[key] = version; + needVitePlus = true; } } + if (needVitePlus) { + // add vite-plus to devDependencies to let vite config `import` rewrite work + pkg.devDependencies = { + ...pkg.devDependencies, + [VITE_PLUS_NAME]: isNpm ? VITE_PLUS_VERSION : 'catalog:', + }; + } // rewrite scripts in package.json rewritePackageJson(pkg); @@ -203,7 +254,16 @@ function rewritePnpmWorkspaceYaml(projectPath: string): void { // minimumReleaseAgeExclude if (doc.has('minimumReleaseAge')) { - // add @voidzero-dev/*, vite, vitest to minimumReleaseAgeExclude + // add @voidzero-dev/*, oxlint, oxlint-tsgolint, oxfmt to minimumReleaseAgeExclude + const excludes = [ + '@voidzero-dev/*', + 'oxlint', + '@oxlint/*', + 'oxlint-tsgolint', + '@oxlint-tsgolint/*', + 'oxfmt', + '@oxfmt/*', + ]; let minimumReleaseAgeExclude = doc.getIn(['minimumReleaseAgeExclude']) as YAMLSeq< Scalar >; @@ -211,12 +271,9 @@ function rewritePnpmWorkspaceYaml(projectPath: string): void { minimumReleaseAgeExclude = new YAMLSeq(); } const existing = new Set(minimumReleaseAgeExclude.items.map((n) => n.value)); - if (!existing.has('@voidzero-dev/*')) { - minimumReleaseAgeExclude.add(scalarString('@voidzero-dev/*')); - } - for (const key of Object.keys(OVERRIDE_PACKAGES)) { - if (!existing.has(key)) { - minimumReleaseAgeExclude.add(scalarString(key)); + for (const exclude of excludes) { + if (!existing.has(exclude)) { + minimumReleaseAgeExclude.add(scalarString(exclude)); } } doc.setIn(['minimumReleaseAgeExclude'], minimumReleaseAgeExclude); @@ -406,21 +463,38 @@ function rewriteNpmrc(projectPath: string): void { } } +/** + * Rewrite vite.config.ts to use vite-plus + * - rewrite `import from 'vite'` to `import from 'vite-plus'` + * - rewrite `import from 'vitest/config'` to `import from 'vite-plus'` + * - merge oxlint config into vite.config.ts + * - merge oxfmt config into vite.config.ts + */ function rewriteViteConfigFile(projectPath: string): void { const configs = detectConfigs(projectPath); + if (configs.viteConfig) { + rewriteViteConfigImport(projectPath, configs.viteConfig); + } + if (configs.vitestConfig) { + rewriteViteConfigImport(projectPath, configs.vitestConfig); + } + if (!configs.oxfmtConfig && !configs.oxlintConfig) { return; } if (!configs.viteConfig) { + // TODO: handle typescript or javascript // create vite.config.ts - configs.viteConfig = path.join(projectPath, 'vite.config.ts'); + configs.viteConfig = 'vite.config.ts'; + const viteConfigPath = path.join(projectPath, 'vite.config.ts'); fs.writeFileSync( - configs.viteConfig, + viteConfigPath, `import { defineConfig } from '${VITE_PLUS_NAME}'; export default defineConfig({}); `, ); + prompts.log.success(`✅ Created vite.config.ts in ${configs.viteConfig}`); } if (configs.oxlintConfig) { // merge oxlint config into vite.config.ts @@ -428,7 +502,7 @@ export default defineConfig({}); } if (configs.oxfmtConfig) { // merge oxfmt config into vite.config.ts - mergeAndRemoveJsonConfig(projectPath, configs.viteConfig, configs.oxfmtConfig, 'format'); + mergeAndRemoveJsonConfig(projectPath, configs.viteConfig, configs.oxfmtConfig, 'fmt'); } } @@ -438,21 +512,30 @@ function mergeAndRemoveJsonConfig( jsonConfigPath: string, configKey: string, ): void { - const result = mergeJsonConfig(viteConfigPath, jsonConfigPath, configKey); - const jsonConfigRelativePath = path.relative(projectPath, jsonConfigPath); - const viteConfigRelativePath = path.relative(projectPath, viteConfigPath); + const fullViteConfigPath = path.join(projectPath, viteConfigPath); + const fullJsonConfigPath = path.join(projectPath, jsonConfigPath); + const result = mergeJsonConfig(fullViteConfigPath, fullJsonConfigPath, configKey); if (result.updated) { - fs.writeFileSync(viteConfigPath, result.content); - fs.unlinkSync(jsonConfigPath); - prompts.log.success(`✅ Merged ${jsonConfigRelativePath} into ${viteConfigRelativePath}`); + fs.writeFileSync(fullViteConfigPath, result.content); + fs.unlinkSync(fullJsonConfigPath); + prompts.log.success(`✅ Merged ${jsonConfigPath} into ${viteConfigPath}`); } else { - prompts.log.warn(`❌ Failed to merge ${jsonConfigRelativePath} into ${viteConfigRelativePath}`); + prompts.log.warn(`❌ Failed to merge ${jsonConfigPath} into ${viteConfigPath}`); prompts.log.info( `Please complete the merge manually and follow the instructions in the documentation: https://viteplus.dev/config/`, ); } } +function rewriteViteConfigImport(projectPath: string, viteConfigPath: string): void { + const fullPath = path.join(projectPath, viteConfigPath); + const result = rewriteImport(fullPath); + if (result.updated) { + fs.writeFileSync(fullPath, result.content); + prompts.log.success(`✅ Rewrote import in ${viteConfigPath}`); + } +} + function setPackageManager( projectDir: string, downloadPackageManager: DownloadPackageManagerResult, diff --git a/rfcs/migration-command.md b/rfcs/migration-command.md index c803e4a272..9700aa6d80 100644 --- a/rfcs/migration-command.md +++ b/rfcs/migration-command.md @@ -197,7 +197,8 @@ Next steps: **Important**: - `overrides.vite` ensures any dependency requiring `vite` gets `vite-plus` instead -- Code using `import from 'vite'` automatically resolves to vite-plus +- rewrite `import from 'vite'` to `import from 'vite-plus'` on `vite.config.ts` +- rewrite `import from 'vitest/config'` to `import from 'vite-plus'` on `vitest.config.ts` or `vite.config.ts` **Note**: For Yarn, use `resolutions` instead of `overrides`. @@ -257,7 +258,7 @@ export default defineConfig({ plugins: [], // Oxfmt configuration - format: { + fmt: { printWidth: 100, tabWidth: 2, semi: true, @@ -267,6 +268,61 @@ export default defineConfig({ }); ``` +### import namespace change to vite-plus + +effect files: + +- vitest.config.ts +- vite.config.ts + +**Before (import from 'vitest/config'):** + +```typescript +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + }, +}); +``` + +**After (import from 'vite-plus'):** + +```typescript +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + test: { + globals: true, + }, +}); +``` + +**Before (import from 'vite'):** + +```typescript +import { defineConfig } from 'vite'; + +export default defineConfig({ + test: { + globals: true, + }, +}); +``` + +**After (import from 'vite-plus'):** + +```typescript +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + test: { + globals: true, + }, +}); +``` + ### Complete Example **Before:** @@ -296,7 +352,7 @@ my-package/ ```typescript // Import from 'vite' still works - overrides maps it to vite-plus import react from '@vitejs/plugin-react'; -import { defineConfig } from 'vite'; +import { defineConfig } from 'vite-plus'; export default defineConfig({ // Vite configuration @@ -318,7 +374,7 @@ export default defineConfig({ }, // format configuration (merged from .oxfmtrc) - format: { + fmt: { printWidth: 100, tabWidth: 2, semi: true, @@ -328,6 +384,18 @@ export default defineConfig({ }); ``` +**vitest.config.ts (after migration):** + +```typescript +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + test: { + globals: true, + }, +}); +``` + ## Monorepo Configuration Migration ### for pnpm From b2d940e38dd00d35634333cb8af40758f31033bd Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Tue, 2 Dec 2025 19:09:18 +0800 Subject: [PATCH 3/6] Update vite_config.rs --- crates/vite_migration/src/vite_config.rs | 2 +- .../global/snap-tests/gen-check/steps.json | 1 + .../snap-tests/gen-create-tsdown/steps.json | 1 + .../snap-tests/gen-create-vite/steps.json | 1 + .../snap-tests/gen-vite-monorepo/steps.json | 1 + .../.lintstagedrc.json | 0 .../package.json | 0 .../snap.txt | 2 + .../steps.json | 0 .../.lintstagedrc | 3 + .../.lintstagedrc.yaml | 3 + .../lint-staged.config.mjs | 3 + .../package.json | 3 + .../snap.txt | 49 ++++ .../steps.json | 12 + .../package.json | 22 ++ .../packages/app/package.json | 13 + .../pnpm-workspace.yaml | 11 + .../snap.txt | 77 ++++++ .../steps.json | 12 + .../vite.config.ts | 6 + .../migration-monorepo-pnpm/package.json | 5 + .../packages/only-oxlint/.oxlintrc.json | 5 + .../packages/only-oxlint/package.json | 9 + .../migration-monorepo-pnpm/snap.txt | 33 +++ .../migration-monorepo-pnpm/steps.json | 5 +- .../migration-not-supported-npm8.2/snap.txt | 4 +- .../migration-not-supported-pnpm9.4/snap.txt | 4 +- .../migration-not-supported-vite6/snap.txt | 4 +- .../migration-not-supported-vitest3/snap.txt | 4 +- .../__snapshots__/migrator.spec.ts.snap | 42 +++- .../src/migration/__tests__/migrator.spec.ts | 52 +++- packages/global/src/migration/bin.ts | 26 +- packages/global/src/migration/migrator.ts | 234 ++++++++++++------ packages/global/src/utils/json.ts | 9 + packages/global/src/utils/path.ts | 4 + rfcs/migration-command.md | 4 +- 37 files changed, 562 insertions(+), 104 deletions(-) rename packages/global/snap-tests/{migration-lintstagedrc => migration-lintstagedrc-json}/.lintstagedrc.json (100%) rename packages/global/snap-tests/{migration-lintstagedrc => migration-lintstagedrc-json}/package.json (100%) rename packages/global/snap-tests/{migration-lintstagedrc => migration-lintstagedrc-json}/snap.txt (95%) rename packages/global/snap-tests/{migration-lintstagedrc => migration-lintstagedrc-json}/steps.json (100%) create mode 100644 packages/global/snap-tests/migration-lintstagedrc-not-support/.lintstagedrc create mode 100644 packages/global/snap-tests/migration-lintstagedrc-not-support/.lintstagedrc.yaml create mode 100644 packages/global/snap-tests/migration-lintstagedrc-not-support/lint-staged.config.mjs create mode 100644 packages/global/snap-tests/migration-lintstagedrc-not-support/package.json create mode 100644 packages/global/snap-tests/migration-lintstagedrc-not-support/snap.txt create mode 100644 packages/global/snap-tests/migration-lintstagedrc-not-support/steps.json create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/package.json create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/packages/app/package.json create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/pnpm-workspace.yaml create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/steps.json create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/vite.config.ts create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm/packages/only-oxlint/.oxlintrc.json create mode 100644 packages/global/snap-tests/migration-monorepo-pnpm/packages/only-oxlint/package.json diff --git a/crates/vite_migration/src/vite_config.rs b/crates/vite_migration/src/vite_config.rs index c1ce6b65c7..1e23192fd6 100644 --- a/crates/vite_migration/src/vite_config.rs +++ b/crates/vite_migration/src/vite_config.rs @@ -181,7 +181,7 @@ fn rewrite_import_content(vite_config_content: &str) -> Result installed │ +◆ ✅ Rewrote lint-staged config in .lintstagedrc.json +│ └ ✨ Migration completed! diff --git a/packages/global/snap-tests/migration-lintstagedrc/steps.json b/packages/global/snap-tests/migration-lintstagedrc-json/steps.json similarity index 100% rename from packages/global/snap-tests/migration-lintstagedrc/steps.json rename to packages/global/snap-tests/migration-lintstagedrc-json/steps.json diff --git a/packages/global/snap-tests/migration-lintstagedrc-not-support/.lintstagedrc b/packages/global/snap-tests/migration-lintstagedrc-not-support/.lintstagedrc new file mode 100644 index 0000000000..eeb34a1e8a --- /dev/null +++ b/packages/global/snap-tests/migration-lintstagedrc-not-support/.lintstagedrc @@ -0,0 +1,3 @@ +"*.js": + - oxlint + - oxfmt diff --git a/packages/global/snap-tests/migration-lintstagedrc-not-support/.lintstagedrc.yaml b/packages/global/snap-tests/migration-lintstagedrc-not-support/.lintstagedrc.yaml new file mode 100644 index 0000000000..b7d18eb06a --- /dev/null +++ b/packages/global/snap-tests/migration-lintstagedrc-not-support/.lintstagedrc.yaml @@ -0,0 +1,3 @@ +'*.js': + - oxlint + - oxfmt diff --git a/packages/global/snap-tests/migration-lintstagedrc-not-support/lint-staged.config.mjs b/packages/global/snap-tests/migration-lintstagedrc-not-support/lint-staged.config.mjs new file mode 100644 index 0000000000..9edc357fff --- /dev/null +++ b/packages/global/snap-tests/migration-lintstagedrc-not-support/lint-staged.config.mjs @@ -0,0 +1,3 @@ +export default { + '*.js': ['oxlint', 'oxfmt'], +}; diff --git a/packages/global/snap-tests/migration-lintstagedrc-not-support/package.json b/packages/global/snap-tests/migration-lintstagedrc-not-support/package.json new file mode 100644 index 0000000000..e3ad61f083 --- /dev/null +++ b/packages/global/snap-tests/migration-lintstagedrc-not-support/package.json @@ -0,0 +1,3 @@ +{ + "name": "migration-lintstagedrc-not-support" +} diff --git a/packages/global/snap-tests/migration-lintstagedrc-not-support/snap.txt b/packages/global/snap-tests/migration-lintstagedrc-not-support/snap.txt new file mode 100644 index 0000000000..5f147af2db --- /dev/null +++ b/packages/global/snap-tests/migration-lintstagedrc-not-support/snap.txt @@ -0,0 +1,49 @@ +> vp migration # migration should not support non-json format lintstagedrc +┌ Vite+ Migration +│ +● Using default package manager: pnpm +│ +● pnpm@latest installing... +│ +● pnpm@ installed +│ +▲ ❌ .lintstagedrc is not JSON format file, auto migration is not supported +│ +▲ ❌ .lintstagedrc.yaml is not supported by auto migration +│ +▲ ❌ lint-staged.config.mjs is not supported by auto migration +│ +▲ Please migrate the lint-staged config manually, see https://viteplus.dev/migration/#lint-staged for more details +│ +└ ✨ Migration completed! + + +> cat .lintstagedrc # check .lintstagedrc is not updated +"*.js": + - oxlint + - oxfmt + +> cat .lintstagedrc.yaml # check .lintstagedrc.yaml is not updated +'*.js': + - oxlint + - oxfmt + +> cat lint-staged.config.mjs # check lint-staged.config.mjs is not updated +export default { + '*.js': ['oxlint', 'oxfmt'], +}; + +> cat package.json # check package.json +{ + "name": "migration-lintstagedrc-not-support", + "pnpm": { + "overrides": { + "vite": "npm:@voidzero-dev/vite-plus-core@latest", + "vitest": "npm:@voidzero-dev/vite-plus-test@latest" + } + }, + "devDependencies": { + "@voidzero-dev/vite-plus": "latest" + }, + "packageManager": "pnpm@" +} diff --git a/packages/global/snap-tests/migration-lintstagedrc-not-support/steps.json b/packages/global/snap-tests/migration-lintstagedrc-not-support/steps.json new file mode 100644 index 0000000000..96f20beaf5 --- /dev/null +++ b/packages/global/snap-tests/migration-lintstagedrc-not-support/steps.json @@ -0,0 +1,12 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp migration # migration should not support non-json format lintstagedrc", + "cat .lintstagedrc # check .lintstagedrc is not updated", + "cat .lintstagedrc.yaml # check .lintstagedrc.yaml is not updated", + "cat lint-staged.config.mjs # check lint-staged.config.mjs is not updated", + "cat package.json # check package.json" + ] +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/package.json b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/package.json new file mode 100644 index 0000000000..32abafa9ae --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/package.json @@ -0,0 +1,22 @@ +{ + "name": "migration-monorepo-pnpm-overrides-dependency-selector", + "version": "1.0.0", + "packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd", + "devDependencies": { + "vite": "catalog:" + }, + "scripts": { + "dev": "vite" + }, + "pnpm": { + "overrides": { + "react-click-away-listener>react": "0.0.0-experimental-7dc903cd-20251203", + "vite": "npm:rolldown-vite@7.0.12", + "vite-plugin-inspect>vite": "npm:rolldown-vite@7.0.12", + "vite-plugin-svgr>foo>vite": "npm:rolldown-vite@7.0.12", + "@vitejs/plugin-react>vite": "npm:rolldown-vite@7.0.12", + "@vitejs/plugin-react-swc>vite": "npm:rolldown-vite@7.0.12", + "supertest>superagent": "9.0.2" + } + } +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/packages/app/package.json b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/packages/app/package.json new file mode 100644 index 0000000000..a76bb37493 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/packages/app/package.json @@ -0,0 +1,13 @@ +{ + "name": "app", + "devDependencies": { + "vite": "catalog:" + }, + "optionalDependencies": { + "test-vite-plus-other-optional": "1.0.0" + }, + "scripts": { + "dev": "vite --port 3000", + "build": "vite build" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/pnpm-workspace.yaml b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/pnpm-workspace.yaml new file mode 100644 index 0000000000..79284bd068 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/pnpm-workspace.yaml @@ -0,0 +1,11 @@ +packages: + - packages/* + +catalog: + vite: 'npm:rolldown-vite@7.0.12' + +overrides: + 'vite-plugin-svgr>vite': 'npm:rolldown-vite@7.0.12' + '@vitejs/plugin-react>vite': 'npm:rolldown-vite@7.0.12' + 'vite-plugin-svgr>foo>vite': 'npm:rolldown-vite@7.0.12' + 'supertest>superagent': '9.0.2' diff --git a/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt new file mode 100644 index 0000000000..596ad654b0 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt @@ -0,0 +1,77 @@ +> vp migration # migration should merge pnpm overrides with dependency selector +┌ Vite+ Migration +│ +● pnpm@ installing... +│ +● pnpm@ installed +│ +◆ ✅ Rewrote import in vite.config.ts +│ +└ ✨ Migration completed! + + +> cat vite.config.ts # check vite.config.ts +import react from '@vitejs/plugin-react'; +import { defineConfig } from '@voidzero-dev/vite-plus'; + +export default defineConfig({ + plugins: [react()], +}); + +> cat package.json # check package.json +{ + "name": "migration-monorepo-pnpm-overrides-dependency-selector", + "version": "1.0.0", + "packageManager": "pnpm@+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd", + "devDependencies": { + "vite": "catalog:", + "@voidzero-dev/vite-plus": "catalog:" + }, + "scripts": { + "dev": "vite dev" + }, + "pnpm": { + "overrides": { + "react-click-away-listener>react": "0.0.0-experimental-7dc903cd-20251203", + "supertest>superagent": "9.0.2" + } + } +} + +> cat pnpm-workspace.yaml # check pnpm-workspace.yaml +packages: + - packages/* + +catalog: + vite: npm:@voidzero-dev/vite-plus-core@latest + vitest: npm:@voidzero-dev/vite-plus-test@latest + '@voidzero-dev/vite-plus': latest + +overrides: + '@vitejs/plugin-react>vite': 'npm:rolldown-vite@' + 'supertest>superagent': '9.0.2' + vite: 'catalog:' + vitest: 'catalog:' +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: '*' + vitest: '*' + +> cat packages/app/package.json # check app package.json +{ + "name": "app", + "devDependencies": { + "vite": "catalog:", + "@voidzero-dev/vite-plus": "catalog:" + }, + "optionalDependencies": { + "test-vite-plus-other-optional": "1.0.0" + }, + "scripts": { + "dev": "vite dev --port 3000", + "build": "vite build" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/steps.json b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/steps.json new file mode 100644 index 0000000000..ddd9bc5c64 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/steps.json @@ -0,0 +1,12 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp migration # migration should merge pnpm overrides with dependency selector", + "cat vite.config.ts # check vite.config.ts", + "cat package.json # check package.json", + "cat pnpm-workspace.yaml # check pnpm-workspace.yaml", + "cat packages/app/package.json # check app package.json" + ] +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/vite.config.ts b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/vite.config.ts new file mode 100644 index 0000000000..fabde1a8f5 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm-overrides-dependency-selector/vite.config.ts @@ -0,0 +1,6 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/package.json b/packages/global/snap-tests/migration-monorepo-pnpm/package.json index ca655dd69f..823813feef 100644 --- a/packages/global/snap-tests/migration-monorepo-pnpm/package.json +++ b/packages/global/snap-tests/migration-monorepo-pnpm/package.json @@ -6,6 +6,11 @@ "testnpm2": "1.0.0", "vite": "catalog:" }, + "resolutions": { + "vue": "3.5.25", + "vite": "catalog:", + "vitest": "catalog:" + }, "devDependencies": { "vitest": "catalog:", "oxlint": "catalog:", diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/packages/only-oxlint/.oxlintrc.json b/packages/global/snap-tests/migration-monorepo-pnpm/packages/only-oxlint/.oxlintrc.json new file mode 100644 index 0000000000..792367ef22 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/packages/only-oxlint/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-unused-vars": "warn" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/packages/only-oxlint/package.json b/packages/global/snap-tests/migration-monorepo-pnpm/packages/only-oxlint/package.json new file mode 100644 index 0000000000..48532e03c2 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/packages/only-oxlint/package.json @@ -0,0 +1,9 @@ +{ + "name": "@vite-plus-test/only-oxlint", + "devDependencies": { + "oxlint": "catalog:" + }, + "scripts": { + "lint": "oxlint --fix" + } +} diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/snap.txt b/packages/global/snap-tests/migration-monorepo-pnpm/snap.txt index f4635f86ff..5231a9e9c5 100644 --- a/packages/global/snap-tests/migration-monorepo-pnpm/snap.txt +++ b/packages/global/snap-tests/migration-monorepo-pnpm/snap.txt @@ -11,6 +11,10 @@ │ ◆ ✅ Merged .oxfmtrc.json into vite.config.ts │ +◆ ✅ Created vite.config.ts in packages/only-oxlint/vite.config.ts +│ +◆ ✅ Merged packages/only-oxlint/.oxlintrc.json into packages/only-oxlint/vite.config.ts +│ └ ✨ Migration completed! @@ -49,6 +53,9 @@ cat: .oxfmtrc.json: No such file or directory "testnpm2": "1.0.0", "vite": "catalog:" }, + "resolutions": { + "vue": "3.5.25" + }, "devDependencies": { "vitest": "catalog:", "@voidzero-dev/vite-plus": "catalog:" @@ -138,3 +145,29 @@ minimumReleaseAgeExclude: "test": "vite test" } } + +> cat packages/only-oxlint/package.json # check only-oxlint package.json +{ + "name": "@vite-plus-test/only-oxlint", + "devDependencies": { + "@voidzero-dev/vite-plus": "catalog:" + }, + "scripts": { + "lint": "vite lint --fix" + } +} + +> cat packages/only-oxlint/vite.config.ts # check only-oxlint vite.config.ts +import { defineConfig } from '@voidzero-dev/vite-plus'; + +export default defineConfig({ + lint: { + rules: { + 'no-unused-vars': 'warn', + }, + }, + +}); + +> cat packages/only-oxlint/.oxlintrc.json && exit 1 || true # check only-oxlint .oxlintrc.json is removed +cat: packages/only-oxlint/.oxlintrc.json: No such file or directory diff --git a/packages/global/snap-tests/migration-monorepo-pnpm/steps.json b/packages/global/snap-tests/migration-monorepo-pnpm/steps.json index 3b2a96790e..8e75dd1535 100644 --- a/packages/global/snap-tests/migration-monorepo-pnpm/steps.json +++ b/packages/global/snap-tests/migration-monorepo-pnpm/steps.json @@ -10,6 +10,9 @@ "cat package.json # check package.json", "cat pnpm-workspace.yaml # check pnpm-workspace.yaml", "cat packages/app/package.json # check app package.json", - "cat packages/utils/package.json # check utils package.json" + "cat packages/utils/package.json # check utils package.json", + "cat packages/only-oxlint/package.json # check only-oxlint package.json", + "cat packages/only-oxlint/vite.config.ts # check only-oxlint vite.config.ts", + "cat packages/only-oxlint/.oxlintrc.json && exit 1 || true # check only-oxlint .oxlintrc.json is removed" ] } diff --git a/packages/global/snap-tests/migration-not-supported-npm8.2/snap.txt b/packages/global/snap-tests/migration-not-supported-npm8.2/snap.txt index 6190bbdfe2..d37cd790a5 100644 --- a/packages/global/snap-tests/migration-not-supported-npm8.2/snap.txt +++ b/packages/global/snap-tests/migration-not-supported-npm8.2/snap.txt @@ -5,8 +5,8 @@ │ ● npm@ installed │ -■ ❌ npm@ is not supported by migration, please upgrade npm to >=8.3.0 first -└ The project is not supported by migration +■ ❌ npm@ is not supported by auto migration, please upgrade npm to >=8.3.0 first +└ The project is not supported by auto migration > cat package.json # check package.json is not updated diff --git a/packages/global/snap-tests/migration-not-supported-pnpm9.4/snap.txt b/packages/global/snap-tests/migration-not-supported-pnpm9.4/snap.txt index 27bc0a023c..aa5669fdc5 100644 --- a/packages/global/snap-tests/migration-not-supported-pnpm9.4/snap.txt +++ b/packages/global/snap-tests/migration-not-supported-pnpm9.4/snap.txt @@ -5,8 +5,8 @@ │ ● pnpm@ installed │ -■ ❌ pnpm@ is not supported by migration, please upgrade pnpm to >=9.5.0 first -└ The project is not supported by migration +■ ❌ pnpm@ is not supported by auto migration, please upgrade pnpm to >=9.5.0 first +└ The project is not supported by auto migration > cat package.json # check package.json is not updated diff --git a/packages/global/snap-tests/migration-not-supported-vite6/snap.txt b/packages/global/snap-tests/migration-not-supported-vite6/snap.txt index e6a08ea1f7..317edc65b3 100644 --- a/packages/global/snap-tests/migration-not-supported-vite6/snap.txt +++ b/packages/global/snap-tests/migration-not-supported-vite6/snap.txt @@ -6,10 +6,10 @@ │ ● pnpm@ installed │ -■ ❌ vite version is not supported by migration +■ ❌ vite@ in package.json is not supported by auto migration │ ● Please upgrade vite to version >=7.0.0 first -└ The project is not supported by migration +└ The project is not supported by auto migration > cat package.json # check package.json is not updated diff --git a/packages/global/snap-tests/migration-not-supported-vitest3/snap.txt b/packages/global/snap-tests/migration-not-supported-vitest3/snap.txt index c98c634f29..0163bbc528 100644 --- a/packages/global/snap-tests/migration-not-supported-vitest3/snap.txt +++ b/packages/global/snap-tests/migration-not-supported-vitest3/snap.txt @@ -6,10 +6,10 @@ │ ● pnpm@ installed │ -■ ❌ vitest version is not supported by migration +■ ❌ vitest@ in package.json is not supported by auto migration │ ● Please upgrade vitest to version >=4.0.0 first -└ The project is not supported by migration +└ The project is not supported by auto migration > cat package.json # check package.json is not updated diff --git a/packages/global/src/migration/__tests__/__snapshots__/migrator.spec.ts.snap b/packages/global/src/migration/__tests__/__snapshots__/migrator.spec.ts.snap index 9b0675b3ea..cbd722f6ee 100644 --- a/packages/global/src/migration/__tests__/__snapshots__/migrator.spec.ts.snap +++ b/packages/global/src/migration/__tests__/__snapshots__/migrator.spec.ts.snap @@ -1,12 +1,50 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`rewritePackageJson > should remove vite tools from devDependencies and dependencies 1`] = ` +exports[`rewritePackageJson > should rewrite devDependencies and dependencies on npm monorepo project 1`] = ` { "dependencies": { "foo": "1.0.0", "tsdown": "1.0.0", }, - "devDependencies": {}, + "devDependencies": { + "@voidzero-dev/vite-plus": "latest", + }, +} +`; + +exports[`rewritePackageJson > should rewrite devDependencies and dependencies on pnpm monorepo project 1`] = ` +{ + "dependencies": { + "foo": "1.0.0", + "tsdown": "1.0.0", + }, + "devDependencies": { + "@voidzero-dev/vite-plus": "catalog:", + }, +} +`; + +exports[`rewritePackageJson > should rewrite devDependencies and dependencies on standalone project 1`] = ` +{ + "dependencies": { + "foo": "1.0.0", + "tsdown": "1.0.0", + }, + "devDependencies": { + "@voidzero-dev/vite-plus": "latest", + }, +} +`; + +exports[`rewritePackageJson > should rewrite devDependencies and dependencies on yarn monorepo project 1`] = ` +{ + "dependencies": { + "foo": "1.0.0", + "tsdown": "1.0.0", + }, + "devDependencies": { + "@voidzero-dev/vite-plus": "catalog:", + }, } `; diff --git a/packages/global/src/migration/__tests__/migrator.spec.ts b/packages/global/src/migration/__tests__/migrator.spec.ts index 0c9deed6c2..23769c4505 100644 --- a/packages/global/src/migration/__tests__/migrator.spec.ts +++ b/packages/global/src/migration/__tests__/migrator.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; +import { PackageManager } from '../../types/index.ts'; import { rewritePackageJson } from '../migrator.ts'; describe('rewritePackageJson', () => { @@ -42,11 +43,11 @@ describe('rewritePackageJson', () => { '*.ts': 'oxfmt --fix', }, }; - rewritePackageJson(pkg); + rewritePackageJson(pkg, PackageManager.npm); expect(pkg).toMatchSnapshot(); }); - it('should remove vite tools from devDependencies and dependencies', async () => { + it('should rewrite devDependencies and dependencies on standalone project', async () => { const pkg = { devDependencies: { oxlint: '1.0.0', @@ -57,7 +58,52 @@ describe('rewritePackageJson', () => { tsdown: '1.0.0', }, }; - rewritePackageJson(pkg); + rewritePackageJson(pkg, PackageManager.pnpm); + expect(pkg).toMatchSnapshot(); + }); + + it('should rewrite devDependencies and dependencies on pnpm monorepo project', async () => { + const pkg = { + devDependencies: { + oxlint: '1.0.0', + oxfmt: '1.0.0', + }, + dependencies: { + foo: '1.0.0', + tsdown: '1.0.0', + }, + }; + rewritePackageJson(pkg, PackageManager.pnpm, true); + expect(pkg).toMatchSnapshot(); + }); + + it('should rewrite devDependencies and dependencies on npm monorepo project', async () => { + const pkg = { + devDependencies: { + oxlint: '1.0.0', + oxfmt: '1.0.0', + }, + dependencies: { + foo: '1.0.0', + tsdown: '1.0.0', + }, + }; + rewritePackageJson(pkg, PackageManager.npm, true); + expect(pkg).toMatchSnapshot(); + }); + + it('should rewrite devDependencies and dependencies on yarn monorepo project', async () => { + const pkg = { + devDependencies: { + oxlint: '1.0.0', + oxfmt: '1.0.0', + }, + dependencies: { + foo: '1.0.0', + tsdown: '1.0.0', + }, + }; + rewritePackageJson(pkg, PackageManager.yarn, true); expect(pkg).toMatchSnapshot(); }); }); diff --git a/packages/global/src/migration/bin.ts b/packages/global/src/migration/bin.ts index 82cd6d7513..f203181ef5 100644 --- a/packages/global/src/migration/bin.ts +++ b/packages/global/src/migration/bin.ts @@ -112,15 +112,6 @@ async function main() { downloadPackageManager: downloadResult, }; - // run vite install first to ensure the project is ready - await runViteInstall(workspaceInfo.rootDir, options.interactive); - // check vite and vitest version is supported by migration - const isViteSupported = checkViteVersion(workspaceInfo.rootDir); - const isVitestSupported = checkVitestVersion(workspaceInfo.rootDir); - if (!isViteSupported || !isVitestSupported) { - cancelAndExit('The project is not supported by migration', 1); - } - // support catalog require yarn@>=4.10.0 https://yarnpkg.com/features/catalogs // if `yarn<4.10.0 && yarn>=4.0.0`, upgrade yarn to stable version if ( @@ -134,18 +125,27 @@ async function main() { ) { // required pnpm@>=9.5.0 to support catalog https://pnpm.io/9.x/catalogs prompts.log.error( - `❌ pnpm@${downloadResult.version} is not supported by migration, please upgrade pnpm to >=9.5.0 first`, + `❌ pnpm@${downloadResult.version} is not supported by auto migration, please upgrade pnpm to >=9.5.0 first`, ); - cancelAndExit('The project is not supported by migration', 1); + cancelAndExit('The project is not supported by auto migration', 1); } else if ( packageManager === PackageManager.npm && semver.satisfies(downloadResult.version, '< 8.3.0') ) { // required npm@>=8.3.0 to support overrides https://github.com/npm/cli/releases/tag/v8.3.0 prompts.log.error( - `❌ npm@${downloadResult.version} is not supported by migration, please upgrade npm to >=8.3.0 first`, + `❌ npm@${downloadResult.version} is not supported by auto migration, please upgrade npm to >=8.3.0 first`, ); - cancelAndExit('The project is not supported by migration', 1); + cancelAndExit('The project is not supported by auto migration', 1); + } + + // run vite install first to ensure the project is ready + await runViteInstall(workspaceInfo.rootDir, options.interactive); + // check vite and vitest version is supported by migration + const isViteSupported = checkViteVersion(workspaceInfo.rootDir); + const isVitestSupported = checkVitestVersion(workspaceInfo.rootDir); + if (!isViteSupported || !isVitestSupported) { + cancelAndExit('The project is not supported by auto migration', 1); } if (workspaceInfo.isMonorepo) { diff --git a/packages/global/src/migration/migrator.ts b/packages/global/src/migration/migrator.ts index 392d8eb199..60d53f03a4 100644 --- a/packages/global/src/migration/migrator.ts +++ b/packages/global/src/migration/migrator.ts @@ -18,6 +18,8 @@ import { editYamlFile, rulesDir, type YamlDocument, + isJsonFile, + displayRelative, } from '../utils/index.ts'; import { detectConfigs, detectPackageMetadata } from './detector.ts'; @@ -29,11 +31,6 @@ const OVERRIDE_PACKAGES = { } as const; const REMOVE_PACKAGES = ['oxlint', 'oxlint-tsgolint', 'oxfmt']; -/** - * Check the vite version is supported by migration - * @param projectPath - The path to the project - * @returns true if the vite version is supported by migration - */ export function checkViteVersion(projectPath: string): boolean { return checkPackageVersion(projectPath, 'vite', '7.0.0'); } @@ -43,11 +40,11 @@ export function checkVitestVersion(projectPath: string): boolean { } /** - * Check the package version is supported by migration + * Check the package version is supported by auto migration * @param projectPath - The path to the project * @param name - The name of the package * @param minVersion - The minimum version of the package - * @returns true if the package version is supported by migration + * @returns true if the package version is supported by auto migration */ function checkPackageVersion(projectPath: string, name: string, minVersion: string): boolean { const metadata = detectPackageMetadata(projectPath, name); @@ -55,7 +52,10 @@ function checkPackageVersion(projectPath: string, name: string, minVersion: stri return true; } if (semver.satisfies(metadata.version, `<${minVersion}`)) { - prompts.log.error(`❌ ${name} version ${metadata.version} is not supported by migration`); + const packageJsonFilePath = path.join(projectPath, 'package.json'); + prompts.log.error( + `❌ ${name}@${metadata.version} in ${displayRelative(packageJsonFilePath)} is not supported by auto migration`, + ); prompts.log.info(`Please upgrade ${name} to version >=${minVersion} first`); return false; } @@ -105,24 +105,24 @@ export function rewriteStandaloneProject(projectPath: string, workspaceInfo: Wor ...OVERRIDE_PACKAGES, }, }; - } - - for (const [key, version] of Object.entries(OVERRIDE_PACKAGES)) { - if (pkg.devDependencies?.[key]) { - pkg.devDependencies[key] = version; - } - if (pkg.dependencies?.[key]) { - pkg.dependencies[key] = version; + // remove packages from `resolutions` field if they exist + // https://pnpm.io/9.x/package_json#resolutions + for (const key of [...Object.keys(OVERRIDE_PACKAGES), ...REMOVE_PACKAGES]) { + if (pkg.resolutions?.[key]) { + delete pkg.resolutions[key]; + } } } - // add vite-plus to devDependencies - pkg.devDependencies = { - ...pkg.devDependencies, - [VITE_PLUS_NAME]: VITE_PLUS_VERSION, - }; + rewritePackageJson(pkg, packageManager); - rewritePackageJson(pkg); + // ensure vite-plus is in devDependencies + if (!pkg.devDependencies?.[VITE_PLUS_NAME]) { + pkg.devDependencies = { + ...pkg.devDependencies, + [VITE_PLUS_NAME]: VITE_PLUS_VERSION, + }; + } return pkg; }); @@ -180,29 +180,8 @@ export function rewriteMonorepoProject(projectPath: string, packageManager: Pack dependencies?: Record; scripts?: Record; }>(packageJsonPath, (pkg) => { - const isNpm = packageManager === PackageManager.npm; - let needVitePlus = false; - for (const [key, value] of Object.entries(OVERRIDE_PACKAGES)) { - const version = isNpm ? value : 'catalog:'; - if (pkg.devDependencies?.[key]) { - pkg.devDependencies[key] = version; - needVitePlus = true; - } - if (pkg.dependencies?.[key]) { - pkg.dependencies[key] = version; - needVitePlus = true; - } - } - if (needVitePlus) { - // add vite-plus to devDependencies to let vite config `import` rewrite work - pkg.devDependencies = { - ...pkg.devDependencies, - [VITE_PLUS_NAME]: isNpm ? VITE_PLUS_VERSION : 'catalog:', - }; - } - // rewrite scripts in package.json - rewritePackageJson(pkg); + rewritePackageJson(pkg, packageManager, true); return pkg; }); } @@ -223,7 +202,17 @@ function rewritePnpmWorkspaceYaml(projectPath: string): void { // overrides for (const key of Object.keys(OVERRIDE_PACKAGES)) { - doc.setIn(['overrides', key], scalarString('catalog:')); + doc.setIn(['overrides', scalarString(key)], scalarString('catalog:')); + } + // remove dependency selector from vite, e.g. "vite-plugin-svgr>vite": "npm:rolldown-vite@7.0.12" + const overrides = doc.getIn(['overrides']) as YAMLMap, Scalar>; + for (const item of overrides.items) { + if (item.key.value.includes('>')) { + const splits = item.key.value.split('>'); + if (splits[splits.length - 1].trim() === 'vite') { + overrides.delete(item.key); + } + } } // peerDependencyRules.allowAny @@ -248,6 +237,7 @@ function rewritePnpmWorkspaceYaml(projectPath: string): void { allowedVersions = new YAMLMap, Scalar>(); } for (const key of Object.keys(OVERRIDE_PACKAGES)) { + // - vite: '*' allowedVersions.set(scalarString(key), scalarString('*')); } doc.setIn(['peerDependencyRules', 'allowedVersions'], allowedVersions); @@ -344,6 +334,9 @@ function rewriteRootWorkspacePackageJson( resolutions?: Record; overrides?: Record; devDependencies?: Record; + pnpm?: { + overrides?: Record; + }; }>(packageJsonPath, (pkg) => { if (packageManager === PackageManager.yarn) { pkg.resolutions = { @@ -357,14 +350,37 @@ function rewriteRootWorkspacePackageJson( ...pkg.overrides, ...OVERRIDE_PACKAGES, }; + } else if (packageManager === PackageManager.pnpm) { + // pnpm use overrides field at pnpm-workspace.yaml + // so we don't need to set overrides field at package.json + // remove packages from `resolutions` field and `pnpm.overrides` field if they exist + // https://pnpm.io/9.x/package_json#resolutions + for (const key of [...Object.keys(OVERRIDE_PACKAGES), ...REMOVE_PACKAGES]) { + if (pkg.pnpm?.overrides?.[key]) { + delete pkg.pnpm.overrides[key]; + } + if (pkg.resolutions?.[key]) { + delete pkg.resolutions[key]; + } + } + // remove dependency selector from vite, e.g. "vite-plugin-svgr>vite": "npm:rolldown-vite@7.0.12" + for (const key in pkg.pnpm?.overrides) { + if (key.includes('>')) { + const splits = key.split('>'); + if (splits[splits.length - 1].trim() === 'vite') { + delete pkg.pnpm.overrides[key]; + } + } + } } - // pnpm use overrides field at pnpm-workspace.yaml - // add vite-plus to devDependencies - pkg.devDependencies = { - ...pkg.devDependencies, - [VITE_PLUS_NAME]: packageManager === PackageManager.npm ? VITE_PLUS_VERSION : 'catalog:', - }; + // ensure vite-plus is in devDependencies + if (!pkg.devDependencies?.[VITE_PLUS_NAME]) { + pkg.devDependencies = { + ...pkg.devDependencies, + [VITE_PLUS_NAME]: packageManager === PackageManager.npm ? VITE_PLUS_VERSION : 'catalog:', + }; + } return pkg; }); @@ -374,12 +390,16 @@ function rewriteRootWorkspacePackageJson( const RULES_YAML_PATH = path.join(rulesDir, 'vite-tools.yml'); -export function rewritePackageJson(pkg: { - scripts?: Record; - 'lint-staged'?: Record; - devDependencies?: Record; - dependencies?: Record; -}): void { +export function rewritePackageJson( + pkg: { + scripts?: Record; + 'lint-staged'?: Record; + devDependencies?: Record; + dependencies?: Record; + }, + packageManager: PackageManager, + isMonorepo?: boolean, +): void { if (pkg.scripts) { const updated = rewriteScripts( JSON.stringify(pkg.scripts), @@ -398,34 +418,95 @@ export function rewritePackageJson(pkg: { pkg['lint-staged'] = JSON.parse(updated); } } + const supportCatalog = isMonorepo && packageManager !== PackageManager.npm; + let needVitePlus = false; + for (const [key, version] of Object.entries(OVERRIDE_PACKAGES)) { + const value = supportCatalog ? 'catalog:' : version; + if (pkg.devDependencies?.[key]) { + pkg.devDependencies[key] = value; + needVitePlus = true; + } + if (pkg.dependencies?.[key]) { + pkg.dependencies[key] = value; + needVitePlus = true; + } + } // remove packages that are replaced with vite-plus for (const name of REMOVE_PACKAGES) { if (pkg.devDependencies?.[name]) { delete pkg.devDependencies[name]; + needVitePlus = true; } if (pkg.dependencies?.[name]) { delete pkg.dependencies[name]; + needVitePlus = true; } } + if (needVitePlus) { + // add vite-plus to devDependencies + const version = supportCatalog ? 'catalog:' : VITE_PLUS_VERSION; + pkg.devDependencies = { + ...pkg.devDependencies, + [VITE_PLUS_NAME]: version, + }; + } } -// https://github.com/lint-staged/lint-staged?tab=readme-ov-file#configuration +// https://github.com/lint-staged/lint-staged#configuration // only support json format function rewriteLintStagedConfigFile(projectPath: string): void { - const names = ['.lintstagedrc.json', '.lintstagedrc']; - for (const name of names) { - const lintStagedConfigJsonPath = path.join(projectPath, name); - if (fs.existsSync(lintStagedConfigJsonPath)) { - editJsonFile>(lintStagedConfigJsonPath, (config) => { - const updated = rewriteScripts( - JSON.stringify(config), - fs.readFileSync(RULES_YAML_PATH, 'utf8'), + let hasUnsupported = false; + const filenames = ['.lintstagedrc.json', '.lintstagedrc']; + for (const filename of filenames) { + const lintStagedConfigJsonPath = path.join(projectPath, filename); + if (!fs.existsSync(lintStagedConfigJsonPath)) { + continue; + } + if (filename === '.lintstagedrc' && !isJsonFile(lintStagedConfigJsonPath)) { + prompts.log.warn( + `❌ ${displayRelative(lintStagedConfigJsonPath)} is not JSON format file, auto migration is not supported`, + ); + hasUnsupported = true; + continue; + } + editJsonFile>(lintStagedConfigJsonPath, (config) => { + const updated = rewriteScripts( + JSON.stringify(config), + fs.readFileSync(RULES_YAML_PATH, 'utf8'), + ); + if (updated) { + prompts.log.success( + `✅ Rewrote lint-staged config in ${displayRelative(lintStagedConfigJsonPath)}`, ); - if (updated) { - return JSON.parse(updated); - } - }); + return JSON.parse(updated); + } + }); + } + // others non-json files + const others = [ + '.lintstagedrc.yaml', + '.lintstagedrc.yml', + 'lintstagedrc.mjs', + 'lint-staged.config.mjs', + 'lintstagedrc.cjs', + 'lint-staged.config.cjs', + '.lintstagedrc.js', + 'lint-staged.config.js', + ]; + for (const filename of others) { + const lintStagedConfigPath = path.join(projectPath, filename); + if (!fs.existsSync(lintStagedConfigPath)) { + continue; } + prompts.log.warn( + `❌ ${displayRelative(lintStagedConfigPath)} is not supported by auto migration`, + ); + hasUnsupported = true; + } + if (hasUnsupported) { + prompts.log.warn( + `Please migrate the lint-staged config manually, see https://viteplus.dev/migration/#lint-staged for more details`, + ); } } @@ -494,13 +575,14 @@ function rewriteViteConfigFile(projectPath: string): void { export default defineConfig({}); `, ); - prompts.log.success(`✅ Created vite.config.ts in ${configs.viteConfig}`); + prompts.log.success(`✅ Created vite.config.ts in ${displayRelative(viteConfigPath)}`); } if (configs.oxlintConfig) { // merge oxlint config into vite.config.ts mergeAndRemoveJsonConfig(projectPath, configs.viteConfig, configs.oxlintConfig, 'lint'); } if (configs.oxfmtConfig) { + // TODO: handle jsonc file // merge oxfmt config into vite.config.ts mergeAndRemoveJsonConfig(projectPath, configs.viteConfig, configs.oxfmtConfig, 'fmt'); } @@ -518,9 +600,13 @@ function mergeAndRemoveJsonConfig( if (result.updated) { fs.writeFileSync(fullViteConfigPath, result.content); fs.unlinkSync(fullJsonConfigPath); - prompts.log.success(`✅ Merged ${jsonConfigPath} into ${viteConfigPath}`); + prompts.log.success( + `✅ Merged ${displayRelative(fullJsonConfigPath)} into ${displayRelative(fullViteConfigPath)}`, + ); } else { - prompts.log.warn(`❌ Failed to merge ${jsonConfigPath} into ${viteConfigPath}`); + prompts.log.warn( + `❌ Failed to merge ${displayRelative(fullJsonConfigPath)} into ${displayRelative(fullViteConfigPath)}`, + ); prompts.log.info( `Please complete the merge manually and follow the instructions in the documentation: https://viteplus.dev/config/`, ); @@ -532,7 +618,7 @@ function rewriteViteConfigImport(projectPath: string, viteConfigPath: string): v const result = rewriteImport(fullPath); if (result.updated) { fs.writeFileSync(fullPath, result.content); - prompts.log.success(`✅ Rewrote import in ${viteConfigPath}`); + prompts.log.success(`✅ Rewrote import in ${displayRelative(fullPath)}`); } } diff --git a/packages/global/src/utils/json.ts b/packages/global/src/utils/json.ts index 8c21708525..98a7e6497e 100644 --- a/packages/global/src/utils/json.ts +++ b/packages/global/src/utils/json.ts @@ -30,3 +30,12 @@ export function editJsonFile>( writeJsonFile(file, newJson); } } + +export function isJsonFile(file: string): boolean { + try { + readJsonFile(file); + return true; + } catch { + return false; + } +} diff --git a/packages/global/src/utils/path.ts b/packages/global/src/utils/path.ts index a1c81aa566..fe78995bbb 100644 --- a/packages/global/src/utils/path.ts +++ b/packages/global/src/utils/path.ts @@ -9,3 +9,7 @@ export const pkgRoot = import.meta.dirname.endsWith('dist') export const templatesDir = path.join(pkgRoot, 'templates'); export const rulesDir = path.join(pkgRoot, 'rules'); + +export function displayRelative(to: string, from = process.cwd()): string { + return path.relative(from, to).replaceAll('\\', '/'); +} diff --git a/rfcs/migration-command.md b/rfcs/migration-command.md index 9700aa6d80..91d051cdce 100644 --- a/rfcs/migration-command.md +++ b/rfcs/migration-command.md @@ -416,8 +416,8 @@ peerDependencyRules: - vite - vitest allowedVersions: - vite: '*' - vitest: '*' + vite: 'npm:@voidzero-dev/vite-plus-core@*' + vitest: 'npm:@voidzero-dev/vite-plus-test@*' ``` ### for npm From 2b08b1f472ffac56e4baf045f8f6b04111aa5c88 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Fri, 5 Dec 2025 21:45:23 +0800 Subject: [PATCH 4/6] Update vite_config.rs --- crates/vite_migration/src/vite_config.rs | 2 +- rfcs/migration-command.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/vite_migration/src/vite_config.rs b/crates/vite_migration/src/vite_config.rs index 1e23192fd6..e404bb7d07 100644 --- a/crates/vite_migration/src/vite_config.rs +++ b/crates/vite_migration/src/vite_config.rs @@ -78,7 +78,7 @@ pub struct RewriteResult { /// /// * `vite_config_path` - Path to the vite.config.ts or vite.config.js file /// * `json_config_path` - Path to the JSON config file (e.g., .oxlintrc.json, .oxfmtrc.json) -/// * `config_key` - The key to use in the vite config (e.g., "lint", "format") +/// * `config_key` - The key to use in the vite config (e.g., "lint", "fmt") /// /// # Returns /// diff --git a/rfcs/migration-command.md b/rfcs/migration-command.md index 91d051cdce..9700aa6d80 100644 --- a/rfcs/migration-command.md +++ b/rfcs/migration-command.md @@ -416,8 +416,8 @@ peerDependencyRules: - vite - vitest allowedVersions: - vite: 'npm:@voidzero-dev/vite-plus-core@*' - vitest: 'npm:@voidzero-dev/vite-plus-test@*' + vite: '*' + vitest: '*' ``` ### for npm From 8bdbabc6f1818e138fe0969e9528fe6bfd8fb2e9 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Fri, 5 Dec 2025 21:52:22 +0800 Subject: [PATCH 5/6] Update migration.rs --- packages/cli/binding/src/migration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/binding/src/migration.rs b/packages/cli/binding/src/migration.rs index ac95c1f352..139a85819b 100644 --- a/packages/cli/binding/src/migration.rs +++ b/packages/cli/binding/src/migration.rs @@ -47,7 +47,7 @@ pub struct MergeJsonConfigResult { /// /// * `vite_config_path` - Path to the vite.config.ts or vite.config.js file /// * `json_config_path` - Path to the JSON config file (e.g., .oxlintrc, .oxfmtrc) -/// * `config_key` - The key to use in the vite config (e.g., "lint", "format") +/// * `config_key` - The key to use in the vite config (e.g., "lint", "fmt") /// /// # Returns /// From 421f492175bc98bedc090420e704b30f2832cd86 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Fri, 5 Dec 2025 21:53:17 +0800 Subject: [PATCH 6/6] Update vite_config.rs --- crates/vite_migration/src/vite_config.rs | 4 ++-- packages/cli/binding/index.d.ts | 2 +- .../snap-tests/gen-create-vite-with-scope-name/steps.json | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/vite_migration/src/vite_config.rs b/crates/vite_migration/src/vite_config.rs index e404bb7d07..a2edc3ab16 100644 --- a/crates/vite_migration/src/vite_config.rs +++ b/crates/vite_migration/src/vite_config.rs @@ -99,11 +99,11 @@ pub struct RewriteResult { /// Path::new(".oxlintrc"), /// "lint", /// )?; -/// -/// // Merge oxfmt config with "format" key +/// // Merge oxfmt config with "fmt" key /// let result = merge_json_config( /// Path::new("vite.config.ts"), /// Path::new(".oxfmtrc.json"), +/// "fmt", /// "format", /// )?; /// diff --git a/packages/cli/binding/index.d.ts b/packages/cli/binding/index.d.ts index 63c5e6327f..9af5416aea 100644 --- a/packages/cli/binding/index.d.ts +++ b/packages/cli/binding/index.d.ts @@ -137,7 +137,7 @@ export interface JsCommandResolvedResult { * * * `vite_config_path` - Path to the vite.config.ts or vite.config.js file * * `json_config_path` - Path to the JSON config file (e.g., .oxlintrc, .oxfmtrc) - * * `config_key` - The key to use in the vite config (e.g., "lint", "format") + * * `config_key` - The key to use in the vite config (e.g., "lint", "fmt") * * # Returns * diff --git a/packages/global/snap-tests/gen-create-vite-with-scope-name/steps.json b/packages/global/snap-tests/gen-create-vite-with-scope-name/steps.json index cbf1bda3be..e3ce141af2 100644 --- a/packages/global/snap-tests/gen-create-vite-with-scope-name/steps.json +++ b/packages/global/snap-tests/gen-create-vite-with-scope-name/steps.json @@ -1,4 +1,5 @@ { + "ignoredPlatforms": ["win32"], "env": { "VITE_DISABLE_AUTO_INSTALL": "1" },