@jetstart/core 1.7.0 → 2.0.1
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/dist/build/gradle.js
CHANGED
|
@@ -37,12 +37,244 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
37
|
};
|
|
38
38
|
})();
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
exports.GradleExecutor = void 0;
|
|
40
|
+
exports.GradleExecutor = exports.AdbHelper = void 0;
|
|
41
41
|
const child_process_1 = require("child_process");
|
|
42
42
|
const fs = __importStar(require("fs"));
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const os = __importStar(require("os"));
|
|
45
45
|
const parser_1 = require("./parser");
|
|
46
|
+
/**
|
|
47
|
+
* ADB Helper for auto-installing APKs
|
|
48
|
+
*/
|
|
49
|
+
class AdbHelper {
|
|
50
|
+
adbPath = null;
|
|
51
|
+
connectedDevices = new Map();
|
|
52
|
+
constructor() {
|
|
53
|
+
this.adbPath = this.findAdb();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Find adb executable
|
|
57
|
+
*/
|
|
58
|
+
findAdb() {
|
|
59
|
+
const isWindows = os.platform() === 'win32';
|
|
60
|
+
// Check common locations
|
|
61
|
+
const commonPaths = isWindows ? [
|
|
62
|
+
'C:\\Android\\platform-tools\\adb.exe',
|
|
63
|
+
path.join(os.homedir(), 'AppData', 'Local', 'Android', 'Sdk', 'platform-tools', 'adb.exe'),
|
|
64
|
+
] : [
|
|
65
|
+
path.join(os.homedir(), 'Android', 'Sdk', 'platform-tools', 'adb'),
|
|
66
|
+
path.join(os.homedir(), 'Library', 'Android', 'sdk', 'platform-tools', 'adb'),
|
|
67
|
+
'/opt/android-sdk/platform-tools/adb',
|
|
68
|
+
];
|
|
69
|
+
for (const p of commonPaths) {
|
|
70
|
+
if (fs.existsSync(p)) {
|
|
71
|
+
console.log(`[ADB] Found at: ${p}`);
|
|
72
|
+
return p;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Try PATH
|
|
76
|
+
try {
|
|
77
|
+
const result = (0, child_process_1.execSync)(isWindows ? 'where adb' : 'which adb', { encoding: 'utf8' });
|
|
78
|
+
const adbPath = result.trim().split('\n')[0];
|
|
79
|
+
if (fs.existsSync(adbPath)) {
|
|
80
|
+
console.log(`[ADB] Found in PATH: ${adbPath}`);
|
|
81
|
+
return adbPath;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Not in PATH
|
|
86
|
+
}
|
|
87
|
+
console.warn('[ADB] Not found. Auto-install disabled.');
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get list of connected devices (FULLY READY devices only)
|
|
92
|
+
* Returns only devices in "device" state (connected and authorized)
|
|
93
|
+
*/
|
|
94
|
+
getDevices() {
|
|
95
|
+
if (!this.adbPath)
|
|
96
|
+
return [];
|
|
97
|
+
try {
|
|
98
|
+
const output = (0, child_process_1.execSync)(`"${this.adbPath}" devices`, { encoding: 'utf8' });
|
|
99
|
+
const lines = output.trim().split('\n').slice(1); // Skip header
|
|
100
|
+
return lines
|
|
101
|
+
.filter(line => line.includes('\tdevice'))
|
|
102
|
+
.map(line => line.split('\t')[0]);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
console.error('[ADB] Failed to get devices:', err);
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get ALL devices including those in "connecting" or "offline" state
|
|
111
|
+
* Useful for debugging and understanding device availability
|
|
112
|
+
*/
|
|
113
|
+
getAllDeviceStates() {
|
|
114
|
+
if (!this.adbPath)
|
|
115
|
+
return [];
|
|
116
|
+
try {
|
|
117
|
+
const output = (0, child_process_1.execSync)(`"${this.adbPath}" devices`, { encoding: 'utf8' });
|
|
118
|
+
const lines = output.trim().split('\n').slice(1); // Skip header
|
|
119
|
+
return lines
|
|
120
|
+
.filter(line => line.trim().length > 0)
|
|
121
|
+
.map(line => {
|
|
122
|
+
const parts = line.split(/\s+/);
|
|
123
|
+
return { id: parts[0], state: parts[1] || 'unknown' };
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.error('[ADB] Failed to get device states:', err);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Install APK on a device
|
|
133
|
+
*/
|
|
134
|
+
async installApk(apkPath, deviceId) {
|
|
135
|
+
if (!this.adbPath) {
|
|
136
|
+
return { success: false, error: 'ADB not found' };
|
|
137
|
+
}
|
|
138
|
+
if (!fs.existsSync(apkPath)) {
|
|
139
|
+
return { success: false, error: `APK not found: ${apkPath}` };
|
|
140
|
+
}
|
|
141
|
+
const devices = this.getDevices();
|
|
142
|
+
if (devices.length === 0) {
|
|
143
|
+
return { success: false, error: 'No devices connected' };
|
|
144
|
+
}
|
|
145
|
+
const target = deviceId || devices[0];
|
|
146
|
+
console.log(`[ADB] Installing APK on device: ${target}`);
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
const args = ['-s', target, 'install', '-r', apkPath];
|
|
149
|
+
const proc = (0, child_process_1.spawn)(this.adbPath, args, { shell: true });
|
|
150
|
+
let output = '';
|
|
151
|
+
let errorOutput = '';
|
|
152
|
+
proc.stdout.on('data', (data) => {
|
|
153
|
+
output += data.toString();
|
|
154
|
+
console.log(`[ADB] ${data.toString().trim()}`);
|
|
155
|
+
});
|
|
156
|
+
proc.stderr.on('data', (data) => {
|
|
157
|
+
errorOutput += data.toString();
|
|
158
|
+
console.error(`[ADB] ${data.toString().trim()}`);
|
|
159
|
+
});
|
|
160
|
+
proc.on('close', (code) => {
|
|
161
|
+
if (code === 0 && output.includes('Success')) {
|
|
162
|
+
console.log('[ADB] ✅ APK installed successfully!');
|
|
163
|
+
resolve({ success: true });
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
resolve({ success: false, error: errorOutput || output || `Exit code: ${code}` });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
proc.on('error', (err) => {
|
|
170
|
+
resolve({ success: false, error: err.message });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Launch app on device
|
|
176
|
+
*/
|
|
177
|
+
async launchApp(packageName, activityName, deviceId) {
|
|
178
|
+
if (!this.adbPath)
|
|
179
|
+
return false;
|
|
180
|
+
const devices = this.getDevices();
|
|
181
|
+
if (devices.length === 0)
|
|
182
|
+
return false;
|
|
183
|
+
const target = deviceId || devices[0];
|
|
184
|
+
try {
|
|
185
|
+
(0, child_process_1.execSync)(`"${this.adbPath}" -s ${target} shell am start -n ${packageName}/${activityName}`, { encoding: 'utf8' });
|
|
186
|
+
console.log(`[ADB] ✅ Launched ${packageName}`);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
console.error('[ADB] Failed to launch app:', err);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Connect to a device via wireless ADB with retry logic
|
|
196
|
+
* Called when the JetStart app connects via WebSocket
|
|
197
|
+
*
|
|
198
|
+
* Handles timing issues with wireless ADB:
|
|
199
|
+
* - Devices may need time for user approval
|
|
200
|
+
* - Network handshake can be slow
|
|
201
|
+
* - Retries automatically if device not ready
|
|
202
|
+
*/
|
|
203
|
+
connectWireless(ipAddress, retryCount = 0) {
|
|
204
|
+
if (!this.adbPath) {
|
|
205
|
+
console.warn('[ADB] ADB not found, cannot connect wireless device');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const target = `${ipAddress}:5555`;
|
|
209
|
+
const maxRetries = 5;
|
|
210
|
+
const retryDelays = [0, 1000, 2000, 3000, 5000]; // Escalating delays
|
|
211
|
+
try {
|
|
212
|
+
// console.log(`[ADB] Attempting wireless connection to ${target}...${retryCount > 0 ? ` (retry ${retryCount}/${maxRetries})` : ''}`);
|
|
213
|
+
// Use longer timeout: 15 seconds for wireless ADB handshake
|
|
214
|
+
// This allows time for:
|
|
215
|
+
// - User approval on device
|
|
216
|
+
// - Network handshake
|
|
217
|
+
// - ADB daemon initialization
|
|
218
|
+
(0, child_process_1.execSync)(`"${this.adbPath}" connect ${target}`, {
|
|
219
|
+
encoding: 'utf8',
|
|
220
|
+
timeout: 15000,
|
|
221
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
222
|
+
});
|
|
223
|
+
// CRITICAL: After connect, device needs time to reach "device" state
|
|
224
|
+
// Poll for device state readiness
|
|
225
|
+
this.waitForDeviceReady(target, retryCount, maxRetries, retryDelays);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
// Handle timeout or connection errors with retry
|
|
229
|
+
if (retryCount < maxRetries) {
|
|
230
|
+
const delay = retryDelays[retryCount + 1] || 5000;
|
|
231
|
+
console.warn(`[ADB] Connection failed: ${err.message}`);
|
|
232
|
+
console.log(`[ADB] Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
|
|
233
|
+
setTimeout(() => this.connectWireless(ipAddress, retryCount + 1), delay);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.error(`[ADB] Failed to connect ${target} after ${maxRetries} retries: ${err.message}`);
|
|
237
|
+
console.warn(`[ADB] Device may need user authorization on the phone. Check your device!`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Wait for a device to reach "device" state after adb connect
|
|
243
|
+
* The device may be "connecting" or "offline" initially
|
|
244
|
+
*/
|
|
245
|
+
waitForDeviceReady(target, connectRetryCount, maxConnectRetries, connectRetryDelays) {
|
|
246
|
+
const maxWaitAttempts = 10;
|
|
247
|
+
const waitInterval = 500; // Check every 500ms
|
|
248
|
+
const checkDeviceState = (attemptNum = 0) => {
|
|
249
|
+
if (attemptNum > maxWaitAttempts) {
|
|
250
|
+
console.warn(`[ADB] Device ${target} not ready after ${maxWaitAttempts * waitInterval}ms, will retry on next build`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const allDevices = this.getAllDeviceStates();
|
|
254
|
+
const device = allDevices.find(d => d.id === target);
|
|
255
|
+
if (device?.state === 'device') {
|
|
256
|
+
// ✅ Device is ready!
|
|
257
|
+
console.log(`[ADB] ✅ Wireless ADB connected and ready: ${target}`);
|
|
258
|
+
this.connectedDevices.set(target, { lastConnected: Date.now(), retryCount: 0 });
|
|
259
|
+
}
|
|
260
|
+
else if (device?.state === 'connecting' || device?.state === 'offline' || device?.state === 'unknown') {
|
|
261
|
+
// Device is still connecting, check again later
|
|
262
|
+
console.log(`[ADB] Device state: ${device?.state || 'not found'}, waiting...`);
|
|
263
|
+
setTimeout(() => checkDeviceState(attemptNum + 1), waitInterval);
|
|
264
|
+
}
|
|
265
|
+
else if (!device) {
|
|
266
|
+
// Device not found yet, retry connection
|
|
267
|
+
if (connectRetryCount < maxConnectRetries) {
|
|
268
|
+
const delay = connectRetryDelays[connectRetryCount + 1] || 5000;
|
|
269
|
+
setTimeout(() => this.connectWireless(target.split(':')[0], connectRetryCount + 1), delay);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
// Start polling for device readiness
|
|
274
|
+
setTimeout(() => checkDeviceState(), 100);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
exports.AdbHelper = AdbHelper;
|
|
46
278
|
class GradleExecutor {
|
|
47
279
|
javaHome;
|
|
48
280
|
androidHome;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hot Reload Service
|
|
3
|
+
* Orchestrates Kotlin compilation, DEX generation, and sending to app
|
|
4
|
+
*/
|
|
5
|
+
export interface HotReloadResult {
|
|
6
|
+
success: boolean;
|
|
7
|
+
dexBase64: string | null;
|
|
8
|
+
classNames: string[];
|
|
9
|
+
errors: string[];
|
|
10
|
+
compileTime: number;
|
|
11
|
+
dexTime: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class HotReloadService {
|
|
14
|
+
private kotlinCompiler;
|
|
15
|
+
private dexGenerator;
|
|
16
|
+
private overrideGenerator;
|
|
17
|
+
private projectPath;
|
|
18
|
+
constructor(projectPath: string);
|
|
19
|
+
/**
|
|
20
|
+
* Perform hot reload for a changed Kotlin file
|
|
21
|
+
* Returns DEX bytes ready to be sent to the app
|
|
22
|
+
*/
|
|
23
|
+
hotReload(filePath: string): Promise<HotReloadResult>;
|
|
24
|
+
/**
|
|
25
|
+
* Extract fully qualified class names from class file paths
|
|
26
|
+
*/
|
|
27
|
+
private extractClassNames;
|
|
28
|
+
/**
|
|
29
|
+
* Check if the environment is properly set up for hot reload
|
|
30
|
+
*/
|
|
31
|
+
checkEnvironment(): Promise<{
|
|
32
|
+
ready: boolean;
|
|
33
|
+
issues: string[];
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=hot-reload-service.d.ts.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hot Reload Service
|
|
4
|
+
* Orchestrates Kotlin compilation, DEX generation, and sending to app
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.HotReloadService = void 0;
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const kotlin_compiler_1 = require("./kotlin-compiler");
|
|
45
|
+
const dex_generator_1 = require("./dex-generator");
|
|
46
|
+
const override_generator_1 = require("./override-generator");
|
|
47
|
+
const logger_1 = require("../utils/logger");
|
|
48
|
+
class HotReloadService {
|
|
49
|
+
kotlinCompiler;
|
|
50
|
+
dexGenerator;
|
|
51
|
+
overrideGenerator;
|
|
52
|
+
projectPath;
|
|
53
|
+
constructor(projectPath) {
|
|
54
|
+
this.projectPath = projectPath;
|
|
55
|
+
this.kotlinCompiler = new kotlin_compiler_1.KotlinCompiler(projectPath);
|
|
56
|
+
this.dexGenerator = new dex_generator_1.DexGenerator();
|
|
57
|
+
this.overrideGenerator = new override_generator_1.OverrideGenerator();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Perform hot reload for a changed Kotlin file
|
|
61
|
+
* Returns DEX bytes ready to be sent to the app
|
|
62
|
+
*/
|
|
63
|
+
async hotReload(filePath) {
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
(0, logger_1.log)(`🔥 Hot reload starting for: ${path.basename(filePath)}`);
|
|
66
|
+
// Step 1: Compile Kotlin to .class
|
|
67
|
+
const compileStart = Date.now();
|
|
68
|
+
const compileResult = await this.kotlinCompiler.compileFile(filePath);
|
|
69
|
+
const compileTime = Date.now() - compileStart;
|
|
70
|
+
if (!compileResult.success) {
|
|
71
|
+
(0, logger_1.error)(`Compilation failed: ${compileResult.errors.join(', ')}`);
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
dexBase64: null,
|
|
75
|
+
classNames: [],
|
|
76
|
+
errors: compileResult.errors,
|
|
77
|
+
compileTime,
|
|
78
|
+
dexTime: 0
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
(0, logger_1.log)(`Compilation completed in ${compileTime}ms (${compileResult.classFiles.length} classes)`);
|
|
82
|
+
// Step 2: Generate $Override classes (Phase 2)
|
|
83
|
+
const overrideDir = path.join(os.tmpdir(), 'jetstart-overrides', Date.now().toString());
|
|
84
|
+
fs.mkdirSync(overrideDir, { recursive: true });
|
|
85
|
+
const overrideResult = await this.overrideGenerator.generateOverrides(compileResult.classFiles, filePath, overrideDir);
|
|
86
|
+
if (!overrideResult.success) {
|
|
87
|
+
(0, logger_1.log)(`⚠️ Override generation failed: ${overrideResult.errors.join(', ')}`);
|
|
88
|
+
(0, logger_1.log)(`📝 Falling back to direct class hot reload (less efficient)`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
(0, logger_1.log)(`Generated ${overrideResult.overrideClassFiles.length} override classes`);
|
|
92
|
+
// Compile override source files to .class
|
|
93
|
+
const allOverrideClassFiles = [];
|
|
94
|
+
for (const overrideFile of overrideResult.overrideClassFiles) {
|
|
95
|
+
const compRes = await this.kotlinCompiler.compileFile(overrideFile);
|
|
96
|
+
if (compRes.success) {
|
|
97
|
+
allOverrideClassFiles.push(...compRes.classFiles);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
(0, logger_1.log)(`⚠️ Failed to compile override ${path.basename(overrideFile)}: ${compRes.errors.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (allOverrideClassFiles.length > 0) {
|
|
104
|
+
(0, logger_1.log)(`Compiled ${allOverrideClassFiles.length} override classes`);
|
|
105
|
+
// Add override classes to DEX generation
|
|
106
|
+
compileResult.classFiles.push(...allOverrideClassFiles);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Step 3: Convert .class to .dex
|
|
110
|
+
const dexStart = Date.now();
|
|
111
|
+
const dexResult = await this.dexGenerator.generateDex(compileResult.classFiles);
|
|
112
|
+
const dexTime = Date.now() - dexStart;
|
|
113
|
+
if (!dexResult.success || !dexResult.dexBytes) {
|
|
114
|
+
(0, logger_1.error)(`DEX generation failed: ${dexResult.errors.join(', ')}`);
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
dexBase64: null,
|
|
118
|
+
classNames: [],
|
|
119
|
+
errors: dexResult.errors,
|
|
120
|
+
compileTime,
|
|
121
|
+
dexTime
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
(0, logger_1.log)(`DEX generated in ${dexTime}ms (${dexResult.dexBytes.length} bytes)`);
|
|
125
|
+
// Extract class names from file paths
|
|
126
|
+
const classNames = this.extractClassNames(compileResult.classFiles, compileResult.outputDir);
|
|
127
|
+
const totalTime = Date.now() - startTime;
|
|
128
|
+
(0, logger_1.success)(`🔥 Hot reload complete in ${totalTime}ms (compile: ${compileTime}ms, dex: ${dexTime}ms)`);
|
|
129
|
+
return {
|
|
130
|
+
success: true,
|
|
131
|
+
dexBase64: dexResult.dexBytes.toString('base64'),
|
|
132
|
+
classNames,
|
|
133
|
+
errors: [],
|
|
134
|
+
compileTime,
|
|
135
|
+
dexTime
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Extract fully qualified class names from class file paths
|
|
140
|
+
*/
|
|
141
|
+
extractClassNames(classFiles, outputDir) {
|
|
142
|
+
return classFiles.map(classFile => {
|
|
143
|
+
// Remove output dir prefix and .class suffix
|
|
144
|
+
let relativePath = classFile
|
|
145
|
+
.replace(outputDir, '')
|
|
146
|
+
.replace(/^[\/\\]/, '')
|
|
147
|
+
.replace('.class', '');
|
|
148
|
+
// Convert path separators to dots
|
|
149
|
+
return relativePath.replace(/[\/\\]/g, '.');
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Check if the environment is properly set up for hot reload
|
|
154
|
+
*/
|
|
155
|
+
async checkEnvironment() {
|
|
156
|
+
const issues = [];
|
|
157
|
+
// Check kotlinc
|
|
158
|
+
const kotlinc = await this.kotlinCompiler.findKotlinc();
|
|
159
|
+
if (!kotlinc) {
|
|
160
|
+
issues.push('kotlinc not found - install Kotlin or set KOTLIN_HOME');
|
|
161
|
+
}
|
|
162
|
+
// Check d8
|
|
163
|
+
const d8 = await this.dexGenerator.findD8();
|
|
164
|
+
if (!d8) {
|
|
165
|
+
issues.push('d8 not found - set ANDROID_HOME and install build-tools');
|
|
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
|
+
return {
|
|
173
|
+
ready: issues.length === 0,
|
|
174
|
+
issues
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
exports.HotReloadService = HotReloadService;
|
|
179
|
+
//# sourceMappingURL=hot-reload-service.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JsCompilerService
|
|
3
|
+
* Compiles Kotlin Compose source to browser-executable ES modules via kotlinc-js.
|
|
4
|
+
*
|
|
5
|
+
* Pipeline (two steps required by K2 compiler):
|
|
6
|
+
* 1. user.kt → screen.klib (compile)
|
|
7
|
+
* 2. screen.klib → screen.mjs (link, includes stdlib + stubs)
|
|
8
|
+
*
|
|
9
|
+
* The resulting .mjs exports renderScreen() which builds a plain JS object tree.
|
|
10
|
+
* The browser imports it, calls renderScreen(() => ScreenName()), renders tree as HTML.
|
|
11
|
+
*/
|
|
12
|
+
export interface JsCompileResult {
|
|
13
|
+
success: boolean;
|
|
14
|
+
jsBase64?: string;
|
|
15
|
+
byteSize?: number;
|
|
16
|
+
compileTimeMs?: number;
|
|
17
|
+
screenFunctionName?: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class JsCompilerService {
|
|
21
|
+
private kotlincJsPath;
|
|
22
|
+
private stdlibKlib;
|
|
23
|
+
private stubsKlib;
|
|
24
|
+
private readonly workDir;
|
|
25
|
+
constructor();
|
|
26
|
+
private init;
|
|
27
|
+
isAvailable(): boolean;
|
|
28
|
+
private writeStubsSource;
|
|
29
|
+
/** Build the Compose shims klib once. Returns klib path or null. */
|
|
30
|
+
private ensureStubsKlib;
|
|
31
|
+
/**
|
|
32
|
+
* Compile a .kt Compose file to a browser-runnable ES module.
|
|
33
|
+
* Returns base64-encoded .mjs content.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Remove horizontalArrangement = ... lines that appear inside non-Row containers.
|
|
37
|
+
* LazyVerticalStaggeredGrid etc. don't accept this param in our stubs.
|
|
38
|
+
* Row/LazyRow DO accept it — those lines are kept.
|
|
39
|
+
*/
|
|
40
|
+
private removeHorizontalArrangementOutsideRow;
|
|
41
|
+
/**
|
|
42
|
+
* Remove a brace-balanced block (including optional trailing else block).
|
|
43
|
+
* Used to surgically strip LaunchedEffect, forEach, broken if-else blocks etc.
|
|
44
|
+
*/
|
|
45
|
+
private removeBalancedBlock;
|
|
46
|
+
/**
|
|
47
|
+
* Remove "} else { BLOCK }" where the else body matches a predicate.
|
|
48
|
+
*/
|
|
49
|
+
private removeElseBlockIf;
|
|
50
|
+
/**
|
|
51
|
+
* Preprocess a Kotlin Compose source file for kotlinc-js compilation.
|
|
52
|
+
*
|
|
53
|
+
* The user's file uses Android/Compose/Java imports that don't exist in
|
|
54
|
+
* a browser context. This method transforms the file so that only the
|
|
55
|
+
* Compose UI structure remains — which maps directly to our jetstart.compose
|
|
56
|
+
* shims. The result compiles cleanly with kotlinc-js.
|
|
57
|
+
*/
|
|
58
|
+
private preprocessFile;
|
|
59
|
+
compile(ktFilePath: string): Promise<JsCompileResult>;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=js-compiler-service.d.ts.map
|