@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
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Kotlin Compiler Service
3
+ * Compiles Kotlin files to .class files for hot reload
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ import { spawn } from 'child_process';
10
+ import { log, error as logError } from '../utils/logger';
11
+
12
+ export interface CompileResult {
13
+ success: boolean;
14
+ classFiles: string[];
15
+ errors: string[];
16
+ outputDir: string;
17
+ }
18
+
19
+ export class KotlinCompiler {
20
+ private static readonly TAG = 'KotlinCompiler';
21
+ private kotlincPath: string | null = null;
22
+ private composeCompilerPath: string | null = null;
23
+ private staticClasspath: string[] = []; // SDK + deps cached; project classes always fresh
24
+
25
+ constructor(private projectPath: string) {}
26
+
27
+ /**
28
+ * Find Compose compiler plugin JAR
29
+ * For Kotlin 2.0+, the Compose compiler is bundled with kotlinc
30
+ */
31
+ async findComposeCompiler(): Promise<string | null> {
32
+ if (this.composeCompilerPath) return this.composeCompilerPath;
33
+
34
+ // First check if kotlinc has a bundled Compose compiler (Kotlin 2.0+)
35
+ const kotlincPath = await this.findKotlinc();
36
+ if (kotlincPath) {
37
+ const kotlincDir = path.dirname(path.dirname(kotlincPath)); // Go up from bin/kotlinc
38
+ const bundledComposePlugin = path.join(kotlincDir, 'lib', 'compose-compiler-plugin.jar');
39
+
40
+ if (fs.existsSync(bundledComposePlugin)) {
41
+ this.composeCompilerPath = bundledComposePlugin;
42
+ log(`Found bundled Compose compiler (Kotlin 2.0+)`);
43
+ return this.composeCompilerPath;
44
+ }
45
+ }
46
+
47
+ // Fallback to Gradle cache for older Kotlin versions
48
+ const gradleCache = path.join(os.homedir(), '.gradle', 'caches', 'modules-2', 'files-2.1');
49
+ const composeCompilerDir = path.join(gradleCache, 'androidx.compose.compiler', 'compiler');
50
+
51
+ if (!fs.existsSync(composeCompilerDir)) {
52
+ log('Compose compiler not found');
53
+ return null;
54
+ }
55
+
56
+ // Find latest version
57
+ const versions = fs.readdirSync(composeCompilerDir)
58
+ .filter(v => fs.statSync(path.join(composeCompilerDir, v)).isDirectory())
59
+ .sort().reverse();
60
+
61
+ for (const version of versions) {
62
+ const versionDir = path.join(composeCompilerDir, version);
63
+ const hashes = fs.readdirSync(versionDir);
64
+
65
+ for (const hash of hashes) {
66
+ const hashDir = path.join(versionDir, hash);
67
+ if (!fs.statSync(hashDir).isDirectory()) continue;
68
+
69
+ const files = fs.readdirSync(hashDir);
70
+ for (const file of files) {
71
+ if (file.endsWith('.jar') && !file.endsWith('-sources.jar')) {
72
+ this.composeCompilerPath = path.join(hashDir, file);
73
+ log(`Found Compose compiler: ${version}`);
74
+ return this.composeCompilerPath;
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ return null;
81
+ }
82
+
83
+ /**
84
+ * Find kotlinc executable
85
+ */
86
+ async findKotlinc(): Promise<string | null> {
87
+ if (this.kotlincPath) return this.kotlincPath;
88
+
89
+ // Check common locations
90
+ const locations = [
91
+ // From environment variable
92
+ process.env.KOTLIN_HOME ? path.join(process.env.KOTLIN_HOME, 'bin', 'kotlinc') : null,
93
+ // From Android Studio
94
+ process.env.ANDROID_STUDIO_HOME ? path.join(process.env.ANDROID_STUDIO_HOME, 'plugins', 'Kotlin', 'kotlinc', 'bin', 'kotlinc') : null,
95
+ // System-wide installation (Windows)
96
+ 'C:\\Program Files\\kotlinc\\bin\\kotlinc.bat',
97
+ 'C:\\kotlinc\\bin\\kotlinc.bat',
98
+ // System-wide installation (Unix)
99
+ '/usr/local/bin/kotlinc',
100
+ '/usr/bin/kotlinc',
101
+ // Homebrew (macOS)
102
+ '/opt/homebrew/bin/kotlinc',
103
+ ].filter(Boolean) as string[];
104
+
105
+ for (const loc of locations) {
106
+ const execPath = os.platform() === 'win32' && !loc.endsWith('.bat') ? `${loc}.bat` : loc;
107
+ if (fs.existsSync(execPath)) {
108
+ this.kotlincPath = execPath;
109
+ log(`Found kotlinc at: ${execPath}`);
110
+ return execPath;
111
+ }
112
+ }
113
+
114
+ // Try to find via 'where' (Windows) or 'which' (Unix)
115
+ try {
116
+ const cmd = os.platform() === 'win32' ? 'where' : 'which';
117
+ const result = await this.runCommand(cmd, ['kotlinc']);
118
+ if (result.success && result.stdout.trim()) {
119
+ this.kotlincPath = result.stdout.trim().split('\n')[0];
120
+ log(`Found kotlinc via ${cmd}: ${this.kotlincPath}`);
121
+ return this.kotlincPath;
122
+ }
123
+ } catch (e) {
124
+ // Ignore
125
+ }
126
+
127
+ logError('kotlinc not found. Please install Kotlin or set KOTLIN_HOME');
128
+ return null;
129
+ }
130
+
131
+ /**
132
+ * Build the classpath for compilation
133
+ * This needs to include Android SDK, Compose, and project dependencies
134
+ */
135
+ async buildClasspath(): Promise<string[]> {
136
+ if (this.staticClasspath.length > 0) {
137
+ return [...this.staticClasspath, ...this.getProjectClasspathEntries()];
138
+ }
139
+
140
+ const classpath: string[] = [];
141
+ // Check multiple locations for Android SDK
142
+ let androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
143
+
144
+ // Fallback to common Windows locations
145
+ if (!androidHome) {
146
+ const commonLocations = [
147
+ 'C:\\Android',
148
+ path.join(os.homedir(), 'AppData', 'Local', 'Android', 'Sdk'),
149
+ 'C:\\Users\\Public\\Android\\Sdk',
150
+ ];
151
+ for (const loc of commonLocations) {
152
+ if (fs.existsSync(path.join(loc, 'platforms'))) {
153
+ androidHome = loc;
154
+ log(`Found Android SDK at: ${loc}`);
155
+ break;
156
+ }
157
+ }
158
+ }
159
+
160
+ if (!androidHome) {
161
+ logError('ANDROID_HOME or ANDROID_SDK_ROOT not set');
162
+ return classpath;
163
+ }
164
+
165
+ // Find android.jar
166
+ const platformsDir = path.join(androidHome, 'platforms');
167
+ if (fs.existsSync(platformsDir)) {
168
+ const platforms = fs.readdirSync(platformsDir)
169
+ .filter(d => d.startsWith('android-'))
170
+ .sort((a, b) => {
171
+ const aNum = parseInt(a.replace('android-', ''));
172
+ const bNum = parseInt(b.replace('android-', ''));
173
+ return bNum - aNum;
174
+ });
175
+
176
+ if (platforms.length > 0) {
177
+ const androidJar = path.join(platformsDir, platforms[0], 'android.jar');
178
+ if (fs.existsSync(androidJar)) {
179
+ classpath.push(androidJar);
180
+ log(`Using Android SDK: ${platforms[0]}`);
181
+ }
182
+ }
183
+ }
184
+
185
+ // Add ALL Gradle cached dependencies (Compose, AndroidX, Kotlin, etc.)
186
+ const gradleCache = path.join(os.homedir(), '.gradle', 'caches', 'modules-2', 'files-2.1');
187
+ if (fs.existsSync(gradleCache)) {
188
+ // Scan for all required dependency groups
189
+ const requiredGroups = [
190
+ 'androidx.compose.runtime',
191
+ 'androidx.compose.ui',
192
+ 'androidx.compose.foundation',
193
+ 'androidx.compose.material3',
194
+ 'androidx.compose.material',
195
+ 'androidx.compose.animation',
196
+ 'androidx.annotation',
197
+ 'androidx.core',
198
+ 'androidx.activity',
199
+ 'androidx.lifecycle',
200
+ 'androidx.savedstate',
201
+ 'androidx.collection',
202
+ 'org.jetbrains.kotlin',
203
+ 'org.jetbrains.kotlinx',
204
+ 'org.jetbrains.annotations',
205
+ ];
206
+
207
+ for (const group of requiredGroups) {
208
+ const groupDir = path.join(gradleCache, group);
209
+ if (fs.existsSync(groupDir)) {
210
+ // Get all artifacts in this group
211
+ const artifacts = fs.readdirSync(groupDir);
212
+ for (const artifact of artifacts) {
213
+ const artifactDir = path.join(groupDir, artifact);
214
+ if (!fs.statSync(artifactDir).isDirectory()) continue;
215
+
216
+ // Find latest version
217
+ const versions = fs.readdirSync(artifactDir)
218
+ .filter(v => fs.statSync(path.join(artifactDir, v)).isDirectory())
219
+ .sort().reverse();
220
+
221
+ if (versions.length > 0) {
222
+ const versionDir = path.join(artifactDir, versions[0]);
223
+ const hashes = fs.readdirSync(versionDir);
224
+ for (const hash of hashes) {
225
+ const hashDir = path.join(versionDir, hash);
226
+ if (!fs.statSync(hashDir).isDirectory()) continue;
227
+
228
+ const files = fs.readdirSync(hashDir);
229
+ // Add all JARs (not sources or javadoc)
230
+ for (const file of files) {
231
+ if (file.endsWith('.jar') &&
232
+ !file.endsWith('-sources.jar') &&
233
+ !file.endsWith('-javadoc.jar')) {
234
+ classpath.push(path.join(hashDir, file));
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ // Scan transforms-3 cache - grab ALL classes.jar (Compose, Material3, Room, DivKit, etc.)
245
+ const transformsCache = path.join(os.homedir(), '.gradle', 'caches', 'transforms-3');
246
+ if (fs.existsSync(transformsCache)) {
247
+ try {
248
+ for (const hash of fs.readdirSync(transformsCache)) {
249
+ const transformedDir = path.join(transformsCache, hash, 'transformed');
250
+ if (!fs.existsSync(transformedDir)) continue;
251
+ try {
252
+ for (const pkg of fs.readdirSync(transformedDir)) {
253
+ const classesJar = path.join(transformedDir, pkg, 'jars', 'classes.jar');
254
+ if (fs.existsSync(classesJar) && !classpath.includes(classesJar)) {
255
+ classpath.push(classesJar);
256
+ }
257
+ }
258
+ } catch (e) { /* ignore */ }
259
+ }
260
+ log(`Added ${classpath.length} transforms-3 JARs to classpath`);
261
+ } catch (e) { /* ignore */ }
262
+ }
263
+
264
+ // Cache static entries; project build outputs always fetched fresh
265
+ this.staticClasspath = [...classpath];
266
+ const projectEntries = this.getProjectClasspathEntries();
267
+ log(`Built static classpath with ${classpath.length} entries + ${projectEntries.length} project entries`);
268
+ return [...classpath, ...projectEntries];
269
+ }
270
+
271
+ /**
272
+ * Get project build output classpath entries.
273
+ * Always called fresh ΓÇö never cached ΓÇö so new class files from Gradle builds are always visible.
274
+ */
275
+ private getProjectClasspathEntries(): string[] {
276
+ const entries: string[] = [];
277
+ const candidates = [
278
+ path.join(this.projectPath, 'app', 'build', 'tmp', 'kotlin-classes', 'debug'),
279
+ path.join(this.projectPath, 'app', 'build', 'intermediates', 'javac', 'debug', 'classes'),
280
+ path.join(this.projectPath, 'app', 'build', 'intermediates', 'compile_and_runtime_not_namespaced_r_class_jar', 'debug', 'R.jar'),
281
+ path.join(this.projectPath, 'app', 'build', 'intermediates', 'classes', 'debug', 'transformDebugClassesWithAsm', 'jars', '0.jar'),
282
+ ];
283
+ for (const c of candidates) {
284
+ if (fs.existsSync(c)) entries.push(c);
285
+ }
286
+ return entries;
287
+ }
288
+
289
+ /**
290
+ * Recursively find JAR files in a directory up to maxDepth
291
+ */
292
+ private findJarsRecursive(dir: string, classpath: string[], maxDepth: number, currentDepth = 0): void {
293
+ if (currentDepth > maxDepth || !fs.existsSync(dir)) return;
294
+
295
+ try {
296
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
297
+ for (const entry of entries) {
298
+ const fullPath = path.join(dir, entry.name);
299
+ if (entry.isDirectory()) {
300
+ this.findJarsRecursive(fullPath, classpath, maxDepth, currentDepth + 1);
301
+ } else if (entry.name.endsWith('.jar') && !classpath.includes(fullPath)) {
302
+ classpath.push(fullPath);
303
+ }
304
+ }
305
+ } catch (e) {
306
+ // Ignore permission errors
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Compile a single Kotlin file
312
+ */
313
+ async compileFile(filePath: string): Promise<CompileResult> {
314
+ const kotlinc = await this.findKotlinc();
315
+ if (!kotlinc) {
316
+ return {
317
+ success: false,
318
+ classFiles: [],
319
+ errors: ['kotlinc not found'],
320
+ outputDir: ''
321
+ };
322
+ }
323
+
324
+ const classpath = await this.buildClasspath();
325
+ if (classpath.length === 0) {
326
+ return {
327
+ success: false,
328
+ classFiles: [],
329
+ errors: ['Failed to build classpath - Android SDK not found'],
330
+ outputDir: ''
331
+ };
332
+ }
333
+
334
+ // Create temp output directory
335
+ const outputDir = path.join(os.tmpdir(), 'jetstart-compile', Date.now().toString());
336
+ fs.mkdirSync(outputDir, { recursive: true });
337
+
338
+ log(`Compiling ${path.basename(filePath)}...`);
339
+
340
+ // Find Compose compiler plugin for @Composable support
341
+ const composeCompiler = await this.findComposeCompiler();
342
+
343
+ // On Windows, command line can be too long with many classpath entries
344
+ // Use an argument file (@argfile) to avoid this limitation
345
+ const classpathStr = classpath.join(os.platform() === 'win32' ? ';' : ':');
346
+ const argLines = [
347
+ `-d`,
348
+ outputDir,
349
+ `-classpath`,
350
+ classpathStr,
351
+ `-jvm-target`,
352
+ `17`,
353
+ `-Xskip-prerelease-check`,
354
+ `-Xno-call-assertions`,
355
+ `-Xno-param-assertions`,
356
+ ];
357
+
358
+ // Add Compose compiler plugin if found
359
+ if (composeCompiler) {
360
+ argLines.push(`-Xplugin=${composeCompiler}`);
361
+ log(`Using Compose compiler plugin`);
362
+ }
363
+
364
+ argLines.push(filePath);
365
+ const argFileContent = argLines.join('\n');
366
+
367
+ const argFilePath = path.join(outputDir, 'kotlinc-args.txt');
368
+ fs.writeFileSync(argFilePath, argFileContent);
369
+
370
+ log(`Using argument file: ${argFilePath}`);
371
+
372
+ // Build kotlinc arguments using @argfile
373
+ const args = [`@${argFilePath}`];
374
+
375
+ const result = await this.runCommand(kotlinc, args);
376
+
377
+ if (!result.success) {
378
+ return {
379
+ success: false,
380
+ classFiles: [],
381
+ errors: [result.stderr || 'Compilation failed'],
382
+ outputDir
383
+ };
384
+ }
385
+
386
+ // Find generated class files
387
+ const classFiles = this.findClassFiles(outputDir);
388
+
389
+ log(`Compiled ${classFiles.length} class files`);
390
+
391
+ return {
392
+ success: true,
393
+ classFiles,
394
+ errors: [],
395
+ outputDir
396
+ };
397
+ }
398
+
399
+ /**
400
+ * Find all .class files in a directory
401
+ */
402
+ private findClassFiles(dir: string): string[] {
403
+ const files: string[] = [];
404
+
405
+ const walk = (d: string) => {
406
+ if (!fs.existsSync(d)) return;
407
+ const entries = fs.readdirSync(d, { withFileTypes: true });
408
+ for (const entry of entries) {
409
+ const fullPath = path.join(d, entry.name);
410
+ if (entry.isDirectory()) {
411
+ walk(fullPath);
412
+ } else if (entry.name.endsWith('.class')) {
413
+ files.push(fullPath);
414
+ }
415
+ }
416
+ };
417
+
418
+ walk(dir);
419
+ return files;
420
+ }
421
+
422
+ /**
423
+ * Run a command and return result
424
+ */
425
+ private runCommand(cmd: string, args: string[]): Promise<{ success: boolean; stdout: string; stderr: string }> {
426
+ return new Promise((resolve) => {
427
+ const proc = spawn(cmd, args, {
428
+ shell: os.platform() === 'win32',
429
+ env: process.env
430
+ });
431
+
432
+ let stdout = '';
433
+ let stderr = '';
434
+
435
+ proc.stdout?.on('data', (data) => {
436
+ stdout += data.toString();
437
+ });
438
+
439
+ proc.stderr?.on('data', (data) => {
440
+ stderr += data.toString();
441
+ });
442
+
443
+ proc.on('close', (code) => {
444
+ resolve({
445
+ success: code === 0,
446
+ stdout,
447
+ stderr
448
+ });
449
+ });
450
+
451
+ proc.on('error', (err) => {
452
+ resolve({
453
+ success: false,
454
+ stdout: '',
455
+ stderr: err.message
456
+ });
457
+ });
458
+ });
459
+ }
460
+ }