@jetstart/core 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/.eslintrc.json +6 -0
  2. package/README.md +124 -0
  3. package/dist/build/builder.d.ts +57 -0
  4. package/dist/build/builder.d.ts.map +1 -0
  5. package/dist/build/builder.js +151 -0
  6. package/dist/build/builder.js.map +1 -0
  7. package/dist/build/cache.d.ts +51 -0
  8. package/dist/build/cache.d.ts.map +1 -0
  9. package/dist/build/cache.js +152 -0
  10. package/dist/build/cache.js.map +1 -0
  11. package/dist/build/dsl-parser.d.ts +54 -0
  12. package/dist/build/dsl-parser.d.ts.map +1 -0
  13. package/dist/build/dsl-parser.js +373 -0
  14. package/dist/build/dsl-parser.js.map +1 -0
  15. package/dist/build/dsl-types.d.ts +47 -0
  16. package/dist/build/dsl-types.d.ts.map +1 -0
  17. package/dist/build/dsl-types.js +7 -0
  18. package/dist/build/dsl-types.js.map +1 -0
  19. package/dist/build/gradle-injector.d.ts +14 -0
  20. package/dist/build/gradle-injector.d.ts.map +1 -0
  21. package/dist/build/gradle-injector.js +77 -0
  22. package/dist/build/gradle-injector.js.map +1 -0
  23. package/dist/build/gradle.d.ts +43 -0
  24. package/dist/build/gradle.d.ts.map +1 -0
  25. package/dist/build/gradle.js +281 -0
  26. package/dist/build/gradle.js.map +1 -0
  27. package/dist/build/index.d.ts +10 -0
  28. package/dist/build/index.d.ts.map +1 -0
  29. package/dist/build/index.js +26 -0
  30. package/dist/build/index.js.map +1 -0
  31. package/dist/build/parser.d.ts +12 -0
  32. package/dist/build/parser.d.ts.map +1 -0
  33. package/dist/build/parser.js +71 -0
  34. package/dist/build/parser.js.map +1 -0
  35. package/dist/build/watcher.d.ts +30 -0
  36. package/dist/build/watcher.d.ts.map +1 -0
  37. package/dist/build/watcher.js +120 -0
  38. package/dist/build/watcher.js.map +1 -0
  39. package/dist/index.d.ts +11 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +26 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/server/http.d.ts +12 -0
  44. package/dist/server/http.d.ts.map +1 -0
  45. package/dist/server/http.js +32 -0
  46. package/dist/server/http.js.map +1 -0
  47. package/dist/server/index.d.ts +35 -0
  48. package/dist/server/index.d.ts.map +1 -0
  49. package/dist/server/index.js +262 -0
  50. package/dist/server/index.js.map +1 -0
  51. package/dist/server/middleware.d.ts +7 -0
  52. package/dist/server/middleware.d.ts.map +1 -0
  53. package/dist/server/middleware.js +42 -0
  54. package/dist/server/middleware.js.map +1 -0
  55. package/dist/server/routes.d.ts +7 -0
  56. package/dist/server/routes.d.ts.map +1 -0
  57. package/dist/server/routes.js +104 -0
  58. package/dist/server/routes.js.map +1 -0
  59. package/dist/types/index.d.ts +20 -0
  60. package/dist/types/index.d.ts.map +1 -0
  61. package/dist/types/index.js +6 -0
  62. package/dist/types/index.js.map +1 -0
  63. package/dist/utils/index.d.ts +7 -0
  64. package/dist/utils/index.d.ts.map +1 -0
  65. package/dist/utils/index.js +23 -0
  66. package/dist/utils/index.js.map +1 -0
  67. package/dist/utils/logger.d.ts +10 -0
  68. package/dist/utils/logger.d.ts.map +1 -0
  69. package/dist/utils/logger.js +33 -0
  70. package/dist/utils/logger.js.map +1 -0
  71. package/dist/utils/qr.d.ts +8 -0
  72. package/dist/utils/qr.d.ts.map +1 -0
  73. package/dist/utils/qr.js +48 -0
  74. package/dist/utils/qr.js.map +1 -0
  75. package/dist/utils/session.d.ts +18 -0
  76. package/dist/utils/session.d.ts.map +1 -0
  77. package/dist/utils/session.js +49 -0
  78. package/dist/utils/session.js.map +1 -0
  79. package/dist/websocket/handler.d.ts +25 -0
  80. package/dist/websocket/handler.d.ts.map +1 -0
  81. package/dist/websocket/handler.js +126 -0
  82. package/dist/websocket/handler.js.map +1 -0
  83. package/dist/websocket/index.d.ts +18 -0
  84. package/dist/websocket/index.d.ts.map +1 -0
  85. package/dist/websocket/index.js +40 -0
  86. package/dist/websocket/index.js.map +1 -0
  87. package/dist/websocket/manager.d.ts +16 -0
  88. package/dist/websocket/manager.d.ts.map +1 -0
  89. package/dist/websocket/manager.js +58 -0
  90. package/dist/websocket/manager.js.map +1 -0
  91. package/package.json +78 -0
  92. package/src/build/builder.ts +192 -0
  93. package/src/build/cache.ts +144 -0
  94. package/src/build/dsl-parser.ts +382 -0
  95. package/src/build/dsl-types.ts +50 -0
  96. package/src/build/gradle-injector.ts +64 -0
  97. package/src/build/gradle.ts +305 -0
  98. package/src/build/index.ts +10 -0
  99. package/src/build/parser.ts +75 -0
  100. package/src/build/watcher.ts +103 -0
  101. package/src/index.ts +20 -0
  102. package/src/server/http.ts +38 -0
  103. package/src/server/index.ts +272 -0
  104. package/src/server/middleware.ts +43 -0
  105. package/src/server/routes.ts +116 -0
  106. package/src/types/index.ts +21 -0
  107. package/src/utils/index.ts +7 -0
  108. package/src/utils/logger.ts +28 -0
  109. package/src/utils/qr.ts +46 -0
  110. package/src/utils/session.ts +58 -0
  111. package/src/websocket/handler.ts +150 -0
  112. package/src/websocket/index.ts +56 -0
  113. package/src/websocket/manager.ts +63 -0
  114. package/tests/build.test.ts +13 -0
  115. package/tests/server.test.ts +13 -0
  116. package/tsconfig.json +25 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Build Service
