@jetstart/core 1.6.0 → 2.0.0

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 (40) hide show
  1. package/README.md +189 -74
  2. package/dist/build/dex-generator.d.ts +27 -0
  3. package/dist/build/dex-generator.js +202 -0
  4. package/dist/build/dsl-parser.d.ts +3 -30
  5. package/dist/build/dsl-parser.js +67 -240
  6. package/dist/build/dsl-types.d.ts +8 -0
  7. package/dist/build/gradle.d.ts +51 -0
  8. package/dist/build/gradle.js +233 -1
  9. package/dist/build/hot-reload-service.d.ts +36 -0
  10. package/dist/build/hot-reload-service.js +179 -0
  11. package/dist/build/js-compiler-service.d.ts +61 -0
  12. package/dist/build/js-compiler-service.js +421 -0
  13. package/dist/build/kotlin-compiler.d.ts +54 -0
  14. package/dist/build/kotlin-compiler.js +450 -0
  15. package/dist/build/kotlin-parser.d.ts +91 -0
  16. package/dist/build/kotlin-parser.js +1030 -0
  17. package/dist/build/override-generator.d.ts +54 -0
  18. package/dist/build/override-generator.js +430 -0
  19. package/dist/server/index.d.ts +16 -1
  20. package/dist/server/index.js +147 -42
  21. package/dist/websocket/handler.d.ts +20 -4
  22. package/dist/websocket/handler.js +73 -38
  23. package/dist/websocket/index.d.ts +8 -0
  24. package/dist/websocket/index.js +15 -11
  25. package/dist/websocket/manager.d.ts +2 -2
  26. package/dist/websocket/manager.js +1 -1
  27. package/package.json +3 -3
  28. package/src/build/dex-generator.ts +197 -0
  29. package/src/build/dsl-parser.ts +73 -272
  30. package/src/build/dsl-types.ts +9 -0
  31. package/src/build/gradle.ts +259 -1
  32. package/src/build/hot-reload-service.ts +178 -0
  33. package/src/build/js-compiler-service.ts +411 -0
  34. package/src/build/kotlin-compiler.ts +460 -0
  35. package/src/build/kotlin-parser.ts +1043 -0
  36. package/src/build/override-generator.ts +478 -0
  37. package/src/server/index.ts +162 -54
  38. package/src/websocket/handler.ts +94 -56
  39. package/src/websocket/index.ts +27 -14
  40. package/src/websocket/manager.ts +2 -2
