@@ -16,6 +16,7 @@ use crate::helpers::emojis::*;
1616use crate :: helpers:: { self } ;
1717use crate :: project_context:: ProjectContext ;
1818use crate :: { config, sourcedirs} ;
19+ use ahash:: AHashSet ;
1920use anyhow:: { Context , Result , anyhow} ;
2021use build_types:: * ;
2122use console:: style;
@@ -548,3 +549,203 @@ pub fn build(
548549 }
549550 }
550551}
552+
553+ /// Compile a single ReScript file and return its JavaScript output.
554+ ///
555+ /// This function performs a targeted one-shot compilation:
556+ /// 1. Initializes build state (reusing cached artifacts from previous builds)
557+ /// 2. Finds the target module from the file path
558+ /// 3. Calculates the dependency closure (all transitive dependencies)
559+ /// 4. Marks target + dependencies as dirty for compilation
560+ /// 5. Runs incremental build to compile only what's needed
561+ /// 6. Reads and returns the generated JavaScript file
562+ ///
563+ /// # Workflow
564+ /// Unlike the watch mode which expands UPWARD to dependents when a file changes,
565+ /// this expands DOWNWARD to dependencies to ensure everything needed is compiled.
566+ ///
567+ /// # Example
568+ /// If compiling `App.res` which imports `Component.res` which imports `Utils.res`:
569+ /// - Dependency closure: {Utils, Component, App}
570+ /// - Compilation order (via wave algorithm): Utils → Component → App
571+ ///
572+ /// # Errors
573+ /// Returns error if:
574+ /// - File doesn't exist or isn't part of the project
575+ /// - Compilation fails (parse errors, type errors, etc.)
576+ /// - Generated JavaScript file cannot be found
577+ pub fn compile_one (
578+ target_file : & Path ,
579+ project_root : & Path ,
580+ plain_output : bool ,
581+ warn_error : Option < String > ,
582+ ) -> Result < String > {
583+ use std:: fs;
584+
585+ // Step 1: Initialize build state
586+ // This leverages any existing .ast/.cmi files from previous builds
587+ let mut build_state = initialize_build (
588+ None ,
589+ & None , // no filter
590+ false , // no progress output (keep stderr clean)
591+ project_root,
592+ plain_output,
593+ warn_error,
594+ ) ?;
595+
596+ // Step 2: Find target module from file path
597+ let target_module_name = find_module_for_file ( & build_state, target_file)
598+ . ok_or_else ( || anyhow ! ( "File not found in project: {}" , target_file. display( ) ) ) ?;
599+
600+ // Step 3: Mark only the target file as parse_dirty
601+ // This ensures we parse the latest version of the target file
602+ if let Some ( module) = build_state. modules . get_mut ( & target_module_name) {
603+ if let SourceType :: SourceFile ( source_file) = & mut module. source_type {
604+ source_file. implementation . parse_dirty = true ;
605+ if let Some ( interface) = & mut source_file. interface {
606+ interface. parse_dirty = true ;
607+ }
608+ }
609+ }
610+
611+ // Step 4: Get dependency closure (downward traversal)
612+ // Unlike compile universe (upward to dependents), we need all dependencies
613+ let dependency_closure = get_dependency_closure ( & target_module_name, & build_state) ;
614+
615+ // Step 5: Mark all dependencies as compile_dirty
616+ for module_name in & dependency_closure {
617+ if let Some ( module) = build_state. modules . get_mut ( module_name) {
618+ module. compile_dirty = true ;
619+ }
620+ }
621+
622+ // Step 6: Run incremental build
623+ // The wave compilation algorithm will compile dependencies first, then the target
624+ incremental_build (
625+ & mut build_state,
626+ None ,
627+ false , // not initial build
628+ false , // no progress output
629+ true , // only incremental (no cleanup step)
630+ false , // no sourcedirs
631+ plain_output,
632+ )
633+ . map_err ( |e| anyhow ! ( "Compilation failed: {}" , e) ) ?;
634+
635+ // Step 7: Find and read the generated JavaScript file
636+ let js_path = get_js_output_path ( & build_state, & target_module_name, target_file) ?;
637+ let js_content = fs:: read_to_string ( & js_path)
638+ . map_err ( |e| anyhow ! ( "Failed to read generated JS file {}: {}" , js_path. display( ) , e) ) ?;
639+
640+ Ok ( js_content)
641+ }
642+
643+ /// Find the module name for a given file path by searching through all modules.
644+ ///
645+ /// This performs a linear search through the build state's modules to match
646+ /// the canonical file path. Returns the module name if found.
647+ fn find_module_for_file ( build_state : & BuildCommandState , target_file : & Path ) -> Option < String > {
648+ let canonical_target = target_file. canonicalize ( ) . ok ( ) ?;
649+
650+ for ( module_name, module) in & build_state. modules {
651+ if let SourceType :: SourceFile ( source_file) = & module. source_type {
652+ let package = build_state. packages . get ( & module. package_name ) ?;
653+
654+ // Check implementation file
655+ let impl_path = package. path . join ( & source_file. implementation . path ) ;
656+ if impl_path. canonicalize ( ) . ok ( ) . as_ref ( ) == Some ( & canonical_target) {
657+ return Some ( module_name. clone ( ) ) ;
658+ }
659+
660+ // Check interface file if present
661+ if let Some ( interface) = & source_file. interface {
662+ let iface_path = package. path . join ( & interface. path ) ;
663+ if iface_path. canonicalize ( ) . ok ( ) . as_ref ( ) == Some ( & canonical_target) {
664+ return Some ( module_name. clone ( ) ) ;
665+ }
666+ }
667+ }
668+ }
669+
670+ None
671+ }
672+
673+ /// Calculate the transitive closure of all dependencies for a given module.
674+ ///
675+ /// This performs a downward traversal (dependencies, not dependents):
676+ /// - Module A depends on B and C
677+ /// - B depends on D
678+ /// - Result: {A, B, C, D}
679+ ///
680+ /// This is the opposite of the "compile universe" which expands upward to dependents.
681+ fn get_dependency_closure ( module_name : & str , build_state : & BuildState ) -> AHashSet < String > {
682+ let mut closure = AHashSet :: new ( ) ;
683+ let mut to_process = vec ! [ module_name. to_string( ) ] ;
684+
685+ while let Some ( current) = to_process. pop ( ) {
686+ if !closure. contains ( & current) {
687+ closure. insert ( current. clone ( ) ) ;
688+
689+ if let Some ( module) = build_state. get_module ( & current) {
690+ // Add all dependencies to process queue
691+ for dep in & module. deps {
692+ if !closure. contains ( dep) {
693+ to_process. push ( dep. clone ( ) ) ;
694+ }
695+ }
696+ }
697+ }
698+ }
699+
700+ closure
701+ }
702+
703+ /// Get the path to the generated JavaScript file for a module.
704+ ///
705+ /// Respects the package's configuration for output location and format:
706+ /// - in-source: JS file next to the .res file
707+ /// - out-of-source: JS file in lib/js or lib/es6
708+ /// - Uses first package spec to determine .js vs .mjs extension
709+ fn get_js_output_path (
710+ build_state : & BuildCommandState ,
711+ module_name : & str ,
712+ _original_file : & Path ,
713+ ) -> Result < PathBuf > {
714+ let module = build_state
715+ . get_module ( module_name)
716+ . ok_or_else ( || anyhow ! ( "Module not found: {}" , module_name) ) ?;
717+
718+ let package = build_state
719+ . get_package ( & module. package_name )
720+ . ok_or_else ( || anyhow ! ( "Package not found: {}" , module. package_name) ) ?;
721+
722+ let root_config = build_state. get_root_config ( ) ;
723+ let package_specs = root_config. get_package_specs ( ) ;
724+ let package_spec = package_specs
725+ . first ( )
726+ . ok_or_else ( || anyhow ! ( "No package specs configured" ) ) ?;
727+
728+ let suffix = root_config. get_suffix ( package_spec) ;
729+
730+ if let SourceType :: SourceFile ( source_file) = & module. source_type {
731+ let source_path = & source_file. implementation . path ;
732+
733+ if package_spec. in_source {
734+ // in-source: JS file next to source file
735+ let js_file = source_path. with_extension ( & suffix[ 1 ..] ) ; // remove leading dot
736+ Ok ( package. path . join ( js_file) )
737+ } else {
738+ // out-of-source: in lib/js or lib/es6
739+ let base_path = if package_spec. is_common_js ( ) {
740+ package. get_js_path ( )
741+ } else {
742+ package. get_es6_path ( )
743+ } ;
744+
745+ let js_file = source_path. with_extension ( & suffix[ 1 ..] ) ;
746+ Ok ( base_path. join ( js_file) )
747+ }
748+ } else {
749+ Err ( anyhow ! ( "Cannot get JS output for non-source module" ) )
750+ }
751+ }
0 commit comments