diff --git a/Cargo.lock b/Cargo.lock index 186de225a4..8ce9fafe70 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" @@ -4391,6 +4402,7 @@ dependencies = [ "ast-grep-core", "ast-grep-language", "serde_json", + "tempfile", "vite_error", ] 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/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/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 6e54c1f2a7..499c820e1c 100644 --- a/crates/vite_migration/src/lib.rs +++ b/crates/vite_migration/src/lib.rs @@ -1,3 +1,6 @@ +mod ast_grep; mod package; +mod vite_config; pub use package::rewrite_scripts; +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 new file mode 100644 index 0000000000..a2edc3ab16 --- /dev/null +++ b/crates/vite_migration/src/vite_config.rs @@ -0,0 +1,1269 @@ +use std::path::Path; + +use ast_grep_config::{GlobalRules, RuleConfig, from_yaml_string}; +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 { + /// 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, +} + +/// 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 +/// 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", "fmt") +/// +/// # 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 "fmt" key +/// let result = merge_json_config( +/// Path::new("vite.config.ts"), +/// Path::new(".oxfmtrc.json"), +/// "fmt", +/// "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) +} + +/// 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. +/// 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", "fmt") +/// +/// # 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); + + // Apply the transformation + let (content, updated) = ast_grep::apply_rules(vite_config_content, &rule_yaml)?; + + Ok(MergeResult { content, 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 std::io::Write; + + use tempfile::tempdir; + + 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() { + // Create temporary directory (automatically cleaned up when dropped) + let temp_dir = tempdir().unwrap(); + + 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(); + write!( + 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(); + write!( + 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: [], +});"# + ); + } + + #[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: [], +})" + ); + } + + #[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 2ea2cc4c65..9af5416aea 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", "fmt") + * + * # 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 */ @@ -137,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 57a6c23bbc..0d93fe7980 100644 --- a/packages/cli/binding/index.js +++ b/packages/cli/binding/index.js @@ -744,9 +744,19 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`); } -const { detectWorkspace, downloadPackageManager, rewriteScripts, run, runCommand } = nativeBinding; +const { + detectWorkspace, + downloadPackageManager, + mergeJsonConfig, + rewriteImport, + rewriteScripts, + run, + runCommand, +} = nativeBinding; 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 9d7c100e20..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::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 76792e5499..139a85819b 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,96 @@ 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, + }) +} + +/// 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/gen-check/steps.json b/packages/global/snap-tests/gen-check/steps.json index 46fba90809..2349ef6912 100644 --- a/packages/global/snap-tests/gen-check/steps.json +++ b/packages/global/snap-tests/gen-check/steps.json @@ -1,4 +1,5 @@ { + "ignoredPlatforms": ["win32"], "commands": [ "vp gen --help # show help", "vp gen --list # list templates", diff --git a/packages/global/snap-tests/gen-create-tsdown/steps.json b/packages/global/snap-tests/gen-create-tsdown/steps.json index bc66907ee6..c45ece5fb6 100644 --- a/packages/global/snap-tests/gen-create-tsdown/steps.json +++ b/packages/global/snap-tests/gen-create-tsdown/steps.json @@ -1,4 +1,5 @@ { + "ignoredPlatforms": ["win32"], "env": { "VITE_DISABLE_AUTO_INSTALL": "1" }, 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" }, diff --git a/packages/global/snap-tests/gen-create-vite/steps.json b/packages/global/snap-tests/gen-create-vite/steps.json index 9365c56345..0d12ce8887 100644 --- a/packages/global/snap-tests/gen-create-vite/steps.json +++ b/packages/global/snap-tests/gen-create-vite/steps.json @@ -1,4 +1,5 @@ { + "ignoredPlatforms": ["win32"], "env": { "VITE_DISABLE_AUTO_INSTALL": "1" }, diff --git a/packages/global/snap-tests/gen-vite-monorepo/steps.json b/packages/global/snap-tests/gen-vite-monorepo/steps.json index 37840f556c..0b70c14710 100644 --- a/packages/global/snap-tests/gen-vite-monorepo/steps.json +++ b/packages/global/snap-tests/gen-vite-monorepo/steps.json @@ -1,4 +1,5 @@ { + "ignoredPlatforms": ["win32"], "commands": [ { "command": "vp gen vite:monorepo --no-interactive # create monorepo with default values", 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..be863dfd90 --- /dev/null +++ b/packages/global/snap-tests/migration-auto-create-vite-config/snap.txt @@ -0,0 +1,55 @@ +> vp migration # migration should auto create vite.config.ts and remove oxlintrc and oxfmtrc +┌ Vite+ Migration +│ +● 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 +│ +└ ✨ Migration completed! + + +> cat vite.config.ts # check vite.config.ts +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', + }, + }, +}); + +> 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": { + "@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 new file mode 100644 index 0000000000..a5f6122791 --- /dev/null +++ b/packages/global/snap-tests/migration-auto-create-vite-config/steps.json @@ -0,0 +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 package.json # check package.json" + ] +} diff --git a/packages/global/snap-tests/migration-lintstagedrc/.lintstagedrc.json b/packages/global/snap-tests/migration-lintstagedrc-json/.lintstagedrc.json similarity index 100% rename from packages/global/snap-tests/migration-lintstagedrc/.lintstagedrc.json rename to packages/global/snap-tests/migration-lintstagedrc-json/.lintstagedrc.json diff --git a/packages/global/snap-tests/migration-lintstagedrc/package.json b/packages/global/snap-tests/migration-lintstagedrc-json/package.json similarity index 100% rename from packages/global/snap-tests/migration-lintstagedrc/package.json rename to packages/global/snap-tests/migration-lintstagedrc-json/package.json diff --git a/packages/global/snap-tests/migration-lintstagedrc/snap.txt b/packages/global/snap-tests/migration-lintstagedrc-json/snap.txt similarity index 95% rename from packages/global/snap-tests/migration-lintstagedrc/snap.txt rename to packages/global/snap-tests/migration-lintstagedrc-json/snap.txt index 88e6d1db08..ccd9fded5a 100644 --- a/packages/global/snap-tests/migration-lintstagedrc/snap.txt +++ b/packages/global/snap-tests/migration-lintstagedrc-json/snap.txt @@ -32,6 +32,8 @@ Aliases: migrate │ ● pnpm@ 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 81% rename from packages/global/snap-tests/migration-lintstagedrc/steps.json rename to packages/global/snap-tests/migration-lintstagedrc-json/steps.json index 7c3592f75c..a01566d37a 100644 --- a/packages/global/snap-tests/migration-lintstagedrc/steps.json +++ b/packages/global/snap-tests/migration-lintstagedrc-json/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-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-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-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/.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..823813feef --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/package.json @@ -0,0 +1,30 @@ +{ + "name": "migration-monorepo-pnpm", + "version": "1.0.0", + "packageManager": "pnpm@10.18.0", + "dependencies": { + "testnpm2": "1.0.0", + "vite": "catalog:" + }, + "resolutions": { + "vue": "3.5.25", + "vite": "catalog:", + "vitest": "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/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/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..5231a9e9c5 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/snap.txt @@ -0,0 +1,173 @@ +> 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 +│ +◆ ✅ 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! + + +> 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:" + }, + "resolutions": { + "vue": "3.5.25" + }, + "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" + } +} + +> 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 new file mode 100644 index 0000000000..8e75dd1535 --- /dev/null +++ b/packages/global/snap-tests/migration-monorepo-pnpm/steps.json @@ -0,0 +1,18 @@ +{ + "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", + "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-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..d37cd790a5 --- /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 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 +{ + "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..aa5669fdc5 --- /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 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 +{ + "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..317edc65b3 --- /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@ in package.json is not supported by auto migration +│ +● Please upgrade vite to version >=7.0.0 first +└ The project is not supported by auto 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..0163bbc528 --- /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@ in package.json is not supported by auto migration +│ +● Please upgrade vitest to version >=4.0.0 first +└ The project is not supported by auto 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/__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 1ab5add3f9..f203181ef5 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; @@ -108,18 +114,48 @@ async function main() { // 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 auto migration, please upgrade pnpm to >=9.5.0 first`, + ); + 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 auto migration, please upgrade npm to >=8.3.0 first`, + ); + 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) { 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 39430dbf21..60d53f03a4 100644 --- a/packages/global/src/migration/migrator.ts +++ b/packages/global/src/migration/migrator.ts @@ -1,7 +1,14 @@ 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, + 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'; @@ -11,7 +18,10 @@ import { editYamlFile, rulesDir, type YamlDocument, + isJsonFile, + displayRelative, } from '../utils/index.ts'; +import { detectConfigs, detectPackageMetadata } from './detector.ts'; const VITE_PLUS_NAME = '@voidzero-dev/vite-plus'; const VITE_PLUS_VERSION = 'latest'; @@ -21,6 +31,37 @@ const OVERRIDE_PACKAGES = { } as const; const REMOVE_PACKAGES = ['oxlint', 'oxlint-tsgolint', 'oxfmt']; +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 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 auto 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}`)) { + 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; + } + return true; +} + /** * Rewrite standalone project to add vite-plus dependencies * @param projectPath - The path to the project @@ -64,30 +105,31 @@ 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; }); // set .npmrc to use vite-plus rewriteNpmrc(projectPath); rewriteLintStagedConfigFile(projectPath); + rewriteViteConfigFile(projectPath); // set package manager setPackageManager(projectPath, workspaceInfo.downloadPackageManager); } @@ -116,6 +158,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); } @@ -125,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; @@ -135,19 +180,8 @@ export function rewriteMonorepoProject(projectPath: string, packageManager: Pack dependencies?: Record; scripts?: Record; }>(packageJsonPath, (pkg) => { - const isNpm = packageManager === PackageManager.npm; - for (const [key, value] of Object.entries(OVERRIDE_PACKAGES)) { - const version = isNpm ? value : 'catalog:'; - if (pkg.devDependencies?.[key]) { - pkg.devDependencies[key] = version; - } - if (pkg.dependencies?.[key]) { - pkg.dependencies[key] = version; - } - } - // rewrite scripts in package.json - rewritePackageJson(pkg); + rewritePackageJson(pkg, packageManager, true); return pkg; }); } @@ -168,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 @@ -193,13 +237,23 @@ 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); // 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 >; @@ -207,12 +261,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); @@ -283,6 +334,9 @@ function rewriteRootWorkspacePackageJson( resolutions?: Record; overrides?: Record; devDependencies?: Record; + pnpm?: { + overrides?: Record; + }; }>(packageJsonPath, (pkg) => { if (packageManager === PackageManager.yarn) { pkg.resolutions = { @@ -296,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; }); @@ -313,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), @@ -337,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`, + ); } } @@ -402,6 +544,84 @@ 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 = 'vite.config.ts'; + const viteConfigPath = path.join(projectPath, 'vite.config.ts'); + fs.writeFileSync( + viteConfigPath, + `import { defineConfig } from '${VITE_PLUS_NAME}'; + +export default defineConfig({}); +`, + ); + 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'); + } +} + +function mergeAndRemoveJsonConfig( + projectPath: string, + viteConfigPath: string, + jsonConfigPath: string, + configKey: string, +): void { + const fullViteConfigPath = path.join(projectPath, viteConfigPath); + const fullJsonConfigPath = path.join(projectPath, jsonConfigPath); + const result = mergeJsonConfig(fullViteConfigPath, fullJsonConfigPath, configKey); + if (result.updated) { + fs.writeFileSync(fullViteConfigPath, result.content); + fs.unlinkSync(fullJsonConfigPath); + prompts.log.success( + `✅ Merged ${displayRelative(fullJsonConfigPath)} into ${displayRelative(fullViteConfigPath)}`, + ); + } else { + 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/`, + ); + } +} + +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 ${displayRelative(fullPath)}`); + } +} + function setPackageManager( projectDir: string, downloadPackageManager: DownloadPackageManagerResult, 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 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