@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
@@ -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
+ }