@@ -37,6 +37,7 @@ exports.DSLParser = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const logger_1 = require("../utils/logger");
40
+ const kotlin_parser_1 = require("./kotlin-parser");
40
41
  /**
41
42
  * DSL Parser
42
43
  * Converts Kotlin Compose code to JSON DSL for runtime interpretation
@@ -70,17 +71,16 @@ class DSLParser {
70
71
  static parseContent(content, filePath) {
71
72
  try {
72
73
  (0, logger_1.log)(`Parsing Kotlin file: ${path.basename(filePath)}`);
73
- // FIRST: Check if there's a getDefaultDSL() function with JSON
74
+ // FIRST: Check if there's a getDefaultDSL() function
74
75
  const dslFromFunction = this.extractDSLFromFunction(content);
75
76
  if (dslFromFunction) {
76
- (0, logger_1.log)(`Extracted DSL from getDefaultDSL(): ${dslFromFunction.length} bytes`);
77
77
  return {
78
78
  success: true,
79
79
  dsl: JSON.parse(dslFromFunction)
80
80
  };
81
81
  }
82
- // FALLBACK: Try to find @Composable functions with Compose code
83
- const composableMatch = this.findMainComposable(content);
82
+ // FALLBACK: Find @Composable function
83
+ const { main: composableMatch, library } = this.extractComposables(content);
84
84
  if (!composableMatch) {
85
85
  (0, logger_1.log)('No main composable found, generating default DSL');
86
86
  return {
@@ -88,11 +88,16 @@ class DSLParser {
88
88
  dsl: this.generateDefaultDSL()
89
89
  };
90
90
  }
91
- // Parse the composable body
92
- const element = this.parseComposableBody(composableMatch.body);
91
+ // Tokenize and Parse the body using Recursive Descent Parser
92
+ (0, logger_1.log)(`Tokenizing composable body (${composableMatch.name})...`);
93
+ const tokenizer = new kotlin_parser_1.Tokenizer(composableMatch.body);
94
+ const tokens = tokenizer.tokenize();
95
+ (0, logger_1.log)(`Generated ${tokens.length} tokens. Parsing...`);
96
+ const parser = new kotlin_parser_1.KotlinParser(tokens, library);
97
+ const rootElement = parser.parse();
93
98
  const dsl = {
94
99
  version: '1.0',
95
- screen: element
100
+ screen: rootElement
96
101
  };
97
102
  (0, logger_1.log)(`Successfully parsed DSL: ${JSON.stringify(dsl).length} bytes`);
98
103
  return {
@@ -108,262 +113,84 @@ class DSLParser {
108
113
  };
109
114
  }
110
115
  }
111
- /**
112
- * Extract DSL JSON from getDefaultDSL() or similar function (legacy support)
113
- */
114
116
  static extractDSLFromFunction(content) {
115
- // Look for functions that return JSON strings (legacy approach)
116
117
  const functionRegex = /fun\s+getDefaultDSL\s*\(\s*\)\s*:\s*String\s*\{\s*return\s*"""([\s\S]*?)"""/;
117
118
  const match = content.match(functionRegex);
118
119
  if (match && match[1]) {
119
- let jsonString = match[1].trim();
120
- jsonString = jsonString.replace(/\.trimIndent\(\)/, '');
121
- return jsonString;
120
+ return match[1].trim().replace(/\.trimIndent\(\)/, '');
122
121
  }
123
122
  return null;
124
123
  }
125
124
  /**
126
125
  * Find the main @Composable function in the file
127
126
  */
128
- static findMainComposable(content) {
129
- // Look for @Composable functions (AppContent, MainScreen, etc.)
130
- const composableRegex = /@Composable\s+fun\s+(\w+)\s*\([^)]*\)\s*\{/g;
131
- const matches = [...content.matchAll(composableRegex)];
132
- (0, logger_1.log)(`Found ${matches.length} @Composable functions`);
133
- if (matches.length === 0) {
134
- (0, logger_1.log)('No @Composable functions found in file');
135
- return null;
136
- }
137
- // Use the first composable function (should be AppContent, not LoadingScreen)
138
- const match = matches[0];
139
- const functionName = match[1];
140
- (0, logger_1.log)(`Parsing composable function: ${functionName}`);
141
- const startIndex = match.index + match[0].length;
142
- // Extract the function body (handle nested braces)
143
- const body = this.extractFunctionBody(content, startIndex);
144
- (0, logger_1.log)(`Extracted function body: ${body.substring(0, 100)}...`);
145
- return {
146
- name: functionName,
147
- body
148
- };
149
- }
150
127
  /**
151
- * Extract function body handling nested braces
128
+ * Find the main @Composable and a library of all others
152
129
  */
153
- static extractFunctionBody(content, startIndex) {
154
- let braceCount = 1;
155
- let endIndex = startIndex;
156
- while (braceCount > 0 && endIndex < content.length) {
157
- if (content[endIndex] === '{')
158
- braceCount++;
159
- if (content[endIndex] === '}')
160
- braceCount--;
161
- endIndex++;
162
- }
163
- return content.substring(startIndex, endIndex - 1).trim();
164
- }
165
- /**
166
- * Parse the composable body and extract UI structure
167
- */
168
- static parseComposableBody(body) {
169
- // Try to find the root element (Column, Row, Box, etc.)
170
- const layoutMatch = body.match(/(Column|Row|Box)\s*\(/);
171
- if (!layoutMatch) {
172
- // Fallback: Simple text content
173
- const textMatch = body.match(/Text\s*\(\s*text\s*=\s*"([^"]+)"/);
174
- if (textMatch) {
175
- return {
176
- type: 'Text',
177
- text: textMatch[1]
178
- };
179
- }
180
- // Default fallback
181
- return {
182
- type: 'Column',
183
- modifier: { fillMaxSize: true, padding: 16 },
184
- horizontalAlignment: 'CenterHorizontally',
185
- verticalArrangement: 'Center',
186
- children: [
187
- {
188
- type: 'Text',
189
- text: 'Hot Reload Active',
190
- style: 'headlineMedium'
191
- }
192
- ]
193
- };
194
- }
195
- const layoutType = layoutMatch[1];
196
- const layoutStartIndex = layoutMatch.index + layoutMatch[0].length;
197
- // Extract FULL layout declaration (parameters + body with children)
198
- // We need to extract from after "Column(" to the end, including ) { ... }
199
- const layoutFullContent = body.substring(layoutStartIndex);
200
- return this.parseLayout(layoutType, layoutFullContent);
201
- }
202
- /**
203
- * Parse a layout element (Column, Row, Box)
204
- */
205
- static parseLayout(type, content) {
206
- const element = { type };
207
- // Parse modifier
208
- const modifierMatch = content.match(/modifier\s*=\s*Modifier([^,\n}]+)/);
209
- if (modifierMatch) {
210
- element.modifier = this.parseModifier(modifierMatch[1]);
211
- }
212
- // Parse alignment
213
- const alignmentMatch = content.match(/horizontalAlignment\s*=\s*Alignment\.(\w+)/);
214
- if (alignmentMatch) {
215
- element.horizontalAlignment = alignmentMatch[1];
216
- }
217
- const arrangementMatch = content.match(/verticalArrangement\s*=\s*Arrangement\.(\w+)/);
218
- if (arrangementMatch) {
219
- element.verticalArrangement = arrangementMatch[1];
220
- }
221
- // Parse children (content inside the braces)
222
- const childrenMatch = content.match(/\)\s*\{([\s\S]+)\}$/);
223
- if (childrenMatch) {
224
- element.children = this.parseChildren(childrenMatch[1]);
225
- }
226
- return element;
227
- }
228
- /**
229
- * Parse modifier chain
230
- */
231
- static parseModifier(modifierChain) {
232
- const modifier = {};
233
- if (modifierChain.includes('.fillMaxSize()'))
234
- modifier.fillMaxSize = true;
235
- if (modifierChain.includes('.fillMaxWidth()'))
236
- modifier.fillMaxWidth = true;
237
- if (modifierChain.includes('.fillMaxHeight()'))
238
- modifier.fillMaxHeight = true;
239
- const paddingMatch = modifierChain.match(/\.padding\((\d+)\.dp\)/);
240
- if (paddingMatch) {
241
- modifier.padding = parseInt(paddingMatch[1]);
242
- }
243
- const sizeMatch = modifierChain.match(/\.size\((\d+)\.dp\)/);
244
- if (sizeMatch) {
245
- modifier.size = parseInt(sizeMatch[1]);
246
- }
247
- const heightMatch = modifierChain.match(/\.height\((\d+)\.dp\)/);
248
- if (heightMatch) {
249
- modifier.height = parseInt(heightMatch[1]);
250
- }
251
- const widthMatch = modifierChain.match(/\.width\((\d+)\.dp\)/);
252
- if (widthMatch) {
253
- modifier.width = parseInt(widthMatch[1]);
254
- }
255
- return modifier;
256
- }
257
- /**
258
- * Parse children elements (handles multi-line elements)
259
- * Maintains source code order
260
- */
261
- static parseChildren(content) {
262
- // Remove all newlines and extra whitespace for easier parsing
263
- const normalized = content.replace(/\s+/g, ' ');
264
- // Track elements with their positions for proper ordering
265
- const elements = [];
266
- const usedText = new Set();
267
- // First pass: Parse Button elements and track their text to avoid duplicates
268
- const buttonRegex = /Button\s*\(\s*onClick\s*=\s*\{[^}]*\}(?:[^)]*modifier\s*=\s*Modifier\.fillMaxWidth\s*\(\s*\))?[^)]*\)\s*\{\s*Text\s*\(\s*"([^"]+)"\s*\)/g;
269
- let match;
270
- while ((match = buttonRegex.exec(normalized)) !== null) {
271
- const buttonText = match[1];
272
- elements.push({
273
- position: match.index,
274
- element: {
275
- type: 'Button',
276
- text: buttonText,
277
- onClick: 'handleButtonClick',
278
- modifier: normalized.includes('fillMaxWidth') ? { fillMaxWidth: true } : undefined
279
- }
280
- });
281
- usedText.add(buttonText);
282
- }
283
- // Parse Spacer elements
284
- const spacerRegex = /Spacer\s*\(\s*modifier\s*=\s*Modifier\.height\s*\(\s*(\d+)\.dp\s*\)/g;
285
- while ((match = spacerRegex.exec(normalized)) !== null) {
286
- elements.push({
287
- position: match.index,
288
- element: {
289
- type: 'Spacer',
290
- height: parseInt(match[1])
291
- }
292
- });
293
- }
294
- // Parse Text elements (multiple patterns, skip if text is in a button)
295
- const textPatterns = [
296
- /Text\s*\(\s*text\s*=\s*"([^"]+)"[^)]*style\s*=\s*MaterialTheme\.typography\.(\w+)/g,
297
- /Text\s*\(\s*"([^"]+)"[^)]*style\s*=\s*MaterialTheme\.typography\.(\w+)/g,
298
- /Text\s*\(\s*text\s*=\s*"([^"]+)"/g,
299
- /Text\s*\(\s*"([^"]+)"\s*\)/g
300
- ];
301
- for (const regex of textPatterns) {
302
- while ((match = regex.exec(normalized)) !== null) {
303
- const text = match[1];
304
- // Skip if this text is already used in a button
305
- if (!usedText.has(text)) {
306
- elements.push({
307
- position: match.index,
308
- element: {
309
- type: 'Text',
310
- text: text,
311
- style: match[2] || undefined
312
- }
313
- });
314
- usedText.add(text);
130
+ static extractComposables(content) {
131
+ const library = new Map();
132
+ let main = null;
133
+ const composableIndices = [...content.matchAll(/@Composable/g)].map(m => m.index);
134
+ for (const startIndex of composableIndices) {
135
+ const funRegex = /fun\s+(\w+)/g;
136
+ funRegex.lastIndex = startIndex;
137
+ const funMatch = funRegex.exec(content);
138
+ if (!funMatch || (funMatch.index - startIndex > 200))
139
+ continue;
140
+ const functionName = funMatch[1];
141
+ const funIndex = funMatch.index;
142
+ const openParenIndex = content.indexOf('(', funIndex);
143
+ let bodyStartIndex = -1;
144
+ if (openParenIndex !== -1) {
145
+ const closeParenIndex = this.findMatchingBracket(content, openParenIndex, '(', ')');
146
+ if (closeParenIndex !== -1) {
147
+ bodyStartIndex = content.indexOf('{', closeParenIndex);
315
148
  }
316
149
  }
150
+ else {
151
+ bodyStartIndex = content.indexOf('{', funIndex);
152
+ }
153
+ if (bodyStartIndex === -1)
154
+ continue;
155
+ const bodyEndIndex = this.findMatchingBracket(content, bodyStartIndex, '{', '}');
156
+ if (bodyEndIndex !== -1) {
157
+ const body = content.substring(bodyStartIndex + 1, bodyEndIndex).trim();
158
+ library.set(functionName, body);
159
+ // Heuristic for Main: 'Screen' suffix or simply the largest/last one?
160
+ // For now, let's assume the one named 'NotesScreen' or similar is Main.
161
+ // Or just keep the logic: "NotesScreen" (file name matches?)
162
+ // For now, ensure we capture everything.
163
+ if (!main)
164
+ main = { name: functionName, body };
165
+ // If explicitly named 'Screen', prefer it
166
+ if (functionName.endsWith('Screen'))
167
+ main = { name: functionName, body };
168
+ }
317
169
  }
318
- // Sort by position to maintain source order
319
- elements.sort((a, b) => a.position - b.position);
320
- // Return just the elements, in correct order
321
- return elements.map(e => e.element);
170
+ return { main, library };
322
171
  }
323
- /**
324
- * Extract content within parentheses (handles nesting)
325
- */
326
- static extractParenthesesContent(content, startIndex) {
327
- let parenCount = 1;
328
- let endIndex = startIndex;
329
- while (parenCount > 0 && endIndex < content.length) {
330
- if (content[endIndex] === '(')
331
- parenCount++;
332
- if (content[endIndex] === ')')
333
- parenCount--;
334
- endIndex++;
335
- }
336
- return content.substring(startIndex, endIndex - 1);
172
+ static findMatchingBracket(content, startIndex, openChar, closeChar) {
173
+ let count = 0;
174
+ for (let i = startIndex; i < content.length; i++) {
175
+ if (content[i] === openChar)
176
+ count++;
177
+ else if (content[i] === closeChar)
178
+ count--;
179
+ if (count === 0)
180
+ return i;
181
+ }
182
+ return -1;
337
183
  }
338
- /**
339
- * Generate default DSL when parsing fails
340
- */
184
+ // ... removed obsolete methods ...
341
185
  static generateDefaultDSL() {
342
186
  return {
343
187
  version: '1.0',
344
188
  screen: {
345
189
  type: 'Column',
346
- modifier: {
347
- fillMaxSize: true,
348
- padding: 16
349
- },
350
- horizontalAlignment: 'CenterHorizontally',
351
- verticalArrangement: 'Center',
190
+ modifier: { fillMaxSize: true, padding: 16 },
352
191
  children: [
353
- {
354
- type: 'Text',
355
- text: 'Welcome to JetStart! 🚀',
356
- style: 'headlineMedium'
357
- },
358
- {
359
- type: 'Spacer',
360
- height: 16
361
- },
362
- {
363
- type: 'Text',
364
- text: 'Edit your code to see hot reload',
365
- style: 'bodyMedium'
366
- }
192
+ { type: 'Text', text: 'Welcome to JetStart! 🚀', style: 'headlineMedium' },
193
+ { type: 'Text', text: 'Edit your code to see hot reload', style: 'bodyMedium' }
367
194
  ]
368
195
  }
369
196
  };
@@ -23,6 +23,10 @@ export interface DSLElement {
23
23
  tint?: string;
24
24
  contentDescription?: string;
25
25
  children?: DSLElement[];
26
+ floatingActionButton?: DSLElement;
27
+ placeholder?: string;
28
+ label?: string;
29
+ value?: string;
26
30
  }
27
31
  export interface DSLModifier {
28
32
  fillMaxSize?: boolean;
@@ -31,6 +35,10 @@ export interface DSLModifier {
31
35
  padding?: number;
32
36
  paddingHorizontal?: number;
33
37
  paddingVertical?: number;
38
+ paddingStart?: number;
39
+ paddingEnd?: number;
40
+ paddingTop?: number;
41
+ paddingBottom?: number;
34
42
  size?: number;
35
43
  height?: number;
36
44
  width?: number;
@@ -3,6 +3,57 @@
3
3
  * Spawns and manages Gradle build processes
4
4
  */
5
5
  import { BuildConfig, BuildResult } from '@jetstart/shared';
6
+ /**
7
+ * ADB Helper for auto-installing APKs
8
+ */
9
+ export declare class AdbHelper {
10
+ private adbPath;
11
+ private connectedDevices;
12
+ constructor();
13
+ /**
14
+ * Find adb executable
15
+ */
16
+ private findAdb;
17
+ /**
18
+ * Get list of connected devices (FULLY READY devices only)
19
+ * Returns only devices in "device" state (connected and authorized)
20
+ */
21
+ getDevices(): string[];
22
+ /**
23
+ * Get ALL devices including those in "connecting" or "offline" state
24
+ * Useful for debugging and understanding device availability
25
+ */
26
+ getAllDeviceStates(): {
27
+ id: string;
28
+ state: string;
29
+ }[];
30
+ /**
31
+ * Install APK on a device
32
+ */
33
+ installApk(apkPath: string, deviceId?: string): Promise<{
34
+ success: boolean;
35
+ error?: string;
36
+ }>;
37
+ /**
38
+ * Launch app on device
39
+ */
40
+ launchApp(packageName: string, activityName: string, deviceId?: string): Promise<boolean>;
41
+ /**
42
+ * Connect to a device via wireless ADB with retry logic
43
+ * Called when the JetStart app connects via WebSocket
44
+ *
45
+ * Handles timing issues with wireless ADB:
46
+ * - Devices may need time for user approval
47
+ * - Network handshake can be slow
48
+ * - Retries automatically if device not ready
49
+ */
50
+ connectWireless(ipAddress: string, retryCount?: number): void;
51
+ /**
52
+ * Wait for a device to reach "device" state after adb connect
53
+ * The device may be "connecting" or "offline" initially
54
+ */
55
+ private waitForDeviceReady;
56
+ }
6
57
  export interface GradleExecutorOptions {
7
58
  javaHome?: string;
8
59
  androidHome?: string;