@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/dist/server/index.js
CHANGED
|
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
exports.JetStartServer = void 0;
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
42
43
|
const os = __importStar(require("os"));
|
|
43
44
|
const events_1 = require("events");
|
|
44
45
|
const http_1 = require("./http");
|
|
@@ -47,8 +48,10 @@ const logger_1 = require("../utils/logger");
|
|
|
47
48
|
const shared_1 = require("@jetstart/shared");
|
|
48
49
|
const session_1 = require("../utils/session");
|
|
49
50
|
const build_1 = require("../build");
|
|
50
|
-
const dsl_parser_1 = require("../build/dsl-parser");
|
|
51
51
|
const gradle_injector_1 = require("../build/gradle-injector");
|
|
52
|
+
const hot_reload_service_1 = require("../build/hot-reload-service");
|
|
53
|
+
const js_compiler_service_1 = require("../build/js-compiler-service");
|
|
54
|
+
const gradle_1 = require("../build/gradle");
|
|
52
55
|
const logs_1 = require("@jetstart/logs");
|
|
53
56
|
class JetStartServer extends events_1.EventEmitter {
|
|
54
57
|
httpServer;
|
|
@@ -58,9 +61,15 @@ class JetStartServer extends events_1.EventEmitter {
|
|
|
58
61
|
config;
|
|
59
62
|
sessionManager;
|
|
60
63
|
buildService;
|
|
64
|
+
hotReloadService = null;
|
|
65
|
+
jsCompiler = null;
|
|
61
66
|
currentSession = null;
|
|
62
67
|
buildMutex = false; // Prevent concurrent builds
|
|
63
68
|
latestApkPath = null; // Store latest built APK path
|
|
69
|
+
useTrueHotReload = false; // Use DEX-based hot reload (DISABLED - use APK builds instead)
|
|
70
|
+
adbHelper; // Auto-install APKs via ADB
|
|
71
|
+
autoInstall = true; // Auto-install APK after build
|
|
72
|
+
isFileChangeBuild = false; // Track if build is from file change
|
|
64
73
|
constructor(config = {}) {
|
|
65
74
|
super();
|
|
66
75
|
const bindHost = config.host || '0.0.0.0';
|
|
@@ -72,6 +81,8 @@ class JetStartServer extends events_1.EventEmitter {
|
|
|
72
81
|
displayHost: displayHost,
|
|
73
82
|
projectPath: config.projectPath || process.cwd(),
|
|
74
83
|
projectName: config.projectName || path.basename(config.projectPath || process.cwd()),
|
|
84
|
+
emulatorHost: config.emulatorHost,
|
|
85
|
+
webEnabled: config.webEnabled ?? false,
|
|
75
86
|
};
|
|
76
87
|
this.sessionManager = new session_1.SessionManager();
|
|
77
88
|
this.logsServer = new logs_1.LogsServer();
|
|
@@ -80,10 +91,7 @@ class JetStartServer extends events_1.EventEmitter {
|
|
|
80
91
|
cachePath: path.join(os.tmpdir(), 'jetstart-cache'),
|
|
81
92
|
watchEnabled: true,
|
|
82
93
|
});
|
|
83
|
-
|
|
84
|
-
// This creates a circular import if we import logger here, but we imported 'log' etc.
|
|
85
|
-
// We need to import loggerEvents.
|
|
86
|
-
// The previous tool replacement added loggerEvents export.
|
|
94
|
+
this.adbHelper = new gradle_1.AdbHelper();
|
|
87
95
|
}
|
|
88
96
|
async start() {
|
|
89
97
|
try {
|
|
@@ -105,12 +113,29 @@ class JetStartServer extends events_1.EventEmitter {
|
|
|
105
113
|
projectPath: this.config.projectPath,
|
|
106
114
|
});
|
|
107
115
|
// Inject server URL into build.gradle for hot reload
|
|
108
|
-
|
|
116
|
+
// For Android emulators, use 10.0.2.2 (host gateway) in BuildConfig; physical phones use the network IP
|
|
117
|
+
const buildConfigHost = this.config.emulatorHost || this.config.displayHost;
|
|
118
|
+
const serverUrl = `ws://${buildConfigHost}:${this.config.wsPort}`;
|
|
109
119
|
await (0, gradle_injector_1.injectBuildConfigFields)(this.config.projectPath, [
|
|
110
120
|
{ type: 'String', name: 'JETSTART_SERVER_URL', value: serverUrl },
|
|
111
121
|
{ type: 'String', name: 'JETSTART_SESSION_ID', value: this.currentSession.id }
|
|
112
122
|
]);
|
|
113
123
|
(0, logger_1.log)(`Injected server URL: ${serverUrl}`);
|
|
124
|
+
// Initialize Hot Reload Service
|
|
125
|
+
this.hotReloadService = new hot_reload_service_1.HotReloadService(this.config.projectPath);
|
|
126
|
+
if (this.config.webEnabled) {
|
|
127
|
+
this.jsCompiler = new js_compiler_service_1.JsCompilerService();
|
|
128
|
+
}
|
|
129
|
+
const envCheck = await this.hotReloadService.checkEnvironment();
|
|
130
|
+
if (envCheck.ready) {
|
|
131
|
+
(0, logger_1.log)('🔥 True hot reload enabled (DEX-based)');
|
|
132
|
+
this.useTrueHotReload = true;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
(0, logger_1.log)('True hot reload unavailable - file changes will trigger full Gradle builds');
|
|
136
|
+
(0, logger_1.log)(`Issues: ${envCheck.issues.join(', ')}`);
|
|
137
|
+
this.useTrueHotReload = false;
|
|
138
|
+
}
|
|
114
139
|
// Start HTTP server
|
|
115
140
|
this.httpServer = await (0, http_1.createHttpServer)({
|
|
116
141
|
port: this.config.httpPort,
|
|
@@ -122,10 +147,22 @@ class JetStartServer extends events_1.EventEmitter {
|
|
|
122
147
|
const wsResult = await (0, websocket_1.createWebSocketServer)({
|
|
123
148
|
port: this.config.wsPort,
|
|
124
149
|
logsServer: this.logsServer,
|
|
150
|
+
adbHelper: this.adbHelper, // Enable wireless ADB auto-connect
|
|
151
|
+
expectedSessionId: this.currentSession?.id, // Reject old-session devices
|
|
152
|
+
expectedToken: this.currentSession?.token, // Validate token too
|
|
153
|
+
projectName: this.config.projectName,
|
|
125
154
|
onClientConnected: async (sessionId) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
155
|
+
(0, logger_1.log)(`Client connected (session: ${sessionId}). Triggering initial build...`);
|
|
156
|
+
// If APK already built, just notify this new client - no rebuild needed
|
|
157
|
+
if (this.latestApkPath && fs.existsSync(this.latestApkPath)) {
|
|
158
|
+
(0, logger_1.log)(`APK already built, re-sending build complete to new client`);
|
|
159
|
+
const downloadUrl = `http://${this.config.displayHost}:${this.config.httpPort}/download/app.apk`;
|
|
160
|
+
this.wsHandler?.sendBuildComplete(sessionId, downloadUrl);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
this.isFileChangeBuild = false; // Don't auto-install for initial build
|
|
164
|
+
await this.handleRebuild();
|
|
165
|
+
}
|
|
129
166
|
},
|
|
130
167
|
});
|
|
131
168
|
this.wsServer = wsResult.server;
|
|
@@ -134,22 +171,27 @@ class JetStartServer extends events_1.EventEmitter {
|
|
|
134
171
|
this.setupBuildListeners();
|
|
135
172
|
// Start watching for file changes
|
|
136
173
|
this.buildService.startWatching(this.config.projectPath, async (files) => {
|
|
137
|
-
(0, logger_1.log)(`Files changed: ${files.map(f =>
|
|
138
|
-
// Check if
|
|
139
|
-
const
|
|
140
|
-
f.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
174
|
+
(0, logger_1.log)(`Files changed: ${files.map(f => f.split(/[/\\]/).pop()).join(', ')}`);
|
|
175
|
+
// Check if all changed files support hot reload (.kt files that aren't in build directory)
|
|
176
|
+
const hotReloadFiles = files.filter(f => {
|
|
177
|
+
const normalized = f.replace(/\\/g, '/');
|
|
178
|
+
// Support hot reload for all .kt files EXCEPT those in /build/ directory
|
|
179
|
+
return normalized.endsWith('.kt') &&
|
|
180
|
+
!normalized.includes('/build/') &&
|
|
181
|
+
!normalized.includes('build.gradle');
|
|
182
|
+
});
|
|
183
|
+
// Use TRUE hot reload for Kotlin files (sends DEX via WebSocket - no USB needed!)
|
|
184
|
+
if (hotReloadFiles.length > 0 && hotReloadFiles.length === files.length && this.useTrueHotReload) {
|
|
185
|
+
(0, logger_1.log)('🔥 Kotlin files changed, using TRUE hot reload (DEX via WebSocket)');
|
|
186
|
+
for (const file of hotReloadFiles) {
|
|
187
|
+
await this.handleUIUpdate(file);
|
|
188
|
+
}
|
|
148
189
|
}
|
|
149
190
|
else {
|
|
150
|
-
//
|
|
151
|
-
(0, logger_1.log)('📦
|
|
191
|
+
// Build files or mixed changes - use full Gradle build
|
|
192
|
+
(0, logger_1.log)('📦 Build files changed, triggering Gradle build + ADB install');
|
|
152
193
|
this.buildService.clearCache();
|
|
194
|
+
this.isFileChangeBuild = true; // Mark as file change build for auto-install
|
|
153
195
|
await this.handleRebuild();
|
|
154
196
|
}
|
|
155
197
|
});
|
|
@@ -196,7 +238,7 @@ class JetStartServer extends events_1.EventEmitter {
|
|
|
196
238
|
// Re-emit for external listeners (e.g., dev command)
|
|
197
239
|
this.emit('build:start');
|
|
198
240
|
});
|
|
199
|
-
this.buildService.on('build:complete', (result) => {
|
|
241
|
+
this.buildService.on('build:complete', async (result) => {
|
|
200
242
|
(0, logger_1.success)(`Build completed in ${result.buildTime}ms`);
|
|
201
243
|
if (this.wsHandler && this.currentSession) {
|
|
202
244
|
// Store the APK path
|
|
@@ -205,6 +247,47 @@ class JetStartServer extends events_1.EventEmitter {
|
|
|
205
247
|
const downloadUrl = `http://${this.config.displayHost}:${this.config.httpPort}/download/app.apk`;
|
|
206
248
|
this.wsHandler.sendBuildComplete(this.currentSession.id, downloadUrl);
|
|
207
249
|
(0, logger_1.log)(`APK download URL: ${downloadUrl}`);
|
|
250
|
+
// Auto-install APK via ADB only for file change builds (not initial builds)
|
|
251
|
+
if (this.autoInstall && this.latestApkPath && this.isFileChangeBuild) {
|
|
252
|
+
const readyDevices = this.adbHelper.getDevices();
|
|
253
|
+
if (readyDevices.length > 0) {
|
|
254
|
+
(0, logger_1.log)(`📱 Auto-installing APK on ${readyDevices.length} device(s)...`);
|
|
255
|
+
const installResult = await this.adbHelper.installApk(this.latestApkPath);
|
|
256
|
+
if (installResult.success) {
|
|
257
|
+
(0, logger_1.success)('📱 APK auto-installed! App will restart with new code.');
|
|
258
|
+
// Launch the app
|
|
259
|
+
// Read actual package name from jetstart.config.json
|
|
260
|
+
let appPackageName = 'com.example.app';
|
|
261
|
+
try {
|
|
262
|
+
const configPath = path.join(this.config.projectPath, 'jetstart.config.json');
|
|
263
|
+
if (fs.existsSync(configPath)) {
|
|
264
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
265
|
+
appPackageName = cfg.packageName || appPackageName;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (_) { /* empty */ }
|
|
269
|
+
await this.adbHelper.launchApp(appPackageName, '.MainActivity');
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
(0, logger_1.error)(`Auto-install failed: ${installResult.error}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
// Check if devices are connecting
|
|
277
|
+
const allDevices = this.adbHelper.getAllDeviceStates();
|
|
278
|
+
if (allDevices.length > 0) {
|
|
279
|
+
const states = allDevices.map(d => `${d.id} (${d.state})`).join(', ');
|
|
280
|
+
(0, logger_1.log)(`⏳ Devices found but not ready: ${states}`);
|
|
281
|
+
(0, logger_1.log)('ℹ️ Device may need user authorization on the phone.');
|
|
282
|
+
(0, logger_1.log)('ℹ️ Auto-install will retry on the next file change.');
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
(0, logger_1.log)('ℹ️ No devices connected for auto-install. Scan QR or download APK manually.');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Reset flag after handling
|
|
289
|
+
this.isFileChangeBuild = false;
|
|
290
|
+
}
|
|
208
291
|
}
|
|
209
292
|
// Re-emit for external listeners (e.g., dev command) with result
|
|
210
293
|
this.emit('build:complete', result);
|
|
@@ -260,34 +343,56 @@ class JetStartServer extends events_1.EventEmitter {
|
|
|
260
343
|
}
|
|
261
344
|
}
|
|
262
345
|
/**
|
|
263
|
-
* Handle UI file updates
|
|
346
|
+
* Handle UI file updates - uses TRUE hot reload (DEX-based) when available
|
|
347
|
+
*/
|
|
348
|
+
/**
|
|
349
|
+
* Compile a .kt file to a browser ES module and broadcast to web clients.
|
|
350
|
+
* Physical devices use DEX (already sent above). This targets browsers only.
|
|
264
351
|
*/
|
|
265
|
-
|
|
266
|
-
if (!this.currentSession || !this.wsHandler)
|
|
352
|
+
async sendJsPreview(filePath) {
|
|
353
|
+
if (!this.jsCompiler?.isAvailable() || !this.currentSession || !this.wsHandler)
|
|
267
354
|
return;
|
|
268
|
-
}
|
|
269
355
|
try {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const dslContent = JSON.stringify(parseResult.dsl);
|
|
274
|
-
(0, logger_1.log)(`DSL generated: ${dslContent.length} bytes`);
|
|
275
|
-
// Send DSL update via WebSocket (instant hot reload!)
|
|
276
|
-
this.wsHandler.sendUIUpdate(this.currentSession.id, dslContent, [path.basename(filePath)]);
|
|
277
|
-
(0, logger_1.success)(`UI hot reload sent in <100ms ⚡`);
|
|
356
|
+
const result = await this.jsCompiler.compile(filePath);
|
|
357
|
+
if (result.success && result.jsBase64) {
|
|
358
|
+
this.wsHandler.sendJsUpdate(this.currentSession.id, result.jsBase64, path.basename(filePath), result.byteSize ?? 0, result.screenFunctionName ?? 'Screen');
|
|
278
359
|
}
|
|
279
360
|
else {
|
|
280
|
-
(0, logger_1.
|
|
281
|
-
// Fallback to full build
|
|
282
|
-
(0, logger_1.log)('Falling back to full Gradle build...');
|
|
283
|
-
this.handleRebuild();
|
|
361
|
+
(0, logger_1.log)(`[JsCompiler] Web preview skipped: ${result.error?.slice(0, 100)}`);
|
|
284
362
|
}
|
|
285
363
|
}
|
|
286
364
|
catch (err) {
|
|
287
|
-
(0, logger_1.
|
|
288
|
-
|
|
289
|
-
|
|
365
|
+
(0, logger_1.log)(`[JsCompiler] Web preview error: ${err.message}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async handleUIUpdate(filePath) {
|
|
369
|
+
if (!this.currentSession || !this.wsHandler)
|
|
370
|
+
return;
|
|
371
|
+
// TRUE hot reload: compile to DEX and push to device via WebSocket
|
|
372
|
+
if (this.useTrueHotReload && this.hotReloadService) {
|
|
373
|
+
try {
|
|
374
|
+
(0, logger_1.log)(`Hot reloading: ${path.basename(filePath)}...`);
|
|
375
|
+
const result = await this.hotReloadService.hotReload(filePath);
|
|
376
|
+
if (result.success && result.dexBase64) {
|
|
377
|
+
this.wsHandler.sendDexReload(this.currentSession.id, result.dexBase64, result.classNames);
|
|
378
|
+
(0, logger_1.success)(`Hot reload complete! (${result.compileTime + result.dexTime}ms)`);
|
|
379
|
+
// Run JS compilation in parallel — does not block DEX delivery
|
|
380
|
+
this.sendJsPreview(filePath).catch(() => { });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// DEX compilation failed — fall through to full Gradle rebuild
|
|
384
|
+
(0, logger_1.error)(`Hot reload compile failed: ${result.errors[0] ?? 'unknown'}`);
|
|
385
|
+
(0, logger_1.log)('Triggering full Gradle rebuild...');
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
(0, logger_1.error)(`Hot reload error: ${err.message}`);
|
|
389
|
+
(0, logger_1.log)('Triggering full Gradle rebuild...');
|
|
390
|
+
}
|
|
290
391
|
}
|
|
392
|
+
// Fall back to full Gradle build (catches build-file changes, new dependencies, etc.)
|
|
393
|
+
this.buildService.clearCache();
|
|
394
|
+
this.isFileChangeBuild = true;
|
|
395
|
+
await this.handleRebuild();
|
|
291
396
|
}
|
|
292
397
|
}
|
|
293
398
|
exports.JetStartServer = JetStartServer;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WebSocket Message Handler
|
|
3
|
-
* Processes incoming WebSocket messages
|
|
3
|
+
* Processes incoming WebSocket messages with session + token validation.
|
|
4
|
+
*
|
|
5
|
+
* Security model:
|
|
6
|
+
* - Every connecting client must supply the correct sessionId AND token.
|
|
7
|
+
* - Both are embedded in the QR code shown by `jetstart dev`.
|
|
8
|
+
* - A device that was built against a previous session (different token)
|
|
9
|
+
* is rejected immediately — it cannot hijack the current dev session.
|
|
4
10
|
*/
|
|
5
11
|
import { ConnectionManager } from './manager';
|
|
6
12
|
import { LogsServer } from '@jetstart/logs';
|
|
@@ -8,9 +14,18 @@ export declare class WebSocketHandler {
|
|
|
8
14
|
private connectionManager;
|
|
9
15
|
private onClientConnected?;
|
|
10
16
|
private logsServer?;
|
|
17
|
+
/** The session ID this server owns — clients must match this exactly. */
|
|
18
|
+
private expectedSessionId;
|
|
19
|
+
/** The token this server owns — clients must match this exactly. */
|
|
20
|
+
private expectedToken;
|
|
21
|
+
/** Human-readable project name for the `core:connected` response. */
|
|
22
|
+
private projectName;
|
|
11
23
|
constructor(connectionManager: ConnectionManager, options?: {
|
|
12
24
|
onClientConnected?: (sessionId: string) => void;
|
|
13
25
|
logsServer?: LogsServer;
|
|
26
|
+
expectedSessionId?: string;
|
|
27
|
+
expectedToken?: string;
|
|
28
|
+
projectName?: string;
|
|
14
29
|
});
|
|
15
30
|
handleMessage(clientId: string, data: Buffer): void;
|
|
16
31
|
private isClientMessage;
|
|
@@ -19,11 +34,12 @@ export declare class WebSocketHandler {
|
|
|
19
34
|
private handleDisconnect;
|
|
20
35
|
sendBuildStart(sessionId: string): void;
|
|
21
36
|
sendBuildComplete(sessionId: string, apkUrl: string): void;
|
|
37
|
+
sendDexReload(sessionId: string, dexBase64: string, classNames: string[]): void;
|
|
22
38
|
/**
|
|
23
|
-
* Send
|
|
39
|
+
* Send compiled Kotlin→JS ES module to web emulator clients.
|
|
40
|
+
* The browser imports it dynamically and renders the Compose UI as HTML.
|
|
24
41
|
*/
|
|
25
|
-
|
|
26
|
-
private generateHash;
|
|
42
|
+
sendJsUpdate(sessionId: string, jsBase64: string, sourceFile: string, byteSize: number, screenFunctionName: string): void;
|
|
27
43
|
sendLogBroadcast(sessionId: string, logEntry: any): void;
|
|
28
44
|
}
|
|
29
45
|
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* WebSocket Message Handler
|
|
4
|
-
* Processes incoming WebSocket messages
|
|
4
|
+
* Processes incoming WebSocket messages with session + token validation.
|
|
5
|
+
*
|
|
6
|
+
* Security model:
|
|
7
|
+
* - Every connecting client must supply the correct sessionId AND token.
|
|
8
|
+
* - Both are embedded in the QR code shown by `jetstart dev`.
|
|
9
|
+
* - A device that was built against a previous session (different token)
|
|
10
|
+
* is rejected immediately — it cannot hijack the current dev session.
|
|
5
11
|
*/
|
|
6
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
13
|
exports.WebSocketHandler = void 0;
|
|
@@ -10,16 +16,24 @@ class WebSocketHandler {
|
|
|
10
16
|
connectionManager;
|
|
11
17
|
onClientConnected;
|
|
12
18
|
logsServer;
|
|
19
|
+
/** The session ID this server owns — clients must match this exactly. */
|
|
20
|
+
expectedSessionId;
|
|
21
|
+
/** The token this server owns — clients must match this exactly. */
|
|
22
|
+
expectedToken;
|
|
23
|
+
/** Human-readable project name for the `core:connected` response. */
|
|
24
|
+
projectName;
|
|
13
25
|
constructor(connectionManager, options) {
|
|
14
26
|
this.connectionManager = connectionManager;
|
|
15
27
|
this.onClientConnected = options?.onClientConnected;
|
|
16
28
|
this.logsServer = options?.logsServer;
|
|
29
|
+
this.expectedSessionId = options?.expectedSessionId ?? '';
|
|
30
|
+
this.expectedToken = options?.expectedToken ?? '';
|
|
31
|
+
this.projectName = options?.projectName ?? 'JetStart Project';
|
|
17
32
|
}
|
|
18
33
|
handleMessage(clientId, data) {
|
|
19
34
|
try {
|
|
20
35
|
const message = JSON.parse(data.toString());
|
|
21
36
|
(0, logger_1.log)(`Received message from ${clientId}: ${message.type}`);
|
|
22
|
-
// Route message based on type
|
|
23
37
|
if (this.isClientMessage(message)) {
|
|
24
38
|
this.handleClientMessage(clientId, message);
|
|
25
39
|
}
|
|
@@ -40,22 +54,19 @@ class WebSocketHandler {
|
|
|
40
54
|
(0, logger_1.log)(`Client ${clientId} status: ${message.status}`);
|
|
41
55
|
break;
|
|
42
56
|
case 'client:log':
|
|
43
|
-
// Add to Logs Server (for CLI/Web Dashboard)
|
|
44
57
|
if (this.logsServer) {
|
|
45
58
|
this.logsServer.addLog(message.log);
|
|
46
59
|
}
|
|
47
|
-
// Broadcast log to all clients in the session (e.g. Logs Viewer in App)
|
|
48
60
|
if (message.sessionId) {
|
|
49
61
|
this.connectionManager.broadcastToSession(message.sessionId, {
|
|
50
62
|
type: 'core:log',
|
|
51
63
|
timestamp: Date.now(),
|
|
52
64
|
sessionId: message.sessionId,
|
|
53
|
-
log: message.log
|
|
65
|
+
log: message.log,
|
|
54
66
|
});
|
|
55
67
|
}
|
|
56
68
|
break;
|
|
57
69
|
case 'client:heartbeat':
|
|
58
|
-
// Update last activity
|
|
59
70
|
break;
|
|
60
71
|
case 'client:disconnect':
|
|
61
72
|
this.handleDisconnect(clientId, message);
|
|
@@ -63,27 +74,50 @@ class WebSocketHandler {
|
|
|
63
74
|
}
|
|
64
75
|
}
|
|
65
76
|
handleConnect(clientId, message) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
77
|
+
const incomingSession = message.sessionId;
|
|
78
|
+
const incomingToken = message.token;
|
|
79
|
+
// ── Session + token validation ──────────────────────────────────────────
|
|
80
|
+
// Only validate when the server has an expected session configured.
|
|
81
|
+
if (this.expectedSessionId) {
|
|
82
|
+
if (incomingSession !== this.expectedSessionId) {
|
|
83
|
+
(0, logger_1.error)(`Rejected client ${clientId}: wrong session "${incomingSession}" (expected "${this.expectedSessionId}")`);
|
|
84
|
+
(0, logger_1.error)('This device was built against a different jetstart dev session. Rescan the QR code.');
|
|
85
|
+
// Close the WebSocket immediately — do not accept this client.
|
|
86
|
+
const ws = this.connectionManager.getConnection(clientId);
|
|
87
|
+
if (ws) {
|
|
88
|
+
ws.close(4001, 'Session mismatch — rescan QR code');
|
|
89
|
+
}
|
|
90
|
+
this.connectionManager.removeConnection(clientId);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (this.expectedToken && incomingToken && incomingToken !== this.expectedToken) {
|
|
94
|
+
(0, logger_1.error)(`Rejected client ${clientId}: wrong token (session ${incomingSession})`);
|
|
95
|
+
const ws = this.connectionManager.getConnection(clientId);
|
|
96
|
+
if (ws) {
|
|
97
|
+
ws.close(4002, 'Token mismatch — rescan QR code');
|
|
98
|
+
}
|
|
99
|
+
this.connectionManager.removeConnection(clientId);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ── Accepted ─────────────────────────────────────────────────────────────
|
|
104
|
+
(0, logger_1.log)(`Client accepted (session: ${incomingSession})`);
|
|
105
|
+
this.connectionManager.setClientSession(clientId, incomingSession);
|
|
70
106
|
const response = {
|
|
71
107
|
type: 'core:connected',
|
|
72
108
|
timestamp: Date.now(),
|
|
73
|
-
sessionId:
|
|
74
|
-
projectName:
|
|
109
|
+
sessionId: incomingSession,
|
|
110
|
+
projectName: this.projectName,
|
|
75
111
|
};
|
|
76
112
|
this.connectionManager.sendToClient(clientId, response);
|
|
77
|
-
// Trigger initial build for the client
|
|
78
113
|
if (this.onClientConnected) {
|
|
79
|
-
(0, logger_1.log)(`Triggering initial build for session: ${
|
|
80
|
-
this.onClientConnected(
|
|
114
|
+
(0, logger_1.log)(`Triggering initial build for session: ${incomingSession}`);
|
|
115
|
+
this.onClientConnected(incomingSession);
|
|
81
116
|
}
|
|
82
117
|
}
|
|
83
118
|
handleDisconnect(clientId, message) {
|
|
84
119
|
(0, logger_1.log)(`Client disconnecting: ${message.reason || 'No reason provided'}`);
|
|
85
120
|
}
|
|
86
|
-
// Send build notifications
|
|
87
121
|
sendBuildStart(sessionId) {
|
|
88
122
|
const message = {
|
|
89
123
|
type: 'core:build-start',
|
|
@@ -111,39 +145,40 @@ class WebSocketHandler {
|
|
|
111
145
|
};
|
|
112
146
|
this.connectionManager.broadcastToSession(sessionId, message);
|
|
113
147
|
}
|
|
148
|
+
sendDexReload(sessionId, dexBase64, classNames) {
|
|
149
|
+
const message = {
|
|
150
|
+
type: 'core:dex-reload',
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
sessionId,
|
|
153
|
+
dexBase64,
|
|
154
|
+
classNames,
|
|
155
|
+
};
|
|
156
|
+
(0, logger_1.log)(`Sending DEX reload: ${dexBase64.length} base64 chars, ${classNames.length} classes`);
|
|
157
|
+
this.connectionManager.broadcastToAll(message);
|
|
158
|
+
}
|
|
114
159
|
/**
|
|
115
|
-
* Send
|
|
160
|
+
* Send compiled Kotlin→JS ES module to web emulator clients.
|
|
161
|
+
* The browser imports it dynamically and renders the Compose UI as HTML.
|
|
116
162
|
*/
|
|
117
|
-
|
|
163
|
+
sendJsUpdate(sessionId, jsBase64, sourceFile, byteSize, screenFunctionName) {
|
|
118
164
|
const message = {
|
|
119
|
-
type: 'core:
|
|
165
|
+
type: 'core:js-update',
|
|
120
166
|
timestamp: Date.now(),
|
|
121
167
|
sessionId,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
168
|
+
jsBase64,
|
|
169
|
+
sourceFile,
|
|
170
|
+
byteSize,
|
|
125
171
|
};
|
|
126
|
-
(0, logger_1.log)(`Sending
|
|
127
|
-
|
|
128
|
-
this.connectionManager.
|
|
172
|
+
(0, logger_1.log)(`Sending JS update: ${sourceFile} (${byteSize} bytes) to web clients`);
|
|
173
|
+
// Broadcast to all — web clients use it, Android clients ignore unknown types
|
|
174
|
+
this.connectionManager.broadcastToAll(message);
|
|
129
175
|
}
|
|
130
|
-
generateHash(content) {
|
|
131
|
-
// Simple hash for caching (in production, use crypto.createHash)
|
|
132
|
-
let hash = 0;
|
|
133
|
-
for (let i = 0; i < content.length; i++) {
|
|
134
|
-
const char = content.charCodeAt(i);
|
|
135
|
-
hash = ((hash << 5) - hash) + char;
|
|
136
|
-
hash = hash & hash; // Convert to 32bit integer
|
|
137
|
-
}
|
|
138
|
-
return hash.toString(16);
|
|
139
|
-
}
|
|
140
|
-
// Broadcast any log entry to the session
|
|
141
176
|
sendLogBroadcast(sessionId, logEntry) {
|
|
142
177
|
this.connectionManager.broadcastToSession(sessionId, {
|
|
143
178
|
type: 'core:log',
|
|
144
179
|
timestamp: Date.now(),
|
|
145
|
-
sessionId
|
|
146
|
-
log: logEntry
|
|
180
|
+
sessionId,
|
|
181
|
+
log: logEntry,
|
|
147
182
|
});
|
|
148
183
|
}
|
|
149
184
|
}
|
|
@@ -5,12 +5,20 @@
|
|
|
5
5
|
import { WebSocketServer } from 'ws';
|
|
6
6
|
import { Server } from 'http';
|
|
7
7
|
import { WebSocketHandler } from './handler';
|
|
8
|
+
import { AdbHelper } from '../build/gradle';
|
|
8
9
|
import { LogsServer } from '@jetstart/logs';
|
|
9
10
|
export interface WebSocketConfig {
|
|
10
11
|
port: number;
|
|
11
12
|
server?: Server;
|
|
12
13
|
logsServer?: LogsServer;
|
|
13
14
|
onClientConnected?: (sessionId: string) => void;
|
|
15
|
+
adbHelper?: AdbHelper;
|
|
16
|
+
/** Session ID this server owns — clients presenting a different ID are rejected. */
|
|
17
|
+
expectedSessionId?: string;
|
|
18
|
+
/** Token this server owns — clients presenting a different token are rejected. */
|
|
19
|
+
expectedToken?: string;
|
|
20
|
+
/** Project name shown in the core:connected response. */
|
|
21
|
+
projectName?: string;
|
|
14
22
|
}
|
|
15
23
|
export interface WebSocketServerResult {
|
|
16
24
|
server: WebSocketServer;
|
package/dist/websocket/index.js
CHANGED
|
@@ -10,27 +10,31 @@ const handler_1 = require("./handler");
|
|
|
10
10
|
const manager_1 = require("./manager");
|
|
11
11
|
const logger_1 = require("../utils/logger");
|
|
12
12
|
async function createWebSocketServer(config) {
|
|
13
|
-
const wss = new ws_1.WebSocketServer({
|
|
14
|
-
port: config.port,
|
|
15
|
-
});
|
|
13
|
+
const wss = new ws_1.WebSocketServer({ port: config.port });
|
|
16
14
|
const connectionManager = new manager_1.ConnectionManager();
|
|
17
15
|
const handler = new handler_1.WebSocketHandler(connectionManager, {
|
|
18
16
|
logsServer: config.logsServer,
|
|
19
17
|
onClientConnected: config.onClientConnected,
|
|
18
|
+
expectedSessionId: config.expectedSessionId,
|
|
19
|
+
expectedToken: config.expectedToken,
|
|
20
|
+
projectName: config.projectName,
|
|
20
21
|
});
|
|
21
|
-
wss.on('connection', (ws,
|
|
22
|
+
wss.on('connection', (ws, request) => {
|
|
22
23
|
const clientId = connectionManager.addConnection(ws);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
const clientIp = request.socket.remoteAddress || 'unknown';
|
|
25
|
+
(0, logger_1.log)(`WebSocket client connected: ${clientId} (IP: ${clientIp})`);
|
|
26
|
+
// Auto-connect wireless ADB for real devices
|
|
27
|
+
if (clientIp && clientIp !== 'localhost' && clientIp !== '127.0.0.1' && clientIp !== '::1') {
|
|
28
|
+
const cleanIp = clientIp.replace(/^::ffff:/, '');
|
|
29
|
+
if (config.adbHelper) {
|
|
30
|
+
process.nextTick(() => config.adbHelper.connectWireless(cleanIp));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
ws.on('message', (data) => handler.handleMessage(clientId, data));
|
|
29
34
|
ws.on('close', () => {
|
|
30
35
|
(0, logger_1.log)(`WebSocket client disconnected: ${clientId}`);
|
|
31
36
|
connectionManager.removeConnection(clientId);
|
|
32
37
|
});
|
|
33
|
-
// Handle errors
|
|
34
38
|
ws.on('error', (err) => {
|
|
35
39
|
console.error(`WebSocket error for ${clientId}:`, err.message);
|
|
36
40
|
});
|
|
@@ -24,9 +24,9 @@ export declare class ConnectionManager {
|
|
|
24
24
|
*/
|
|
25
25
|
broadcastToSession(sessionId: string, message: CoreMessage): void;
|
|
26
26
|
/**
|
|
27
|
-
* Broadcast to ALL clients regardless of session (
|
|
27
|
+
* Broadcast to ALL clients regardless of session (for dev mode hot reload)
|
|
28
28
|
*/
|
|
29
|
-
|
|
29
|
+
broadcastToAll(message: CoreMessage): void;
|
|
30
30
|
getConnectionCount(): number;
|
|
31
31
|
}
|
|
32
32
|
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -80,7 +80,7 @@ class ConnectionManager {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
|
-
* Broadcast to ALL clients regardless of session (
|
|
83
|
+
* Broadcast to ALL clients regardless of session (for dev mode hot reload)
|
|
84
84
|
*/
|
|
85
85
|
broadcastToAll(message) {
|
|
86
86
|
const data = JSON.stringify(message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetstart/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Build server and orchestration for JetStart",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"homepage": "https://github.com/dev-phantom/jetstart#readme",
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@jetstart/shared": "^
|
|
37
|
-
"@jetstart/logs": "^
|
|
36
|
+
"@jetstart/shared": "^2.0.0",
|
|
37
|
+
"@jetstart/logs": "^2.0.0",
|
|
38
38
|
"express": "^4.18.2",
|
|
39
39
|
"ws": "^8.14.2",
|
|
40
40
|
"chokidar": "^3.5.3",
|