@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
@@ -0,0 +1,478 @@
1
+ /**
2
+ * Override Generator
3
+ * Generates $override classes that implement IncrementalChange interface.
4
+ * These classes contain the new method implementations and route calls via access$dispatch.
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ import { spawn } from 'child_process';
11
+ import { log, error as logError } from '../utils/logger';
12
+
13
+ export interface OverrideGeneratorResult {
14
+ success: boolean;
15
+ overrideClassFiles: string[];
16
+ errors: string[];
17
+ }
18
+
19
+ export class OverrideGenerator {
20
+ private static readonly TAG = 'OverrideGenerator';
21
+
22
+ /**
23
+ * Generate $override classes for all compiled class files.
24
+ * The override classes implement IncrementalChange and contain the new method implementations.
25
+ *
26
+ * Strategy: Instead of complex bytecode manipulation, we generate Kotlin source code
27
+ * for the $Override classes and compile them. This is simpler and more maintainable.
28
+ */
29
+ async generateOverrides(
30
+ classFiles: string[],
31
+ sourceFile: string,
32
+ outputDir: string
33
+ ): Promise<OverrideGeneratorResult> {
34
+ const overrideClassFiles: string[] = [];
35
+ const errors: string[] = [];
36
+
37
+ try {
38
+ // Read the original Kotlin source to understand method signatures
39
+ const sourceContent = fs.readFileSync(sourceFile, 'utf-8');
40
+
41
+ // Parse class and method information from source
42
+ const classInfo = this.parseKotlinSource(sourceContent, sourceFile);
43
+
44
+ if (classInfo.length === 0) {
45
+ // No classes found - this is normal for files with only @Composable functions
46
+ // Return success with 0 overrides and let the fallback handle it
47
+ log(`ℹ️ No classes found in ${sourceFile} - using direct class hot reload`);
48
+ return { success: true, overrideClassFiles: [], errors: [] };
49
+ }
50
+
51
+ // Generate override class source for each class
52
+ for (const info of classInfo) {
53
+ const overrideSource = this.generateOverrideSource(info);
54
+ const overrideSourcePath = path.join(outputDir, `${info.className}\$override.kt`);
55
+
56
+ fs.writeFileSync(overrideSourcePath, overrideSource);
57
+ log(`Generated override source: ${info.className}$override.kt`);
58
+
59
+ // Track the generated source file
60
+ overrideClassFiles.push(overrideSourcePath);
61
+ }
62
+
63
+ return {
64
+ success: true,
65
+ overrideClassFiles,
66
+ errors: []
67
+ };
68
+ } catch (e) {
69
+ const errorMsg = e instanceof Error ? e.message : String(e);
70
+ errors.push(errorMsg);
71
+ return { success: false, overrideClassFiles: [], errors };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Parse Kotlin source file to extract class and method information
77
+ */
78
+ private parseKotlinSource(source: string, filePath: string): ClassInfo[] {
79
+ const classes: ClassInfo[] = [];
80
+
81
+ // Extract package name
82
+ const packageMatch = source.match(/^package\s+([\w.]+)/m);
83
+ const packageName = packageMatch ? packageMatch[1] : '';
84
+
85
+ // Simple regex-based parsing for class definitions
86
+ // This handles basic cases - complex nested classes may need more sophisticated parsing
87
+ const classRegex = /(?:class|object)\s+(\w+)(?:\s*:\s*[\w\s,<>]+)?\s*\{/g;
88
+ let classMatch;
89
+
90
+ while ((classMatch = classRegex.exec(source)) !== null) {
91
+ const className = classMatch[1];
92
+ const classStartIndex = classMatch.index;
93
+
94
+ // Find matching closing brace
95
+ let braceCount = 0;
96
+ let classEndIndex = classStartIndex;
97
+ let inString = false;
98
+ let stringChar = '';
99
+
100
+ for (let i = classStartIndex; i < source.length; i++) {
101
+ const char = source[i];
102
+ const prevChar = i > 0 ? source[i - 1] : '';
103
+
104
+ // Handle string literals
105
+ if ((char === '"' || char === '\'') && prevChar !== '\\') {
106
+ if (!inString) {
107
+ inString = true;
108
+ stringChar = char;
109
+ } else if (char === stringChar) {
110
+ inString = false;
111
+ }
112
+ }
113
+
114
+ if (!inString) {
115
+ if (char === '{') braceCount++;
116
+ if (char === '}') {
117
+ braceCount--;
118
+ if (braceCount === 0) {
119
+ classEndIndex = i;
120
+ break;
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ const classBody = source.substring(classStartIndex, classEndIndex + 1);
127
+ const methods = this.parseMethodsFromClassBody(classBody, className);
128
+
129
+ classes.push({
130
+ packageName,
131
+ className,
132
+ fullClassName: packageName ? `${packageName}.${className}` : className,
133
+ methods
134
+ });
135
+ }
136
+
137
+ // Also handle top-level functions (they go in FileNameKt class)
138
+ const topLevelMethods = this.parseTopLevelFunctions(source, filePath);
139
+ if (topLevelMethods.length > 0) {
140
+ const fileName = path.basename(filePath, '.kt');
141
+ const ktClassName = fileName.charAt(0).toUpperCase() + fileName.slice(1) + 'Kt';
142
+
143
+ classes.push({
144
+ packageName,
145
+ className: ktClassName,
146
+ fullClassName: packageName ? `${packageName}.${ktClassName}` : ktClassName,
147
+ methods: topLevelMethods
148
+ });
149
+ }
150
+
151
+ return classes;
152
+ }
153
+
154
+ /**
155
+ * Parse methods from a class body
156
+ */
157
+ private parseMethodsFromClassBody(classBody: string, className: string): MethodInfo[] {
158
+ const methods: MethodInfo[] = [];
159
+
160
+ // Match function definitions
161
+ // This regex handles: fun name(params): ReturnType
162
+ const funRegex = /fun\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([\w<>?,\s]+))?\s*[{=]/g;
163
+ let funMatch;
164
+
165
+ while ((funMatch = funRegex.exec(classBody)) !== null) {
166
+ const methodName = funMatch[1];
167
+ const paramsStr = funMatch[2];
168
+ const returnType = funMatch[3]?.trim() || 'Unit';
169
+
170
+ // Parse parameters
171
+ const params = this.parseParameters(paramsStr);
172
+
173
+ // Generate method signature for dispatch
174
+ const signature = this.generateMethodSignature(methodName, params, returnType);
175
+
176
+ methods.push({
177
+ name: methodName,
178
+ parameters: params,
179
+ returnType,
180
+ signature,
181
+ isStatic: false // Assume instance method, TODO: detect companion object
182
+ });
183
+ }
184
+
185
+ return methods;
186
+ }
187
+
188
+ /**
189
+ * Parse top-level functions (not inside a class)
190
+ */
191
+ private parseTopLevelFunctions(source: string, filePath: string): MethodInfo[] {
192
+ const methods: MethodInfo[] = [];
193
+
194
+ // Remove class bodies first to get only top-level content
195
+ let topLevelContent = source;
196
+
197
+ // Remove all class/object bodies
198
+ const classRegex = /(?:class|object)\s+\w+[^{]*\{/g;
199
+ let match;
200
+ const classPositions: { start: number; end: number }[] = [];
201
+
202
+ while ((match = classRegex.exec(source)) !== null) {
203
+ const startIndex = match.index;
204
+ let braceCount = 0;
205
+ let endIndex = startIndex;
206
+ let inString = false;
207
+ let stringChar = '';
208
+
209
+ for (let i = startIndex; i < source.length; i++) {
210
+ const char = source[i];
211
+ const prevChar = i > 0 ? source[i - 1] : '';
212
+
213
+ if ((char === '"' || char === '\'') && prevChar !== '\\') {
214
+ if (!inString) {
215
+ inString = true;
216
+ stringChar = char;
217
+ } else if (char === stringChar) {
218
+ inString = false;
219
+ }
220
+ }
221
+
222
+ if (!inString) {
223
+ if (char === '{') braceCount++;
224
+ if (char === '}') {
225
+ braceCount--;
226
+ if (braceCount === 0) {
227
+ endIndex = i;
228
+ break;
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ classPositions.push({ start: startIndex, end: endIndex + 1 });
235
+ }
236
+
237
+ // Sort and remove class bodies from end to start to maintain indices
238
+ classPositions.sort((a, b) => b.start - a.start);
239
+ for (const pos of classPositions) {
240
+ topLevelContent = topLevelContent.substring(0, pos.start) + topLevelContent.substring(pos.end);
241
+ }
242
+
243
+ // Now parse functions from top-level content
244
+ const funRegex = /fun\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([\w<>?,\s]+))?\s*[{=]/g;
245
+ let funMatch;
246
+
247
+ while ((funMatch = funRegex.exec(topLevelContent)) !== null) {
248
+ const methodName = funMatch[1];
249
+ const paramsStr = funMatch[2];
250
+ const returnType = funMatch[3]?.trim() || 'Unit';
251
+
252
+ const params = this.parseParameters(paramsStr);
253
+ const signature = this.generateMethodSignature(methodName, params, returnType);
254
+
255
+ methods.push({
256
+ name: methodName,
257
+ parameters: params,
258
+ returnType,
259
+ signature,
260
+ isStatic: true // Top-level functions are static
261
+ });
262
+ }
263
+
264
+ return methods;
265
+ }
266
+
267
+ /**
268
+ * Parse parameter string into typed parameters
269
+ */
270
+ private parseParameters(paramsStr: string): ParameterInfo[] {
271
+ if (!paramsStr.trim()) return [];
272
+
273
+ const params: ParameterInfo[] = [];
274
+
275
+ // Split by comma, but respect generics
276
+ let depth = 0;
277
+ let current = '';
278
+
279
+ for (const char of paramsStr) {
280
+ if (char === '<' || char === '(') depth++;
281
+ if (char === '>' || char === ')') depth--;
282
+
283
+ if (char === ',' && depth === 0) {
284
+ if (current.trim()) {
285
+ const param = this.parseParameter(current.trim());
286
+ if (param) params.push(param);
287
+ }
288
+ current = '';
289
+ } else {
290
+ current += char;
291
+ }
292
+ }
293
+
294
+ if (current.trim()) {
295
+ const param = this.parseParameter(current.trim());
296
+ if (param) params.push(param);
297
+ }
298
+
299
+ return params;
300
+ }
301
+
302
+ /**
303
+ * Parse a single parameter definition
304
+ */
305
+ private parseParameter(paramStr: string): ParameterInfo | null {
306
+ // Handle "name: Type" or "name: Type = default"
307
+ const match = paramStr.match(/(\w+)\s*:\s*([\w<>?,.\s]+?)(?:\s*=.*)?$/);
308
+ if (!match) return null;
309
+
310
+ return {
311
+ name: match[1],
312
+ type: match[2].trim()
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Generate a method signature string for dispatch routing
318
+ */
319
+ private generateMethodSignature(methodName: string, params: ParameterInfo[], returnType: string): string {
320
+ // Format: methodName.(paramTypes)returnType
321
+ // Using JVM-style descriptors
322
+ const paramDescriptors = params.map(p => this.typeToDescriptor(p.type)).join('');
323
+ const returnDescriptor = this.typeToDescriptor(returnType);
324
+ return `${methodName}.(${paramDescriptors})${returnDescriptor}`;
325
+ }
326
+
327
+ /**
328
+ * Convert Kotlin type to JVM descriptor-style string
329
+ */
330
+ private typeToDescriptor(type: string): string {
331
+ // Simplified mapping - expand as needed
332
+ const typeMap: Record<string, string> = {
333
+ 'Unit': 'V',
334
+ 'Int': 'I',
335
+ 'Long': 'J',
336
+ 'Float': 'F',
337
+ 'Double': 'D',
338
+ 'Boolean': 'Z',
339
+ 'Byte': 'B',
340
+ 'Char': 'C',
341
+ 'Short': 'S',
342
+ 'String': 'Ljava/lang/String;',
343
+ 'Any': 'Ljava/lang/Object;',
344
+ };
345
+
346
+ // Handle nullable types
347
+ const nonNullType = type.replace('?', '');
348
+
349
+ if (typeMap[nonNullType]) {
350
+ return typeMap[nonNullType];
351
+ }
352
+
353
+ // Handle generic types like List<String>
354
+ const genericMatch = nonNullType.match(/^(\w+)<.*>$/);
355
+ if (genericMatch) {
356
+ return `L${genericMatch[1]};`;
357
+ }
358
+
359
+ // Default to object type
360
+ return `L${nonNullType.replace('.', '/')};`;
361
+ }
362
+
363
+ /**
364
+ * Generate Kotlin source code for the $override class
365
+ */
366
+ private generateOverrideSource(classInfo: ClassInfo): string {
367
+ const { packageName, className, methods } = classInfo;
368
+
369
+ const lines: string[] = [];
370
+
371
+ // Package declaration
372
+ if (packageName) {
373
+ lines.push(`package ${packageName}`);
374
+ lines.push('');
375
+ }
376
+
377
+ // Import IncrementalChange
378
+ lines.push('import com.jetstart.hotreload.IncrementalChange');
379
+ lines.push('');
380
+
381
+ // Generate override class
382
+ lines.push(`/**`);
383
+ lines.push(` * Generated override class for ${className}`);
384
+ lines.push(` * Implements IncrementalChange to provide hot-reloaded method implementations`);
385
+ lines.push(` * `);
386
+ lines.push(` * Pattern: args[0] is 'this' for instance methods, method parameters follow`);
387
+ lines.push(` */`);
388
+ lines.push(`class ${className}\$override : IncrementalChange {`);
389
+ lines.push('');
390
+
391
+ // Generate access$dispatch method
392
+ lines.push(' override fun access\$dispatch(methodSignature: String, vararg args: Any?): Any? {');
393
+ lines.push(' return when (methodSignature) {');
394
+
395
+ for (const method of methods) {
396
+ const { name, parameters, returnType, signature, isStatic } = method;
397
+
398
+ // Generate case for this method
399
+ lines.push(` "${signature}" -> {`);
400
+
401
+ // For instance methods, cast args[0] to the instance type
402
+ if (!isStatic) {
403
+ lines.push(` val instance = args[0] as ${className}`);
404
+ }
405
+
406
+ // Cast arguments and call the method
407
+ if (parameters.length === 0) {
408
+ if (returnType === 'Unit') {
409
+ if (isStatic) {
410
+ lines.push(` ${className}.${name}()`);
411
+ } else {
412
+ lines.push(` instance.${name}()`);
413
+ }
414
+ lines.push(' null');
415
+ } else {
416
+ if (isStatic) {
417
+ lines.push(` ${className}.${name}()`);
418
+ } else {
419
+ lines.push(` instance.${name}()`);
420
+ }
421
+ }
422
+ } else {
423
+ // Build argument list with casts
424
+ const argCasts = parameters.map((p, i) => {
425
+ // For instance methods: args[0] is 'this', method params start at args[1]
426
+ // For static methods: method params start at args[0]
427
+ const argIndex = isStatic ? i : i + 1;
428
+ return `args[${argIndex}] as ${p.type}`;
429
+ });
430
+
431
+ if (returnType === 'Unit') {
432
+ if (isStatic) {
433
+ lines.push(` ${className}.${name}(${argCasts.join(', ')})`);
434
+ } else {
435
+ lines.push(` instance.${name}(${argCasts.join(', ')})`);
436
+ }
437
+ lines.push(' null');
438
+ } else {
439
+ if (isStatic) {
440
+ lines.push(` ${className}.${name}(${argCasts.join(', ')})`);
441
+ } else {
442
+ lines.push(` instance.${name}(${argCasts.join(', ')})`);
443
+ }
444
+ }
445
+ }
446
+
447
+ lines.push(' }');
448
+ }
449
+
450
+ // Default case
451
+ lines.push(' else -> throw IllegalArgumentException("Unknown method: $methodSignature")');
452
+ lines.push(' }');
453
+ lines.push(' }');
454
+ lines.push('}');
455
+
456
+ return lines.join('\n');
457
+ }
458
+ }
459
+
460
+ interface ClassInfo {
461
+ packageName: string;
462
+ className: string;
463
+ fullClassName: string;
464
+ methods: MethodInfo[];
465
+ }
466
+
467
+ interface MethodInfo {
468
+ name: string;
469
+ parameters: ParameterInfo[];
470
+ returnType: string;
471
+ signature: string;
472
+ isStatic: boolean;
473
+ }
474
+
475
+ interface ParameterInfo {
476
+ name: string;
477
+ type: string;
478
+ }