3
+ * Main orchestrator for build operations, caching, and file watching
4
+ */
5
+
6
+ import { BuildConfig, BuildResult, BuildPhase, BuildStatus } from '@jetstart/shared';
7
+ import { GradleExecutor, GradleExecutorOptions } from './gradle';
8
+ import { BuildCache, BuildCacheOptions } from './cache';
9
+ import { FileWatcher } from './watcher';
10
+ import { EventEmitter } from 'events';
11
+
12
+ export interface BuildServiceOptions {
13
+ cacheEnabled?: boolean;
14
+ cachePath?: string;
15
+ watchEnabled?: boolean;
16
+ javaHome?: string;
17
+ androidHome?: string;
18
+ }
19
+
20
+ export interface BuildServiceEvents {
21
+ 'build:start': () => void;
22
+ 'build:progress': (status: BuildStatus) => void;
23
+ 'build:complete': (result: BuildResult) => void;
24
+ 'build:error': (error: string, details?: any) => void;
25
+ 'watch:change': (files: string[]) => void;
26
+ }
27
+
28
+ export declare interface BuildService {
29
+ on<K extends keyof BuildServiceEvents>(
30
+ event: K,
31
+ listener: BuildServiceEvents[K]
32
+ ): this;
33
+ emit<K extends keyof BuildServiceEvents>(
34
+ event: K,
35
+ ...args: Parameters<BuildServiceEvents[K]>
36
+ ): boolean;
37
+ }
38
+
39
+ export class BuildService extends EventEmitter {
40
+ private gradle: GradleExecutor;
41
+ private cache: BuildCache;
42
+ private watcher: FileWatcher | null = null;
43
+ private watchEnabled: boolean;
44
+ private isBuilding: boolean = false;
45
+
46
+ constructor(options: BuildServiceOptions = {}) {
47
+ super();
48
+
49
+ // Initialize Gradle executor
50
+ const gradleOptions: GradleExecutorOptions = {
51
+ javaHome: options.javaHome,
52
+ androidHome: options.androidHome,
53
+ };
54
+ this.gradle = new GradleExecutor(gradleOptions);
55
+
56
+ // Initialize cache
57
+ const cacheOptions: BuildCacheOptions = {
58
+ enabled: options.cacheEnabled ?? true,
59
+ cachePath: options.cachePath || require('os').tmpdir() + '/jetstart-cache',
60
+ };
61
+ this.cache = new BuildCache(cacheOptions);
62
+
63
+ this.watchEnabled = options.watchEnabled ?? true;
64
+ }
65
+
66
+ /**
67
+ * Build project
68
+ */
69
+ async build(config: BuildConfig): Promise<BuildResult> {
70
+ if (this.isBuilding) {
71
+ return {
72
+ success: false,
73
+ buildTime: 0,
74
+ errors: [{
75
+ file: '',
76
+ line: 0,
77
+ column: 0,
78
+ message: 'Build already in progress',
79
+ severity: 'error' as any,
80
+ }],
81
+ };
82
+ }
83
+
84
+ this.isBuilding = true;
85
+
86
+ try {
87
+ // Check cache
88
+ const cached = this.cache.get(config);
89
+ if (cached) {
90
+ this.emit('build:complete', cached.result);
91
+ return cached.result;
92
+ }
93
+
94
+ // Emit build start
95
+ this.emit('build:start');
96
+ this.emitProgress(BuildPhase.INITIALIZING, 0, 'Initializing build...');
97
+
98
+ // Execute Gradle build
99
+ this.emitProgress(BuildPhase.COMPILING, 20, 'Compiling Kotlin sources...');
100
+ const result = await this.gradle.execute(config);
101
+
102
+ // Cache successful builds
103
+ if (result.success) {
104
+ this.cache.set(config, result);
105
+ this.emitProgress(BuildPhase.COMPLETE, 100, 'Build complete');
106
+ this.emit('build:complete', result);
107
+ } else {
108
+ this.emitProgress(BuildPhase.FAILED, 0, 'Build failed');
109
+ this.emit('build:error', 'Build failed', result.errors);
110
+ }
111
+
112
+ return result;
113
+ } catch (err: any) {
114
+ const errorResult: BuildResult = {
115
+ success: false,
116
+ buildTime: 0,
117
+ errors: [{
118
+ file: '',
119
+ line: 0,
120
+ column: 0,
121
+ message: err.message || 'Unknown build error',
122
+ severity: 'error' as any,
123
+ }],
124
+ };
125
+
126
+ this.emit('build:error', err.message, err);
127
+ return errorResult;
128
+ } finally {
129
+ this.isBuilding = false;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Start watching for file changes
135
+ */
136
+ startWatching(projectPath: string, callback: (files: string[]) => void): void {
137
+ if (!this.watchEnabled) {
138
+ return;
139
+ }
140
+
141
+ if (this.watcher) {
142
+ this.stopWatching();
143
+ }
144
+
145
+ this.watcher = new FileWatcher({
146
+ projectPath,
147
+ callback: (files: string[]) => {
148
+ this.emit('watch:change', files);
149
+ callback(files);
150
+ },
151
+ });
152
+
153
+ this.watcher.watch(projectPath);
154
+ }
155
+
156
+ /**
157
+ * Stop watching for file changes
158
+ */
159
+ stopWatching(): void {
160
+ if (this.watcher) {
161
+ this.watcher.stop();
162
+ this.watcher = null;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Clear build cache
168
+ */
169
+ clearCache(): void {
170
+ this.cache.clear();
171
+ }
172
+
173
+ /**
174
+ * Check if currently building
175
+ */
176
+ isBuildInProgress(): boolean {
177
+ return this.isBuilding;
178
+ }
179
+
180
+ /**
181
+ * Emit build progress
182
+ */
183
+ private emitProgress(phase: BuildPhase, progress: number, message: string): void {
184
+ const status: BuildStatus = {
185
+ phase,
186
+ progress,
187
+ message,
188
+ timestamp: Date.now(),
189
+ };
190
+ this.emit('build:progress', status);
191
+ }
192
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Build Cache
3
+ * Simple file-based caching for successful builds
4
+ */
5
+
6
+ import * as crypto from 'crypto';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { BuildConfig, BuildResult } from '@jetstart/shared';
10
+
11
+ export interface CachedBuild {
12
+ config: BuildConfig;
13
+ result: BuildResult;
14
+ timestamp: number;
15
+ }
16
+
17
+ export interface BuildCacheOptions {
18
+ enabled: boolean;
19
+ cachePath: string;
20
+ maxAge?: number; // milliseconds
21
+ }
22
+
23
+ export class BuildCache {
24
+ private enabled: boolean;
25
+ private cachePath: string;
26
+ private maxAge: number;
27
+ private cache: Map<string, CachedBuild> = new Map();
28
+
29
+ constructor(options: BuildCacheOptions) {
30
+ this.enabled = options.enabled;
31
+ this.cachePath = options.cachePath;
32
+ this.maxAge = options.maxAge || 24 * 60 * 60 * 1000; // 24 hours default
33
+
34
+ if (this.enabled) {
35
+ this.ensureCacheDir();
36
+ this.loadCache();
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get cached build by config
42
+ */
43
+ get(config: BuildConfig): CachedBuild | null {
44
+ if (!this.enabled) {
45
+ return null;
46
+ }
47
+
48
+ const hash = this.hashConfig(config);
49
+ const cached = this.cache.get(hash);
50
+
51
+ if (!cached) {
52
+ return null;
53
+ }
54
+
55
+ // Check if cache is stale
56
+ if (Date.now() - cached.timestamp > this.maxAge) {
57
+ this.cache.delete(hash);
58
+ return null;
59
+ }
60
+
61
+ // Validate APK still exists
62
+ if (cached.result.apkPath && !fs.existsSync(cached.result.apkPath)) {
63
+ this.cache.delete(hash);
64
+ return null;
65
+ }
66
+
67
+ return cached;
68
+ }
69
+
70
+ /**
71
+ * Set cached build
72
+ */
73
+ set(config: BuildConfig, result: BuildResult): void {
74
+ if (!this.enabled || !result.success) {
75
+ return;
76
+ }
77
+
78
+ const hash = this.hashConfig(config);
79
+ const cached: CachedBuild = {
80
+ config,
81
+ result,
82
+ timestamp: Date.now(),
83
+ };
84
+
85
+ this.cache.set(hash, cached);
86
+ this.saveCache();
87
+ }
88
+
89
+ /**
90
+ * Clear entire cache
91
+ */
92
+ clear(): void {
93
+ this.cache.clear();
94
+ this.saveCache();
95
+ }
96
+
97
+ /**
98
+ * Hash build config for cache key
99
+ */
100
+ private hashConfig(config: BuildConfig): string {
101
+ const data = JSON.stringify({
102
+ projectPath: config.projectPath,
103
+ buildType: config.buildType,
104
+ minifyEnabled: config.minifyEnabled,
105
+ debuggable: config.debuggable,
106
+ applicationId: config.applicationId,
107
+ });
108
+ return crypto.createHash('md5').update(data).digest('hex');
109
+ }
110
+
111
+ /**
112
+ * Ensure cache directory exists
113
+ */
114
+ private ensureCacheDir(): void {
115
+ if (!fs.existsSync(this.cachePath)) {
116
+ fs.mkdirSync(this.cachePath, { recursive: true });
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Load cache from disk
122
+ */
123
+ private loadCache(): void {
124
+ const cacheFile = path.join(this.cachePath, 'build-cache.json');
125
+ if (fs.existsSync(cacheFile)) {
126
+ try {
127
+ const data = fs.readFileSync(cacheFile, 'utf-8');
128
+ const cacheData = JSON.parse(data);
129
+ this.cache = new Map(Object.entries(cacheData));
130
+ } catch (err) {
131
+ // Ignore corrupted cache
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Save cache to disk
138
+ */
139
+ private saveCache(): void {
140
+ const cacheFile = path.join(this.cachePath, 'build-cache.json');
141
+ const cacheData = Object.fromEntries(this.cache);
142
+ fs.writeFileSync(cacheFile, JSON.stringify(cacheData, null, 2));
143
+ }
144
+ }
@@ -0,0 +1,382 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { UIDefinition, DSLElement, DSLModifier, ParseResult } from './dsl-types';
4
+ import { log } from '../utils/logger';
5
+
6
+ /**
7
+ * DSL Parser
8
+ * Converts Kotlin Compose code to JSON DSL for runtime interpretation
9
+ */
10
+ export class DSLParser {
11
+ private static readonly TAG = 'DSLParser';
12
+
13
+ /**
14
+ * Parse a Kotlin file and extract UI definition
15
+ */
16
+ static parseFile(filePath: string): ParseResult {
17
+ try {
18
+ if (!fs.existsSync(filePath)) {
19
+ return {
20
+ success: false,
21
+ errors: [`File not found: ${filePath}`]
22
+ };
23
+ }
24
+
25
+ const content = fs.readFileSync(filePath, 'utf-8');
26
+ return this.parseContent(content, filePath);
27
+ } catch (error) {
28
+ return {
29
+ success: false,
30
+ errors: [`Failed to read file: ${error}`]
31
+ };
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Parse Kotlin content and extract UI definition
37
+ */
38
+ static parseContent(content: string, filePath: string): ParseResult {
39
+ try {
40
+ log(`Parsing Kotlin file: ${path.basename(filePath)}`);
41
+
42
+ // FIRST: Check if there's a getDefaultDSL() function with JSON
43
+ const dslFromFunction = this.extractDSLFromFunction(content);
44
+ if (dslFromFunction) {
45
+ log(`Extracted DSL from getDefaultDSL(): ${dslFromFunction.length} bytes`);
46
+ return {
47
+ success: true,
48
+ dsl: JSON.parse(dslFromFunction)
49
+ };
50
+ }
51
+
52
+ // FALLBACK: Try to find @Composable functions with Compose code
53
+ const composableMatch = this.findMainComposable(content);
54
+
55
+ if (!composableMatch) {
56
+ log('No main composable found, generating default DSL');
57
+ return {
58
+ success: true,
59
+ dsl: this.generateDefaultDSL()
60
+ };
61
+ }
62
+
63
+ // Parse the composable body
64
+ const element = this.parseComposableBody(composableMatch.body);
65
+
66
+ const dsl: UIDefinition = {
67
+ version: '1.0',
68
+ screen: element
69
+ };
70
+
71
+ log(`Successfully parsed DSL: ${JSON.stringify(dsl).length} bytes`);
72
+
73
+ return {
74
+ success: true,
75
+ dsl
76
+ };
77
+ } catch (error) {
78
+ log(`Parse error: ${error}`);
79
+ return {
80
+ success: false,
81
+ errors: [`Parse error: ${error}`]
82
+ };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Extract DSL JSON from getDefaultDSL() or similar function (legacy support)
88
+ */
89
+ private static extractDSLFromFunction(content: string): string | null {
90
+ // Look for functions that return JSON strings (legacy approach)
91
+ const functionRegex = /fun\s+getDefaultDSL\s*\(\s*\)\s*:\s*String\s*\{\s*return\s*"""([\s\S]*?)"""/;
92
+ const match = content.match(functionRegex);
93
+
94
+ if (match && match[1]) {
95
+ let jsonString = match[1].trim();
96
+ jsonString = jsonString.replace(/\.trimIndent\(\)/, '');
97
+ return jsonString;
98
+ }
99
+
100
+ return null;
101
+ }
102
+
103
+ /**
104
+ * Find the main @Composable function in the file
105
+ */
106
+ private static findMainComposable(content: string): { name: string; body: string } | null {
107
+ // Look for @Composable functions (AppContent, MainScreen, etc.)
108
+ const composableRegex = /@Composable\s+fun\s+(\w+)\s*\([^)]*\)\s*\{/g;
109
+ const matches = [...content.matchAll(composableRegex)];
110
+
111
+ log(`Found ${matches.length} @Composable functions`);
112
+
113
+ if (matches.length === 0) {
114
+ log('No @Composable functions found in file');
115
+ return null;
116
+ }
117
+
118
+ // Use the first composable function (should be AppContent, not LoadingScreen)
119
+ const match = matches[0];
120
+ const functionName = match[1];
121
+ log(`Parsing composable function: ${functionName}`);
122
+
123
+ const startIndex = match.index! + match[0].length;
124
+
125
+ // Extract the function body (handle nested braces)
126
+ const body = this.extractFunctionBody(content, startIndex);
127
+ log(`Extracted function body: ${body.substring(0, 100)}...`);
128
+
129
+ return {
130
+ name: functionName,
131
+ body
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Extract function body handling nested braces
137
+ */
138
+ private static extractFunctionBody(content: string, startIndex: number): string {
139
+ let braceCount = 1;
140
+ let endIndex = startIndex;
141
+
142
+ while (braceCount > 0 && endIndex < content.length) {
143
+ if (content[endIndex] === '{') braceCount++;
144
+ if (content[endIndex] === '}') braceCount--;
145
+ endIndex++;
146
+ }
147
+
148
+ return content.substring(startIndex, endIndex - 1).trim();
149
+ }
150
+
151
+ /**
152
+ * Parse the composable body and extract UI structure
153
+ */
154
+ private static parseComposableBody(body: string): DSLElement {
155
+ // Try to find the root element (Column, Row, Box, etc.)
156
+ const layoutMatch = body.match(/(Column|Row|Box)\s*\(/);
157
+
158
+ if (!layoutMatch) {
159
+ // Fallback: Simple text content
160
+ const textMatch = body.match(/Text\s*\(\s*text\s*=\s*"([^"]+)"/);
161
+ if (textMatch) {
162
+ return {
163
+ type: 'Text',
164
+ text: textMatch[1]
165
+ };
166
+ }
167
+
168
+ // Default fallback
169
+ return {
170
+ type: 'Column',
171
+ modifier: { fillMaxSize: true, padding: 16 },
172
+ horizontalAlignment: 'CenterHorizontally',
173
+ verticalArrangement: 'Center',
174
+ children: [
175
+ {
176
+ type: 'Text',
177
+ text: 'Hot Reload Active',
178
+ style: 'headlineMedium'
179
+ }
180
+ ]
181
+ };
182
+ }
183
+
184
+ const layoutType = layoutMatch[1];
185
+ const layoutStartIndex = layoutMatch.index! + layoutMatch[0].length;
186
+
187
+ // Extract FULL layout declaration (parameters + body with children)
188
+ // We need to extract from after "Column(" to the end, including ) { ... }
189
+ const layoutFullContent = body.substring(layoutStartIndex);
190
+
191
+ return this.parseLayout(layoutType, layoutFullContent);
192
+ }
193
+
194
+ /**
195
+ * Parse a layout element (Column, Row, Box)
196
+ */
197
+ private static parseLayout(type: string, content: string): DSLElement {
198
+ const element: DSLElement = { type };
199
+
200
+ // Parse modifier
201
+ const modifierMatch = content.match(/modifier\s*=\s*Modifier([^,\n}]+)/);
202
+ if (modifierMatch) {
203
+ element.modifier = this.parseModifier(modifierMatch[1]);
204
+ }
205
+
206
+ // Parse alignment
207
+ const alignmentMatch = content.match(/horizontalAlignment\s*=\s*Alignment\.(\w+)/);
208
+ if (alignmentMatch) {
209
+ element.horizontalAlignment = alignmentMatch[1];
210
+ }
211
+
212
+ const arrangementMatch = content.match(/verticalArrangement\s*=\s*Arrangement\.(\w+)/);
213
+ if (arrangementMatch) {
214
+ element.verticalArrangement = arrangementMatch[1];
215
+ }
216
+
217
+ // Parse children (content inside the braces)
218
+ const childrenMatch = content.match(/\)\s*\{([\s\S]+)\}$/);
219
+ if (childrenMatch) {
220
+ element.children = this.parseChildren(childrenMatch[1]);
221
+ }
222
+
223
+ return element;
224
+ }
225
+
226
+ /**
227
+ * Parse modifier chain
228
+ */
229
+ private static parseModifier(modifierChain: string): DSLModifier {
230
+ const modifier: DSLModifier = {};
231
+
232
+ if (modifierChain.includes('.fillMaxSize()')) modifier.fillMaxSize = true;
233
+ if (modifierChain.includes('.fillMaxWidth()')) modifier.fillMaxWidth = true;
234
+ if (modifierChain.includes('.fillMaxHeight()')) modifier.fillMaxHeight = true;
235
+
236
+ const paddingMatch = modifierChain.match(/\.padding\((\d+)\.dp\)/);
237
+ if (paddingMatch) {
238
+ modifier.padding = parseInt(paddingMatch[1]);
239
+ }
240
+
241
+ const sizeMatch = modifierChain.match(/\.size\((\d+)\.dp\)/);
242
+ if (sizeMatch) {
243
+ modifier.size = parseInt(sizeMatch[1]);
244
+ }
245
+
246
+ const heightMatch = modifierChain.match(/\.height\((\d+)\.dp\)/);
247
+ if (heightMatch) {
248
+ modifier.height = parseInt(heightMatch[1]);
249
+ }
250
+
251
+ const widthMatch = modifierChain.match(/\.width\((\d+)\.dp\)/);
252
+ if (widthMatch) {
253
+ modifier.width = parseInt(widthMatch[1]);
254
+ }
255
+
256
+ return modifier;
257
+ }
258
+
259
+ /**
260
+ * Parse children elements (handles multi-line elements)
261
+ * Maintains source code order
262
+ */
263
+ private static parseChildren(content: string): DSLElement[] {
264
+ // Remove all newlines and extra whitespace for easier parsing
265
+ const normalized = content.replace(/\s+/g, ' ');
266
+
267
+ // Track elements with their positions for proper ordering
268
+ const elements: Array<{ position: number; element: DSLElement }> = [];
269
+ const usedText = new Set<string>();
270
+
271
+ // First pass: Parse Button elements and track their text to avoid duplicates
272
+ const buttonRegex = /Button\s*\(\s*onClick\s*=\s*\{[^}]*\}(?:[^)]*modifier\s*=\s*Modifier\.fillMaxWidth\s*\(\s*\))?[^)]*\)\s*\{\s*Text\s*\(\s*"([^"]+)"\s*\)/g;
273
+ let match;
274
+ while ((match = buttonRegex.exec(normalized)) !== null) {
275
+ const buttonText = match[1];
276
+ elements.push({
277
+ position: match.index!,
278
+ element: {
279
+ type: 'Button',
280
+ text: buttonText,
281
+ onClick: 'handleButtonClick',
282
+ modifier: normalized.includes('fillMaxWidth') ? { fillMaxWidth: true } : undefined
283
+ }
284
+ });
285
+ usedText.add(buttonText);
286
+ }
287
+
288
+ // Parse Spacer elements
289
+ const spacerRegex = /Spacer\s*\(\s*modifier\s*=\s*Modifier\.height\s*\(\s*(\d+)\.dp\s*\)/g;
290
+ while ((match = spacerRegex.exec(normalized)) !== null) {
291
+ elements.push({
292
+ position: match.index!,
293
+ element: {
294
+ type: 'Spacer',
295
+ height: parseInt(match[1])
296
+ }
297
+ });
298
+ }
299
+
300
+ // Parse Text elements (multiple patterns, skip if text is in a button)
301
+ const textPatterns = [
302
+ /Text\s*\(\s*text\s*=\s*"([^"]+)"[^)]*style\s*=\s*MaterialTheme\.typography\.(\w+)/g,
303
+ /Text\s*\(\s*"([^"]+)"[^)]*style\s*=\s*MaterialTheme\.typography\.(\w+)/g,
304
+ /Text\s*\(\s*text\s*=\s*"([^"]+)"/g,
305
+ /Text\s*\(\s*"([^"]+)"\s*\)/g
306
+ ];
307
+
308
+ for (const regex of textPatterns) {
309
+ while ((match = regex.exec(normalized)) !== null) {
310
+ const text = match[1];
311
+ // Skip if this text is already used in a button
312
+ if (!usedText.has(text)) {
313
+ elements.push({
314
+ position: match.index!,
315
+ element: {
316
+ type: 'Text',
317
+ text: text,
318
+ style: match[2] || undefined
319
+ }
320
+ });
321
+ usedText.add(text);
322
+ }
323
+ }
324
+ }
325
+
326
+ // Sort by position to maintain source order
327
+ elements.sort((a, b) => a.position - b.position);
328
+
329
+ // Return just the elements, in correct order
330
+ return elements.map(e => e.element);
331
+ }
332
+
333
+ /**
334
+ * Extract content within parentheses (handles nesting)
335
+ */
336
+ private static extractParenthesesContent(content: string, startIndex: number): string {
337
+ let parenCount = 1;
338
+ let endIndex = startIndex;
339
+
340
+ while (parenCount > 0 && endIndex < content.length) {
341
+ if (content[endIndex] === '(') parenCount++;
342
+ if (content[endIndex] === ')') parenCount--;
343
+ endIndex++;
344
+ }
345
+
346
+ return content.substring(startIndex, endIndex - 1);
347
+ }
348
+
349
+ /**
350
+ * Generate default DSL when parsing fails
351
+ */
352
+ private static generateDefaultDSL(): UIDefinition {
353
+ return {
354
+ version: '1.0',
355
+ screen: {
356
+ type: 'Column',
357
+ modifier: {
358
+ fillMaxSize: true,
359
+ padding: 16
360
+ },
361
+ horizontalAlignment: 'CenterHorizontally',
362
+ verticalArrangement: 'Center',
363
+ children: [
364
+ {
365
+ type: 'Text',
366
+ text: 'Welcome to JetStart! 🚀',
367
+ style: 'headlineMedium'
368
+ },
369
+ {
370
+ type: 'Spacer',
371
+ height: 16
372
+ },
373
+ {
374
+ type: 'Text',
375
+ text: 'Edit your code to see hot reload',
376
+ style: 'bodyMedium'
377
+ }
378
+ ]
379
+ }
380
+ };
381
+ }
382
+ }