@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
package/src/build/gradle.ts
CHANGED
|
@@ -3,13 +3,271 @@
|
|
|
3
3
|
* Spawns and manages Gradle build processes
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { spawn, ChildProcess } from 'child_process';
|
|
6
|
+
import { spawn, ChildProcess, execSync } from 'child_process';
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import * as os from 'os';
|
|
10
10
|
import { BuildConfig, BuildResult } from '@jetstart/shared';
|
|
11
11
|
import { BuildOutputParser } from './parser';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* ADB Helper for auto-installing APKs
|
|
15
|
+
*/
|
|
16
|
+
export class AdbHelper {
|
|
17
|
+
private adbPath: string | null = null;
|
|
18
|
+
private connectedDevices = new Map<string, { lastConnected: number; retryCount: number }>();
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
this.adbPath = this.findAdb();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find adb executable
|
|
26
|
+
*/
|
|
27
|
+
private findAdb(): string | null {
|
|
28
|
+
const isWindows = os.platform() === 'win32';
|
|
29
|
+
|
|
30
|
+
// Check common locations
|
|
31
|
+
const commonPaths = isWindows ? [
|
|
32
|
+
'C:\\Android\\platform-tools\\adb.exe',
|
|
33
|
+
path.join(os.homedir(), 'AppData', 'Local', 'Android', 'Sdk', 'platform-tools', 'adb.exe'),
|
|
34
|
+
] : [
|
|
35
|
+
path.join(os.homedir(), 'Android', 'Sdk', 'platform-tools', 'adb'),
|
|
36
|
+
path.join(os.homedir(), 'Library', 'Android', 'sdk', 'platform-tools', 'adb'),
|
|
37
|
+
'/opt/android-sdk/platform-tools/adb',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
for (const p of commonPaths) {
|
|
41
|
+
if (fs.existsSync(p)) {
|
|
42
|
+
console.log(`[ADB] Found at: ${p}`);
|
|
43
|
+
return p;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Try PATH
|
|
48
|
+
try {
|
|
49
|
+
const result = execSync(isWindows ? 'where adb' : 'which adb', { encoding: 'utf8' });
|
|
50
|
+
const adbPath = result.trim().split('\n')[0];
|
|
51
|
+
if (fs.existsSync(adbPath)) {
|
|
52
|
+
console.log(`[ADB] Found in PATH: ${adbPath}`);
|
|
53
|
+
return adbPath;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// Not in PATH
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.warn('[ADB] Not found. Auto-install disabled.');
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get list of connected devices (FULLY READY devices only)
|
|
65
|
+
* Returns only devices in "device" state (connected and authorized)
|
|
66
|
+
*/
|
|
67
|
+
getDevices(): string[] {
|
|
68
|
+
if (!this.adbPath) return [];
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const output = execSync(`"${this.adbPath}" devices`, { encoding: 'utf8' });
|
|
72
|
+
const lines = output.trim().split('\n').slice(1); // Skip header
|
|
73
|
+
return lines
|
|
74
|
+
.filter(line => line.includes('\tdevice'))
|
|
75
|
+
.map(line => line.split('\t')[0]);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('[ADB] Failed to get devices:', err);
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get ALL devices including those in "connecting" or "offline" state
|
|
84
|
+
* Useful for debugging and understanding device availability
|
|
85
|
+
*/
|
|
86
|
+
getAllDeviceStates(): { id: string; state: string }[] {
|
|
87
|
+
if (!this.adbPath) return [];
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const output = execSync(`"${this.adbPath}" devices`, { encoding: 'utf8' });
|
|
91
|
+
const lines = output.trim().split('\n').slice(1); // Skip header
|
|
92
|
+
return lines
|
|
93
|
+
.filter(line => line.trim().length > 0)
|
|
94
|
+
.map(line => {
|
|
95
|
+
const parts = line.split(/\s+/);
|
|
96
|
+
return { id: parts[0], state: parts[1] || 'unknown' };
|
|
97
|
+
});
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error('[ADB] Failed to get device states:', err);
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Install APK on a device
|
|
106
|
+
*/
|
|
107
|
+
async installApk(apkPath: string, deviceId?: string): Promise<{ success: boolean; error?: string }> {
|
|
108
|
+
if (!this.adbPath) {
|
|
109
|
+
return { success: false, error: 'ADB not found' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(apkPath)) {
|
|
113
|
+
return { success: false, error: `APK not found: ${apkPath}` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const devices = this.getDevices();
|
|
117
|
+
if (devices.length === 0) {
|
|
118
|
+
return { success: false, error: 'No devices connected' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const target = deviceId || devices[0];
|
|
122
|
+
console.log(`[ADB] Installing APK on device: ${target}`);
|
|
123
|
+
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
const args = ['-s', target, 'install', '-r', apkPath];
|
|
126
|
+
const proc = spawn(this.adbPath!, args, { shell: true });
|
|
127
|
+
|
|
128
|
+
let output = '';
|
|
129
|
+
let errorOutput = '';
|
|
130
|
+
|
|
131
|
+
proc.stdout.on('data', (data) => {
|
|
132
|
+
output += data.toString();
|
|
133
|
+
console.log(`[ADB] ${data.toString().trim()}`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
proc.stderr.on('data', (data) => {
|
|
137
|
+
errorOutput += data.toString();
|
|
138
|
+
console.error(`[ADB] ${data.toString().trim()}`);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
proc.on('close', (code) => {
|
|
142
|
+
if (code === 0 && output.includes('Success')) {
|
|
143
|
+
console.log('[ADB] ✅ APK installed successfully!');
|
|
144
|
+
resolve({ success: true });
|
|
145
|
+
} else {
|
|
146
|
+
resolve({ success: false, error: errorOutput || output || `Exit code: ${code}` });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
proc.on('error', (err) => {
|
|
151
|
+
resolve({ success: false, error: err.message });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Launch app on device
|
|
158
|
+
*/
|
|
159
|
+
async launchApp(packageName: string, activityName: string, deviceId?: string): Promise<boolean> {
|
|
160
|
+
if (!this.adbPath) return false;
|
|
161
|
+
|
|
162
|
+
const devices = this.getDevices();
|
|
163
|
+
if (devices.length === 0) return false;
|
|
164
|
+
|
|
165
|
+
const target = deviceId || devices[0];
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
execSync(`"${this.adbPath}" -s ${target} shell am start -n ${packageName}/${activityName}`, { encoding: 'utf8' });
|
|
169
|
+
console.log(`[ADB] ✅ Launched ${packageName}`);
|
|
170
|
+
return true;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error('[ADB] Failed to launch app:', err);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Connect to a device via wireless ADB with retry logic
|
|
179
|
+
* Called when the JetStart app connects via WebSocket
|
|
180
|
+
*
|
|
181
|
+
* Handles timing issues with wireless ADB:
|
|
182
|
+
* - Devices may need time for user approval
|
|
183
|
+
* - Network handshake can be slow
|
|
184
|
+
* - Retries automatically if device not ready
|
|
185
|
+
*/
|
|
186
|
+
connectWireless(ipAddress: string, retryCount: number = 0): void {
|
|
187
|
+
if (!this.adbPath) {
|
|
188
|
+
console.warn('[ADB] ADB not found, cannot connect wireless device');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const target = `${ipAddress}:5555`;
|
|
193
|
+
const maxRetries = 5;
|
|
194
|
+
const retryDelays = [0, 1000, 2000, 3000, 5000]; // Escalating delays
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
// console.log(`[ADB] Attempting wireless connection to ${target}...${retryCount > 0 ? ` (retry ${retryCount}/${maxRetries})` : ''}`);
|
|
198
|
+
|
|
199
|
+
// Use longer timeout: 15 seconds for wireless ADB handshake
|
|
200
|
+
// This allows time for:
|
|
201
|
+
// - User approval on device
|
|
202
|
+
// - Network handshake
|
|
203
|
+
// - ADB daemon initialization
|
|
204
|
+
execSync(`"${this.adbPath}" connect ${target}`, {
|
|
205
|
+
encoding: 'utf8',
|
|
206
|
+
timeout: 15000,
|
|
207
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// CRITICAL: After connect, device needs time to reach "device" state
|
|
211
|
+
// Poll for device state readiness
|
|
212
|
+
this.waitForDeviceReady(target, retryCount, maxRetries, retryDelays);
|
|
213
|
+
} catch (err: any) {
|
|
214
|
+
// Handle timeout or connection errors with retry
|
|
215
|
+
if (retryCount < maxRetries) {
|
|
216
|
+
const delay = retryDelays[retryCount + 1] || 5000;
|
|
217
|
+
console.warn(`[ADB] Connection failed: ${err.message}`);
|
|
218
|
+
console.log(`[ADB] Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
|
|
219
|
+
setTimeout(() => this.connectWireless(ipAddress, retryCount + 1), delay);
|
|
220
|
+
} else {
|
|
221
|
+
console.error(`[ADB] Failed to connect ${target} after ${maxRetries} retries: ${err.message}`);
|
|
222
|
+
console.warn(`[ADB] Device may need user authorization on the phone. Check your device!`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Wait for a device to reach "device" state after adb connect
|
|
229
|
+
* The device may be "connecting" or "offline" initially
|
|
230
|
+
*/
|
|
231
|
+
private waitForDeviceReady(
|
|
232
|
+
target: string,
|
|
233
|
+
connectRetryCount: number,
|
|
234
|
+
maxConnectRetries: number,
|
|
235
|
+
connectRetryDelays: number[]
|
|
236
|
+
): void {
|
|
237
|
+
const maxWaitAttempts = 10;
|
|
238
|
+
const waitInterval = 500; // Check every 500ms
|
|
239
|
+
|
|
240
|
+
const checkDeviceState = (attemptNum: number = 0) => {
|
|
241
|
+
if (attemptNum > maxWaitAttempts) {
|
|
242
|
+
console.warn(`[ADB] Device ${target} not ready after ${maxWaitAttempts * waitInterval}ms, will retry on next build`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const allDevices = this.getAllDeviceStates();
|
|
247
|
+
const device = allDevices.find(d => d.id === target);
|
|
248
|
+
|
|
249
|
+
if (device?.state === 'device') {
|
|
250
|
+
// ✅ Device is ready!
|
|
251
|
+
console.log(`[ADB] ✅ Wireless ADB connected and ready: ${target}`);
|
|
252
|
+
this.connectedDevices.set(target, { lastConnected: Date.now(), retryCount: 0 });
|
|
253
|
+
} else if (device?.state === 'connecting' || device?.state === 'offline' || device?.state === 'unknown') {
|
|
254
|
+
// Device is still connecting, check again later
|
|
255
|
+
console.log(`[ADB] Device state: ${device?.state || 'not found'}, waiting...`);
|
|
256
|
+
setTimeout(() => checkDeviceState(attemptNum + 1), waitInterval);
|
|
257
|
+
} else if (!device) {
|
|
258
|
+
// Device not found yet, retry connection
|
|
259
|
+
if (connectRetryCount < maxConnectRetries) {
|
|
260
|
+
const delay = connectRetryDelays[connectRetryCount + 1] || 5000;
|
|
261
|
+
setTimeout(() => this.connectWireless(target.split(':')[0], connectRetryCount + 1), delay);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Start polling for device readiness
|
|
267
|
+
setTimeout(() => checkDeviceState(), 100);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
13
271
|
export interface GradleExecutorOptions {
|
|
14
272
|
javaHome?: string;
|
|
15
273
|
androidHome?: string;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hot Reload Service
|
|
3
|
+
* Orchestrates Kotlin compilation, DEX generation, and sending to app
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import { KotlinCompiler } from './kotlin-compiler';
|
|
10
|
+
import { DexGenerator } from './dex-generator';
|
|
11
|
+
import { OverrideGenerator } from './override-generator';
|
|
12
|
+
import { log, error as logError, success } from '../utils/logger';
|
|
13
|
+
|
|
14
|
+
export interface HotReloadResult {
|
|
15
|
+
success: boolean;
|
|
16
|
+
dexBase64: string | null;
|
|
17
|
+
classNames: string[];
|
|
18
|
+
errors: string[];
|
|
19
|
+
compileTime: number;
|
|
20
|
+
dexTime: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class HotReloadService {
|
|
24
|
+
private kotlinCompiler: KotlinCompiler;
|
|
25
|
+
private dexGenerator: DexGenerator;
|
|
26
|
+
private overrideGenerator: OverrideGenerator;
|
|
27
|
+
private projectPath: string;
|
|
28
|
+
|
|
29
|
+
constructor(projectPath: string) {
|
|
30
|
+
this.projectPath = projectPath;
|
|
31
|
+
this.kotlinCompiler = new KotlinCompiler(projectPath);
|
|
32
|
+
this.dexGenerator = new DexGenerator();
|
|
33
|
+
this.overrideGenerator = new OverrideGenerator();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Perform hot reload for a changed Kotlin file
|
|
38
|
+
* Returns DEX bytes ready to be sent to the app
|
|
39
|
+
*/
|
|
40
|
+
async hotReload(filePath: string): Promise<HotReloadResult> {
|
|
41
|
+
const startTime = Date.now();
|
|
42
|
+
|
|
43
|
+
log(`🔥 Hot reload starting for: ${path.basename(filePath)}`);
|
|
44
|
+
|
|
45
|
+
// Step 1: Compile Kotlin to .class
|
|
46
|
+
const compileStart = Date.now();
|
|
47
|
+
const compileResult = await this.kotlinCompiler.compileFile(filePath);
|
|
48
|
+
const compileTime = Date.now() - compileStart;
|
|
49
|
+
|
|
50
|
+
if (!compileResult.success) {
|
|
51
|
+
logError(`Compilation failed: ${compileResult.errors.join(', ')}`);
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
dexBase64: null,
|
|
55
|
+
classNames: [],
|
|
56
|
+
errors: compileResult.errors,
|
|
57
|
+
compileTime,
|
|
58
|
+
dexTime: 0
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
log(`Compilation completed in ${compileTime}ms (${compileResult.classFiles.length} classes)`);
|
|
63
|
+
|
|
64
|
+
// Step 2: Generate $Override classes (Phase 2)
|
|
65
|
+
const overrideDir = path.join(os.tmpdir(), 'jetstart-overrides', Date.now().toString());
|
|
66
|
+
fs.mkdirSync(overrideDir, { recursive: true });
|
|
67
|
+
|
|
68
|
+
const overrideResult = await this.overrideGenerator.generateOverrides(
|
|
69
|
+
compileResult.classFiles,
|
|
70
|
+
filePath,
|
|
71
|
+
overrideDir
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (!overrideResult.success) {
|
|
75
|
+
log(`⚠️ Override generation failed: ${overrideResult.errors.join(', ')}`);
|
|
76
|
+
log(`📝 Falling back to direct class hot reload (less efficient)`);
|
|
77
|
+
} else {
|
|
78
|
+
log(`Generated ${overrideResult.overrideClassFiles.length} override classes`);
|
|
79
|
+
|
|
80
|
+
// Compile override source files to .class
|
|
81
|
+
const allOverrideClassFiles: string[] = [];
|
|
82
|
+
for (const overrideFile of overrideResult.overrideClassFiles) {
|
|
83
|
+
const compRes = await this.kotlinCompiler.compileFile(overrideFile);
|
|
84
|
+
if (compRes.success) {
|
|
85
|
+
allOverrideClassFiles.push(...compRes.classFiles);
|
|
86
|
+
} else {
|
|
87
|
+
log(`⚠️ Failed to compile override ${path.basename(overrideFile)}: ${compRes.errors.join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (allOverrideClassFiles.length > 0) {
|
|
92
|
+
log(`Compiled ${allOverrideClassFiles.length} override classes`);
|
|
93
|
+
// Add override classes to DEX generation
|
|
94
|
+
compileResult.classFiles.push(...allOverrideClassFiles);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Step 3: Convert .class to .dex
|
|
99
|
+
const dexStart = Date.now();
|
|
100
|
+
const dexResult = await this.dexGenerator.generateDex(compileResult.classFiles);
|
|
101
|
+
const dexTime = Date.now() - dexStart;
|
|
102
|
+
|
|
103
|
+
if (!dexResult.success || !dexResult.dexBytes) {
|
|
104
|
+
logError(`DEX generation failed: ${dexResult.errors.join(', ')}`);
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
dexBase64: null,
|
|
108
|
+
classNames: [],
|
|
109
|
+
errors: dexResult.errors,
|
|
110
|
+
compileTime,
|
|
111
|
+
dexTime
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
log(`DEX generated in ${dexTime}ms (${dexResult.dexBytes.length} bytes)`);
|
|
116
|
+
|
|
117
|
+
// Extract class names from file paths
|
|
118
|
+
const classNames = this.extractClassNames(compileResult.classFiles, compileResult.outputDir);
|
|
119
|
+
|
|
120
|
+
const totalTime = Date.now() - startTime;
|
|
121
|
+
success(`🔥 Hot reload complete in ${totalTime}ms (compile: ${compileTime}ms, dex: ${dexTime}ms)`);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
success: true,
|
|
125
|
+
dexBase64: dexResult.dexBytes.toString('base64'),
|
|
126
|
+
classNames,
|
|
127
|
+
errors: [],
|
|
128
|
+
compileTime,
|
|
129
|
+
dexTime
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract fully qualified class names from class file paths
|
|
135
|
+
*/
|
|
136
|
+
private extractClassNames(classFiles: string[], outputDir: string): string[] {
|
|
137
|
+
return classFiles.map(classFile => {
|
|
138
|
+
// Remove output dir prefix and .class suffix
|
|
139
|
+
let relativePath = classFile
|
|
140
|
+
.replace(outputDir, '')
|
|
141
|
+
.replace(/^[\/\\]/, '')
|
|
142
|
+
.replace('.class', '');
|
|
143
|
+
|
|
144
|
+
// Convert path separators to dots
|
|
145
|
+
return relativePath.replace(/[\/\\]/g, '.');
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if the environment is properly set up for hot reload
|
|
151
|
+
*/
|
|
152
|
+
async checkEnvironment(): Promise<{ ready: boolean; issues: string[] }> {
|
|
153
|
+
const issues: string[] = [];
|
|
154
|
+
|
|
155
|
+
// Check kotlinc
|
|
156
|
+
const kotlinc = await this.kotlinCompiler.findKotlinc();
|
|
157
|
+
if (!kotlinc) {
|
|
158
|
+
issues.push('kotlinc not found - install Kotlin or set KOTLIN_HOME');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check d8
|
|
162
|
+
const d8 = await this.dexGenerator.findD8();
|
|
163
|
+
if (!d8) {
|
|
164
|
+
issues.push('d8 not found - set ANDROID_HOME and install build-tools');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check classpath
|
|
168
|
+
const classpath = await this.kotlinCompiler.buildClasspath();
|
|
169
|
+
if (classpath.length === 0) {
|
|
170
|
+
issues.push('Cannot build classpath - ANDROID_HOME not set or SDK not installed');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
ready: issues.length === 0,
|
|
175
|
+
issues
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|