|
1 | 1 | import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; |
| 2 | +import { parseAppHostCode, makeId, makeResource, type PlaygroundResource, type ResourceType, type CodeLanguage } from '../utils/appHostParser'; |
2 | 3 | import { |
3 | 4 | Box, |
4 | 5 | Flex, |
@@ -51,48 +52,11 @@ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; |
51 | 52 |
|
52 | 53 | // ─── Types ──────────────────────────────────────────────────────────────────── |
53 | 54 |
|
54 | | -type CodeLanguage = 'csharp' | 'typescript'; |
55 | | - |
56 | | -type ResourceType = |
57 | | - | 'postgres' |
58 | | - | 'redis' |
59 | | - | 'sqlserver' |
60 | | - | 'mongodb' |
61 | | - | 'rabbitmq' |
62 | | - | 'kafka' |
63 | | - | 'project' |
64 | | - | 'container' |
65 | | - | 'npmapp' |
66 | | - | 'pythonapp' |
67 | | - | 'azurestorage' |
68 | | - | 'keyvault' |
69 | | - | 'parameter'; |
70 | | - |
71 | 55 | interface EnvVar { |
72 | 56 | key: string; |
73 | 57 | value: string; |
74 | 58 | } |
75 | 59 |
|
76 | | -interface PlaygroundResource { |
77 | | - id: string; |
78 | | - type: ResourceType; |
79 | | - name: string; |
80 | | - databases: string[]; |
81 | | - image: string; |
82 | | - references: string[]; |
83 | | - waitFor: string[]; |
84 | | - hasDataVolume: boolean; |
85 | | - hasExternalEndpoints: boolean; |
86 | | - ports: string; |
87 | | - envVars: EnvVar[]; |
88 | | - isPersistent: boolean; |
89 | | - isSecret: boolean; // for parameter type |
90 | | - args: string; // for containers |
91 | | - scriptPath: string; // for python apps |
92 | | - projectPath: string; // for npm / python relative path |
93 | | - projectLanguage: CodeLanguage; |
94 | | -} |
95 | | - |
96 | 60 | interface ResourceTemplate { |
97 | 61 | type: ResourceType; |
98 | 62 | label: string; |
@@ -157,29 +121,7 @@ interface Example { |
157 | 121 | resources: PlaygroundResource[]; |
158 | 122 | } |
159 | 123 |
|
160 | | -function makeId(): string { |
161 | | - return `r-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; |
162 | | -} |
163 | | - |
164 | | -function makeResource(overrides: Partial<PlaygroundResource> & { id: string; type: ResourceType; name: string }): PlaygroundResource { |
165 | | - return { |
166 | | - databases: [], |
167 | | - image: '', |
168 | | - references: [], |
169 | | - waitFor: [], |
170 | | - hasDataVolume: false, |
171 | | - hasExternalEndpoints: false, |
172 | | - ports: '', |
173 | | - envVars: [], |
174 | | - isPersistent: false, |
175 | | - isSecret: false, |
176 | | - args: '', |
177 | | - scriptPath: '', |
178 | | - projectPath: '', |
179 | | - projectLanguage: 'csharp', |
180 | | - ...overrides, |
181 | | - }; |
182 | | -} |
| 124 | +// makeId, makeResource, parseAppHostCode imported from ../utils/appHostParser |
183 | 125 |
|
184 | 126 | function buildExamples(): Example[] { |
185 | 127 | const ecomIds = { pg: makeId(), cache: makeId(), mq: makeId(), api: makeId(), web: makeId() }; |
@@ -672,162 +614,7 @@ function makeCSharpScaffold(name: string, refs: PlaygroundResource[]): ProjectSc |
672 | 614 | ]; |
673 | 615 | } |
674 | 616 |
|
675 | | -// ─── Import parser ─────────────────────────────────────────────────────────── |
676 | | - |
677 | | -const METHOD_TO_TYPE: Record<string, ResourceType> = { |
678 | | - AddPostgres: 'postgres', AddRedis: 'redis', AddSqlServer: 'sqlserver', |
679 | | - AddMongoDB: 'mongodb', AddRabbitMQ: 'rabbitmq', AddKafka: 'kafka', |
680 | | - AddProject: 'project', AddCSharpApp: 'project', |
681 | | - AddContainer: 'container', |
682 | | - AddNpmApp: 'npmapp', AddViteApp: 'npmapp', AddJavaScriptApp: 'npmapp', AddNodeApp: 'npmapp', |
683 | | - AddPythonApp: 'pythonapp', AddUvicornApp: 'pythonapp', AddUvApp: 'pythonapp', |
684 | | - AddAzureStorage: 'azurestorage', AddAzureKeyVault: 'keyvault', |
685 | | - AddParameter: 'parameter', AddConnectionString: 'parameter', |
686 | | - AddYarp: 'container', AddElasticsearch: 'container', |
687 | | - AddMySql: 'container', AddOracle: 'container', AddNats: 'container', |
688 | | -}; |
689 | | - |
690 | | -function preprocessCode(code: string): string { |
691 | | - return code |
692 | | - .replace(/\/\/.*$/gm, '') |
693 | | - .replace(/\/\*[\s\S]*?\*\//g, '') |
694 | | - .replace(/#(?:if|else|endif|pragma|region|endregion).*$/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 | | - |
703 | | -function parseAppHostCode(code: string): PlaygroundResource[] { |
704 | | - const resources: PlaygroundResource[] = []; |
705 | | - 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>(); |
708 | | - |
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 | | - /(?:var|const|[A-Z]\w*)\s+(\w+)\s*=\s*builder\.(\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' && /secret\s*:\s*true/i.test(stmt)) r.isSecret = true; |
729 | | - resources.push(r); |
730 | | - } |
731 | | - } |
732 | | - |
733 | | - // Step 2: Extract .AddDatabase() anywhere in the statement |
734 | | - const dbMatches = stmt.matchAll(/\.AddDatabase\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 | | - /(?:var|const|[A-Z]\w*)\s+(\w+)\s*=\s*builder\.(\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*AddDatabase/); |
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 | | - } |
762 | | - } |
763 | | - |
764 | | - // Step 3: Also handle `var dbVar = parentVar.AddDatabase("name")` as a variable assignment |
765 | | - const dbAssignMatch = stmt.match( |
766 | | - /(?:var|const|[A-Z]\w*)\s+(\w+)\s*=\s*(\w+)(?:\.\w+\([^)]*\))*\.AddDatabase\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 | | - } |
779 | | - } |
780 | | - |
781 | | - // Step 4: Extract .WithReference() and .WaitFor() — find the consumer variable |
782 | | - const refMatches = [...stmt.matchAll(/\.(WithReference|WaitFor)\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(/(?:var|const|[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 | | - } |
815 | | - } |
816 | | - } |
817 | | - |
818 | | - // Step 5: Scan statements for attribute methods (.WithDataVolume, .WithLifetime, etc.) |
819 | | - for (const r of resources) { |
820 | | - const varName = [...varToId.entries()].find(([, id]) => id === r.id)?.[0]; |
821 | | - if (!varName) continue; |
822 | | - const stmt = varToStmt.get(varName) ?? ''; |
823 | | - if (/\.WithDataVolume\b/.test(stmt)) r.hasDataVolume = true; |
824 | | - if (/\.WithLifetime\s*\(\s*ContainerLifetime\.Persistent\s*\)/.test(stmt)) r.isPersistent = true; |
825 | | - if (/\.WithExternalHttpEndpoints\b/.test(stmt)) r.hasExternalEndpoints = true; |
826 | | - if (/\.WithHttpEndpoint\b/.test(stmt)) r.hasExternalEndpoints = true; |
827 | | - } |
828 | | - |
829 | | - return resources; |
830 | | -} |
| 617 | +// Parser (parseAppHostCode, makeId, makeResource) imported from ../utils/appHostParser |
831 | 618 |
|
832 | 619 | // ─── localStorage persistence ──────────────────────────────────────────────── |
833 | 620 |
|
|
0 commit comments