@@ -677,73 +677,153 @@ function makeCSharpScaffold(name: string, refs: PlaygroundResource[]): ProjectSc
677677const METHOD_TO_TYPE : Record < string , ResourceType > = {
678678 AddPostgres : 'postgres' , AddRedis : 'redis' , AddSqlServer : 'sqlserver' ,
679679 AddMongoDB : 'mongodb' , AddRabbitMQ : 'rabbitmq' , AddKafka : 'kafka' ,
680- AddProject : 'project' , AddContainer : 'container' ,
681- AddViteApp : 'npmapp' , AddJavaScriptApp : 'npmapp' , AddNodeApp : 'npmapp' ,
682- AddUvicornApp : 'pythonapp' , AddUvApp : 'pythonapp' ,
680+ AddProject : 'project' , AddCSharpApp : 'project' ,
681+ AddContainer : 'container' ,
682+ AddNpmApp : 'npmapp' , AddViteApp : 'npmapp' , AddJavaScriptApp : 'npmapp' , AddNodeApp : 'npmapp' ,
683+ AddPythonApp : 'pythonapp' , AddUvicornApp : 'pythonapp' , AddUvApp : 'pythonapp' ,
683684 AddAzureStorage : 'azurestorage' , AddAzureKeyVault : 'keyvault' ,
684- AddParameter : 'parameter' ,
685+ AddParameter : 'parameter' , AddConnectionString : 'parameter' ,
686+ AddYarp : 'container' , AddElasticsearch : 'container' ,
687+ AddMySql : 'container' , AddOracle : 'container' , AddNats : 'container' ,
685688} ;
686689
690+ function preprocessCode ( code : string ) : string {
691+ return code
692+ . replace ( / \/ \/ .* $ / gm, '' )
693+ . replace ( / \/ \* [ \s \S ] * ?\* \/ / g, '' )
694+ . replace ( / # (?: i f | e l s e | e n d i f | p r a g m a | r e g i o n | e n d r e g i o n ) .* $ / gm, '' )
695+ . replace ( / \r \n / g, '\n' ) ;
696+ }
697+
698+ function splitStatements ( code : string ) : string [ ] {
699+ const flat = code . replace ( / \n / g, ' ' ) . replace ( / \s + / g, ' ' ) ;
700+ return flat . split ( ';' ) . map ( s => s . trim ( ) ) . filter ( Boolean ) ;
701+ }
702+
687703function parseAppHostCode ( code : string ) : PlaygroundResource [ ] {
688704 const resources : PlaygroundResource [ ] = [ ] ;
689705 const varToId = new Map < string , string > ( ) ;
706+ // Map varName to the statement it was defined in (for attribute scanning)
707+ const varToStmt = new Map < string , string > ( ) ;
690708
691- // Match: var <varName> = builder.<Method>("<name>"...)
692- const addRegex = / (?: v a r | c o n s t ) \s + ( \w + ) \s * = \s * b u i l d e r \. ( \w + ) \s * (?: < [ ^ > ] + > ) ? \s * \( " ( [ ^ " ] + ) " (?: \s * , \s * " ( [ ^ " ] * ) " ) ? / g;
693- let match ;
694- while ( ( match = addRegex . exec ( code ) ) !== null ) {
695- const [ , varName , method , name , secondArg ] = match ;
696- const type = METHOD_TO_TYPE [ method ] ;
697- if ( ! type ) continue ;
698-
699- const id = makeId ( ) ;
700- varToId . set ( varName , id ) ;
701-
702- const r = makeResource ( { id, type, name } ) ;
703- if ( type === 'container' && secondArg ) r . image = secondArg ;
704- if ( ( type === 'npmapp' ) && secondArg ) r . projectPath = secondArg ;
705- if ( type === 'pythonapp' && secondArg ) r . projectPath = secondArg ;
706- if ( type === 'parameter' && code . includes ( `${ varName } ` ) && / s e c r e t \s * : \s * t r u e / i. test ( code . substring ( match . index , match . index + 200 ) ) ) {
707- r . isSecret = true ;
709+ const cleaned = preprocessCode ( code ) ;
710+ const statements = splitStatements ( cleaned ) ;
711+
712+ for ( const stmt of statements ) {
713+ // Step 1: Extract builder.Add* resource declarations
714+ const addMatch = stmt . match (
715+ / (?: v a r | c o n s t | [ A - Z ] \w * ) \s + ( \w + ) \s * = \s * b u i l d e r \. ( \w + ) \s * (?: < [ ^ > ] + > ) ? \s * \( \s * " ( [ ^ " ] + ) " (?: \s * , \s * (?: @ ? " ( [ ^ " ] * ) " ) ) ? /
716+ ) ;
717+ if ( addMatch ) {
718+ const [ , varName , method , name , secondArg ] = addMatch ;
719+ const type = METHOD_TO_TYPE [ method ] ;
720+ if ( type ) {
721+ const id = makeId ( ) ;
722+ varToId . set ( varName , id ) ;
723+ varToStmt . set ( varName , stmt ) ;
724+
725+ const r = makeResource ( { id, type, name } ) ;
726+ if ( type === 'container' && secondArg ) r . image = secondArg ;
727+ if ( ( type === 'npmapp' || type === 'pythonapp' || type === 'project' ) && secondArg ) r . projectPath = secondArg ;
728+ if ( type === 'parameter' && / s e c r e t \s * : \s * t r u e / i. test ( stmt ) ) r . isSecret = true ;
729+ resources . push ( r ) ;
730+ }
708731 }
709- resources . push ( r ) ;
710- }
711732
712- // Match: .AddDatabase("<name>")
713- const dbRegex = / ( \w + ) \. A d d D a t a b a s e \s * \( " ( [ ^ " ] + ) " \) / g;
714- while ( ( match = dbRegex . exec ( code ) ) !== null ) {
715- const [ , parentVar , dbName ] = match ;
716- const parentId = varToId . get ( parentVar ) ;
717- if ( parentId ) {
718- const parent = resources . find ( ( r ) => r . id === parentId ) ;
719- if ( parent ) parent . databases . push ( dbName ) ;
733+ // Step 2: Extract .AddDatabase() anywhere in the statement
734+ const dbMatches = stmt . matchAll ( / \. A d d D a t a b a s e \s * \( \s * " ( [ ^ " ] + ) " \s * \) / g) ;
735+ for ( const dbMatch of dbMatches ) {
736+ const dbName = dbMatch [ 1 ] ;
737+ // The parent is the variable assigned in the same statement that contains
738+ // the builder.Add* call, OR the variable whose chain this belongs to.
739+ // First, try to find the builder.Add* parent in this statement.
740+ const parentAddMatch = stmt . match (
741+ / (?: v a r | c o n s t | [ A - Z ] \w * ) \s + ( \w + ) \s * = \s * b u i l d e r \. ( \w + ) /
742+ ) ;
743+ if ( parentAddMatch ) {
744+ // The database is chained onto the resource defined in this statement
745+ const parentVar = parentAddMatch [ 1 ] ;
746+ const parentId = varToId . get ( parentVar ) ;
747+ if ( parentId ) {
748+ const parent = resources . find ( r => r . id === parentId ) ;
749+ if ( parent && ! parent . databases . includes ( dbName ) ) parent . databases . push ( dbName ) ;
750+ }
751+ } else {
752+ // The database call is on a standalone variable: varName.AddDatabase(...)
753+ const standaloneMatch = stmt . match ( / ( \w + ) \s * \. \s * A d d D a t a b a s e / ) ;
754+ if ( standaloneMatch ) {
755+ const parentId = varToId . get ( standaloneMatch [ 1 ] ) ;
756+ if ( parentId ) {
757+ const parent = resources . find ( r => r . id === parentId ) ;
758+ if ( parent && ! parent . databases . includes ( dbName ) ) parent . databases . push ( dbName ) ;
759+ }
760+ }
761+ }
720762 }
721- }
722763
723- // Match: .WithReference(<var>) and .WaitFor(<var>)
724- const chainRegex = / ( \w + ) (?: \. [ \w < > ] + \( [ ^ ) ] * \) ) * \. ( W i t h R e f e r e n c e | W a i t F o r ) \( ( \w + ) \) / g;
725- while ( ( match = chainRegex . exec ( code ) ) !== null ) {
726- const [ , consumerVar , method , depVar ] = match ;
727- const consumerId = varToId . get ( consumerVar ) ;
728- const depId = varToId . get ( depVar ) ;
729- if ( ! consumerId || ! depId ) continue ;
730- const consumer = resources . find ( ( r ) => r . id === consumerId ) ;
731- if ( ! consumer ) continue ;
732- if ( method === 'WithReference' && ! consumer . references . includes ( depId ) ) {
733- consumer . references . push ( depId ) ;
764+ // Step 3: Also handle `var dbVar = parentVar.AddDatabase("name")` as a variable assignment
765+ const dbAssignMatch = stmt . match (
766+ / (?: v a r | c o n s t | [ A - Z ] \w * ) \s + ( \w + ) \s * = \s * ( \w + ) (?: \. \w + \( [ ^ ) ] * \) ) * \. A d d D a t a b a s e \s * \( \s * " ( [ ^ " ] + ) " \s * \) /
767+ ) ;
768+ if ( dbAssignMatch ) {
769+ const [ , dbVarName , , dbName ] = dbAssignMatch ;
770+ // Find the database resource by name and map the variable to it
771+ // The database itself isn't a separate PlaygroundResource — it's stored on the parent.
772+ // But the variable might be referenced via .WithReference(dbVar), so we need to track it.
773+ // Find which parent has this database
774+ const parentForDb = resources . find ( r => r . databases . includes ( dbName ) ) ;
775+ if ( parentForDb ) {
776+ varToId . set ( dbVarName , parentForDb . id ) ;
777+ varToStmt . set ( dbVarName , stmt ) ;
778+ }
734779 }
735- if ( method === 'WaitFor' && ! consumer . waitFor . includes ( depId ) ) {
736- consumer . waitFor . push ( depId ) ;
780+
781+ // Step 4: Extract .WithReference() and .WaitFor() — find the consumer variable
782+ const refMatches = [ ...stmt . matchAll ( / \. ( W i t h R e f e r e n c e | W a i t F o r ) \s * \( \s * ( \w + ) \s * \) / g) ] ;
783+ if ( refMatches . length > 0 ) {
784+ // Find the consumer: the variable being assigned in this statement
785+ const consumerMatch = stmt . match ( / (?: v a r | c o n s t | [ A - Z ] \w * ) \s + ( \w + ) \s * = / ) ;
786+ const consumerVar = consumerMatch ?. [ 1 ] ;
787+ const consumerId = consumerVar ? varToId . get ( consumerVar ) : undefined ;
788+ const consumer = consumerId ? resources . find ( r => r . id === consumerId ) : undefined ;
789+
790+ if ( ! consumer ) {
791+ // Try standalone: varName.WithReference(...)
792+ const standaloneConsumer = stmt . match ( / ^ ( \w + ) \s * \. / ) ;
793+ if ( standaloneConsumer ) {
794+ const scId = varToId . get ( standaloneConsumer [ 1 ] ) ;
795+ const sc = scId ? resources . find ( r => r . id === scId ) : undefined ;
796+ if ( sc ) {
797+ for ( const rm of refMatches ) {
798+ const [ , method , depVar ] = rm ;
799+ const depId = varToId . get ( depVar ) ;
800+ if ( ! depId ) continue ;
801+ if ( method === 'WithReference' && ! sc . references . includes ( depId ) ) sc . references . push ( depId ) ;
802+ if ( method === 'WaitFor' && ! sc . waitFor . includes ( depId ) ) sc . waitFor . push ( depId ) ;
803+ }
804+ }
805+ }
806+ } else {
807+ for ( const rm of refMatches ) {
808+ const [ , method , depVar ] = rm ;
809+ const depId = varToId . get ( depVar ) ;
810+ if ( ! depId ) continue ;
811+ if ( method === 'WithReference' && ! consumer . references . includes ( depId ) ) consumer . references . push ( depId ) ;
812+ if ( method === 'WaitFor' && ! consumer . waitFor . includes ( depId ) ) consumer . waitFor . push ( depId ) ;
813+ }
814+ }
737815 }
738816 }
739817
740- // Match: .WithDataVolume( )
818+ // Step 5: Scan statements for attribute methods ( .WithDataVolume, .WithLifetime, etc. )
741819 for ( const r of resources ) {
742820 const varName = [ ...varToId . entries ( ) ] . find ( ( [ , id ] ) => id === r . id ) ?. [ 0 ] ;
743- if ( varName && new RegExp ( `${ varName } [^;]*\\.WithDataVolume` ) . test ( code ) ) r . hasDataVolume = true ;
744- if ( varName && new RegExp ( `${ varName } [^;]*\\.WithLifetime\\(ContainerLifetime\\.Persistent\\)` ) . test ( code ) ) r . isPersistent = true ;
745- if ( varName && new RegExp ( `${ varName } [^;]*\\.WithExternalHttpEndpoints` ) . test ( code ) ) r . hasExternalEndpoints = true ;
746- if ( varName && new RegExp ( `${ varName } [^;]*\\.WithHttpEndpoint` ) . test ( code ) ) r . hasExternalEndpoints = true ;
821+ if ( ! varName ) continue ;
822+ const stmt = varToStmt . get ( varName ) ?? '' ;
823+ if ( / \. W i t h D a t a V o l u m e \b / . test ( stmt ) ) r . hasDataVolume = true ;
824+ if ( / \. W i t h L i f e t i m e \s * \( \s * C o n t a i n e r L i f e t i m e \. P e r s i s t e n t \s * \) / . test ( stmt ) ) r . isPersistent = true ;
825+ if ( / \. W i t h E x t e r n a l H t t p E n d p o i n t s \b / . test ( stmt ) ) r . hasExternalEndpoints = true ;
826+ if ( / \. W i t h H t t p E n d p o i n t \b / . test ( stmt ) ) r . hasExternalEndpoints = true ;
747827 }
748828
749829 return resources ;
@@ -850,6 +930,7 @@ export default function PlaygroundPage() {
850930 const [ activeTab , setActiveTab ] = useState ( 'canvas' ) ;
851931 const [ importText , setImportText ] = useState ( '' ) ;
852932 const [ showImport , setShowImport ] = useState ( false ) ;
933+ const [ importFeedback , setImportFeedback ] = useState ( '' ) ;
853934 const [ highlightedResource , setHighlightedResource ] = useState < string | null > ( null ) ;
854935
855936 // Undo/redo history — uses refs for index/history to avoid stale closures
@@ -1095,6 +1176,11 @@ export default function PlaygroundPage() {
10951176 setShowImport ( false ) ;
10961177 setImportText ( '' ) ;
10971178 setActiveTab ( 'canvas' ) ;
1179+ setImportFeedback ( `Imported ${ parsed . length } resource${ parsed . length === 1 ? '' : 's' } ` ) ;
1180+ setTimeout ( ( ) => setImportFeedback ( '' ) , 3000 ) ;
1181+ } else {
1182+ setImportFeedback ( 'No resources found — check your AppHost code' ) ;
1183+ setTimeout ( ( ) => setImportFeedback ( '' ) , 3000 ) ;
10981184 }
10991185 } , [ importText ] ) ;
11001186
@@ -1769,6 +1855,12 @@ export default function PlaygroundPage() {
17691855 </ Box >
17701856 ) }
17711857
1858+ { importFeedback && (
1859+ < Box mb = "3" p = "2" bg = { importFeedback . startsWith ( 'No' ) ? 'red.950' : 'green.950' } borderRadius = "sm" border = "1px solid" borderColor = { importFeedback . startsWith ( 'No' ) ? 'red.700' : 'green.700' } >
1860+ < Text fontSize = "xs" color = { importFeedback . startsWith ( 'No' ) ? 'red.300' : 'green.300' } > { importFeedback } </ Text >
1861+ </ Box >
1862+ ) }
1863+
17721864 { /* Name warnings */ }
17731865 { nameWarnings . length > 0 && (
17741866 < Box mb = "3" p = "2" bg = "red.950" borderRadius = "sm" border = "1px solid" borderColor = "red.700" >
0 commit comments