@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,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSL Type Definitions for Server-Side
|
|
3
|
+
* Represents UI elements that can be sent to Android app as JSON
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface UIDefinition {
|
|
7
|
+
version: string;
|
|
8
|
+
screen: DSLElement;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DSLElement {
|
|
12
|
+
type: string;
|
|
13
|
+
text?: string;
|
|
14
|
+
style?: string;
|
|
15
|
+
color?: string;
|
|
16
|
+
modifier?: DSLModifier;
|
|
17
|
+
horizontalAlignment?: string;
|
|
18
|
+
verticalArrangement?: string;
|
|
19
|
+
contentAlignment?: string;
|
|
20
|
+
height?: number;
|
|
21
|
+
width?: number;
|
|
22
|
+
onClick?: string;
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
imageVector?: string;
|
|
25
|
+
tint?: string;
|
|
26
|
+
contentDescription?: string;
|
|
27
|
+
children?: DSLElement[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DSLModifier {
|
|
31
|
+
fillMaxSize?: boolean;
|
|
32
|
+
fillMaxWidth?: boolean;
|
|
33
|
+
fillMaxHeight?: boolean;
|
|
34
|
+
padding?: number;
|
|
35
|
+
paddingHorizontal?: number;
|
|
36
|
+
paddingVertical?: number;
|
|
37
|
+
size?: number;
|
|
38
|
+
height?: number;
|
|
39
|
+
width?: number;
|
|
40
|
+
weight?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse result from Kotlin file
|
|
45
|
+
*/
|
|
46
|
+
export interface ParseResult {
|
|
47
|
+
success: boolean;
|
|
48
|
+
dsl?: UIDefinition;
|
|
49
|
+
errors?: string[];
|
|
50
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gradle BuildConfig Injector
|
|
3
|
+
* Injects buildConfigField values into build.gradle
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs-extra';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { log } from '../utils/logger';
|
|
9
|
+
|
|
10
|
+
export interface BuildConfigField {
|
|
11
|
+
type: string;
|
|
12
|
+
name: string;
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Inject buildConfigFields into app/build.gradle
|
|
18
|
+
*/
|
|
19
|
+
export async function injectBuildConfigFields(
|
|
20
|
+
projectPath: string,
|
|
21
|
+
fields: BuildConfigField[]
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const buildGradlePath = path.join(projectPath, 'app', 'build.gradle');
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(buildGradlePath)) {
|
|
26
|
+
log('Warning: build.gradle not found, skipping injection');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let content = await fs.readFile(buildGradlePath, 'utf-8');
|
|
31
|
+
|
|
32
|
+
// Find the defaultConfig block
|
|
33
|
+
const defaultConfigRegex = /defaultConfig\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/;
|
|
34
|
+
const match = content.match(defaultConfigRegex);
|
|
35
|
+
|
|
36
|
+
if (!match) {
|
|
37
|
+
log('Warning: defaultConfig not found in build.gradle');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const defaultConfigBlock = match[1];
|
|
42
|
+
|
|
43
|
+
// Remove existing JetStart buildConfigFields
|
|
44
|
+
let updatedBlock = defaultConfigBlock.replace(
|
|
45
|
+
/\/\/ JetStart injected fields[\s\S]*?\/\/ End JetStart fields\n/g,
|
|
46
|
+
''
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Add new buildConfigFields
|
|
50
|
+
const fieldLines = fields.map(f => {
|
|
51
|
+
// For String type, escape the value with quotes
|
|
52
|
+
const escapedValue = f.type === 'String' ? `\\"${f.value}\\"` : f.value;
|
|
53
|
+
return ` buildConfigField "${f.type}", "${f.name}", "${escapedValue}"`;
|
|
54
|
+
}).join('\n');
|
|
55
|
+
|
|
56
|
+
updatedBlock = updatedBlock.trimEnd() + '\n\n // JetStart injected fields\n' +
|
|
57
|
+
fieldLines + '\n // End JetStart fields\n ';
|
|
58
|
+
|
|
59
|
+
// Replace the defaultConfig block
|
|
60
|
+
content = content.replace(defaultConfigRegex, `defaultConfig {${updatedBlock}}`);
|
|
61
|
+
|
|
62
|
+
await fs.writeFile(buildGradlePath, content, 'utf-8');
|
|
63
|
+
log('Injected buildConfigFields into build.gradle');
|
|
64
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gradle Executor
|
|
3
|
+
* Spawns and manages Gradle build processes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import { BuildConfig, BuildResult } from '@jetstart/shared';
|
|
11
|
+
import { BuildOutputParser } from './parser';
|
|
12
|
+
|
|
13
|
+
export interface GradleExecutorOptions {
|
|
14
|
+
javaHome?: string;
|
|
15
|
+
androidHome?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class GradleExecutor {
|
|
19
|
+
private javaHome: string | undefined;
|
|
20
|
+
private androidHome: string | undefined;
|
|
21
|
+
|
|
22
|
+
constructor(options: GradleExecutorOptions = {}) {
|
|
23
|
+
this.javaHome = options.javaHome || process.env.JAVA_HOME;
|
|
24
|
+
this.androidHome = options.androidHome || process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Execute Gradle build
|
|
29
|
+
*/
|
|
30
|
+
async execute(config: BuildConfig): Promise<BuildResult> {
|
|
31
|
+
const startTime = Date.now();
|
|
32
|
+
const gradlePath = this.findGradle(config.projectPath);
|
|
33
|
+
|
|
34
|
+
// If Gradle not found, return mock build for testing
|
|
35
|
+
if (!gradlePath) {
|
|
36
|
+
console.log('[Gradle] No Gradle found, returning mock successful build for testing');
|
|
37
|
+
|
|
38
|
+
// Simulate build delay
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
buildTime: Date.now() - startTime,
|
|
44
|
+
apkPath: path.join(config.projectPath, 'build/outputs/apk/debug/app-debug.apk'),
|
|
45
|
+
apkSize: 5242880, // Mock size: 5MB
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Ensure Android SDK is configured
|
|
50
|
+
await this.ensureAndroidSdk(config.projectPath);
|
|
51
|
+
|
|
52
|
+
const args = this.buildGradleArgs(config);
|
|
53
|
+
const env = this.buildEnv();
|
|
54
|
+
|
|
55
|
+
return this.runGradle(gradlePath, args, config.projectPath, env, startTime);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Ensure Android SDK is configured (auto-detect and create local.properties)
|
|
60
|
+
*/
|
|
61
|
+
private async ensureAndroidSdk(projectPath: string): Promise<void> {
|
|
62
|
+
const localPropsPath = path.join(projectPath, 'local.properties');
|
|
63
|
+
|
|
64
|
+
// Check if local.properties already exists
|
|
65
|
+
if (fs.existsSync(localPropsPath)) {
|
|
66
|
+
return; // Already configured
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Try to find Android SDK
|
|
70
|
+
let androidSdkPath = this.androidHome;
|
|
71
|
+
|
|
72
|
+
// If not in environment, check common Windows locations
|
|
73
|
+
if (!androidSdkPath && os.platform() === 'win32') {
|
|
74
|
+
const commonPaths = [
|
|
75
|
+
'C:\\Android', // Check C:\Android first (command-line tools location)
|
|
76
|
+
path.join(os.homedir(), 'AppData', 'Local', 'Android', 'Sdk'),
|
|
77
|
+
'C:\\Android\\Sdk',
|
|
78
|
+
'C:\\Program Files (x86)\\Android\\android-sdk',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
for (const p of commonPaths) {
|
|
82
|
+
if (fs.existsSync(p)) {
|
|
83
|
+
androidSdkPath = p;
|
|
84
|
+
console.log(`[Gradle] Auto-detected Android SDK at: ${androidSdkPath}`);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// If not found on macOS/Linux, check common paths
|
|
91
|
+
if (!androidSdkPath && os.platform() !== 'win32') {
|
|
92
|
+
const commonPaths = [
|
|
93
|
+
path.join(os.homedir(), 'Android', 'Sdk'),
|
|
94
|
+
path.join(os.homedir(), 'Library', 'Android', 'sdk'),
|
|
95
|
+
'/opt/android-sdk',
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
for (const p of commonPaths) {
|
|
99
|
+
if (fs.existsSync(p)) {
|
|
100
|
+
androidSdkPath = p;
|
|
101
|
+
console.log(`[Gradle] Auto-detected Android SDK at: ${androidSdkPath}`);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!androidSdkPath) {
|
|
108
|
+
console.warn('[Gradle] Android SDK not found! Set ANDROID_HOME environment variable.');
|
|
109
|
+
console.warn('[Gradle] Download Android SDK from: https://developer.android.com/studio');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Create local.properties with SDK path
|
|
114
|
+
const localProps = `# Auto-generated by JetStart
|
|
115
|
+
sdk.dir=${androidSdkPath.replace(/\\/g, '\\\\')}
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
fs.writeFileSync(localPropsPath, localProps);
|
|
119
|
+
console.log(`[Gradle] Created local.properties with SDK path: ${androidSdkPath}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Find Gradle executable (prioritize system gradle for speed)
|
|
124
|
+
*/
|
|
125
|
+
private findGradle(projectPath: string): string | null {
|
|
126
|
+
const isWindows = os.platform() === 'win32';
|
|
127
|
+
|
|
128
|
+
// CHECK SYSTEM GRADLE FIRST (instant, no download needed)
|
|
129
|
+
const gradleName = isWindows ? 'gradle.bat' : 'gradle';
|
|
130
|
+
const systemGradle = this.findInPath(gradleName);
|
|
131
|
+
|
|
132
|
+
if (systemGradle) {
|
|
133
|
+
console.log('[Gradle] Using system Gradle (faster than wrapper)');
|
|
134
|
+
return systemGradle;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fallback to gradlew wrapper in project (slower, downloads Gradle)
|
|
138
|
+
const gradlewName = isWindows ? 'gradlew.bat' : 'gradlew';
|
|
139
|
+
const gradlewPath = path.join(projectPath, gradlewName);
|
|
140
|
+
|
|
141
|
+
if (fs.existsSync(gradlewPath)) {
|
|
142
|
+
console.log('[Gradle] Using project wrapper (may download Gradle on first run)');
|
|
143
|
+
return gradlewPath;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Find executable in PATH
|
|
151
|
+
*/
|
|
152
|
+
private findInPath(executableName: string): string | null {
|
|
153
|
+
const pathEnv = process.env.PATH || '';
|
|
154
|
+
const pathSeparator = os.platform() === 'win32' ? ';' : ':';
|
|
155
|
+
const paths = pathEnv.split(pathSeparator);
|
|
156
|
+
|
|
157
|
+
for (const dir of paths) {
|
|
158
|
+
const fullPath = path.join(dir, executableName);
|
|
159
|
+
if (fs.existsSync(fullPath)) {
|
|
160
|
+
return fullPath;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Build Gradle arguments
|
|
169
|
+
*/
|
|
170
|
+
private buildGradleArgs(config: BuildConfig): string[] {
|
|
171
|
+
const args: string[] = [];
|
|
172
|
+
|
|
173
|
+
// Task based on build type
|
|
174
|
+
if (config.buildType === 'debug') {
|
|
175
|
+
args.push('assembleDebug');
|
|
176
|
+
} else {
|
|
177
|
+
args.push('assembleRelease');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Performance optimizations
|
|
181
|
+
args.push('--parallel');
|
|
182
|
+
args.push('--build-cache');
|
|
183
|
+
args.push('--configure-on-demand');
|
|
184
|
+
|
|
185
|
+
// Build universal APK for all architectures (removed single-ABI restriction)
|
|
186
|
+
// This ensures the APK works on all device architectures
|
|
187
|
+
// Note: This makes the APK larger but ensures compatibility
|
|
188
|
+
|
|
189
|
+
// Daemon for faster subsequent builds
|
|
190
|
+
args.push('--daemon');
|
|
191
|
+
|
|
192
|
+
// Console output
|
|
193
|
+
args.push('--console=plain');
|
|
194
|
+
|
|
195
|
+
return args;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build environment variables
|
|
200
|
+
*/
|
|
201
|
+
private buildEnv(): NodeJS.ProcessEnv {
|
|
202
|
+
const env = { ...process.env };
|
|
203
|
+
|
|
204
|
+
if (this.javaHome) {
|
|
205
|
+
env.JAVA_HOME = this.javaHome;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (this.androidHome) {
|
|
209
|
+
env.ANDROID_HOME = this.androidHome;
|
|
210
|
+
env.ANDROID_SDK_ROOT = this.androidHome;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return env;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Run Gradle process and collect output
|
|
218
|
+
*/
|
|
219
|
+
private runGradle(
|
|
220
|
+
gradlePath: string,
|
|
221
|
+
args: string[],
|
|
222
|
+
cwd: string,
|
|
223
|
+
env: NodeJS.ProcessEnv,
|
|
224
|
+
startTime: number
|
|
225
|
+
): Promise<BuildResult> {
|
|
226
|
+
return new Promise((resolve) => {
|
|
227
|
+
let output = '';
|
|
228
|
+
|
|
229
|
+
console.log(`[Gradle] Running: ${gradlePath} ${args.join(' ')}`);
|
|
230
|
+
console.log(`[Gradle] Working directory: ${cwd}`);
|
|
231
|
+
|
|
232
|
+
const process = spawn(gradlePath, args, {
|
|
233
|
+
cwd,
|
|
234
|
+
env,
|
|
235
|
+
shell: true,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
process.stdout.on('data', (data: Buffer) => {
|
|
239
|
+
const text = data.toString();
|
|
240
|
+
output += text;
|
|
241
|
+
// Stream output to console in real-time
|
|
242
|
+
console.log(text.trim());
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
process.stderr.on('data', (data: Buffer) => {
|
|
246
|
+
const text = data.toString();
|
|
247
|
+
output += text;
|
|
248
|
+
// Stream errors to console in real-time
|
|
249
|
+
console.error(text.trim());
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
process.on('close', (code: number) => {
|
|
253
|
+
console.log(`[Gradle] Process exited with code: ${code}`);
|
|
254
|
+
const result = BuildOutputParser.parse(output, startTime);
|
|
255
|
+
|
|
256
|
+
// If process exited with error but no errors were parsed, add generic error
|
|
257
|
+
if (code !== 0 && (!result.errors || result.errors.length === 0)) {
|
|
258
|
+
result.errors = [{
|
|
259
|
+
file: '',
|
|
260
|
+
line: 0,
|
|
261
|
+
column: 0,
|
|
262
|
+
message: `Gradle process exited with code ${code}`,
|
|
263
|
+
severity: 'error' as any,
|
|
264
|
+
}];
|
|
265
|
+
result.success = false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// If build succeeded but APK path not found, manually search for it
|
|
269
|
+
if (result.success && !result.apkPath) {
|
|
270
|
+
const possiblePaths = [
|
|
271
|
+
path.join(cwd, 'app/build/outputs/apk/debug/app-debug.apk'),
|
|
272
|
+
path.join(cwd, 'app/build/intermediates/apk/debug/app-debug.apk'),
|
|
273
|
+
path.join(cwd, 'build/outputs/apk/debug/app-debug.apk'),
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
for (const apkPath of possiblePaths) {
|
|
277
|
+
if (fs.existsSync(apkPath)) {
|
|
278
|
+
result.apkPath = apkPath;
|
|
279
|
+
result.apkSize = fs.statSync(apkPath).size;
|
|
280
|
+
console.log(`[Gradle] Found APK at: ${apkPath} (${(result.apkSize / 1024 / 1024).toFixed(2)} MB)`);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
resolve(result);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
process.on('error', (err: Error) => {
|
|
290
|
+
console.error(`[Gradle] Failed to spawn process: ${err.message}`);
|
|
291
|
+
resolve({
|
|
292
|
+
success: false,
|
|
293
|
+
buildTime: Date.now() - startTime,
|
|
294
|
+
errors: [{
|
|
295
|
+
file: '',
|
|
296
|
+
line: 0,
|
|
297
|
+
column: 0,
|
|
298
|
+
message: `Failed to spawn Gradle process: ${err.message}`,
|
|
299
|
+
severity: 'error' as any,
|
|
300
|
+
}],
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build Output Parser
|
|
3
|
+
* Parses Gradle build output to extract errors, warnings, and build info
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BuildResult, BuildError, BuildWarning, ErrorSeverity } from '@jetstart/shared';
|
|
7
|
+
|
|
8
|
+
export class BuildOutputParser {
|
|
9
|
+
/**
|
|
10
|
+
* Parse Gradle build output
|
|
11
|
+
*/
|
|
12
|
+
static parse(output: string, startTime: number): BuildResult {
|
|
13
|
+
const lines = output.split('\n');
|
|
14
|
+
const errors: BuildError[] = [];
|
|
15
|
+
const warnings: BuildWarning[] = [];
|
|
16
|
+
let apkPath: string | undefined;
|
|
17
|
+
let apkSize: number | undefined;
|
|
18
|
+
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
// Parse Kotlin compiler errors: e: /path/file.kt:10:5: Error message
|
|
21
|
+
const errorMatch = line.match(/^e: (.+):(\d+):(\d+): (.+)$/);
|
|
22
|
+
if (errorMatch) {
|
|
23
|
+
errors.push({
|
|
24
|
+
file: errorMatch[1],
|
|
25
|
+
line: parseInt(errorMatch[2], 10),
|
|
26
|
+
column: parseInt(errorMatch[3], 10),
|
|
27
|
+
message: errorMatch[4],
|
|
28
|
+
severity: ErrorSeverity.ERROR,
|
|
29
|
+
});
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Parse Kotlin compiler warnings: w: /path/file.kt:10:5: Warning message
|
|
34
|
+
const warningMatch = line.match(/^w: (.+):(\d+):(\d+): (.+)$/);
|
|
35
|
+
if (warningMatch) {
|
|
36
|
+
warnings.push({
|
|
37
|
+
file: warningMatch[1],
|
|
38
|
+
line: parseInt(warningMatch[2], 10),
|
|
39
|
+
message: warningMatch[4],
|
|
40
|
+
});
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Parse APK location (check both outputs and intermediates directories)
|
|
45
|
+
if (line.includes('.apk') && (line.includes('app/build/outputs') || line.includes('app\\build\\outputs') || line.includes('app/build/intermediates') || line.includes('app\\build\\intermediates'))) {
|
|
46
|
+
const apkMatch = line.match(/([^\s]+\.apk)/);
|
|
47
|
+
if (apkMatch) {
|
|
48
|
+
apkPath = apkMatch[1];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Parse build success
|
|
53
|
+
if (line.includes('BUILD SUCCESSFUL')) {
|
|
54
|
+
// Success is determined later
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Parse build failure
|
|
58
|
+
if (line.includes('BUILD FAILED')) {
|
|
59
|
+
// Failure is determined by errors array
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const buildTime = Date.now() - startTime;
|
|
64
|
+
const success = errors.length === 0 && output.includes('BUILD SUCCESSFUL');
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
success,
|
|
68
|
+
apkPath,
|
|
69
|
+
apkSize,
|
|
70
|
+
buildTime,
|
|
71
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
72
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Watcher
|
|
3
|
+
* Watches for file changes using chokidar
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as chokidar from 'chokidar';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
|
|
9
|
+
export interface FileWatcherOptions {
|
|
10
|
+
projectPath: string;
|
|
11
|
+
callback: (files: string[]) => void;
|
|
12
|
+
debounceMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class FileWatcher {
|
|
16
|
+
private watcher: chokidar.FSWatcher | null = null;
|
|
17
|
+
private callback: (files: string[]) => void;
|
|
18
|
+
private debounceTimer: NodeJS.Timeout | null = null;
|
|
19
|
+
private debounceMs: number;
|
|
20
|
+
private changedFiles: Set<string> = new Set();
|
|
21
|
+
|
|
22
|
+
constructor(options: FileWatcherOptions) {
|
|
23
|
+
this.callback = options.callback;
|
|
24
|
+
this.debounceMs = options.debounceMs || 300; // 300ms default
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start watching for file changes
|
|
29
|
+
*/
|
|
30
|
+
watch(projectPath: string): void {
|
|
31
|
+
if (this.watcher) {
|
|
32
|
+
this.stop();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Watch .kt, .xml, .gradle files
|
|
36
|
+
const patterns = [
|
|
37
|
+
path.join(projectPath, '**/*.kt'),
|
|
38
|
+
path.join(projectPath, '**/*.xml'),
|
|
39
|
+
path.join(projectPath, '**/*.gradle'),
|
|
40
|
+
path.join(projectPath, '**/*.gradle.kts'),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
this.watcher = chokidar.watch(patterns, {
|
|
44
|
+
ignored: [
|
|
45
|
+
'**/node_modules/**',
|
|
46
|
+
'**/build/**',
|
|
47
|
+
'**/.gradle/**',
|
|
48
|
+
'**/.git/**',
|
|
49
|
+
'**/dist/**',
|
|
50
|
+
],
|
|
51
|
+
persistent: true,
|
|
52
|
+
ignoreInitial: true,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
this.watcher.on('change', (filePath: string) => {
|
|
56
|
+
this.onFileChange(filePath);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.watcher.on('add', (filePath: string) => {
|
|
60
|
+
this.onFileChange(filePath);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.watcher.on('unlink', (filePath: string) => {
|
|
64
|
+
this.onFileChange(filePath);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Stop watching
|
|
70
|
+
*/
|
|
71
|
+
stop(): void {
|
|
72
|
+
if (this.watcher) {
|
|
73
|
+
this.watcher.close();
|
|
74
|
+
this.watcher = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (this.debounceTimer) {
|
|
78
|
+
clearTimeout(this.debounceTimer);
|
|
79
|
+
this.debounceTimer = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.changedFiles.clear();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Handle file change with debouncing
|
|
87
|
+
*/
|
|
88
|
+
private onFileChange(filePath: string): void {
|
|
89
|
+
this.changedFiles.add(filePath);
|
|
90
|
+
|
|
91
|
+
// Clear existing timer
|
|
92
|
+
if (this.debounceTimer) {
|
|
93
|
+
clearTimeout(this.debounceTimer);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Set new timer
|
|
97
|
+
this.debounceTimer = setTimeout(() => {
|
|
98
|
+
const files = Array.from(this.changedFiles);
|
|
99
|
+
this.changedFiles.clear();
|
|
100
|
+
this.callback(files);
|
|
101
|
+
}, this.debounceMs);
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Package Entry Point
|
|
3
|
+
* Central build server and WebSocket orchestration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from './server';
|
|
7
|
+
export * from './websocket';
|
|
8
|
+
export * from './build';
|
|
9
|
+
export * from './types';
|
|
10
|
+
export * from './utils';
|
|
11
|
+
|
|
12
|
+
// Re-export shared types
|
|
13
|
+
export type {
|
|
14
|
+
Session,
|
|
15
|
+
SessionStatus,
|
|
16
|
+
BuildConfig,
|
|
17
|
+
BuildResult,
|
|
18
|
+
DeviceInfo,
|
|
19
|
+
WSMessage,
|
|
20
|
+
} from '@jetstart/shared';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Server
|
|
3
|
+
* Serves APKs, handles REST endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import express, { Express } from 'express';
|
|
7
|
+
import { Server } from 'http';
|
|
8
|
+
import { setupMiddleware } from './middleware';
|
|
9
|
+
import { setupRoutes } from './routes';
|
|
10
|
+
import { log } from '../utils/logger';
|
|
11
|
+
|
|
12
|
+
export interface HttpServerConfig {
|
|
13
|
+
port: number;
|
|
14
|
+
host: string;
|
|
15
|
+
getLatestApk?: () => string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function createHttpServer(config: HttpServerConfig): Promise<Server> {
|
|
19
|
+
const app: Express = express();
|
|
20
|
+
|
|
21
|
+
// Setup middleware
|
|
22
|
+
setupMiddleware(app);
|
|
23
|
+
|
|
24
|
+
// Setup routes
|
|
25
|
+
setupRoutes(app, config.getLatestApk);
|
|
26
|
+
|
|
27
|
+
// Start server
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const server = app.listen(config.port, config.host, () => {
|
|
30
|
+
log(`HTTP server listening on ${config.host}:${config.port}`);
|
|
31
|
+
resolve(server);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
server.on('error', (err: Error) => {
|
|
35
|
+
reject(err);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|