@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
@@ -0,0 +1,197 @@
1
+ /**
2
+ * DEX Generator Service
3
+ * Converts .class files to .dex using d8 (Android DEX compiler)
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ import { spawn } from 'child_process';
10
+ import { log, error as logError } from '../utils/logger';
11
+
12
+ export interface DexResult {
13
+ success: boolean;
14
+ dexPath: string;
15
+ dexBytes: Buffer | null;
16
+ errors: string[];
17
+ }
18
+
19
+ export class DexGenerator {
20
+ private static readonly TAG = 'DexGenerator';
21
+ private d8Path: string | null = null;
22
+
23
+ /**
24
+ * Find d8 executable in Android SDK
25
+ */
26
+ async findD8(): Promise<string | null> {
27
+ if (this.d8Path) return this.d8Path;
28
+
29
+ // Check multiple locations for Android SDK
30
+ let androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
31
+
32
+ // Fallback to common Windows locations
33
+ if (!androidHome) {
34
+ const commonLocations = [
35
+ 'C:\\Android',
36
+ path.join(os.homedir(), 'AppData', 'Local', 'Android', 'Sdk'),
37
+ 'C:\\Users\\Public\\Android\\Sdk',
38
+ ];
39
+ for (const loc of commonLocations) {
40
+ if (fs.existsSync(path.join(loc, 'build-tools'))) {
41
+ androidHome = loc;
42
+ log(`Found Android SDK at: ${loc}`);
43
+ break;
44
+ }
45
+ }
46
+ }
47
+
48
+ if (!androidHome) {
49
+ logError('ANDROID_HOME or ANDROID_SDK_ROOT not set');
50
+ return null;
51
+ }
52
+
53
+ // d8 is in build-tools
54
+ const buildToolsDir = path.join(androidHome, 'build-tools');
55
+ if (!fs.existsSync(buildToolsDir)) {
56
+ logError('Android build-tools not found');
57
+ return null;
58
+ }
59
+
60
+ // Find latest build-tools version
61
+ const versions = fs.readdirSync(buildToolsDir)
62
+ .filter(v => /^\d+\.\d+\.\d+$/.test(v))
63
+ .sort((a, b) => {
64
+ const aParts = a.split('.').map(Number);
65
+ const bParts = b.split('.').map(Number);
66
+ for (let i = 0; i < 3; i++) {
67
+ if (aParts[i] !== bParts[i]) return bParts[i] - aParts[i];
68
+ }
69
+ return 0;
70
+ });
71
+
72
+ if (versions.length === 0) {
73
+ logError('No Android build-tools version found');
74
+ return null;
75
+ }
76
+
77
+ const d8Name = os.platform() === 'win32' ? 'd8.bat' : 'd8';
78
+ const d8Path = path.join(buildToolsDir, versions[0], d8Name);
79
+
80
+ if (!fs.existsSync(d8Path)) {
81
+ logError(`d8 not found at: ${d8Path}`);
82
+ return null;
83
+ }
84
+
85
+ this.d8Path = d8Path;
86
+ log(`Found d8 at: ${d8Path} (build-tools ${versions[0]})`);
87
+ return d8Path;
88
+ }
89
+
90
+ /**
91
+ * Convert .class files to a single .dex file
92
+ */
93
+ async generateDex(classFiles: string[], outputDir?: string): Promise<DexResult> {
94
+ const d8 = await this.findD8();
95
+ if (!d8) {
96
+ return {
97
+ success: false,
98
+ dexPath: '',
99
+ dexBytes: null,
100
+ errors: ['d8 not found - Android SDK build-tools not installed']
101
+ };
102
+ }
103
+
104
+ if (classFiles.length === 0) {
105
+ return {
106
+ success: false,
107
+ dexPath: '',
108
+ dexBytes: null,
109
+ errors: ['No class files provided']
110
+ };
111
+ }
112
+
113
+ // Create output directory
114
+ const dexOutputDir = outputDir || path.join(os.tmpdir(), 'jetstart-dex', Date.now().toString());
115
+ fs.mkdirSync(dexOutputDir, { recursive: true });
116
+
117
+ log(`Generating DEX from ${classFiles.length} class files...`);
118
+
119
+ // Build d8 arguments
120
+ const args = [
121
+ '--output', dexOutputDir,
122
+ '--min-api', '24',
123
+ ...classFiles
124
+ ];
125
+
126
+ const result = await this.runCommand(d8, args);
127
+
128
+ if (!result.success) {
129
+ return {
130
+ success: false,
131
+ dexPath: '',
132
+ dexBytes: null,
133
+ errors: [result.stderr || 'DEX generation failed']
134
+ };
135
+ }
136
+
137
+ // Find generated dex file
138
+ const dexPath = path.join(dexOutputDir, 'classes.dex');
139
+ if (!fs.existsSync(dexPath)) {
140
+ return {
141
+ success: false,
142
+ dexPath: '',
143
+ dexBytes: null,
144
+ errors: ['DEX file not generated']
145
+ };
146
+ }
147
+
148
+ const dexBytes = fs.readFileSync(dexPath);
149
+ log(`Generated DEX: ${dexBytes.length} bytes`);
150
+
151
+ return {
152
+ success: true,
153
+ dexPath,
154
+ dexBytes,
155
+ errors: []
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Run a command and return result
161
+ */
162
+ private runCommand(cmd: string, args: string[]): Promise<{ success: boolean; stdout: string; stderr: string }> {
163
+ return new Promise((resolve) => {
164
+ const proc = spawn(cmd, args, {
165
+ shell: os.platform() === 'win32',
166
+ env: process.env
167
+ });
168
+
169
+ let stdout = '';
170
+ let stderr = '';
171
+
172
+ proc.stdout?.on('data', (data) => {
173
+ stdout += data.toString();
174
+ });
175
+
176
+ proc.stderr?.on('data', (data) => {
177
+ stderr += data.toString();
178
+ });
179
+
180
+ proc.on('close', (code) => {
181
+ resolve({
182
+ success: code === 0,
183
+ stdout,
184
+ stderr
185
+ });
186
+ });
187
+
188
+ proc.on('error', (err) => {
189
+ resolve({
190
+ success: false,
191
+ stdout: '',
192
+ stderr: err.message
193
+ });
194
+ });
195
+ });
196
+ }
197
+ }
@@ -2,6 +2,7 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { UIDefinition, DSLElement, DSLModifier, ParseResult } from './dsl-types';
4
4
  import { log } from '../utils/logger';
5
+ import { Tokenizer, KotlinParser } from './kotlin-parser';
5
6
 
6
7
  /**
7
8
  * DSL Parser
@@ -39,18 +40,17 @@ export class DSLParser {
39
40
  try {
40
41
  log(`Parsing Kotlin file: ${path.basename(filePath)}`);
41
42
 
42
- // FIRST: Check if there's a getDefaultDSL() function with JSON
43
+ // FIRST: Check if there's a getDefaultDSL() function
43
44
  const dslFromFunction = this.extractDSLFromFunction(content);
44
45
  if (dslFromFunction) {
45
- log(`Extracted DSL from getDefaultDSL(): ${dslFromFunction.length} bytes`);
46
46
  return {
47
47
  success: true,
48
48
  dsl: JSON.parse(dslFromFunction)
49
49
  };
50
50
  }
51
51
 
52
- // FALLBACK: Try to find @Composable functions with Compose code
53
- const composableMatch = this.findMainComposable(content);
52
+ // FALLBACK: Find @Composable function
53
+ const { main: composableMatch, library } = this.extractComposables(content);
54
54
 
55
55
  if (!composableMatch) {
56
56
  log('No main composable found, generating default DSL');
@@ -60,12 +60,18 @@ export class DSLParser {
60
60
  };
61
61
  }
62
62
 
63
- // Parse the composable body
64
- const element = this.parseComposableBody(composableMatch.body);
63
+ // Tokenize and Parse the body using Recursive Descent Parser
64
+ log(`Tokenizing composable body (${composableMatch.name})...`);
65
+ const tokenizer = new Tokenizer(composableMatch.body);
66
+ const tokens = tokenizer.tokenize();
67
+
68
+ log(`Generated ${tokens.length} tokens. Parsing...`);
69
+ const parser = new KotlinParser(tokens, library);
70
+ const rootElement = parser.parse();
65
71
 
66
72
  const dsl: UIDefinition = {
67
73
  version: '1.0',
68
- screen: element
74
+ screen: rootElement
69
75
  };
70
76
 
71
77
  log(`Successfully parsed DSL: ${JSON.stringify(dsl).length} bytes`);
@@ -83,298 +89,93 @@ export class DSLParser {
83
89
  }
84
90
  }
85
91
 
86
- /**
87
- * Extract DSL JSON from getDefaultDSL() or similar function (legacy support)
88
- */
89
92
  private static extractDSLFromFunction(content: string): string | null {
90
- // Look for functions that return JSON strings (legacy approach)
91
93
  const functionRegex = /fun\s+getDefaultDSL\s*\(\s*\)\s*:\s*String\s*\{\s*return\s*"""([\s\S]*?)"""/;
92
94
  const match = content.match(functionRegex);
93
-
94
95
  if (match && match[1]) {
95
- let jsonString = match[1].trim();
96
- jsonString = jsonString.replace(/\.trimIndent\(\)/, '');
97
- return jsonString;
96
+ return match[1].trim().replace(/\.trimIndent\(\)/, '');
98
97
  }
99
-
100
98
  return null;
101
99
  }
102
100
 
103
101
  /**
104
102
  * Find the main @Composable function in the file
105
103
  */
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
104
  /**
136
- * Extract function body handling nested braces
105
+ * Find the main @Composable and a library of all others
137
106
  */
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
107
+ private static extractComposables(content: string): { main: { name: string, body: string } | null, library: Map<string, string> } {
108
+ const library = new Map<string, string>();
109
+ let main: { name: string, body: string } | null = null;
110
+
111
+ const composableIndices = [...content.matchAll(/@Composable/g)].map(m => m.index!);
112
+
113
+ for (const startIndex of composableIndices) {
114
+ const funRegex = /fun\s+(\w+)/g;
115
+ funRegex.lastIndex = startIndex;
116
+ const funMatch = funRegex.exec(content);
117
+
118
+ if (!funMatch || (funMatch.index - startIndex > 200)) continue;
119
+
120
+ const functionName = funMatch[1];
121
+ const funIndex = funMatch.index;
122
+
123
+ const openParenIndex = content.indexOf('(', funIndex);
124
+ let bodyStartIndex = -1;
125
+
126
+ if (openParenIndex !== -1) {
127
+ const closeParenIndex = this.findMatchingBracket(content, openParenIndex, '(', ')');
128
+ if (closeParenIndex !== -1) {
129
+ bodyStartIndex = content.indexOf('{', closeParenIndex);
319
130
  }
320
- });
321
- usedText.add(text);
131
+ } else {
132
+ bodyStartIndex = content.indexOf('{', funIndex);
133
+ }
134
+
135
+ if (bodyStartIndex === -1) continue;
136
+
137
+ const bodyEndIndex = this.findMatchingBracket(content, bodyStartIndex, '{', '}');
138
+
139
+ if (bodyEndIndex !== -1) {
140
+ const body = content.substring(bodyStartIndex + 1, bodyEndIndex).trim();
141
+
142
+ library.set(functionName, body);
143
+
144
+ // Heuristic for Main: 'Screen' suffix or simply the largest/last one?
145
+ // For now, let's assume the one named 'NotesScreen' or similar is Main.
146
+ // Or just keep the logic: "NotesScreen" (file name matches?)
147
+ // For now, ensure we capture everything.
148
+
149
+ if (!main) main = { name: functionName, body };
150
+ // If explicitly named 'Screen', prefer it
151
+ if (functionName.endsWith('Screen')) main = { name: functionName, body };
322
152
  }
323
153
  }
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);
154
+
155
+ return { main, library };
331
156
  }
