@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.
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
@@ -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
- // Hook into logger events
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
- const serverUrl = `ws://${this.config.displayHost}:${this.config.wsPort}`;
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
- // Trigger initial build when client connects
127
- (0, logger_1.log)(`Triggering initial build for connected client (session: ${sessionId})`);
128
- await this.handleRebuild();
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 => path.basename(f)).join(', ')}`);
138
- // Check if only UI files changed (MainActivity.kt, screens/, components/)
139
- const uiFiles = files.filter(f => f.includes('MainActivity.kt') ||
140
- f.includes('/screens/') ||
141
- f.includes('\\screens\\') ||
142
- f.includes('/components/') ||
143
- f.includes('\\components\\'));
144
- // If ALL changed files are UI files, use DSL hot reload (FAST)
145
- if (uiFiles.length > 0 && uiFiles.length === files.length) {
146
- (0, logger_1.log)('🚀 UI-only changes detected, using DSL hot reload');
147
- this.handleUIUpdate(uiFiles[0]);
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
- // Otherwise, full Gradle build
151
- (0, logger_1.log)('📦 Non-UI changes detected, triggering full Gradle build');
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 using DSL hot reload (FAST)
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
- handleUIUpdate(filePath) {
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
- (0, logger_1.log)(`Parsing UI file: ${path.basename(filePath)}`);
271
- const parseResult = dsl_parser_1.DSLParser.parseFile(filePath);
272
- if (parseResult.success && parseResult.dsl) {
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.error)(`Failed to parse UI file: ${parseResult.errors?.join(', ')}`);
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.error)(`UI update failed: ${err.message}`);
288
- // Fallback to full build
289
- this.handleRebuild();
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 UI update (DSL-based hot reload) - SECURE, session-isolated
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
- sendUIUpdate(sessionId: string, dslContent: string, screens?: string[]): void;
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
- (0, logger_1.log)(`Client connecting with session: ${message.sessionId}`);
67
- // Associate this client with the session for isolation
68
- this.connectionManager.setClientSession(clientId, message.sessionId);
69
- // Send connected confirmation
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: message.sessionId,
74
- projectName: 'DemoProject', // In real implementation, get from session
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: ${message.sessionId}`);
80
- this.onClientConnected(message.sessionId);
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 UI update (DSL-based hot reload) - SECURE, session-isolated
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
- sendUIUpdate(sessionId, dslContent, screens) {
163
+ sendJsUpdate(sessionId, jsBase64, sourceFile, byteSize, screenFunctionName) {
118
164
  const message = {
119
- type: 'core:ui-update',
165
+ type: 'core:js-update',
120
166
  timestamp: Date.now(),
121
167
  sessionId,
122
- dslContent,
123
- screens,
124
- hash: this.generateHash(dslContent),
168
+ jsBase64,
169
+ sourceFile,
170
+ byteSize,
125
171
  };
126
- (0, logger_1.log)(`Sending UI update to session ${sessionId}: ${dslContent.length} bytes`);
127
- (0, logger_1.log)(`DSL Content: ${dslContent}`);
128
- this.connectionManager.broadcastToSession(sessionId, message);
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: 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;
@@ -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, _request) => {
22
+ wss.on('connection', (ws, request) => {
22
23
  const clientId = connectionManager.addConnection(ws);
23
- (0, logger_1.log)(`WebSocket client connected: ${clientId}`);
24
- // Handle messages
25
- ws.on('message', (data) => {
26
- handler.handleMessage(clientId, data);
27
- });
28
- // Handle disconnection
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 (use with caution)
27
+ * Broadcast to ALL clients regardless of session (for dev mode hot reload)
28
28
  */
29
- private broadcastToAll;
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 (use with caution)
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": "1.7.0",
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": "^1.7.0",
37
- "@jetstart/logs": "^1.7.0",
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",