@jetstart/core 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +6 -0
- package/README.md +124 -0
- package/dist/build/builder.d.ts +57 -0
- package/dist/build/builder.d.ts.map +1 -0
- package/dist/build/builder.js +151 -0
- package/dist/build/builder.js.map +1 -0
- package/dist/build/cache.d.ts +51 -0
- package/dist/build/cache.d.ts.map +1 -0
- package/dist/build/cache.js +152 -0
- package/dist/build/cache.js.map +1 -0
- package/dist/build/dsl-parser.d.ts +54 -0
- package/dist/build/dsl-parser.d.ts.map +1 -0
- package/dist/build/dsl-parser.js +373 -0
- package/dist/build/dsl-parser.js.map +1 -0
- package/dist/build/dsl-types.d.ts +47 -0
- package/dist/build/dsl-types.d.ts.map +1 -0
- package/dist/build/dsl-types.js +7 -0
- package/dist/build/dsl-types.js.map +1 -0
- package/dist/build/gradle-injector.d.ts +14 -0
- package/dist/build/gradle-injector.d.ts.map +1 -0
- package/dist/build/gradle-injector.js +77 -0
- package/dist/build/gradle-injector.js.map +1 -0
- package/dist/build/gradle.d.ts +43 -0
- package/dist/build/gradle.d.ts.map +1 -0
- package/dist/build/gradle.js +281 -0
- package/dist/build/gradle.js.map +1 -0
- package/dist/build/index.d.ts +10 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +26 -0
- package/dist/build/index.js.map +1 -0
- package/dist/build/parser.d.ts +12 -0
- package/dist/build/parser.d.ts.map +1 -0
- package/dist/build/parser.js +71 -0
- package/dist/build/parser.js.map +1 -0
- package/dist/build/watcher.d.ts +30 -0
- package/dist/build/watcher.d.ts.map +1 -0
- package/dist/build/watcher.js +120 -0
- package/dist/build/watcher.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/server/http.d.ts +12 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +32 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/index.d.ts +35 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +262 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/middleware.d.ts +7 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +42 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/routes.d.ts +7 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/routes.js +104 -0
- package/dist/server/routes.js.map +1 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +23 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/qr.d.ts +8 -0
- package/dist/utils/qr.d.ts.map +1 -0
- package/dist/utils/qr.js +48 -0
- package/dist/utils/qr.js.map +1 -0
- package/dist/utils/session.d.ts +18 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +49 -0
- package/dist/utils/session.js.map +1 -0
- package/dist/websocket/handler.d.ts +25 -0
- package/dist/websocket/handler.d.ts.map +1 -0
- package/dist/websocket/handler.js +126 -0
- package/dist/websocket/handler.js.map +1 -0
- package/dist/websocket/index.d.ts +18 -0
- package/dist/websocket/index.d.ts.map +1 -0
- package/dist/websocket/index.js +40 -0
- package/dist/websocket/index.js.map +1 -0
- package/dist/websocket/manager.d.ts +16 -0
- package/dist/websocket/manager.d.ts.map +1 -0
- package/dist/websocket/manager.js +58 -0
- package/dist/websocket/manager.js.map +1 -0
- package/package.json +78 -0
- package/src/build/builder.ts +192 -0
- package/src/build/cache.ts +144 -0
- package/src/build/dsl-parser.ts +382 -0
- package/src/build/dsl-types.ts +50 -0
- package/src/build/gradle-injector.ts +64 -0
- package/src/build/gradle.ts +305 -0
- package/src/build/index.ts +10 -0
- package/src/build/parser.ts +75 -0
- package/src/build/watcher.ts +103 -0
- package/src/index.ts +20 -0
- package/src/server/http.ts +38 -0
- package/src/server/index.ts +272 -0
- package/src/server/middleware.ts +43 -0
- package/src/server/routes.ts +116 -0
- package/src/types/index.ts +21 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/logger.ts +28 -0
- package/src/utils/qr.ts +46 -0
- package/src/utils/session.ts +58 -0
- package/src/websocket/handler.ts +150 -0
- package/src/websocket/index.ts +56 -0
- package/src/websocket/manager.ts +63 -0
- package/tests/build.test.ts +13 -0
- package/tests/server.test.ts +13 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build Service
|
|
3
|
+
* Main orchestrator for build operations, caching, and file watching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BuildConfig, BuildResult, BuildPhase, BuildStatus } from '@jetstart/shared';
|
|
7
|
+
import { GradleExecutor, GradleExecutorOptions } from './gradle';
|
|
8
|
+
import { BuildCache, BuildCacheOptions } from './cache';
|
|
9
|
+
import { FileWatcher } from './watcher';
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
|
|
12
|
+
export interface BuildServiceOptions {
|
|
13
|
+
cacheEnabled?: boolean;
|
|
14
|
+
cachePath?: string;
|
|
15
|
+
watchEnabled?: boolean;
|
|
16
|
+
javaHome?: string;
|
|
17
|
+
androidHome?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BuildServiceEvents {
|
|
21
|
+
'build:start': () => void;
|
|
22
|
+
'build:progress': (status: BuildStatus) => void;
|
|
23
|
+
'build:complete': (result: BuildResult) => void;
|
|
24
|
+
'build:error': (error: string, details?: any) => void;
|
|
25
|
+
'watch:change': (files: string[]) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export declare interface BuildService {
|
|
29
|
+
on<K extends keyof BuildServiceEvents>(
|
|
30
|
+
event: K,
|
|
31
|
+
listener: BuildServiceEvents[K]
|
|
32
|
+
): this;
|
|
33
|
+
emit<K extends keyof BuildServiceEvents>(
|
|
34
|
+
event: K,
|
|
35
|
+
...args: Parameters<BuildServiceEvents[K]>
|
|
36
|
+
): boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class BuildService extends EventEmitter {
|
|
40
|
+
private gradle: GradleExecutor;
|
|
41
|
+
private cache: BuildCache;
|
|
42
|
+
private watcher: FileWatcher | null = null;
|
|
43
|
+
private watchEnabled: boolean;
|
|
44
|
+
private isBuilding: boolean = false;
|
|
45
|
+
|
|
46
|
+
constructor(options: BuildServiceOptions = {}) {
|
|
47
|
+
super();
|
|
48
|
+
|
|
49
|
+
// Initialize Gradle executor
|
|
50
|
+
const gradleOptions: GradleExecutorOptions = {
|
|
51
|
+
javaHome: options.javaHome,
|
|
52
|
+
androidHome: options.androidHome,
|
|
53
|
+
};
|
|
54
|
+
this.gradle = new GradleExecutor(gradleOptions);
|
|
55
|
+
|
|
56
|
+
// Initialize cache
|
|
57
|
+
const cacheOptions: BuildCacheOptions = {
|
|
58
|
+
enabled: options.cacheEnabled ?? true,
|
|
59
|
+
cachePath: options.cachePath || require('os').tmpdir() + '/jetstart-cache',
|
|
60
|
+
};
|
|
61
|
+
this.cache = new BuildCache(cacheOptions);
|
|
62
|
+
|
|
63
|
+
this.watchEnabled = options.watchEnabled ?? true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build project
|
|
68
|
+
*/
|
|
69
|
+
async build(config: BuildConfig): Promise<BuildResult> {
|
|
70
|
+
if (this.isBuilding) {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
buildTime: 0,
|
|
74
|
+
errors: [{
|
|
75
|
+
file: '',
|
|
76
|
+
line: 0,
|
|
77
|
+
column: 0,
|
|
78
|
+
message: 'Build already in progress',
|
|
79
|
+
severity: 'error' as any,
|
|
80
|
+
}],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.isBuilding = true;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// Check cache
|
|
88
|
+
const cached = this.cache.get(config);
|
|
89
|
+
if (cached) {
|
|
90
|
+
this.emit('build:complete', cached.result);
|
|
91
|
+
return cached.result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Emit build start
|
|
95
|
+
this.emit('build:start');
|
|
96
|
+
this.emitProgress(BuildPhase.INITIALIZING, 0, 'Initializing build...');
|
|
97
|
+
|
|
98
|
+
// Execute Gradle build
|
|
99
|
+
this.emitProgress(BuildPhase.COMPILING, 20, 'Compiling Kotlin sources...');
|
|
100
|
+
const result = await this.gradle.execute(config);
|
|
101
|
+
|
|
102
|
+
// Cache successful builds
|
|
103
|
+
if (result.success) {
|
|
104
|
+
this.cache.set(config, result);
|
|
105
|
+
this.emitProgress(BuildPhase.COMPLETE, 100, 'Build complete');
|
|
106
|
+
this.emit('build:complete', result);
|
|
107
|
+
} else {
|
|
108
|
+
this.emitProgress(BuildPhase.FAILED, 0, 'Build failed');
|
|
109
|
+
this.emit('build:error', 'Build failed', result.errors);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
} catch (err: any) {
|
|
114
|
+
const errorResult: BuildResult = {
|
|
115
|
+
success: false,
|
|
116
|
+
buildTime: 0,
|
|
117
|
+
errors: [{
|
|
118
|
+
file: '',
|
|
119
|
+
line: 0,
|
|
120
|
+
column: 0,
|
|
121
|
+
message: err.message || 'Unknown build error',
|
|
122
|
+
severity: 'error' as any,
|
|
123
|
+
}],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
this.emit('build:error', err.message, err);
|
|
127
|
+
return errorResult;
|
|
128
|
+
} finally {
|
|
129
|
+
this.isBuilding = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Start watching for file changes
|
|
135
|
+
*/
|
|
136
|
+
startWatching(projectPath: string, callback: (files: string[]) => void): void {
|
|
137
|
+
if (!this.watchEnabled) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (this.watcher) {
|
|
142
|
+
this.stopWatching();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.watcher = new FileWatcher({
|
|
146
|
+
projectPath,
|
|
147
|
+
callback: (files: string[]) => {
|
|
148
|
+
this.emit('watch:change', files);
|
|
149
|
+
callback(files);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.watcher.watch(projectPath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Stop watching for file changes
|
|
158
|
+
*/
|
|
159
|
+
stopWatching(): void {
|
|
160
|
+
if (this.watcher) {
|
|
161
|
+
this.watcher.stop();
|
|
162
|
+
this.watcher = null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Clear build cache
|
|
168
|
+
*/
|
|
169
|
+
clearCache(): void {
|
|
170
|
+
this.cache.clear();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if currently building
|
|
175
|
+
*/
|
|
176
|
+
isBuildInProgress(): boolean {
|
|
177
|
+
return this.isBuilding;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Emit build progress
|
|
182
|
+
*/
|
|
183
|
+
private emitProgress(phase: BuildPhase, progress: number, message: string): void {
|
|
184
|
+
const status: BuildStatus = {
|
|
185
|
+
phase,
|
|
186
|
+
progress,
|
|
187
|
+
message,
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
};
|
|
190
|
+
this.emit('build:progress', status);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build Cache
|
|
3
|
+
* Simple file-based caching for successful builds
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as crypto from 'crypto';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { BuildConfig, BuildResult } from '@jetstart/shared';
|
|
10
|
+
|
|
11
|
+
export interface CachedBuild {
|
|
12
|
+
config: BuildConfig;
|
|
13
|
+
result: BuildResult;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BuildCacheOptions {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
cachePath: string;
|
|
20
|
+
maxAge?: number; // milliseconds
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class BuildCache {
|
|
24
|
+
private enabled: boolean;
|
|
25
|
+
private cachePath: string;
|
|
26
|
+
private maxAge: number;
|
|
27
|
+
private cache: Map<string, CachedBuild> = new Map();
|
|
28
|
+
|
|
29
|
+
constructor(options: BuildCacheOptions) {
|
|
30
|
+
this.enabled = options.enabled;
|
|
31
|
+
this.cachePath = options.cachePath;
|
|
32
|
+
this.maxAge = options.maxAge || 24 * 60 * 60 * 1000; // 24 hours default
|
|
33
|
+
|
|
34
|
+
if (this.enabled) {
|
|
35
|
+
this.ensureCacheDir();
|
|
36
|
+
this.loadCache();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get cached build by config
|
|
42
|
+
*/
|
|
43
|
+
get(config: BuildConfig): CachedBuild | null {
|
|
44
|
+
if (!this.enabled) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hash = this.hashConfig(config);
|
|
49
|
+
const cached = this.cache.get(hash);
|
|
50
|
+
|
|
51
|
+
if (!cached) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if cache is stale
|
|
56
|
+
if (Date.now() - cached.timestamp > this.maxAge) {
|
|
57
|
+
this.cache.delete(hash);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Validate APK still exists
|
|
62
|
+
if (cached.result.apkPath && !fs.existsSync(cached.result.apkPath)) {
|
|
63
|
+
this.cache.delete(hash);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return cached;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Set cached build
|
|
72
|
+
*/
|
|
73
|
+
set(config: BuildConfig, result: BuildResult): void {
|
|
74
|
+
if (!this.enabled || !result.success) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const hash = this.hashConfig(config);
|
|
79
|
+
const cached: CachedBuild = {
|
|
80
|
+
config,
|
|
81
|
+
result,
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
this.cache.set(hash, cached);
|
|
86
|
+
this.saveCache();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Clear entire cache
|
|
91
|
+
*/
|
|
92
|
+
clear(): void {
|
|
93
|
+
this.cache.clear();
|
|
94
|
+
this.saveCache();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Hash build config for cache key
|
|
99
|
+
*/
|
|
100
|
+
private hashConfig(config: BuildConfig): string {
|
|
101
|
+
const data = JSON.stringify({
|
|
102
|
+
projectPath: config.projectPath,
|
|
103
|
+
buildType: config.buildType,
|
|
104
|
+
minifyEnabled: config.minifyEnabled,
|
|
105
|
+
debuggable: config.debuggable,
|
|
106
|
+
applicationId: config.applicationId,
|
|
107
|
+
});
|
|
108
|
+
return crypto.createHash('md5').update(data).digest('hex');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Ensure cache directory exists
|
|
113
|
+
*/
|
|
114
|
+
private ensureCacheDir(): void {
|
|
115
|
+
if (!fs.existsSync(this.cachePath)) {
|
|
116
|
+
fs.mkdirSync(this.cachePath, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Load cache from disk
|
|
122
|
+
*/
|
|
123
|
+
private loadCache(): void {
|
|
124
|
+
const cacheFile = path.join(this.cachePath, 'build-cache.json');
|
|
125
|
+
if (fs.existsSync(cacheFile)) {
|
|
126
|
+
try {
|
|
127
|
+
const data = fs.readFileSync(cacheFile, 'utf-8');
|
|
128
|
+
const cacheData = JSON.parse(data);
|
|
129
|
+
this.cache = new Map(Object.entries(cacheData));
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// Ignore corrupted cache
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Save cache to disk
|
|
138
|
+
*/
|
|
139
|
+
private saveCache(): void {
|
|
140
|
+
const cacheFile = path.join(this.cachePath, 'build-cache.json');
|
|
141
|
+
const cacheData = Object.fromEntries(this.cache);
|
|
142
|
+
fs.writeFileSync(cacheFile, JSON.stringify(cacheData, null, 2));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { UIDefinition, DSLElement, DSLModifier, ParseResult } from './dsl-types';
|
|
4
|
+
import { log } from '../utils/logger';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DSL Parser
|
|
8
|
+
* Converts Kotlin Compose code to JSON DSL for runtime interpretation
|
|
9
|
+
*/
|
|
10
|
+
export class DSLParser {
|
|
11
|
+
private static readonly TAG = 'DSLParser';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a Kotlin file and extract UI definition
|
|
15
|
+
*/
|
|
16
|
+
static parseFile(filePath: string): ParseResult {
|
|
17
|
+
try {
|
|
18
|
+
if (!fs.existsSync(filePath)) {
|
|
19
|
+
return {
|
|
20
|
+
success: false,
|
|
21
|
+
errors: [`File not found: ${filePath}`]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
26
|
+
return this.parseContent(content, filePath);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
errors: [`Failed to read file: ${error}`]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse Kotlin content and extract UI definition
|
|
37
|
+
*/
|
|
38
|
+
static parseContent(content: string, filePath: string): ParseResult {
|
|
39
|
+
try {
|
|
40
|
+
log(`Parsing Kotlin file: ${path.basename(filePath)}`);
|
|
41
|
+
|
|
42
|
+
// FIRST: Check if there's a getDefaultDSL() function with JSON
|
|
43
|
+
const dslFromFunction = this.extractDSLFromFunction(content);
|
|
44
|
+
if (dslFromFunction) {
|
|
45
|
+
log(`Extracted DSL from getDefaultDSL(): ${dslFromFunction.length} bytes`);
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
dsl: JSON.parse(dslFromFunction)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// FALLBACK: Try to find @Composable functions with Compose code
|
|
53
|
+
const composableMatch = this.findMainComposable(content);
|
|
54
|
+
|
|
55
|
+
if (!composableMatch) {
|
|
56
|
+
log('No main composable found, generating default DSL');
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
dsl: this.generateDefaultDSL()
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parse the composable body
|
|
64
|
+
const element = this.parseComposableBody(composableMatch.body);
|
|
65
|
+
|
|
66
|
+
const dsl: UIDefinition = {
|
|
67
|
+
version: '1.0',
|
|
68
|
+
screen: element
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
log(`Successfully parsed DSL: ${JSON.stringify(dsl).length} bytes`);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
dsl
|
|
76
|
+
};
|
|
77
|
+
} catch (error) {
|
|
78
|
+
log(`Parse error: ${error}`);
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
errors: [`Parse error: ${error}`]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract DSL JSON from getDefaultDSL() or similar function (legacy support)
|
|
88
|
+
*/
|
|
89
|
+
private static extractDSLFromFunction(content: string): string | null {
|
|
90
|
+
// Look for functions that return JSON strings (legacy approach)
|
|
91
|
+
const functionRegex = /fun\s+getDefaultDSL\s*\(\s*\)\s*:\s*String\s*\{\s*return\s*"""([\s\S]*?)"""/;
|
|
92
|
+
const match = content.match(functionRegex);
|
|
93
|
+
|
|
94
|
+
if (match && match[1]) {
|
|
95
|
+
let jsonString = match[1].trim();
|
|
96
|
+
jsonString = jsonString.replace(/\.trimIndent\(\)/, '');
|
|
97
|
+
return jsonString;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Find the main @Composable function in the file
|
|
105
|
+
*/
|
|
106
|
+
private static findMainComposable(content: string): { name: string; body: string } | null {
|
|
107
|
+
// Look for @Composable functions (AppContent, MainScreen, etc.)
|
|
108
|
+
const composableRegex = /@Composable\s+fun\s+(\w+)\s*\([^)]*\)\s*\{/g;
|
|
109
|
+
const matches = [...content.matchAll(composableRegex)];
|
|
110
|
+
|
|
111
|
+
log(`Found ${matches.length} @Composable functions`);
|
|
112
|
+
|
|
113
|
+
if (matches.length === 0) {
|
|
114
|
+
log('No @Composable functions found in file');
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Use the first composable function (should be AppContent, not LoadingScreen)
|
|
119
|
+
const match = matches[0];
|
|
120
|
+
const functionName = match[1];
|
|
121
|
+
log(`Parsing composable function: ${functionName}`);
|
|
122
|
+
|
|
123
|
+
const startIndex = match.index! + match[0].length;
|
|
124
|
+
|
|
125
|
+
// Extract the function body (handle nested braces)
|
|
126
|
+
const body = this.extractFunctionBody(content, startIndex);
|
|
127
|
+
log(`Extracted function body: ${body.substring(0, 100)}...`);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
name: functionName,
|
|
131
|
+
body
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Extract function body handling nested braces
|
|
137
|
+
*/
|
|
138
|
+
private static extractFunctionBody(content: string, startIndex: number): string {
|
|
139
|
+
let braceCount = 1;
|
|
140
|
+
let endIndex = startIndex;
|
|
141
|
+
|
|
142
|
+
while (braceCount > 0 && endIndex < content.length) {
|
|
143
|
+
if (content[endIndex] === '{') braceCount++;
|
|
144
|
+
if (content[endIndex] === '}') braceCount--;
|
|
145
|
+
endIndex++;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return content.substring(startIndex, endIndex - 1).trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse the composable body and extract UI structure
|
|
153
|
+
*/
|
|
154
|
+
private static parseComposableBody(body: string): DSLElement {
|
|
155
|
+
// Try to find the root element (Column, Row, Box, etc.)
|
|
156
|
+
const layoutMatch = body.match(/(Column|Row|Box)\s*\(/);
|
|
157
|
+
|
|
158
|
+
if (!layoutMatch) {
|
|
159
|
+
// Fallback: Simple text content
|
|
160
|
+
const textMatch = body.match(/Text\s*\(\s*text\s*=\s*"([^"]+)"/);
|
|
161
|
+
if (textMatch) {
|
|
162
|
+
return {
|
|
163
|
+
type: 'Text',
|
|
164
|
+
text: textMatch[1]
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Default fallback
|
|
169
|
+
return {
|
|
170
|
+
type: 'Column',
|
|
171
|
+
modifier: { fillMaxSize: true, padding: 16 },
|
|
172
|
+
horizontalAlignment: 'CenterHorizontally',
|
|
173
|
+
verticalArrangement: 'Center',
|
|
174
|
+
children: [
|
|
175
|
+
{
|
|
176
|
+
type: 'Text',
|
|
177
|
+
text: 'Hot Reload Active',
|
|
178
|
+
style: 'headlineMedium'
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const layoutType = layoutMatch[1];
|
|
185
|
+
const layoutStartIndex = layoutMatch.index! + layoutMatch[0].length;
|
|
186
|
+
|
|
187
|
+
// Extract FULL layout declaration (parameters + body with children)
|
|
188
|
+
// We need to extract from after "Column(" to the end, including ) { ... }
|
|
189
|
+
const layoutFullContent = body.substring(layoutStartIndex);
|
|
190
|
+
|
|
191
|
+
return this.parseLayout(layoutType, layoutFullContent);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Parse a layout element (Column, Row, Box)
|
|
196
|
+
*/
|
|
197
|
+
private static parseLayout(type: string, content: string): DSLElement {
|
|
198
|
+
const element: DSLElement = { type };
|
|
199
|
+
|
|
200
|
+
// Parse modifier
|
|
201
|
+
const modifierMatch = content.match(/modifier\s*=\s*Modifier([^,\n}]+)/);
|
|
202
|
+
if (modifierMatch) {
|
|
203
|
+
element.modifier = this.parseModifier(modifierMatch[1]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Parse alignment
|
|
207
|
+
const alignmentMatch = content.match(/horizontalAlignment\s*=\s*Alignment\.(\w+)/);
|
|
208
|
+
if (alignmentMatch) {
|
|
209
|
+
element.horizontalAlignment = alignmentMatch[1];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const arrangementMatch = content.match(/verticalArrangement\s*=\s*Arrangement\.(\w+)/);
|
|
213
|
+
if (arrangementMatch) {
|
|
214
|
+
element.verticalArrangement = arrangementMatch[1];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Parse children (content inside the braces)
|
|
218
|
+
const childrenMatch = content.match(/\)\s*\{([\s\S]+)\}$/);
|
|
219
|
+
if (childrenMatch) {
|
|
220
|
+
element.children = this.parseChildren(childrenMatch[1]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return element;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Parse modifier chain
|
|
228
|
+
*/
|
|
229
|
+
private static parseModifier(modifierChain: string): DSLModifier {
|
|
230
|
+
const modifier: DSLModifier = {};
|
|
231
|
+
|
|
232
|
+
if (modifierChain.includes('.fillMaxSize()')) modifier.fillMaxSize = true;
|
|
233
|
+
if (modifierChain.includes('.fillMaxWidth()')) modifier.fillMaxWidth = true;
|
|
234
|
+
if (modifierChain.includes('.fillMaxHeight()')) modifier.fillMaxHeight = true;
|
|
235
|
+
|
|
236
|
+
const paddingMatch = modifierChain.match(/\.padding\((\d+)\.dp\)/);
|
|
237
|
+
if (paddingMatch) {
|
|
238
|
+
modifier.padding = parseInt(paddingMatch[1]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const sizeMatch = modifierChain.match(/\.size\((\d+)\.dp\)/);
|
|
242
|
+
if (sizeMatch) {
|
|
243
|
+
modifier.size = parseInt(sizeMatch[1]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const heightMatch = modifierChain.match(/\.height\((\d+)\.dp\)/);
|
|
247
|
+
if (heightMatch) {
|
|
248
|
+
modifier.height = parseInt(heightMatch[1]);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const widthMatch = modifierChain.match(/\.width\((\d+)\.dp\)/);
|
|
252
|
+
if (widthMatch) {
|
|
253
|
+
modifier.width = parseInt(widthMatch[1]);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return modifier;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Parse children elements (handles multi-line elements)
|
|
261
|
+
* Maintains source code order
|
|
262
|
+
*/
|
|
263
|
+
private static parseChildren(content: string): DSLElement[] {
|
|
264
|
+
// Remove all newlines and extra whitespace for easier parsing
|
|
265
|
+
const normalized = content.replace(/\s+/g, ' ');
|
|
266
|
+
|
|
267
|
+
// Track elements with their positions for proper ordering
|
|
268
|
+
const elements: Array<{ position: number; element: DSLElement }> = [];
|
|
269
|
+
const usedText = new Set<string>();
|
|
270
|
+
|
|
271
|
+
// First pass: Parse Button elements and track their text to avoid duplicates
|
|
272
|
+
const buttonRegex = /Button\s*\(\s*onClick\s*=\s*\{[^}]*\}(?:[^)]*modifier\s*=\s*Modifier\.fillMaxWidth\s*\(\s*\))?[^)]*\)\s*\{\s*Text\s*\(\s*"([^"]+)"\s*\)/g;
|
|
273
|
+
let match;
|
|
274
|
+
while ((match = buttonRegex.exec(normalized)) !== null) {
|
|
275
|
+
const buttonText = match[1];
|
|
276
|
+
elements.push({
|
|
277
|
+
position: match.index!,
|
|
278
|
+
element: {
|
|
279
|
+
type: 'Button',
|
|
280
|
+
text: buttonText,
|
|
281
|
+
onClick: 'handleButtonClick',
|
|
282
|
+
modifier: normalized.includes('fillMaxWidth') ? { fillMaxWidth: true } : undefined
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
usedText.add(buttonText);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Parse Spacer elements
|
|
289
|
+
const spacerRegex = /Spacer\s*\(\s*modifier\s*=\s*Modifier\.height\s*\(\s*(\d+)\.dp\s*\)/g;
|
|
290
|
+
while ((match = spacerRegex.exec(normalized)) !== null) {
|
|
291
|
+
elements.push({
|
|
292
|
+
position: match.index!,
|
|
293
|
+
element: {
|
|
294
|
+
type: 'Spacer',
|
|
295
|
+
height: parseInt(match[1])
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Parse Text elements (multiple patterns, skip if text is in a button)
|
|
301
|
+
const textPatterns = [
|
|
302
|
+
/Text\s*\(\s*text\s*=\s*"([^"]+)"[^)]*style\s*=\s*MaterialTheme\.typography\.(\w+)/g,
|
|
303
|
+
/Text\s*\(\s*"([^"]+)"[^)]*style\s*=\s*MaterialTheme\.typography\.(\w+)/g,
|
|
304
|
+
/Text\s*\(\s*text\s*=\s*"([^"]+)"/g,
|
|
305
|
+
/Text\s*\(\s*"([^"]+)"\s*\)/g
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
for (const regex of textPatterns) {
|
|
309
|
+
while ((match = regex.exec(normalized)) !== null) {
|
|
310
|
+
const text = match[1];
|
|
311
|
+
// Skip if this text is already used in a button
|
|
312
|
+
if (!usedText.has(text)) {
|
|
313
|
+
elements.push({
|
|
314
|
+
position: match.index!,
|
|
315
|
+
element: {
|
|
316
|
+
type: 'Text',
|
|
317
|
+
text: text,
|
|
318
|
+
style: match[2] || undefined
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
usedText.add(text);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Sort by position to maintain source order
|
|
327
|
+
elements.sort((a, b) => a.position - b.position);
|
|
328
|
+
|
|
329
|
+
// Return just the elements, in correct order
|
|
330
|
+
return elements.map(e => e.element);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Extract content within parentheses (handles nesting)
|
|
335
|
+
*/
|
|
336
|
+
private static extractParenthesesContent(content: string, startIndex: number): string {
|
|
337
|
+
let parenCount = 1;
|
|
338
|
+
let endIndex = startIndex;
|
|
339
|
+
|
|
340
|
+
while (parenCount > 0 && endIndex < content.length) {
|
|
341
|
+
if (content[endIndex] === '(') parenCount++;
|
|
342
|
+
if (content[endIndex] === ')') parenCount--;
|
|
343
|
+
endIndex++;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return content.substring(startIndex, endIndex - 1);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Generate default DSL when parsing fails
|
|
351
|
+
*/
|
|
352
|
+
private static generateDefaultDSL(): UIDefinition {
|
|
353
|
+
return {
|
|
354
|
+
version: '1.0',
|
|
355
|
+
screen: {
|
|
356
|
+
type: 'Column',
|
|
357
|
+
modifier: {
|
|
358
|
+
fillMaxSize: true,
|
|
359
|
+
padding: 16
|
|
360
|
+
},
|
|
361
|
+
horizontalAlignment: 'CenterHorizontally',
|
|
362
|
+
verticalArrangement: 'Center',
|
|
363
|
+
children: [
|
|
364
|
+
{
|
|
365
|
+
type: 'Text',
|
|
366
|
+
text: 'Welcome to JetStart! 🚀',
|
|
367
|
+
style: 'headlineMedium'
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
type: 'Spacer',
|
|
371
|
+
height: 16
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
type: 'Text',
|
|
375
|
+
text: 'Edit your code to see hot reload',
|
|
376
|
+
style: 'bodyMedium'
|
|
377
|
+
}
|
|
378
|
+
]
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|