diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/custom.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/custom.json new file mode 100644 index 0000000000..2ff50f91ec --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/custom.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-console": "error" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/package.json new file mode 100644 index 0000000000..544998f804 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/package.json @@ -0,0 +1,5 @@ +{ + "name": "@test/lint-config-arg", + "version": "1.0.0", + "private": true +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/snapshots.toml new file mode 100644 index 0000000000..2f018bccbe --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/snapshots.toml @@ -0,0 +1,9 @@ +[[case]] +name = "lint_config_arg" +vp = "local" +steps = [ + { argv = ["vp", "lint", "-c", "custom.json"], comment = "Should apply lint from custom.json", continue-on-failure = true }, + { argv = ["vp", "lint", "-c=custom.json"], comment = "Should apply lint from custom.json", continue-on-failure = true }, + { argv = ["vp", "lint", "--config", "custom.json"], comment = "Should apply lint from custom.json", continue-on-failure = true }, + { argv = ["vp", "lint", "--config=custom.json"], comment = "Should apply lint from custom.json", continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/snapshots/lint_config_arg.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/snapshots/lint_config_arg.md new file mode 100644 index 0000000000..c896999fc8 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/snapshots/lint_config_arg.md @@ -0,0 +1,85 @@ +# lint_config_arg + +## `vp lint -c custom.json` + +Should apply lint from custom.json + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[src/invalid.js:2:3] + 1 │ export function example() { + 2 │ console.log('hello'); + · ─────────── + 3 │ return 'hello'; + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 3 files with rules using threads. +``` + +## `vp lint -c=custom.json` + +Should apply lint from custom.json + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[src/invalid.js:2:3] + 1 │ export function example() { + 2 │ console.log('hello'); + · ─────────── + 3 │ return 'hello'; + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 3 files with rules using threads. +``` + +## `vp lint --config custom.json` + +Should apply lint from custom.json + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[src/invalid.js:2:3] + 1 │ export function example() { + 2 │ console.log('hello'); + · ─────────── + 3 │ return 'hello'; + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 3 files with rules using threads. +``` + +## `vp lint --config=custom.json` + +Should apply lint from custom.json + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[src/invalid.js:2:3] + 1 │ export function example() { + 2 │ console.log('hello'); + · ─────────── + 3 │ return 'hello'; + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 3 files with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/src/invalid.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/src/invalid.js new file mode 100644 index 0000000000..7678544f29 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/src/invalid.js @@ -0,0 +1,4 @@ +export function example() { + console.log('hello'); + return 'hello'; +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/src/valid.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/src/valid.js new file mode 100644 index 0000000000..56e48cba39 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/src/valid.js @@ -0,0 +1,3 @@ +export function example() { + return 'hello'; +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/vite.config.ts new file mode 100644 index 0000000000..c1a657f0b3 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_config_arg/vite.config.ts @@ -0,0 +1,7 @@ +export default { + lint: { + rules: { + 'no-console': 'off', + }, + }, +}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_config_arg/.oxlintrc.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_config_arg/.oxlintrc.json new file mode 100644 index 0000000000..2ff50f91ec --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_config_arg/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-console": "error" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_config_arg/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_config_arg/package.json new file mode 100644 index 0000000000..4b5d35cd8f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_config_arg/package.json @@ -0,0 +1,8 @@ +{ + "scripts": { + "lint": "oxlint -c .oxlintrc.json" + }, + "devDependencies": { + "oxlint": "1.82.0" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_config_arg/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_config_arg/snapshots.toml new file mode 100644 index 0000000000..4c528c3219 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_config_arg/snapshots.toml @@ -0,0 +1,9 @@ +[[case]] +name = "migration_oxlint_config_arg" +vp = "global" +steps = [ + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "Migrate a project with an Oxlint config argument", continue-on-failure = true }, + { argv = ["vpt", "print-file", "package.json"], comment = "oxlint -c .oxlintrc.json should be rewritten to vp lint", continue-on-failure = true }, + { argv = ["vpt", "print-file", "vite.config.ts"], comment = ".oxlintrc.json should be merged into vite.config.ts", continue-on-failure = true }, + { argv = ["vpt", "stat-file", ".oxlintrc.json", "--assert-not", "file"], comment = "The merged .oxlintrc.json should be removed", continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_config_arg/snapshots/migration_oxlint_config_arg.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_config_arg/snapshots/migration_oxlint_config_arg.md new file mode 100644 index 0000000000..c72caa69f7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_config_arg/snapshots/migration_oxlint_config_arg.md @@ -0,0 +1,72 @@ +# migration_oxlint_config_arg + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +Migrate a project with an Oxlint config argument + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Migrated . to Vite+ +• Node pnpm +• 2 config updates applied +``` + +## `vpt print-file package.json` + +oxlint -c .oxlintrc.json should be rewritten to vp lint + +``` +{ + "scripts": { + "lint": "vp lint" + }, + "devDependencies": { + "vite": "catalog:", + "vite-plus": "catalog:" + }, + "devEngines": { + "packageManager": { + "name": "pnpm", + "version": "", + "onFail": "download" + } + } +} +``` + +## `vpt print-file vite.config.ts` + +.oxlintrc.json should be merged into vite.config.ts + +``` +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + fmt: {}, + lint: { + "rules": { + "no-console": "error", + "vite-plus/prefer-vite-plus-imports": "error" + }, + "options": { + "typeAware": true, + "typeCheck": true + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] + }, +}); +``` + +## `vpt stat-file .oxlintrc.json --assert-not file` + +The merged .oxlintrc.json should be removed + +``` +.oxlintrc.json: missing +``` diff --git a/crates/vp_migration/src/lib.rs b/crates/vp_migration/src/lib.rs index 427db5ea50..a4c9a411d2 100644 --- a/crates/vp_migration/src/lib.rs +++ b/crates/vp_migration/src/lib.rs @@ -21,7 +21,7 @@ pub use import_rewriter::{ BatchRewriteResult, RewriteImportsOptions, rewrite_imports_in_directory, rewrite_imports_in_directory_with_options, }; -pub use package::{rewrite_eslint, rewrite_prettier, rewrite_scripts}; +pub use package::{rewrite_eslint, rewrite_oxlint, rewrite_prettier, rewrite_scripts}; pub use vite_config::{ MergeResult, has_config_key, merge_json_config, merge_tsdown_config, upsert_json_config, wrap_lazy_plugins, diff --git a/crates/vp_migration/src/package.rs b/crates/vp_migration/src/package.rs index 59a3ce07ca..8d22e3bb0f 100644 --- a/crates/vp_migration/src/package.rs +++ b/crates/vp_migration/src/package.rs @@ -7,7 +7,10 @@ use crate::{ ast_grep, eslint::rewrite_eslint_script, prettier::rewrite_prettier_script, - script_rewrite::{rewrite_bunx_commands, rewrite_pack_flags}, + script_rewrite::{ + ScriptRewriteConfig, rewrite_bunx_commands, rewrite_pack_flags, + rewrite_script_stripping_flags_with_matching_value, + }, }; // Marker to replace "cross-env " before ast-grep processing @@ -15,6 +18,24 @@ use crate::{ const CROSS_ENV_MARKER: &str = "__CROSS_ENV__=1 "; const CROSS_ENV_REPLACEMENT: &str = "cross-env "; +/// Rewrite Oxlint commands and strip config arguments referencing the migrated config. +fn rewrite_oxlint_script(script: &str, oxlint_config_path: &str) -> String { + if !script.contains("oxlint") { + return script.to_owned(); + } + rewrite_script_stripping_flags_with_matching_value( + script, + &ScriptRewriteConfig { + source_command: "oxlint", + target_subcommand: "lint", + boolean_flags: &[], + value_flags: &["-c", "--config"], + flag_conversions: &[], + }, + oxlint_config_path, + ) +} + /// rewrite a single script command string using rules fn rewrite_script(script: &str, rules: &[RuleConfig]) -> String { // Only handle cross-env replacement if it's present in the script @@ -100,10 +121,18 @@ pub fn rewrite_prettier(scripts_json: &str) -> Result, Error> { transform_scripts_json(scripts_json, rewrite_prettier_script) } -/// rewrite scripts json content using rules from `rules_yaml` +/// Rewrite Oxlint commands and strip config arguments referencing a config that will be merged. +pub fn rewrite_oxlint( + scripts_json: &str, + oxlint_config_path: &str, +) -> Result, Error> { + transform_scripts_json(scripts_json, |script| rewrite_oxlint_script(script, oxlint_config_path)) +} + +/// Rewrite scripts json content using rules from `rules_yaml`. pub fn rewrite_scripts(scripts_json: &str, rules_yaml: &str) -> Result, Error> { let rules = ast_grep::load_rules(rules_yaml)?; - transform_scripts_json(scripts_json, |raw_script| rewrite_script(raw_script, &rules)) + transform_scripts_json(scripts_json, |script| rewrite_script(script, &rules)) } #[cfg(test)] @@ -392,6 +421,50 @@ fix: vp pack assert_eq!(rewrite_script("husky || true", &rules), "husky || true"); } + #[test] + fn test_rewrite_script_with_oxlint_config() { + let rules = ast_grep::load_rules(RULES_YAML).unwrap(); + for (input, expected) in [ + // Basic cases + ("oxlint -c .oxlintrc.json", "vp lint"), + ("oxlint -c=.oxlintrc.json", "vp lint"), + ("oxlint --config .oxlintrc.json", "vp lint"), + ("oxlint --config=.oxlintrc.json", "vp lint"), + // With quotes + ("oxlint -c \"./.oxlintrc.json\"", "vp lint"), + ("oxlint -c=\"./.oxlintrc.json\"", "vp lint"), + ("oxlint --config \"./.oxlintrc.json\"", "vp lint"), + ("oxlint --config=\"./.oxlintrc.json\"", "vp lint"), + ("oxlint -c './.oxlintrc.json'", "vp lint"), + ("oxlint -c='./.oxlintrc.json'", "vp lint"), + ("oxlint --config './.oxlintrc.json'", "vp lint"), + ("oxlint --config='./.oxlintrc.json'", "vp lint"), + // With custom file + ("oxlint -c custom.json", "vp lint -c custom.json"), + ("oxlint -c=custom.json", "vp lint -c=custom.json"), + ("oxlint --config custom.json", "vp lint --config custom.json"), + ("oxlint --config=custom.json", "vp lint --config=custom.json"), + // With combined flags + ("oxlint -c .oxlintrc.json --fix", "vp lint --fix"), + ("oxlint -c .oxlintrc.json --type-aware", "vp lint --type-aware"), + ("oxlint -c .oxlintrc.json --fix --type-aware", "vp lint --fix --type-aware"), + ("oxlint --config .oxlintrc.json --fix", "vp lint --fix"), + ("oxlint --config .oxlintrc.json --type-aware", "vp lint --type-aware"), + ("oxlint --config .oxlintrc.json --fix --type-aware", "vp lint --fix --type-aware"), + // With combined commands + ("oxlint -c .oxlintrc.json --fix && pnpm run test", "vp lint --fix && pnpm run test"), + ("cross-env MODE=test oxlint -c .oxlintrc.json", "cross-env MODE=test vp lint"), + ("bunx --bun oxlint --config=./.oxlintrc.json", "bunx --bun vp lint"), + // With a Windows path separator + ("oxlint -c .\\.oxlintrc.json", "vp lint"), + ] { + assert_eq!( + rewrite_script(&rewrite_oxlint_script(input, ".oxlintrc.json"), &rules), + expected + ); + } + } + #[test] fn test_rewrite_package_json_scripts_success() { let package_json_scripts = r#" diff --git a/crates/vp_migration/src/script_rewrite.rs b/crates/vp_migration/src/script_rewrite.rs index 6436fcf1bb..317a87752a 100644 --- a/crates/vp_migration/src/script_rewrite.rs +++ b/crates/vp_migration/src/script_rewrite.rs @@ -32,9 +32,27 @@ const SHELL_CONTINUATION_KEYWORDS: &[&str] = &["then", "do", "else", "elif", "in /// Rewrite a shell script: find `source_command`, rename to `vp `, /// strip tool-specific flags, and normalize the output. pub fn rewrite_script(script: &str, config: &ScriptRewriteConfig) -> String { + rewrite_script_inner(script, config, None) +} + +/// Rewrite a shell script, but only strip value flags whose value matches +/// `matching_value`. +pub fn rewrite_script_stripping_flags_with_matching_value( + script: &str, + config: &ScriptRewriteConfig, + matching_value: &str, +) -> String { + rewrite_script_inner(script, config, Some(matching_value)) +} + +fn rewrite_script_inner( + script: &str, + config: &ScriptRewriteConfig, + value_to_strip: Option<&str>, +) -> String { let rewritten_bunx = - rewrite_bunx_commands(script, |inner| rewrite_direct_script(inner, config)); - rewrite_direct_script(&rewritten_bunx, config) + rewrite_bunx_commands(script, |inner| rewrite_direct_script(inner, config, value_to_strip)); + rewrite_direct_script(&rewritten_bunx, config, value_to_strip) } /// Rename the removed tsdown copy flag only in `vp pack` invocations. The @@ -98,7 +116,11 @@ fn rewrite_direct_pack_flags(script: &str) -> String { } } -fn rewrite_direct_script(script: &str, config: &ScriptRewriteConfig) -> String { +fn rewrite_direct_script( + script: &str, + config: &ScriptRewriteConfig, + value_to_strip: Option<&str>, +) -> String { let mut parser = brush_parser::Parser::new( script.as_bytes(), &brush_parser::ParserOptions::default(), @@ -108,15 +130,21 @@ fn rewrite_direct_script(script: &str, config: &ScriptRewriteConfig) -> String { return script.to_owned(); }; - if !rewrite_in_program(&mut program, config) { + if !rewrite_in_program(&mut program, config, value_to_strip) { return script.to_owned(); } let output = normalize_pipe_spacing(&program.to_string()); collapse_newlines(&output) } -fn rewrite_in_program(program: &mut ast::Program, config: &ScriptRewriteConfig) -> bool { - visit_simple_commands(program, &mut |cmd| rewrite_in_simple_command(cmd, config)) +fn rewrite_in_program( + program: &mut ast::Program, + config: &ScriptRewriteConfig, + value_to_strip: Option<&str>, +) -> bool { + visit_simple_commands(program, &mut |cmd| { + rewrite_in_simple_command(cmd, config, value_to_strip) + }) } fn visit_simple_commands( @@ -449,7 +477,11 @@ fn make_suffix_word(value: &str) -> ast::CommandPrefixOrSuffixItem { ast::CommandPrefixOrSuffixItem::Word(ast::Word { value: value.to_owned(), loc: None }) } -fn rewrite_in_simple_command(cmd: &mut ast::SimpleCommand, config: &ScriptRewriteConfig) -> bool { +fn rewrite_in_simple_command( + cmd: &mut ast::SimpleCommand, + config: &ScriptRewriteConfig, + value_to_strip: Option<&str>, +) -> bool { let cmd_name = cmd.word_or_name.as_ref().map(|w| w.value.as_str()); if cmd_name == Some(config.source_command) { @@ -463,18 +495,22 @@ fn rewrite_in_simple_command(cmd: &mut ast::SimpleCommand, config: &ScriptRewrit Some(ast::CommandSuffix(vec![make_suffix_word(config.target_subcommand)])); } } - strip_flags_from_suffix(cmd, 1, config); + strip_flags_from_suffix(cmd, 1, config, value_to_strip); return true; } if cmd_name == Some("cross-env") || cmd_name == Some("cross-env-shell") { - return rewrite_in_cross_env(cmd, config); + return rewrite_in_cross_env(cmd, config, value_to_strip); } false } -fn rewrite_in_cross_env(cmd: &mut ast::SimpleCommand, config: &ScriptRewriteConfig) -> bool { +fn rewrite_in_cross_env( + cmd: &mut ast::SimpleCommand, + config: &ScriptRewriteConfig, + value_to_strip: Option<&str>, +) -> bool { let suffix = match &mut cmd.suffix { Some(s) => s, None => return false, @@ -492,7 +528,7 @@ fn rewrite_in_cross_env(cmd: &mut ast::SimpleCommand, config: &ScriptRewriteConf } suffix.0.insert(idx + 1, make_suffix_word(config.target_subcommand)); - strip_flags_from_suffix(cmd, idx + 2, config); + strip_flags_from_suffix(cmd, idx + 2, config, value_to_strip); true } @@ -503,10 +539,11 @@ fn strip_flags_from_suffix( cmd: &mut ast::SimpleCommand, start_idx: usize, config: &ScriptRewriteConfig, + value_to_strip: Option<&str>, ) { let suffix = cmd.suffix.as_mut().expect("suffix was just set"); let items = std::mem::take(&mut suffix.0); - let mut iter = items.into_iter().enumerate(); + let mut iter = items.into_iter().enumerate().peekable(); // Keep items before start_idx unconditionally for (i, item) in iter.by_ref() { @@ -516,15 +553,16 @@ fn strip_flags_from_suffix( } } - let mut skip_next = false; // One dedup tracker per flag conversion rule (no allocation when empty) let mut conversion_emitted = vec![false; config.flag_conversions.len()]; + let value_matches = |value: &str, expected: &str| { + let value = value.trim_matches(['\'', '"']); + value == expected + || value.strip_prefix("./") == Some(expected) + || value.strip_prefix(".\\") == Some(expected) + }; - for (_, item) in iter { - if skip_next { - skip_next = false; - continue; - } + while let Some((_, item)) = iter.next() { if let ast::CommandPrefixOrSuffixItem::Word(ref w) = item { let val = w.value.as_str(); @@ -536,14 +574,23 @@ fn strip_flags_from_suffix( // Value flags: --flag=value form if let Some(eq_pos) = val.find('=') && config.value_flags.contains(&&val[..eq_pos]) + && value_to_strip.is_none_or(|expected| value_matches(&val[eq_pos + 1..], expected)) { continue; } // Value flags: --flag value form (strip flag + next token) if config.value_flags.contains(&val) { - skip_next = true; - continue; + let should_strip = value_to_strip.is_none_or(|expected| { + iter.peek().is_some_and(|(_, item)| { + matches!(item, ast::CommandPrefixOrSuffixItem::Word(word) + if value_matches(&word.value, expected)) + }) + }); + if should_strip { + iter.next(); + continue; + } } // Flag conversions + dedup tracking in a single pass diff --git a/packages/cli/binding/index.cjs b/packages/cli/binding/index.cjs index 67109dc1e8..6a59427ef3 100644 --- a/packages/cli/binding/index.cjs +++ b/packages/cli/binding/index.cjs @@ -976,6 +976,7 @@ module.exports.parseMigrateArgs = nativeBinding.parseMigrateArgs; module.exports.parseStagedArgs = nativeBinding.parseStagedArgs; module.exports.rewriteEslint = nativeBinding.rewriteEslint; module.exports.rewriteImportsInDirectory = nativeBinding.rewriteImportsInDirectory; +module.exports.rewriteOxlint = nativeBinding.rewriteOxlint; module.exports.rewritePrettier = nativeBinding.rewritePrettier; module.exports.rewriteScripts = nativeBinding.rewriteScripts; module.exports.run = nativeBinding.run; diff --git a/packages/cli/binding/index.d.cts b/packages/cli/binding/index.d.cts index 7e637156df..fa308f2764 100644 --- a/packages/cli/binding/index.d.cts +++ b/packages/cli/binding/index.d.cts @@ -3805,6 +3805,9 @@ export declare function rewriteImportsInDirectory( oxlintOwnerDirs?: Array | undefined | null, ): BatchRewriteResult; +/** Rewrite Oxlint commands and strip config arguments referencing a config that will be merged. */ +export declare function rewriteOxlint(scriptsJson: string, oxlintConfigPath: string): string | null; + /** * Rewrite Prettier scripts: rename `prettier` → `vp fmt` and strip Prettier-only flags. * @@ -3828,7 +3831,6 @@ export declare function rewritePrettier(scriptsJson: string): string | null; * * * `scripts_json` - The scripts section of the package.json file as a JSON string * * `rules_yaml` - The ast-grep rules.yaml as a YAML string - * * # Returns * * * `updated` - The updated scripts section of the package.json file as a JSON string, or `null` if no updates were made diff --git a/packages/cli/binding/src/cli/resolver.rs b/packages/cli/binding/src/cli/resolver.rs index 0063540873..36ca75f96a 100644 --- a/packages/cli/binding/src/cli/resolver.rs +++ b/packages/cli/binding/src/cli/resolver.rs @@ -85,7 +85,11 @@ impl SubcommandResolver { &owned_resolved_vite_config }; - if let (Some(_), Some(config_file)) = + if !args.iter().take_while(|arg| arg.as_str() != "--").any(|arg| { + matches!(arg.as_str(), "-c" | "--config") + || arg.starts_with("-c=") + || arg.starts_with("--config=") + }) && let (Some(_), Some(config_file)) = (&resolved_vite_config.lint, &resolved_vite_config.config_file) { args.insert(0, "-c".to_string()); diff --git a/packages/cli/binding/src/migration.rs b/packages/cli/binding/src/migration.rs index 39501fc176..6544d7ae5d 100644 --- a/packages/cli/binding/src/migration.rs +++ b/packages/cli/binding/src/migration.rs @@ -9,7 +9,6 @@ use napi_derive::napi; /// /// * `scripts_json` - The scripts section of the package.json file as a JSON string /// * `rules_yaml` - The ast-grep rules.yaml as a YAML string -/// /// # Returns /// /// * `updated` - The updated scripts section of the package.json file as a JSON string, or `null` if no updates were made @@ -27,6 +26,14 @@ pub fn rewrite_scripts(scripts_json: String, rules_yaml: String) -> Result Result> { + let updated = vp_migration::rewrite_oxlint(&scripts_json, &oxlint_config_path) + .map_err(anyhow::Error::from)?; + Ok(updated) +} + /// Rewrite ESLint scripts: rename `eslint` → `vp lint` and strip ESLint-only flags. /// /// Uses brush-parser to parse shell commands, so it correctly handles env var prefixes, diff --git a/packages/cli/src/migration/__tests__/migrator.spec.ts b/packages/cli/src/migration/__tests__/migrator.spec.ts index 026874eb0a..ff484f6690 100644 --- a/packages/cli/src/migration/__tests__/migrator.spec.ts +++ b/packages/cli/src/migration/__tests__/migrator.spec.ts @@ -9618,3 +9618,67 @@ describe('collectMigrationSetupPlan non-interactive editor conflicts', () => { expect(plan.editorConflictDecisions.get('workspace.xml')).toBe('skip'); }); }); + +describe('Oxlint config arg', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-oxlint-config-arg-')); + fs.writeFileSync(path.join(tmpDir, '.oxlintrc.json'), '{}'); + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + scripts: { + short: 'oxlint -c .oxlintrc.json', + shortQuoted: 'oxlint -c "./.oxlintrc.json"', + shortEquals: 'oxlint -c=.oxlintrc.json', + long: 'oxlint --config .oxlintrc.json', + longQuoted: 'oxlint --config "./.oxlintrc.json"', + longEquals: 'oxlint --config=.oxlintrc.json', + windows: 'oxlint -c .\\.oxlintrc.json', + }, + devDependencies: { oxlint: '^1.0.0' }, + }), + ); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('removes the config argument - standalone project', () => { + rewriteStandaloneProject(tmpDir, makeWorkspaceInfo(tmpDir, PackageManager.npm), true, true); + + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + scripts: Record; + }; + expect(pkg.scripts).toEqual({ + short: 'vp lint', + shortQuoted: 'vp lint', + shortEquals: 'vp lint', + long: 'vp lint', + longQuoted: 'vp lint', + longEquals: 'vp lint', + windows: 'vp lint', + }); + expect(fs.existsSync(path.join(tmpDir, '.oxlintrc.json'))).toBe(false); + }); + + it('removes the config argument - monorepo project', () => { + rewriteMonorepoProject(tmpDir, PackageManager.npm, true, true); + + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + scripts: Record; + }; + expect(pkg.scripts).toEqual({ + short: 'vp lint', + shortQuoted: 'vp lint', + shortEquals: 'vp lint', + long: 'vp lint', + longQuoted: 'vp lint', + longEquals: 'vp lint', + windows: 'vp lint', + }); + expect(fs.existsSync(path.join(tmpDir, '.oxlintrc.json'))).toBe(false); + }); +}); diff --git a/packages/cli/src/migration/migrator/orchestrators.ts b/packages/cli/src/migration/migrator/orchestrators.ts index 908162a4a8..2be34a1c48 100644 --- a/packages/cli/src/migration/migrator/orchestrators.ts +++ b/packages/cli/src/migration/migrator/orchestrators.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import { PackageManager, type WorkspaceInfo, type WorkspacePackage } from '../../types/index.ts'; import { VITE_PLUS_NAME, VITE_PLUS_VERSION, isForceOverrideMode } from '../../utils/constants.ts'; import { editJsonFile } from '../../utils/json.ts'; +import { detectConfigs } from '../detector.ts'; import { applyBuildAllowanceToPackageJsonPnpm, applyYarnWorkspaceHoistingFix, @@ -76,6 +77,7 @@ export function rewriteStandaloneProject( } const packageManager = workspaceInfo.packageManager; + const oxlintConfigPath = detectConfigs(projectPath).oxlintConfig; const catalogDependencyResolver = createCatalogDependencyResolver(projectPath, packageManager); const vitestEcosystemPackages = collectVitestEcosystemInstallDependencyNames(projectPath); // Captured before `rewritePackageJson` strips `oxlint`; the import rewriter @@ -254,6 +256,7 @@ export function rewriteStandaloneProject( retainedVitestModule, requiredVitestPeer, providerCatalogAdditions, + oxlintConfigPath, ); // ensure vite-plus is in devDependencies — but only when it isn't already a @@ -509,6 +512,7 @@ export function rewriteMonorepoProject( // catalog entry rather than pin a concrete version. See #2005. providerCatalogAdditions: ReadonlySet = new Set(), ): void { + const oxlintConfigPath = detectConfigs(projectPath).oxlintConfig; cleanupDeprecatedTsconfigOptions(projectPath, silent, report); rewriteTsconfigTypes(projectPath, silent, report); mergeViteConfigFiles( @@ -567,6 +571,7 @@ export function rewriteMonorepoProject( retainedVitestModule, requiredVitestPeer, providerCatalogAdditions, + oxlintConfigPath, ); // If this SUB-workspace now depends on `vite-plus` and Yarn isolates its // hoisting (via the root `nmHoistingLimits` OR the workspace's own diff --git a/packages/cli/src/migration/migrator/package-json.ts b/packages/cli/src/migration/migrator/package-json.ts index 4c78dd163e..76c58c1131 100644 --- a/packages/cli/src/migration/migrator/package-json.ts +++ b/packages/cli/src/migration/migrator/package-json.ts @@ -1,4 +1,4 @@ -import { rewriteScripts } from '../../../binding/index.js'; +import { rewriteOxlint, rewriteScripts } from '../../../binding/index.js'; import { PackageManager } from '../../types/index.ts'; import { VITEST_VERSION, @@ -72,12 +72,16 @@ export function rewritePackageJson( // one only through source/a shim). An already-installed copy of such a provider // must REFERENCE that catalog entry, not pin a concrete version. See #2005. providerCatalogAdditions: ReadonlySet = new Set(), + // Strip -c/--config when it points to the Oxlint config being merged into + // vite.config.ts. Custom config paths remain in package scripts. + oxlintConfigPath?: string, ): Record | null { if (pkg.scripts) { - const updated = rewriteScripts( - JSON.stringify(pkg.scripts), - getScriptRulesYaml(skipStagedMigration), - ); + const scriptsJson = JSON.stringify(pkg.scripts); + const oxlintUpdated = oxlintConfigPath ? rewriteOxlint(scriptsJson, oxlintConfigPath) : null; + const updated = + rewriteScripts(oxlintUpdated ?? scriptsJson, getScriptRulesYaml(skipStagedMigration)) ?? + oxlintUpdated; if (updated) { pkg.scripts = JSON.parse(updated); }