@@ -4,6 +4,9 @@ use std::{
44 sync:: LazyLock ,
55} ;
66
7+ use ast_grep_config:: RuleConfig ;
8+ use ast_grep_language:: SupportLang ;
9+ use rayon:: prelude:: * ;
710use regex:: Regex ;
811use vite_error:: Error ;
912
@@ -276,6 +279,18 @@ transform:
276279fix: $NEW_IMPORT
277280"# ;
278281
282+ static PARSED_VITE_RULES : LazyLock < Vec < RuleConfig < SupportLang > > > = LazyLock :: new ( || {
283+ ast_grep:: load_rules ( REWRITE_VITE_RULES ) . expect ( "failed to parse vite rewrite rules" )
284+ } ) ;
285+
286+ static PARSED_VITEST_RULES : LazyLock < Vec < RuleConfig < SupportLang > > > = LazyLock :: new ( || {
287+ ast_grep:: load_rules ( REWRITE_VITEST_RULES ) . expect ( "failed to parse vitest rewrite rules" )
288+ } ) ;
289+
290+ static PARSED_TSDOWN_RULES : LazyLock < Vec < RuleConfig < SupportLang > > > = LazyLock :: new ( || {
291+ ast_grep:: load_rules ( REWRITE_TSDOWN_RULES ) . expect ( "failed to parse tsdown rewrite rules" )
292+ } ) ;
293+
279294// Regex patterns for rewriting `/// <reference types="..." />` directives.
280295// These cannot be handled by ast-grep because triple-slash references are parsed as comments.
281296
@@ -502,7 +517,7 @@ fn rewrite_reference_types(content: &mut String, skip_packages: &SkipPackages) -
502517}
503518
504519/// Packages to skip rewriting based on peerDependencies or dependencies
505- #[ derive( Debug , Clone , Default ) ]
520+ #[ derive( Debug , Clone , Copy , Default ) ]
506521struct SkipPackages {
507522 /// Skip rewriting vite imports (vite is in peerDependencies or dependencies)
508523 skip_vite : bool ,
@@ -593,6 +608,12 @@ pub struct BatchRewriteResult {
593608 pub errors : Vec < ( PathBuf , String ) > ,
594609}
595610
611+ enum FileResult {
612+ Modified ,
613+ Unchanged ,
614+ Error ( String ) ,
615+ }
616+
596617/// Rewrite imports in all TypeScript/JavaScript files under a directory
597618///
598619/// This function finds all TypeScript and JavaScript files in the specified directory
@@ -625,53 +646,65 @@ pub struct BatchRewriteResult {
625646pub fn rewrite_imports_in_directory ( root : & Path ) -> Result < BatchRewriteResult , Error > {
626647 let walk_result = file_walker:: find_ts_files ( root) ?;
627648
628- let mut result = BatchRewriteResult {
629- modified_files : Vec :: new ( ) ,
630- unchanged_files : Vec :: new ( ) ,
631- errors : Vec :: new ( ) ,
632- } ;
633-
634- // Cache package.json lookups to avoid re-reading the same file
649+ // Pre-compute skip_packages for each file (requires mutable cache, done sequentially)
635650 let mut skip_packages_cache: HashMap < PathBuf , SkipPackages > = HashMap :: new ( ) ;
651+ let files_with_skip: Vec < ( PathBuf , SkipPackages ) > = walk_result
652+ . files
653+ . into_iter ( )
654+ . map ( |file_path| {
655+ let skip_packages =
656+ if let Some ( package_json_path) = find_nearest_package_json ( & file_path, root) {
657+ * skip_packages_cache
658+ . entry ( package_json_path. clone ( ) )
659+ . or_insert_with ( || get_skip_packages_from_package_json ( & package_json_path) )
660+ } else {
661+ SkipPackages :: default ( )
662+ } ;
663+ ( file_path, skip_packages)
664+ } )
665+ . collect ( ) ;
666+
667+ // Process files in parallel using rayon
668+ let results: Vec < ( PathBuf , FileResult ) > = files_with_skip
669+ . into_par_iter ( )
670+ . map ( |( file_path, skip_packages) | {
671+ if skip_packages. all_skipped ( ) {
672+ return ( file_path, FileResult :: Unchanged ) ;
673+ }
636674
637- for file_path in walk_result. files {
638- // Find the nearest package.json for this file
639- let skip_packages =
640- if let Some ( package_json_path) = find_nearest_package_json ( & file_path, root) {
641- skip_packages_cache
642- . entry ( package_json_path. clone ( ) )
643- . or_insert_with ( || get_skip_packages_from_package_json ( & package_json_path) )
644- . clone ( )
645- } else {
646- SkipPackages :: default ( )
647- } ;
648-
649- // If all packages are in peerDeps for this file's package, skip it
650- if skip_packages. all_skipped ( ) {
651- result. unchanged_files . push ( file_path) ;
652- continue ;
653- }
654-
655- match rewrite_import ( & file_path, & skip_packages) {
656- Ok ( rewrite_result) => {
657- if rewrite_result. updated {
658- // Write the modified content back
659- if let Err ( e) = std:: fs:: write ( & file_path, & rewrite_result. content ) {
660- result. errors . push ( ( file_path, e. to_string ( ) ) ) ;
675+ match rewrite_import ( & file_path, & skip_packages) {
676+ Ok ( rewrite_result) => {
677+ if rewrite_result. updated {
678+ if let Err ( e) = std:: fs:: write ( & file_path, & rewrite_result. content ) {
679+ ( file_path, FileResult :: Error ( e. to_string ( ) ) )
680+ } else {
681+ ( file_path, FileResult :: Modified )
682+ }
661683 } else {
662- result . modified_files . push ( file_path) ;
684+ ( file_path, FileResult :: Unchanged )
663685 }
664- } else {
665- result. unchanged_files . push ( file_path) ;
666686 }
687+ Err ( e) => ( file_path, FileResult :: Error ( e. to_string ( ) ) ) ,
667688 }
668- Err ( e) => {
669- result. errors . push ( ( file_path, e. to_string ( ) ) ) ;
670- }
689+ } )
690+ . collect ( ) ;
691+
692+ // Collect results
693+ let mut batch_result = BatchRewriteResult {
694+ modified_files : Vec :: new ( ) ,
695+ unchanged_files : Vec :: new ( ) ,
696+ errors : Vec :: new ( ) ,
697+ } ;
698+
699+ for ( file_path, file_result) in results {
700+ match file_result {
701+ FileResult :: Modified => batch_result. modified_files . push ( file_path) ,
702+ FileResult :: Unchanged => batch_result. unchanged_files . push ( file_path) ,
703+ FileResult :: Error ( msg) => batch_result. errors . push ( ( file_path, msg) ) ,
671704 }
672705 }
673706
674- Ok ( result )
707+ Ok ( batch_result )
675708}
676709
677710/// Rewrite imports in a TypeScript/JavaScript file from vite/vitest to vite-plus
@@ -698,6 +731,24 @@ fn rewrite_import(file_path: &Path, skip_packages: &SkipPackages) -> Result<Rewr
698731 rewrite_import_content ( & content, skip_packages)
699732}
700733
734+ /// Fast pre-filter to skip expensive AST parsing for files with no relevant imports.
735+ fn content_may_need_rewriting ( content : & str , skip_packages : & SkipPackages ) -> bool {
736+ // "vite" also matches "vitest" as a substring, covering both packages
737+ if !skip_packages. skip_vite || !skip_packages. skip_vitest {
738+ if content. contains ( "vite" ) {
739+ return true ;
740+ }
741+ }
742+ // When only skip_vite is set, we still need to catch @vitest/ scoped packages
743+ if !skip_packages. skip_vitest && content. contains ( "@vitest/" ) {
744+ return true ;
745+ }
746+ if !skip_packages. skip_tsdown && content. contains ( "tsdown" ) {
747+ return true ;
748+ }
749+ false
750+ }
751+
701752/// Rewrite imports in content from vite/vitest to vite-plus
702753///
703754/// This is the internal function that performs the actual rewrite using ast-grep.
@@ -706,33 +757,36 @@ fn rewrite_import_content(
706757 content : & str ,
707758 skip_packages : & SkipPackages ,
708759) -> Result < RewriteResult , Error > {
760+ // Fast path: skip AST parsing if the file doesn't contain any target strings
761+ if !content_may_need_rewriting ( content, skip_packages) {
762+ return Ok ( RewriteResult { content : content. to_string ( ) , updated : false } ) ;
763+ }
764+
709765 let mut new_content = content. to_string ( ) ;
710766 let mut updated = false ;
711767
712- // Apply vite rules if not skipped
768+ // Apply vite rules if not skipped (using pre-parsed rules)
713769 if !skip_packages. skip_vite {
714- let ( vite_content, vite_updated ) = ast_grep:: apply_rules ( & new_content, REWRITE_VITE_RULES ) ? ;
715- if vite_updated {
770+ let vite_content = ast_grep:: apply_loaded_rules ( & new_content, & PARSED_VITE_RULES ) ;
771+ if vite_content != new_content {
716772 new_content = vite_content;
717773 updated = true ;
718774 }
719775 }
720776
721- // Apply vitest rules if not skipped
777+ // Apply vitest rules if not skipped (using pre-parsed rules)
722778 if !skip_packages. skip_vitest {
723- let ( vitest_content, vitest_updated) =
724- ast_grep:: apply_rules ( & new_content, REWRITE_VITEST_RULES ) ?;
725- if vitest_updated {
779+ let vitest_content = ast_grep:: apply_loaded_rules ( & new_content, & PARSED_VITEST_RULES ) ;
780+ if vitest_content != new_content {
726781 new_content = vitest_content;
727782 updated = true ;
728783 }
729784 }
730785
731- // Apply tsdown rules if not skipped
786+ // Apply tsdown rules if not skipped (using pre-parsed rules)
732787 if !skip_packages. skip_tsdown {
733- let ( tsdown_content, tsdown_updated) =
734- ast_grep:: apply_rules ( & new_content, REWRITE_TSDOWN_RULES ) ?;
735- if tsdown_updated {
788+ let tsdown_content = ast_grep:: apply_loaded_rules ( & new_content, & PARSED_TSDOWN_RULES ) ;
789+ if tsdown_content != new_content {
736790 new_content = tsdown_content;
737791 updated = true ;
738792 }
0 commit comments