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