@jetstart/core 1.7.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.
- package/README.md +189 -74
- package/dist/build/dex-generator.d.ts +27 -0
- package/dist/build/dex-generator.js +202 -0
- package/dist/build/dsl-parser.d.ts +3 -30
- package/dist/build/dsl-parser.js +67 -240
- package/dist/build/dsl-types.d.ts +8 -0
- package/dist/build/gradle.d.ts +51 -0
- package/dist/build/gradle.js +233 -1
- package/dist/build/hot-reload-service.d.ts +36 -0
- package/dist/build/hot-reload-service.js +179 -0
- package/dist/build/js-compiler-service.d.ts +61 -0
- package/dist/build/js-compiler-service.js +421 -0
- package/dist/build/kotlin-compiler.d.ts +54 -0
- package/dist/build/kotlin-compiler.js +450 -0
- package/dist/build/kotlin-parser.d.ts +91 -0
- package/dist/build/kotlin-parser.js +1030 -0
- package/dist/build/override-generator.d.ts +54 -0
- package/dist/build/override-generator.js +430 -0
- package/dist/server/index.d.ts +16 -1
- package/dist/server/index.js +147 -42
- package/dist/websocket/handler.d.ts +20 -4
- package/dist/websocket/handler.js +73 -38
- package/dist/websocket/index.d.ts +8 -0
- package/dist/websocket/index.js +15 -11
- package/dist/websocket/manager.d.ts +2 -2
- package/dist/websocket/manager.js +1 -1
- package/package.json +3 -3
- package/src/build/dex-generator.ts +197 -0
- package/src/build/dsl-parser.ts +73 -272
- package/src/build/dsl-types.ts +9 -0
- package/src/build/gradle.ts +259 -1
- package/src/build/hot-reload-service.ts +178 -0
- package/src/build/js-compiler-service.ts +411 -0
- package/src/build/kotlin-compiler.ts +460 -0
- package/src/build/kotlin-parser.ts +1043 -0
- package/src/build/override-generator.ts +478 -0
- package/src/server/index.ts +162 -54
- package/src/websocket/handler.ts +94 -56
- package/src/websocket/index.ts +27 -14
- 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
|
+
}
|
package/src/build/dsl-parser.ts
CHANGED
|
@@ -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
|
|
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:
|
|
53
|
-
const composableMatch = this.
|
|
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
|
|
64
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
105
|
+
* Find the main @Composable and a library of all others
|
|
137
106
|
*/
|
|
138
|
-
private static
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/src/build/dsl-types.ts
CHANGED
|
@@ -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;
|