@@ -2,13 +2,14 @@ import {
22 Charm ,
33 charmId ,
44 compileAndRunRecipe ,
5+ compileRecipe ,
56 extractVersionTag ,
67 getIframeRecipe ,
78 getRecipeIdFromCharm ,
89 modifyCharm ,
910} from "@commontools/charm" ;
1011import { useCharmReferences } from "@/hooks/use-charm-references.ts" ;
11- import { isCell , isStream , Runtime } from "@commontools/runner" ;
12+ import { isCell , isStream , Runtime , RuntimeProgram } from "@commontools/runner" ;
1213import { isObject } from "@commontools/utils/types" ;
1314import {
1415 CheckboxToggle ,
@@ -719,8 +720,20 @@ const CodeTab = () => {
719720 const [ showFullCode , setShowFullCode ] = useState ( false ) ;
720721 const { loading, setLoading } = useCharmOperationContext ( ) ;
721722 const [ regularRecipeSource , setRegularRecipeSource ] = useState < string > ( "" ) ;
722- const [ workingRegularRecipeSource , setWorkingRegularRecipeSource ] = useState < string > ( "" ) ;
723+ const [ workingRegularRecipeSource , setWorkingRegularRecipeSource ] = useState <
724+ string
725+ > ( "" ) ;
723726 const [ isRegularRecipe , setIsRegularRecipe ] = useState ( false ) ;
727+ const [ isMultiFileRecipe , setIsMultiFileRecipe ] = useState ( false ) ;
728+ const [ recipeFiles , setRecipeFiles ] = useState <
729+ Array < { name : string ; contents : string } >
730+ > ( [ ] ) ;
731+ const [ workingRecipeFiles , setWorkingRecipeFiles ] = useState <
732+ Array < { name : string ; contents : string } >
733+ > ( [ ] ) ;
734+ const [ selectedFileIndex , setSelectedFileIndex ] = useState ( 0 ) ;
735+ const [ mainFile , setMainFile ] = useState < string > ( "/main.tsx" ) ;
736+ const [ mainExport , setMainExport ] = useState < string | undefined > ( ) ;
724737 const navigate = useNavigate ( ) ;
725738 const { replicaName } = useParams < CharmRouteParams > ( ) ;
726739
@@ -729,17 +742,29 @@ const CodeTab = () => {
729742 function loadRegularRecipeSource ( ) {
730743 if ( ! charm || iframeRecipe ) {
731744 setIsRegularRecipe ( false ) ;
745+ setIsMultiFileRecipe ( false ) ;
732746 return ;
733747 }
734-
748+
735749 // This is a regular recipe
736750 setIsRegularRecipe ( true ) ;
737-
751+
738752 try {
739753 const recipeId = getRecipeIdFromCharm ( charm ) ;
740754 if ( recipeId ) {
741755 const recipeMeta = runtime . recipeManager . getRecipeMeta ( { recipeId } ) ;
742- if ( recipeMeta ?. src ) {
756+
757+ // Check if it's a multi-file recipe
758+ if ( recipeMeta ?. program ) {
759+ setIsMultiFileRecipe ( true ) ;
760+ setRecipeFiles ( recipeMeta . program . files ) ;
761+ setWorkingRecipeFiles ( [ ...recipeMeta . program . files ] ) ;
762+ setMainFile ( recipeMeta . program . main ) ;
763+ setMainExport ( recipeMeta . program . mainExport ) ;
764+ setSelectedFileIndex ( 0 ) ;
765+ } else if ( recipeMeta ?. src ) {
766+ // Single file recipe
767+ setIsMultiFileRecipe ( false ) ;
743768 setRegularRecipeSource ( recipeMeta . src ) ;
744769 setWorkingRegularRecipeSource ( recipeMeta . src ) ;
745770 }
@@ -748,7 +773,7 @@ const CodeTab = () => {
748773 console . error ( "Error loading regular recipe source:" , error ) ;
749774 }
750775 }
751-
776+
752777 loadRegularRecipeSource ( ) ;
753778 } , [ charm , iframeRecipe , runtime ] ) ;
754779
@@ -823,14 +848,21 @@ const CodeTab = () => {
823848 return activeSchema === "argument" ? "argumentSchema" : "resultSchema" ;
824849 } ;
825850
826- // For regular recipes, just show a full code editor
851+ // For regular recipes, show either single or multi-file editor
827852 if ( isRegularRecipe ) {
853+ // Check if changes have been made
854+ const hasChanges = isMultiFileRecipe
855+ ? JSON . stringify ( workingRecipeFiles ) !== JSON . stringify ( recipeFiles )
856+ : workingRegularRecipeSource !== regularRecipeSource ;
857+
828858 return (
829859 < div className = "h-full flex flex-col overflow-hidden" >
830860 < div className = "flex items-center justify-between p-4 pb-2" >
831- < div className = "text-sm font-semibold" > Recipe Code</ div >
861+ < div className = "text-sm font-semibold" >
862+ { isMultiFileRecipe ? "Recipe Files" : "Recipe Code" }
863+ </ div >
832864 { /* Save button for regular recipes */ }
833- { workingRegularRecipeSource !== regularRecipeSource && (
865+ { hasChanges && (
834866 < button
835867 type = "button"
836868 onClick = { async ( ) => {
@@ -839,22 +871,52 @@ const CodeTab = () => {
839871 try {
840872 // Get the recipe spec and argument from the current charm
841873 const recipeId = getRecipeIdFromCharm ( charm ) ;
842- const recipeMeta = runtime . recipeManager . getRecipeMeta ( { recipeId } ) ;
874+ const recipeMeta = runtime . recipeManager . getRecipeMeta ( {
875+ recipeId,
876+ } ) ;
843877 const spec = recipeMeta ?. spec || "" ;
844878 const argument = charmManager . getArgument ( charm ) ;
845-
846- // Compile and run the updated recipe
847- const newCharm = await compileAndRunRecipe (
848- charmManager ,
849- workingRegularRecipeSource ,
850- spec ,
851- argument ,
852- recipeId ? [ recipeId ] : undefined ,
853- ) ;
854-
879+
880+ let newCharm ;
881+ if ( isMultiFileRecipe ) {
882+ // For multi-file recipes, we need to use compileRecipe directly
883+ const program : RuntimeProgram = {
884+ main : mainFile ,
885+ files : workingRecipeFiles ,
886+ ...( mainExport && { mainExport } ) ,
887+ } ;
888+
889+ // Use compileRecipe which accepts RuntimeProgram
890+ const recipe = await compileRecipe (
891+ program ,
892+ spec ,
893+ runtime ,
894+ charmManager . getSpace ( ) ,
895+ recipeId ? [ recipeId ] : undefined ,
896+ ) ;
897+
898+ // Run the recipe
899+ newCharm = await charmManager . runPersistent (
900+ recipe ,
901+ argument ,
902+ ) ;
903+ } else {
904+ // Single file recipe - use the existing compileAndRunRecipe
905+ newCharm = await compileAndRunRecipe (
906+ charmManager ,
907+ workingRegularRecipeSource ,
908+ spec ,
909+ argument ,
910+ recipeId ? [ recipeId ] : undefined ,
911+ ) ;
912+ }
913+
855914 if ( newCharm && replicaName ) {
856915 navigate (
857- createPath ( "charmShow" , { charmId : charmId ( newCharm ) ! , replicaName } ) ,
916+ createPath ( "charmShow" , {
917+ charmId : charmId ( newCharm ) ! ,
918+ replicaName,
919+ } ) ,
858920 ) ;
859921 }
860922 } catch ( error ) {
@@ -886,15 +948,51 @@ const CodeTab = () => {
886948 </ button >
887949 ) }
888950 </ div >
889-
951+
952+ { /* File tabs for multi-file recipes */ }
953+ { isMultiFileRecipe && (
954+ < div className = "px-4 pb-2" >
955+ < div className = "flex gap-2 overflow-x-auto" >
956+ { workingRecipeFiles . map ( ( file , index ) => (
957+ < button
958+ key = { file . name }
959+ type = "button"
960+ onClick = { ( ) => setSelectedFileIndex ( index ) }
961+ className = { `px-3 py-1 text-sm border ${
962+ selectedFileIndex === index
963+ ? "bg-black text-white border-black"
964+ : "bg-white text-black border-gray-300 hover:border-black"
965+ } `}
966+ >
967+ { file . name . substring ( 1 ) } { /* Remove leading slash */ }
968+ { file . name === mainFile && " (main)" }
969+ </ button >
970+ ) ) }
971+ </ div >
972+ </ div >
973+ ) }
974+
890975 < div className = "px-4 flex-grow flex flex-col overflow-hidden" >
891976 < CharmCodeEditor
892977 docs = { [
893978 {
894979 key : "code" ,
895- label : "Recipe Code" ,
896- value : workingRegularRecipeSource ,
897- onChange : setWorkingRegularRecipeSource ,
980+ label : isMultiFileRecipe
981+ ? workingRecipeFiles [ selectedFileIndex ] ?. name . substring ( 1 )
982+ : "Recipe Code" ,
983+ value : isMultiFileRecipe
984+ ? workingRecipeFiles [ selectedFileIndex ] ?. contents || ""
985+ : workingRegularRecipeSource ,
986+ onChange : isMultiFileRecipe
987+ ? ( newContent : string ) => {
988+ const updatedFiles = [ ...workingRecipeFiles ] ;
989+ updatedFiles [ selectedFileIndex ] = {
990+ ...updatedFiles [ selectedFileIndex ] ,
991+ contents : newContent ,
992+ } ;
993+ setWorkingRecipeFiles ( updatedFiles ) ;
994+ }
995+ : setWorkingRegularRecipeSource ,
898996 language : "javascript" as const ,
899997 readOnly : false ,
900998 } ,
0 commit comments