332
157
 
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++;
158
+ private static findMatchingBracket(content: string, startIndex: number, openChar: string, closeChar: string): number {
159
+ let count = 0;
160
+ for (let i = startIndex; i < content.length; i++) {
161
+ if (content[i] === openChar) count++;
162
+ else if (content[i] === closeChar) count--;
163
+ if (count === 0) return i;
344
164
  }
345
-
346
- return content.substring(startIndex, endIndex - 1);
165
+ return -1;
347
166
  }
348
167
 
349
- /**
350
- * Generate default DSL when parsing fails
351
- */
168
+
169
+ // ... removed obsolete methods ...
352
170
  private static generateDefaultDSL(): UIDefinition {
353
171
  return {
354
172
  version: '1.0',
355
173
  screen: {
356
174
  type: 'Column',
357
- modifier: {
358
- fillMaxSize: true,
359
- padding: 16
360
- },
361
- horizontalAlignment: 'CenterHorizontally',
362
- verticalArrangement: 'Center',
175
+ modifier: { fillMaxSize: true, padding: 16 },
363
176
  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
- }
177
+ { type: 'Text', text: 'Welcome to JetStart! 🚀', style: 'headlineMedium' },
178
+ { type: 'Text', text: 'Edit your code to see hot reload', style: 'bodyMedium' }
378
179
  ]
379
180
  }
380
181
  };
@@ -25,6 +25,11 @@ export interface DSLElement {
25
25
  tint?: string;
26
26
  contentDescription?: string;
27
27
  children?: DSLElement[];
28
+ floatingActionButton?: DSLElement;
29
+ // Form input properties
30
+ placeholder?: string;
31
+ label?: string;
32
+ value?: string;
28
33
  }
29
34
 
30
35
  export interface DSLModifier {
@@ -34,6 +39,10 @@ export interface DSLModifier {
34
39
  padding?: number;
35
40
  paddingHorizontal?: number;
36
41
  paddingVertical?: number;
42
+ paddingStart?: number;
43
+ paddingEnd?: number;
44
+ paddingTop?: number;
45
+ paddingBottom?: number;
37
46
  size?: number;
38
47
  height?: number;
39
48
  width?: number;