@jetstart/cli 1.2.0 → 1.5.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.
- package/dist/cli.js +18 -1
- package/dist/commands/android-emulator.d.ts +8 -0
- package/dist/commands/android-emulator.js +280 -0
- package/dist/commands/create.d.ts +1 -6
- package/dist/commands/create.js +119 -0
- package/dist/commands/dev.d.ts +1 -7
- package/dist/commands/dev.js +69 -10
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +5 -1
- package/dist/commands/install-audit.d.ts +9 -0
- package/dist/commands/install-audit.js +185 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.js +8 -0
- package/dist/utils/android-sdk.d.ts +81 -0
- package/dist/utils/android-sdk.js +432 -0
- package/dist/utils/downloader.d.ts +35 -0
- package/dist/utils/downloader.js +214 -0
- package/dist/utils/emulator-deployer.d.ts +29 -0
- package/dist/utils/emulator-deployer.js +224 -0
- package/dist/utils/emulator.d.ts +101 -0
- package/dist/utils/emulator.js +410 -0
- package/dist/utils/java.d.ts +25 -0
- package/dist/utils/java.js +363 -0
- package/dist/utils/system-tools.d.ts +93 -0
- package/dist/utils/system-tools.js +599 -0
- package/dist/utils/template.js +777 -748
- package/package.json +7 -3
- package/src/cli.ts +20 -2
- package/src/commands/android-emulator.ts +304 -0
- package/src/commands/create.ts +128 -5
- package/src/commands/dev.ts +71 -18
- package/src/commands/index.ts +3 -1
- package/src/commands/install-audit.ts +227 -0
- package/src/types/index.ts +30 -0
- package/src/utils/android-sdk.ts +478 -0
- package/src/utils/downloader.ts +201 -0
- package/src/utils/emulator-deployer.ts +210 -0
- package/src/utils/emulator.ts +463 -0
- package/src/utils/java.ts +369 -0
- package/src/utils/system-tools.ts +648 -0
- package/src/utils/template.ts +875 -867
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/build.d.ts.map +0 -1
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/dev.d.ts.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/index.d.ts.map +0 -1
- package/dist/commands/index.js.map +0 -1
- package/dist/commands/logs.d.ts.map +0 -1
- package/dist/commands/logs.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/prompt.d.ts.map +0 -1
- package/dist/utils/prompt.js.map +0 -1
- package/dist/utils/spinner.d.ts.map +0 -1
- package/dist/utils/spinner.js.map +0 -1
- package/dist/utils/template.d.ts.map +0 -1
- package/dist/utils/template.js.map +0 -1
package/dist/utils/template.js
CHANGED
|
@@ -23,9 +23,7 @@ async function generateProjectTemplate(projectPath, options) {
|
|
|
23
23
|
await generateGradleProperties(projectPath);
|
|
24
24
|
await generateGradleWrapper(projectPath);
|
|
25
25
|
await generateMainActivity(projectPath, packageName);
|
|
26
|
-
await
|
|
27
|
-
await generateDSLInterpreter(projectPath, packageName);
|
|
28
|
-
await generateDSLTypes(projectPath, packageName);
|
|
26
|
+
await generateJetStart(projectPath, packageName);
|
|
29
27
|
await generateAndroidManifest(projectPath, options);
|
|
30
28
|
await generateResourceFiles(projectPath, projectName);
|
|
31
29
|
await generateLocalProperties(projectPath);
|
|
@@ -33,457 +31,438 @@ async function generateProjectTemplate(projectPath, options) {
|
|
|
33
31
|
await generateGitignore(projectPath);
|
|
34
32
|
await generateReadme(projectPath, projectName);
|
|
35
33
|
}
|
|
36
|
-
async function
|
|
37
|
-
const dirs = [
|
|
38
|
-
'app/src/main/java',
|
|
39
|
-
'app/src/main/res/layout',
|
|
40
|
-
'app/src/main/res/values',
|
|
41
|
-
'app/src/main/res/drawable',
|
|
42
|
-
'gradle/wrapper',
|
|
43
|
-
];
|
|
44
|
-
for (const dir of dirs) {
|
|
45
|
-
await fs_extra_1.default.ensureDir(path_1.default.join(projectPath, dir));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
async function generateBuildGradle(projectPath, options) {
|
|
49
|
-
const content = `plugins {
|
|
50
|
-
id 'com.android.application'
|
|
51
|
-
id 'org.jetbrains.kotlin.android'
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
android {
|
|
55
|
-
namespace '${options.packageName}'
|
|
56
|
-
compileSdk ${shared_1.TARGET_ANDROID_API_LEVEL}
|
|
57
|
-
|
|
58
|
-
defaultConfig {
|
|
59
|
-
applicationId "${options.packageName}"
|
|
60
|
-
minSdk ${shared_1.MIN_ANDROID_API_LEVEL}
|
|
61
|
-
targetSdk ${shared_1.TARGET_ANDROID_API_LEVEL}
|
|
62
|
-
versionCode 1
|
|
63
|
-
versionName "1.0.0"
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
buildTypes {
|
|
67
|
-
release {
|
|
68
|
-
minifyEnabled false
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
compileOptions {
|
|
73
|
-
sourceCompatibility JavaVersion.VERSION_17
|
|
74
|
-
targetCompatibility JavaVersion.VERSION_17
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
kotlinOptions {
|
|
78
|
-
jvmTarget = '17'
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
buildFeatures {
|
|
82
|
-
compose true
|
|
83
|
-
buildConfig true // Required for JetStart hot reload
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
composeOptions {
|
|
87
|
-
kotlinCompilerExtensionVersion = '1.5.6'
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
dependencies {
|
|
92
|
-
implementation 'androidx.core:core-ktx:1.12.0'
|
|
93
|
-
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
|
|
94
|
-
implementation 'androidx.activity:activity-compose:1.8.1'
|
|
95
|
-
implementation platform('androidx.compose:compose-bom:2023.10.01')
|
|
96
|
-
implementation 'androidx.compose.ui:ui'
|
|
97
|
-
implementation 'androidx.compose.ui:ui-graphics'
|
|
98
|
-
implementation 'androidx.compose.ui:ui-tooling-preview'
|
|
99
|
-
implementation 'androidx.compose.material3:material3'
|
|
100
|
-
|
|
101
|
-
// JetStart Hot Reload dependencies
|
|
102
|
-
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
|
103
|
-
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
|
104
|
-
}`;
|
|
105
|
-
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/build.gradle'), content);
|
|
106
|
-
}
|
|
107
|
-
async function generateSettingsGradle(projectPath, projectName) {
|
|
108
|
-
const content = `rootProject.name = "${projectName}"
|
|
109
|
-
include ':app'`;
|
|
110
|
-
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'settings.gradle'), content);
|
|
111
|
-
}
|
|
112
|
-
async function generateGradleProperties(projectPath) {
|
|
113
|
-
const content = `org.gradle.jvmargs=-Xmx2048m
|
|
114
|
-
android.useAndroidX=true
|
|
115
|
-
kotlin.code.style=official`;
|
|
116
|
-
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'gradle.properties'), content);
|
|
117
|
-
}
|
|
118
|
-
async function generateMainActivity(projectPath, packageName) {
|
|
34
|
+
async function generateJetStart(projectPath, packageName) {
|
|
119
35
|
const packagePath = packageName.replace(/\./g, '/');
|
|
120
|
-
const
|
|
36
|
+
const jetStartPath = path_1.default.join(projectPath, 'app/src/main/java', packagePath, 'JetStart.kt');
|
|
121
37
|
const content = `package ${packageName}
|
|
122
38
|
|
|
123
|
-
|
|
124
|
-
import
|
|
125
|
-
import
|
|
39
|
+
// Android
|
|
40
|
+
import android.app.Activity
|
|
41
|
+
import android.content.Intent
|
|
42
|
+
import android.net.Uri
|
|
43
|
+
import android.os.Build
|
|
44
|
+
import android.util.Log
|
|
45
|
+
import androidx.core.content.FileProvider
|
|
46
|
+
|
|
47
|
+
// Compose
|
|
126
48
|
import androidx.compose.foundation.layout.*
|
|
127
49
|
import androidx.compose.material3.*
|
|
128
50
|
import androidx.compose.runtime.*
|
|
129
51
|
import androidx.compose.ui.Alignment
|
|
130
52
|
import androidx.compose.ui.Modifier
|
|
53
|
+
import androidx.compose.ui.graphics.Color
|
|
54
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
131
55
|
import androidx.compose.ui.unit.dp
|
|
132
56
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// Initialize hot reload - reads from BuildConfig injected by jetstart dev
|
|
138
|
-
try {
|
|
139
|
-
val serverUrl = BuildConfig.JETSTART_SERVER_URL
|
|
140
|
-
val sessionId = BuildConfig.JETSTART_SESSION_ID
|
|
141
|
-
HotReload.connect(this, serverUrl, sessionId)
|
|
142
|
-
} catch (e: Exception) {
|
|
143
|
-
// BuildConfig not available yet, hot reload will be disabled
|
|
144
|
-
android.util.Log.w("MainActivity", "Hot reload not configured: \${e.message}")
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
setContent {
|
|
148
|
-
MaterialTheme {
|
|
149
|
-
Surface(
|
|
150
|
-
modifier = Modifier.fillMaxSize(),
|
|
151
|
-
color = MaterialTheme.colorScheme.background
|
|
152
|
-
) {
|
|
153
|
-
// Check if we should render from DSL (hot reload mode)
|
|
154
|
-
val dsl by DSLInterpreter.currentDSL.collectAsState()
|
|
57
|
+
// Third-party
|
|
58
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
59
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
60
|
+
import okhttp3.*
|
|
155
61
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
AppContent()
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
62
|
+
// Standard library & JSON
|
|
63
|
+
import org.json.JSONArray
|
|
64
|
+
import org.json.JSONObject
|
|
65
|
+
import java.io.File
|
|
66
|
+
import java.io.IOException
|
|
167
67
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
}
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// DSL Type Definitions
|
|
70
|
+
// ============================================================================
|
|
173
71
|
|
|
174
72
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
73
|
+
* DSL Type Definitions
|
|
74
|
+
* Represents UI elements in JSON format that can be interpreted at runtime
|
|
177
75
|
*/
|
|
178
|
-
@Composable
|
|
179
|
-
fun AppContent() {
|
|
180
|
-
Column(
|
|
181
|
-
modifier = Modifier
|
|
182
|
-
.fillMaxSize()
|
|
183
|
-
.padding(16.dp),
|
|
184
|
-
horizontalAlignment = Alignment.CenterHorizontally,
|
|
185
|
-
verticalArrangement = Arrangement.Center
|
|
186
|
-
) {
|
|
187
|
-
Text(
|
|
188
|
-
text = "Welcome to JetStart! 🚀",
|
|
189
|
-
style = MaterialTheme.typography.headlineMedium
|
|
190
|
-
)
|
|
191
76
|
|
|
192
|
-
|
|
77
|
+
data class UIDefinition(
|
|
78
|
+
val version: String = "1.0",
|
|
79
|
+
val screen: DSLElement
|
|
80
|
+
)
|
|
193
81
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
82
|
+
data class DSLElement(
|
|
83
|
+
val type: String,
|
|
84
|
+
val text: String? = null,
|
|
85
|
+
val style: String? = null,
|
|
86
|
+
val color: String? = null,
|
|
87
|
+
val modifier: DSLModifier? = null,
|
|
88
|
+
val horizontalAlignment: String? = null,
|
|
89
|
+
val verticalArrangement: String? = null,
|
|
90
|
+
val contentAlignment: String? = null,
|
|
91
|
+
val height: Int? = null,
|
|
92
|
+
val width: Int? = null,
|
|
93
|
+
val onClick: String? = null,
|
|
94
|
+
val enabled: Boolean? = true,
|
|
95
|
+
val imageVector: String? = null,
|
|
96
|
+
val tint: String? = null,
|
|
97
|
+
val contentDescription: String? = null,
|
|
98
|
+
val children: List<DSLElement>? = null
|
|
99
|
+
)
|
|
198
100
|
|
|
199
|
-
|
|
101
|
+
data class DSLModifier(
|
|
102
|
+
val fillMaxSize: Boolean? = null,
|
|
103
|
+
val fillMaxWidth: Boolean? = null,
|
|
104
|
+
val fillMaxHeight: Boolean? = null,
|
|
105
|
+
val padding: Int? = null,
|
|
106
|
+
val paddingHorizontal: Int? = null,
|
|
107
|
+
val paddingVertical: Int? = null,
|
|
108
|
+
val size: Int? = null,
|
|
109
|
+
val height: Int? = null,
|
|
110
|
+
val width: Int? = null,
|
|
111
|
+
val weight: Float? = null
|
|
112
|
+
)
|
|
200
113
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}`;
|
|
209
|
-
await fs_extra_1.default.ensureDir(path_1.default.dirname(activityPath));
|
|
210
|
-
await fs_extra_1.default.writeFile(activityPath, content);
|
|
211
|
-
}
|
|
212
|
-
async function generateAndroidManifest(projectPath, options) {
|
|
213
|
-
const themeName = options.projectName.replace(/[^a-zA-Z0-9]/g, '');
|
|
214
|
-
const content = `<?xml version="1.0" encoding="utf-8"?>
|
|
215
|
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
114
|
+
/**
|
|
115
|
+
* Parse JSON string to UIDefinition
|
|
116
|
+
*/
|
|
117
|
+
fun parseUIDefinition(json: String): UIDefinition {
|
|
118
|
+
val obj = JSONObject(json)
|
|
119
|
+
val version = obj.optString("version", "1.0")
|
|
120
|
+
val screenObj = obj.getJSONObject("screen")
|
|
216
121
|
|
|
217
|
-
|
|
122
|
+
return UIDefinition(
|
|
123
|
+
version = version,
|
|
124
|
+
screen = parseDSLElement(screenObj)
|
|
125
|
+
)
|
|
126
|
+
}
|
|
218
127
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<category android:name="android.intent.category.LAUNCHER" />
|
|
230
|
-
</intent-filter>
|
|
231
|
-
</activity>
|
|
232
|
-
</application>
|
|
128
|
+
/**
|
|
129
|
+
* Parse JSONObject to DSLElement
|
|
130
|
+
*/
|
|
131
|
+
fun parseDSLElement(obj: JSONObject): DSLElement {
|
|
132
|
+
val children = if (obj.has("children")) {
|
|
133
|
+
val childrenArray = obj.getJSONArray("children")
|
|
134
|
+
List(childrenArray.length()) { i ->
|
|
135
|
+
parseDSLElement(childrenArray.getJSONObject(i))
|
|
136
|
+
}
|
|
137
|
+
} else null
|
|
233
138
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
139
|
+
val modifier = if (obj.has("modifier")) {
|
|
140
|
+
val modObj = obj.getJSONObject("modifier")
|
|
141
|
+
DSLModifier(
|
|
142
|
+
fillMaxSize = modObj.optBoolean("fillMaxSize"),
|
|
143
|
+
fillMaxWidth = modObj.optBoolean("fillMaxWidth"),
|
|
144
|
+
fillMaxHeight = modObj.optBoolean("fillMaxHeight"),
|
|
145
|
+
padding = if (modObj.has("padding")) modObj.getInt("padding") else null,
|
|
146
|
+
paddingHorizontal = if (modObj.has("paddingHorizontal")) modObj.getInt("paddingHorizontal") else null,
|
|
147
|
+
paddingVertical = if (modObj.has("paddingVertical")) modObj.getInt("paddingVertical") else null,
|
|
148
|
+
size = if (modObj.has("size")) modObj.getInt("size") else null,
|
|
149
|
+
height = if (modObj.has("height")) modObj.getInt("height") else null,
|
|
150
|
+
width = if (modObj.has("width")) modObj.getInt("width") else null,
|
|
151
|
+
weight = if (modObj.has("weight")) modObj.getDouble("weight").toFloat() else null
|
|
152
|
+
)
|
|
153
|
+
} else null
|
|
154
|
+
|
|
155
|
+
return DSLElement(
|
|
156
|
+
type = obj.getString("type"),
|
|
157
|
+
text = if (obj.has("text")) obj.getString("text") else null,
|
|
158
|
+
style = if (obj.has("style")) obj.getString("style") else null,
|
|
159
|
+
color = if (obj.has("color")) obj.getString("color") else null,
|
|
160
|
+
modifier = modifier,
|
|
161
|
+
horizontalAlignment = if (obj.has("horizontalAlignment")) obj.getString("horizontalAlignment") else null,
|
|
162
|
+
verticalArrangement = if (obj.has("verticalArrangement")) obj.getString("verticalArrangement") else null,
|
|
163
|
+
contentAlignment = if (obj.has("contentAlignment")) obj.getString("contentAlignment") else null,
|
|
164
|
+
height = if (obj.has("height")) obj.getInt("height") else null,
|
|
165
|
+
width = if (obj.has("width")) obj.getInt("width") else null,
|
|
166
|
+
onClick = if (obj.has("onClick")) obj.getString("onClick") else null,
|
|
167
|
+
enabled = obj.optBoolean("enabled", true),
|
|
168
|
+
imageVector = if (obj.has("imageVector")) obj.getString("imageVector") else null,
|
|
169
|
+
tint = if (obj.has("tint")) obj.getString("tint") else null,
|
|
170
|
+
contentDescription = if (obj.has("contentDescription")) obj.getString("contentDescription") else null,
|
|
171
|
+
children = children
|
|
172
|
+
)
|
|
250
173
|
}
|
|
251
|
-
async function generateGitignore(projectPath) {
|
|
252
|
-
const content = `# Build
|
|
253
|
-
/build
|
|
254
|
-
/app/build
|
|
255
|
-
.gradle
|
|
256
|
-
*.hprof
|
|
257
174
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
.vscode
|
|
262
|
-
.DS_Store
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// DSL Interpreter
|
|
177
|
+
// ============================================================================
|
|
263
178
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
179
|
+
/**
|
|
180
|
+
* DSL Interpreter
|
|
181
|
+
* Converts JSON DSL to Compose UI at runtime
|
|
182
|
+
*/
|
|
183
|
+
object DSLInterpreter {
|
|
184
|
+
private const val TAG = "DSLInterpreter"
|
|
267
185
|
|
|
268
|
-
|
|
269
|
-
|
|
186
|
+
private val _currentDSL = MutableStateFlow<UIDefinition?>(null)
|
|
187
|
+
val currentDSL: StateFlow<UIDefinition?> = _currentDSL
|
|
270
188
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
.cxx
|
|
189
|
+
/**
|
|
190
|
+
* Update the current DSL definition
|
|
191
|
+
*/
|
|
192
|
+
fun updateDSL(jsonString: String) {
|
|
193
|
+
try {
|
|
194
|
+
val definition = parseUIDefinition(jsonString)
|
|
195
|
+
_currentDSL.value = definition
|
|
196
|
+
Log.d(TAG, "DSL updated successfully")
|
|
197
|
+
} catch (e: Exception) {
|
|
198
|
+
Log.e(TAG, "Failed to parse DSL: \${e.message}", e)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
284
201
|
|
|
285
|
-
|
|
286
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Render DSL as Compose UI
|
|
204
|
+
*/
|
|
205
|
+
@Composable
|
|
206
|
+
fun RenderDSL(definition: UIDefinition) {
|
|
207
|
+
RenderElement(definition.screen)
|
|
208
|
+
}
|
|
287
209
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Render individual DSL element
|
|
212
|
+
*/
|
|
213
|
+
@Composable
|
|
214
|
+
fun RenderElement(element: DSLElement) {
|
|
215
|
+
when (element.type) {
|
|
216
|
+
"Column" -> RenderColumn(element)
|
|
217
|
+
"Row" -> RenderRow(element)
|
|
218
|
+
"Box" -> RenderBox(element)
|
|
219
|
+
"Text" -> RenderText(element)
|
|
220
|
+
"Button" -> RenderButton(element)
|
|
221
|
+
"Spacer" -> RenderSpacer(element)
|
|
222
|
+
else -> {
|
|
223
|
+
Log.w(TAG, "Unknown element type: \${element.type}")
|
|
224
|
+
Text("Unsupported: \${element.type}", color = Color.Red)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
295
228
|
|
|
296
|
-
|
|
229
|
+
@Composable
|
|
230
|
+
private fun RenderColumn(element: DSLElement) {
|
|
231
|
+
Column(
|
|
232
|
+
modifier = parseModifier(element.modifier),
|
|
233
|
+
horizontalAlignment = parseHorizontalAlignment(element.horizontalAlignment),
|
|
234
|
+
verticalArrangement = parseVerticalArrangement(element.verticalArrangement)
|
|
235
|
+
) {
|
|
236
|
+
element.children?.forEach { child ->
|
|
237
|
+
RenderElement(child)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
297
241
|
|
|
298
|
-
|
|
242
|
+
@Composable
|
|
243
|
+
private fun RenderRow(element: DSLElement) {
|
|
244
|
+
Row(
|
|
245
|
+
modifier = parseModifier(element.modifier),
|
|
246
|
+
verticalAlignment = parseVerticalAlignment(element.horizontalAlignment),
|
|
247
|
+
horizontalArrangement = parseHorizontalArrangement(element.verticalArrangement)
|
|
248
|
+
) {
|
|
249
|
+
element.children?.forEach { child ->
|
|
250
|
+
RenderElement(child)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
299
254
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
255
|
+
@Composable
|
|
256
|
+
private fun RenderBox(element: DSLElement) {
|
|
257
|
+
Box(
|
|
258
|
+
modifier = parseModifier(element.modifier),
|
|
259
|
+
contentAlignment = parseContentAlignment(element.contentAlignment)
|
|
260
|
+
) {
|
|
261
|
+
element.children?.forEach { child ->
|
|
262
|
+
RenderElement(child)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
303
266
|
|
|
304
|
-
|
|
305
|
-
|
|
267
|
+
@Composable
|
|
268
|
+
private fun RenderText(element: DSLElement) {
|
|
269
|
+
Text(
|
|
270
|
+
text = element.text ?: "",
|
|
271
|
+
style = parseTextStyle(element.style),
|
|
272
|
+
color = parseColor(element.color) ?: Color.Unspecified,
|
|
273
|
+
modifier = parseModifier(element.modifier)
|
|
274
|
+
)
|
|
275
|
+
}
|
|
306
276
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
277
|
+
@Composable
|
|
278
|
+
private fun RenderButton(element: DSLElement) {
|
|
279
|
+
Button(
|
|
280
|
+
onClick = { handleClick(element.onClick, element.text) },
|
|
281
|
+
modifier = parseModifier(element.modifier),
|
|
282
|
+
enabled = element.enabled ?: true
|
|
283
|
+
) {
|
|
284
|
+
Text(element.text ?: "Button")
|
|
285
|
+
}
|
|
286
|
+
}
|
|
310
287
|
|
|
311
|
-
|
|
288
|
+
@Composable
|
|
289
|
+
private fun RenderSpacer(element: DSLElement) {
|
|
290
|
+
Spacer(
|
|
291
|
+
modifier = Modifier
|
|
292
|
+
.height(element.height?.dp ?: 0.dp)
|
|
293
|
+
.width(element.width?.dp ?: 0.dp)
|
|
294
|
+
)
|
|
295
|
+
}
|
|
312
296
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
│ ├── java/ # Kotlin source files
|
|
319
|
-
│ └── res/ # Resources
|
|
320
|
-
├── jetstart.config.json # JetStart configuration
|
|
321
|
-
└── build.gradle # Gradle build file
|
|
322
|
-
\`\`\`
|
|
297
|
+
/**
|
|
298
|
+
* Parse DSL modifier to Compose Modifier
|
|
299
|
+
*/
|
|
300
|
+
private fun parseModifier(dslModifier: DSLModifier?): Modifier {
|
|
301
|
+
var modifier: Modifier = Modifier
|
|
323
302
|
|
|
324
|
-
|
|
303
|
+
dslModifier?.let { m ->
|
|
304
|
+
if (m.fillMaxSize == true) modifier = modifier.fillMaxSize()
|
|
305
|
+
if (m.fillMaxWidth == true) modifier = modifier.fillMaxWidth()
|
|
306
|
+
if (m.fillMaxHeight == true) modifier = modifier.fillMaxHeight()
|
|
325
307
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'README.md'), content);
|
|
330
|
-
}
|
|
331
|
-
async function generateRootBuildGradle(projectPath) {
|
|
332
|
-
const content = `// Top-level build file
|
|
333
|
-
buildscript {
|
|
334
|
-
ext {
|
|
335
|
-
kotlin_version = '1.9.21'
|
|
336
|
-
compose_version = '1.5.4'
|
|
337
|
-
}
|
|
338
|
-
repositories {
|
|
339
|
-
google()
|
|
340
|
-
mavenCentral()
|
|
341
|
-
}
|
|
342
|
-
dependencies {
|
|
343
|
-
classpath 'com.android.tools.build:gradle:8.2.0'
|
|
344
|
-
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
345
|
-
}
|
|
346
|
-
}
|
|
308
|
+
m.padding?.let { modifier = modifier.padding(it.dp) }
|
|
309
|
+
m.paddingHorizontal?.let { modifier = modifier.padding(horizontal = it.dp) }
|
|
310
|
+
m.paddingVertical?.let { modifier = modifier.padding(vertical = it.dp) }
|
|
347
311
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
mavenCentral()
|
|
352
|
-
}
|
|
353
|
-
}
|
|
312
|
+
m.size?.let { modifier = modifier.size(it.dp) }
|
|
313
|
+
m.height?.let { modifier = modifier.height(it.dp) }
|
|
314
|
+
m.width?.let { modifier = modifier.width(it.dp) }
|
|
354
315
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}`;
|
|
358
|
-
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'build.gradle'), content);
|
|
359
|
-
}
|
|
360
|
-
async function generateGradleWrapper(projectPath) {
|
|
361
|
-
// Use system Gradle to initialize proper wrapper
|
|
362
|
-
// This generates:
|
|
363
|
-
// - gradle/wrapper/gradle-wrapper.jar
|
|
364
|
-
// - gradle/wrapper/gradle-wrapper.properties
|
|
365
|
-
// - gradlew (Unix shell script)
|
|
366
|
-
// - gradlew.bat (Windows batch script)
|
|
367
|
-
return new Promise((resolve) => {
|
|
368
|
-
// Try to use system gradle to generate wrapper
|
|
369
|
-
const gradleCmd = process.platform === 'win32' ? 'gradle.bat' : 'gradle';
|
|
370
|
-
const gradleProcess = (0, child_process_1.spawn)(gradleCmd, ['wrapper', '--gradle-version', '8.2'], {
|
|
371
|
-
cwd: projectPath,
|
|
372
|
-
shell: true,
|
|
373
|
-
});
|
|
374
|
-
gradleProcess.on('close', (code) => {
|
|
375
|
-
// Continue regardless of success/failure
|
|
376
|
-
// If gradle wrapper command fails, the build will fall back to system gradle
|
|
377
|
-
resolve();
|
|
378
|
-
});
|
|
379
|
-
gradleProcess.on('error', () => {
|
|
380
|
-
// Continue even if gradle command not found
|
|
381
|
-
resolve();
|
|
382
|
-
});
|
|
383
|
-
// Timeout after 30 seconds
|
|
384
|
-
setTimeout(() => {
|
|
385
|
-
gradleProcess.kill();
|
|
386
|
-
resolve();
|
|
387
|
-
}, 30000);
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
async function generateResourceFiles(projectPath, projectName) {
|
|
391
|
-
// Generate strings.xml
|
|
392
|
-
const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
393
|
-
<resources>
|
|
394
|
-
<string name="app_name">${projectName}</string>
|
|
395
|
-
</resources>`;
|
|
396
|
-
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/res/values/strings.xml'), stringsXml);
|
|
397
|
-
// Generate colors.xml
|
|
398
|
-
const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
399
|
-
<resources>
|
|
400
|
-
<color name="purple_200">#FFBB86FC</color>
|
|
401
|
-
<color name="purple_500">#FF6200EE</color>
|
|
402
|
-
<color name="purple_700">#FF3700B3</color>
|
|
403
|
-
<color name="teal_200">#FF03DAC5</color>
|
|
404
|
-
<color name="teal_700">#FF018786</color>
|
|
405
|
-
<color name="black">#FF000000</color>
|
|
406
|
-
<color name="white">#FFFFFFFF</color>
|
|
407
|
-
</resources>`;
|
|
408
|
-
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/res/values/colors.xml'), colorsXml);
|
|
409
|
-
// Generate themes.xml
|
|
410
|
-
const themesXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
411
|
-
<resources>
|
|
412
|
-
<style name="Theme.${projectName.replace(/[^a-zA-Z0-9]/g, '')}" parent="android:Theme.Material.Light.NoActionBar" />
|
|
413
|
-
</resources>`;
|
|
414
|
-
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/res/values/themes.xml'), themesXml);
|
|
415
|
-
// Generate network_security_config.xml for development (allows cleartext traffic)
|
|
416
|
-
const networkSecurityConfig = `<?xml version="1.0" encoding="utf-8"?>
|
|
417
|
-
<network-security-config>
|
|
418
|
-
<base-config cleartextTrafficPermitted="true">
|
|
419
|
-
<trust-anchors>
|
|
420
|
-
<certificates src="system" />
|
|
421
|
-
</trust-anchors>
|
|
422
|
-
</base-config>
|
|
423
|
-
</network-security-config>`;
|
|
424
|
-
await fs_extra_1.default.ensureDir(path_1.default.join(projectPath, 'app/src/main/res/xml'));
|
|
425
|
-
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/res/xml/network_security_config.xml'), networkSecurityConfig);
|
|
426
|
-
}
|
|
427
|
-
async function generateLocalProperties(projectPath) {
|
|
428
|
-
// Auto-detect Android SDK location
|
|
429
|
-
let androidSdkPath;
|
|
430
|
-
// Check environment variables first
|
|
431
|
-
androidSdkPath = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
432
|
-
// If not found, check common Windows locations
|
|
433
|
-
if (!androidSdkPath && process.platform === 'win32') {
|
|
434
|
-
const commonPaths = [
|
|
435
|
-
'C:\\Android',
|
|
436
|
-
path_1.default.join(require('os').homedir(), 'AppData', 'Local', 'Android', 'Sdk'),
|
|
437
|
-
'C:\\Android\\Sdk',
|
|
438
|
-
'C:\\Program Files (x86)\\Android\\android-sdk',
|
|
439
|
-
];
|
|
440
|
-
for (const p of commonPaths) {
|
|
441
|
-
if (fs_extra_1.default.existsSync(p)) {
|
|
442
|
-
androidSdkPath = p;
|
|
443
|
-
break;
|
|
444
|
-
}
|
|
316
|
+
// Note: weight() is only available in RowScope/ColumnScope
|
|
317
|
+
// We'll handle it separately when needed
|
|
445
318
|
}
|
|
319
|
+
|
|
320
|
+
return modifier
|
|
446
321
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Parse alignment strings
|
|
325
|
+
*/
|
|
326
|
+
private fun parseHorizontalAlignment(alignment: String?): Alignment.Horizontal {
|
|
327
|
+
return when (alignment?.lowercase()) {
|
|
328
|
+
"start" -> Alignment.Start
|
|
329
|
+
"centerhorizontally", "center" -> Alignment.CenterHorizontally
|
|
330
|
+
"end" -> Alignment.End
|
|
331
|
+
else -> Alignment.Start
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private fun parseVerticalAlignment(alignment: String?): Alignment.Vertical {
|
|
336
|
+
return when (alignment?.lowercase()) {
|
|
337
|
+
"top" -> Alignment.Top
|
|
338
|
+
"centervertically", "center" -> Alignment.CenterVertically
|
|
339
|
+
"bottom" -> Alignment.Bottom
|
|
340
|
+
else -> Alignment.Top
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private fun parseContentAlignment(alignment: String?): Alignment {
|
|
345
|
+
return when (alignment?.lowercase()) {
|
|
346
|
+
"center" -> Alignment.Center
|
|
347
|
+
"topcenter" -> Alignment.TopCenter
|
|
348
|
+
"topstart" -> Alignment.TopStart
|
|
349
|
+
"topend" -> Alignment.TopEnd
|
|
350
|
+
"bottomcenter" -> Alignment.BottomCenter
|
|
351
|
+
"bottomstart" -> Alignment.BottomStart
|
|
352
|
+
"bottomend" -> Alignment.BottomEnd
|
|
353
|
+
"centerstart" -> Alignment.CenterStart
|
|
354
|
+
"centerend" -> Alignment.CenterEnd
|
|
355
|
+
else -> Alignment.TopStart
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private fun parseVerticalArrangement(arrangement: String?): Arrangement.Vertical {
|
|
360
|
+
return when (arrangement?.lowercase()) {
|
|
361
|
+
"top" -> Arrangement.Top
|
|
362
|
+
"center" -> Arrangement.Center
|
|
363
|
+
"bottom" -> Arrangement.Bottom
|
|
364
|
+
"spacebetween" -> Arrangement.SpaceBetween
|
|
365
|
+
"spacearound" -> Arrangement.SpaceAround
|
|
366
|
+
"spaceevenly" -> Arrangement.SpaceEvenly
|
|
367
|
+
else -> Arrangement.Top
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private fun parseHorizontalArrangement(arrangement: String?): Arrangement.Horizontal {
|
|
372
|
+
return when (arrangement?.lowercase()) {
|
|
373
|
+
"start" -> Arrangement.Start
|
|
374
|
+
"center" -> Arrangement.Center
|
|
375
|
+
"end" -> Arrangement.End
|
|
376
|
+
"spacebetween" -> Arrangement.SpaceBetween
|
|
377
|
+
"spacearound" -> Arrangement.SpaceAround
|
|
378
|
+
"spaceevenly" -> Arrangement.SpaceEvenly
|
|
379
|
+
else -> Arrangement.Start
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Parse text style
|
|
385
|
+
*/
|
|
386
|
+
@Composable
|
|
387
|
+
private fun parseTextStyle(style: String?): androidx.compose.ui.text.TextStyle {
|
|
388
|
+
return when (style?.lowercase()) {
|
|
389
|
+
"headlinelarge" -> MaterialTheme.typography.headlineLarge
|
|
390
|
+
"headlinemedium" -> MaterialTheme.typography.headlineMedium
|
|
391
|
+
"headlinesmall" -> MaterialTheme.typography.headlineSmall
|
|
392
|
+
"titlelarge" -> MaterialTheme.typography.titleLarge
|
|
393
|
+
"titlemedium" -> MaterialTheme.typography.titleMedium
|
|
394
|
+
"titlesmall" -> MaterialTheme.typography.titleSmall
|
|
395
|
+
"bodylarge" -> MaterialTheme.typography.bodyLarge
|
|
396
|
+
"bodymedium" -> MaterialTheme.typography.bodyMedium
|
|
397
|
+
"bodysmall" -> MaterialTheme.typography.bodySmall
|
|
398
|
+
"labellarge" -> MaterialTheme.typography.labelLarge
|
|
399
|
+
"labelmedium" -> MaterialTheme.typography.labelMedium
|
|
400
|
+
"labelsmall" -> MaterialTheme.typography.labelSmall
|
|
401
|
+
else -> MaterialTheme.typography.bodyMedium
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Parse color from string
|
|
407
|
+
*/
|
|
408
|
+
private fun parseColor(colorString: String?): Color? {
|
|
409
|
+
if (colorString == null) return null
|
|
410
|
+
|
|
411
|
+
return try {
|
|
412
|
+
when {
|
|
413
|
+
colorString.startsWith("#") -> {
|
|
414
|
+
// Hex color
|
|
415
|
+
Color(android.graphics.Color.parseColor(colorString))
|
|
416
|
+
}
|
|
417
|
+
else -> null
|
|
458
418
|
}
|
|
419
|
+
} catch (e: Exception) {
|
|
420
|
+
Log.w(TAG, "Failed to parse color: \$colorString")
|
|
421
|
+
null
|
|
459
422
|
}
|
|
460
423
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Handle click events
|
|
427
|
+
*/
|
|
428
|
+
private fun handleClick(action: String?, text: String?) {
|
|
429
|
+
if (action != null) {
|
|
430
|
+
Log.d(TAG, "Button clicked: \$action")
|
|
431
|
+
|
|
432
|
+
// Send click event to dev server
|
|
433
|
+
sendClickEvent(action, "Button", text)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Send click event to dev server via WebSocket
|
|
439
|
+
*/
|
|
440
|
+
private fun sendClickEvent(action: String, elementType: String, elementText: String?) {
|
|
441
|
+
try {
|
|
442
|
+
val ws = HotReload.getWebSocket()
|
|
443
|
+
if (ws != null) {
|
|
444
|
+
val message = JSONObject().apply {
|
|
445
|
+
put("type", "client:click")
|
|
446
|
+
put("timestamp", System.currentTimeMillis())
|
|
447
|
+
put("action", action)
|
|
448
|
+
put("elementType", elementType)
|
|
449
|
+
elementText?.let { put("elementText", it) }
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
ws.send(message.toString())
|
|
453
|
+
Log.d(TAG, "Sent click event to server: \$action")
|
|
454
|
+
} else {
|
|
455
|
+
Log.w(TAG, "WebSocket not available, cannot send click event")
|
|
456
|
+
}
|
|
457
|
+
} catch (e: Exception) {
|
|
458
|
+
Log.e(TAG, "Failed to send click event: \${e.message}")
|
|
459
|
+
}
|
|
464
460
|
}
|
|
465
|
-
// Create local.properties with SDK path
|
|
466
|
-
const content = `# Auto-generated by JetStart
|
|
467
|
-
sdk.dir=${androidSdkPath.replace(/\\/g, '\\\\')}
|
|
468
|
-
`;
|
|
469
|
-
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'local.properties'), content);
|
|
470
|
-
console.log(`[JetStart] Created local.properties with SDK: ${androidSdkPath}`);
|
|
471
461
|
}
|
|
472
|
-
async function generateHotReload(projectPath, packageName) {
|
|
473
|
-
const packagePath = packageName.replace(/\./g, '/');
|
|
474
|
-
const hotReloadPath = path_1.default.join(projectPath, 'app/src/main/java', packagePath, 'HotReload.kt');
|
|
475
|
-
const content = `package ${packageName}
|
|
476
462
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
import android.os.Build
|
|
481
|
-
import android.util.Log
|
|
482
|
-
import androidx.core.content.FileProvider
|
|
483
|
-
import okhttp3.*
|
|
484
|
-
import org.json.JSONObject
|
|
485
|
-
import java.io.File
|
|
486
|
-
import java.io.IOException
|
|
463
|
+
// ============================================================================
|
|
464
|
+
// Hot Reload Manager
|
|
465
|
+
// ============================================================================
|
|
487
466
|
|
|
488
467
|
/**
|
|
489
468
|
* Hot Reload Manager
|
|
@@ -497,6 +476,11 @@ object HotReload {
|
|
|
497
476
|
private var ignoreFirstBuild = true
|
|
498
477
|
private val httpClient = OkHttpClient()
|
|
499
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Get the current WebSocket connection (for sending messages)
|
|
481
|
+
*/
|
|
482
|
+
fun getWebSocket(): WebSocket? = webSocket
|
|
483
|
+
|
|
500
484
|
fun connect(activity: Activity, serverUrl: String, sessionId: String) {
|
|
501
485
|
this.activity = activity
|
|
502
486
|
|
|
@@ -693,398 +677,443 @@ object HotReload {
|
|
|
693
677
|
}
|
|
694
678
|
}
|
|
695
679
|
`;
|
|
696
|
-
await fs_extra_1.default.ensureDir(path_1.default.dirname(
|
|
697
|
-
await fs_extra_1.default.writeFile(
|
|
680
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(jetStartPath));
|
|
681
|
+
await fs_extra_1.default.writeFile(jetStartPath, content);
|
|
698
682
|
}
|
|
699
|
-
async function
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
import androidx.compose.ui.Modifier
|
|
710
|
-
import androidx.compose.ui.graphics.Color
|
|
711
|
-
import androidx.compose.ui.text.font.FontWeight
|
|
712
|
-
import androidx.compose.ui.unit.dp
|
|
713
|
-
import kotlinx.coroutines.flow.MutableStateFlow
|
|
714
|
-
import kotlinx.coroutines.flow.StateFlow
|
|
715
|
-
|
|
716
|
-
/**
|
|
717
|
-
* DSL Interpreter
|
|
718
|
-
* Converts JSON DSL to Compose UI at runtime
|
|
719
|
-
*/
|
|
720
|
-
object DSLInterpreter {
|
|
721
|
-
private const val TAG = "DSLInterpreter"
|
|
722
|
-
|
|
723
|
-
private val _currentDSL = MutableStateFlow<UIDefinition?>(null)
|
|
724
|
-
val currentDSL: StateFlow<UIDefinition?> = _currentDSL
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* Update the current DSL definition
|
|
728
|
-
*/
|
|
729
|
-
fun updateDSL(jsonString: String) {
|
|
730
|
-
try {
|
|
731
|
-
val definition = parseUIDefinition(jsonString)
|
|
732
|
-
_currentDSL.value = definition
|
|
733
|
-
Log.d(TAG, "DSL updated successfully")
|
|
734
|
-
} catch (e: Exception) {
|
|
735
|
-
Log.e(TAG, "Failed to parse DSL: \${e.message}", e)
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
/**
|
|
740
|
-
* Render DSL as Compose UI
|
|
741
|
-
*/
|
|
742
|
-
@Composable
|
|
743
|
-
fun RenderDSL(definition: UIDefinition) {
|
|
744
|
-
RenderElement(definition.screen)
|
|
683
|
+
async function createDirectoryStructure(projectPath) {
|
|
684
|
+
const dirs = [
|
|
685
|
+
'app/src/main/java',
|
|
686
|
+
'app/src/main/res/layout',
|
|
687
|
+
'app/src/main/res/values',
|
|
688
|
+
'app/src/main/res/drawable',
|
|
689
|
+
'gradle/wrapper',
|
|
690
|
+
];
|
|
691
|
+
for (const dir of dirs) {
|
|
692
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(projectPath, dir));
|
|
745
693
|
}
|
|
694
|
+
}
|
|
695
|
+
async function generateBuildGradle(projectPath, options) {
|
|
696
|
+
const content = `plugins {
|
|
697
|
+
id 'com.android.application'
|
|
698
|
+
id 'org.jetbrains.kotlin.android'
|
|
699
|
+
}
|
|
746
700
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
@Composable
|
|
751
|
-
fun RenderElement(element: DSLElement) {
|
|
752
|
-
when (element.type) {
|
|
753
|
-
"Column" -> RenderColumn(element)
|
|
754
|
-
"Row" -> RenderRow(element)
|
|
755
|
-
"Box" -> RenderBox(element)
|
|
756
|
-
"Text" -> RenderText(element)
|
|
757
|
-
"Button" -> RenderButton(element)
|
|
758
|
-
"Spacer" -> RenderSpacer(element)
|
|
759
|
-
else -> {
|
|
760
|
-
Log.w(TAG, "Unknown element type: \${element.type}")
|
|
761
|
-
Text("Unsupported: \${element.type}", color = Color.Red)
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
701
|
+
android {
|
|
702
|
+
namespace '${options.packageName}'
|
|
703
|
+
compileSdk ${shared_1.TARGET_ANDROID_API_LEVEL}
|
|
765
704
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
) {
|
|
773
|
-
element.children?.forEach { child ->
|
|
774
|
-
RenderElement(child)
|
|
775
|
-
}
|
|
776
|
-
}
|
|
705
|
+
defaultConfig {
|
|
706
|
+
applicationId "${options.packageName}"
|
|
707
|
+
minSdk ${shared_1.MIN_ANDROID_API_LEVEL}
|
|
708
|
+
targetSdk ${shared_1.TARGET_ANDROID_API_LEVEL}
|
|
709
|
+
versionCode 1
|
|
710
|
+
versionName "1.0.0"
|
|
777
711
|
}
|
|
778
712
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
modifier = parseModifier(element.modifier),
|
|
783
|
-
verticalAlignment = parseVerticalAlignment(element.horizontalAlignment),
|
|
784
|
-
horizontalArrangement = parseHorizontalArrangement(element.verticalArrangement)
|
|
785
|
-
) {
|
|
786
|
-
element.children?.forEach { child ->
|
|
787
|
-
RenderElement(child)
|
|
788
|
-
}
|
|
713
|
+
buildTypes {
|
|
714
|
+
release {
|
|
715
|
+
minifyEnabled false
|
|
789
716
|
}
|
|
790
717
|
}
|
|
791
718
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
modifier = parseModifier(element.modifier),
|
|
796
|
-
contentAlignment = parseContentAlignment(element.contentAlignment)
|
|
797
|
-
) {
|
|
798
|
-
element.children?.forEach { child ->
|
|
799
|
-
RenderElement(child)
|
|
800
|
-
}
|
|
801
|
-
}
|
|
719
|
+
compileOptions {
|
|
720
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
721
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
802
722
|
}
|
|
803
723
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
Text(
|
|
807
|
-
text = element.text ?: "",
|
|
808
|
-
style = parseTextStyle(element.style),
|
|
809
|
-
color = parseColor(element.color) ?: Color.Unspecified,
|
|
810
|
-
modifier = parseModifier(element.modifier)
|
|
811
|
-
)
|
|
724
|
+
kotlinOptions {
|
|
725
|
+
jvmTarget = '17'
|
|
812
726
|
}
|
|
813
727
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
onClick = { handleClick(element.onClick) },
|
|
818
|
-
modifier = parseModifier(element.modifier),
|
|
819
|
-
enabled = element.enabled ?: true
|
|
820
|
-
) {
|
|
821
|
-
Text(element.text ?: "Button")
|
|
822
|
-
}
|
|
728
|
+
buildFeatures {
|
|
729
|
+
compose true
|
|
730
|
+
buildConfig true // Required for JetStart hot reload
|
|
823
731
|
}
|
|
824
732
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
Spacer(
|
|
828
|
-
modifier = Modifier
|
|
829
|
-
.height(element.height?.dp ?: 0.dp)
|
|
830
|
-
.width(element.width?.dp ?: 0.dp)
|
|
831
|
-
)
|
|
733
|
+
composeOptions {
|
|
734
|
+
kotlinCompilerExtensionVersion = '1.5.6'
|
|
832
735
|
}
|
|
736
|
+
}
|
|
833
737
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
738
|
+
dependencies {
|
|
739
|
+
implementation 'androidx.core:core-ktx:1.12.0'
|
|
740
|
+
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
|
|
741
|
+
implementation 'androidx.activity:activity-compose:1.8.1'
|
|
742
|
+
implementation platform('androidx.compose:compose-bom:2023.10.01')
|
|
743
|
+
implementation 'androidx.compose.ui:ui'
|
|
744
|
+
implementation 'androidx.compose.ui:ui-graphics'
|
|
745
|
+
implementation 'androidx.compose.ui:ui-tooling-preview'
|
|
746
|
+
implementation 'androidx.compose.material3:material3'
|
|
839
747
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
748
|
+
// JetStart Hot Reload dependencies
|
|
749
|
+
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
|
750
|
+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
|
751
|
+
}`;
|
|
752
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/build.gradle'), content);
|
|
753
|
+
}
|
|
754
|
+
async function generateSettingsGradle(projectPath, projectName) {
|
|
755
|
+
const content = `rootProject.name = "${projectName}"
|
|
756
|
+
include ':app'`;
|
|
757
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'settings.gradle'), content);
|
|
758
|
+
}
|
|
759
|
+
async function generateGradleProperties(projectPath) {
|
|
760
|
+
const content = `org.gradle.jvmargs=-Xmx2048m
|
|
761
|
+
android.useAndroidX=true
|
|
762
|
+
kotlin.code.style=official`;
|
|
763
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'gradle.properties'), content);
|
|
764
|
+
}
|
|
765
|
+
async function generateMainActivity(projectPath, packageName) {
|
|
766
|
+
const packagePath = packageName.replace(/\./g, '/');
|
|
767
|
+
const activityPath = path_1.default.join(projectPath, 'app/src/main/java', packagePath, 'MainActivity.kt');
|
|
768
|
+
const content = `package ${packageName}
|
|
844
769
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
770
|
+
import android.os.Bundle
|
|
771
|
+
import androidx.activity.ComponentActivity
|
|
772
|
+
import androidx.activity.compose.setContent
|
|
773
|
+
import androidx.compose.foundation.layout.*
|
|
774
|
+
import androidx.compose.material3.*
|
|
775
|
+
import androidx.compose.runtime.*
|
|
776
|
+
import androidx.compose.ui.Alignment
|
|
777
|
+
import androidx.compose.ui.Modifier
|
|
778
|
+
import androidx.compose.ui.unit.dp
|
|
848
779
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
780
|
+
class MainActivity : ComponentActivity() {
|
|
781
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
782
|
+
super.onCreate(savedInstanceState)
|
|
852
783
|
|
|
853
|
-
|
|
854
|
-
|
|
784
|
+
// Initialize hot reload - reads from BuildConfig injected by jetstart dev
|
|
785
|
+
try {
|
|
786
|
+
val serverUrl = BuildConfig.JETSTART_SERVER_URL
|
|
787
|
+
val sessionId = BuildConfig.JETSTART_SESSION_ID
|
|
788
|
+
HotReload.connect(this, serverUrl, sessionId)
|
|
789
|
+
} catch (e: Exception) {
|
|
790
|
+
// BuildConfig not available yet, hot reload will be disabled
|
|
791
|
+
android.util.Log.w("MainActivity", "Hot reload not configured: \${e.message}")
|
|
855
792
|
}
|
|
856
793
|
|
|
857
|
-
|
|
858
|
-
|
|
794
|
+
setContent {
|
|
795
|
+
MaterialTheme {
|
|
796
|
+
Surface(
|
|
797
|
+
modifier = Modifier.fillMaxSize(),
|
|
798
|
+
color = MaterialTheme.colorScheme.background
|
|
799
|
+
) {
|
|
800
|
+
// Check if we should render from DSL (hot reload mode)
|
|
801
|
+
val dsl by DSLInterpreter.currentDSL.collectAsState()
|
|
859
802
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
803
|
+
if (dsl != null) {
|
|
804
|
+
// Hot reload mode: render from DSL sent by server
|
|
805
|
+
DSLInterpreter.RenderDSL(dsl!!)
|
|
806
|
+
} else {
|
|
807
|
+
// Normal mode: render actual Compose code
|
|
808
|
+
AppContent()
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
869
812
|
}
|
|
870
813
|
}
|
|
871
814
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
"centervertically", "center" -> Alignment.CenterVertically
|
|
876
|
-
"bottom" -> Alignment.Bottom
|
|
877
|
-
else -> Alignment.Top
|
|
878
|
-
}
|
|
815
|
+
override fun onDestroy() {
|
|
816
|
+
super.onDestroy()
|
|
817
|
+
HotReload.disconnect()
|
|
879
818
|
}
|
|
819
|
+
}
|
|
880
820
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
821
|
+
/**
|
|
822
|
+
* Main App Content - REAL Kotlin Compose Code!
|
|
823
|
+
* This gets parsed to DSL and sent via hot reload
|
|
824
|
+
*/
|
|
825
|
+
@Composable
|
|
826
|
+
fun AppContent() {
|
|
827
|
+
Column(
|
|
828
|
+
modifier = Modifier
|
|
829
|
+
.fillMaxSize()
|
|
830
|
+
.padding(16.dp),
|
|
831
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
832
|
+
verticalArrangement = Arrangement.Center
|
|
833
|
+
) {
|
|
834
|
+
Text(
|
|
835
|
+
text = "Welcome to JetStart! 🚀",
|
|
836
|
+
style = MaterialTheme.typography.headlineMedium
|
|
837
|
+
)
|
|
895
838
|
|
|
896
|
-
|
|
897
|
-
return when (arrangement?.lowercase()) {
|
|
898
|
-
"top" -> Arrangement.Top
|
|
899
|
-
"center" -> Arrangement.Center
|
|
900
|
-
"bottom" -> Arrangement.Bottom
|
|
901
|
-
"spacebetween" -> Arrangement.SpaceBetween
|
|
902
|
-
"spacearound" -> Arrangement.SpaceAround
|
|
903
|
-
"spaceevenly" -> Arrangement.SpaceEvenly
|
|
904
|
-
else -> Arrangement.Top
|
|
905
|
-
}
|
|
906
|
-
}
|
|
839
|
+
Spacer(modifier = Modifier.height(16.dp))
|
|
907
840
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
"end" -> Arrangement.End
|
|
913
|
-
"spacebetween" -> Arrangement.SpaceBetween
|
|
914
|
-
"spacearound" -> Arrangement.SpaceAround
|
|
915
|
-
"spaceevenly" -> Arrangement.SpaceEvenly
|
|
916
|
-
else -> Arrangement.Start
|
|
917
|
-
}
|
|
918
|
-
}
|
|
841
|
+
Text(
|
|
842
|
+
text = "Edit this code and save to see hot reload!",
|
|
843
|
+
style = MaterialTheme.typography.bodyMedium
|
|
844
|
+
)
|
|
919
845
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
"
|
|
927
|
-
"headlinemedium" -> MaterialTheme.typography.headlineMedium
|
|
928
|
-
"headlinesmall" -> MaterialTheme.typography.headlineSmall
|
|
929
|
-
"titlelarge" -> MaterialTheme.typography.titleLarge
|
|
930
|
-
"titlemedium" -> MaterialTheme.typography.titleMedium
|
|
931
|
-
"titlesmall" -> MaterialTheme.typography.titleSmall
|
|
932
|
-
"bodylarge" -> MaterialTheme.typography.bodyLarge
|
|
933
|
-
"bodymedium" -> MaterialTheme.typography.bodyMedium
|
|
934
|
-
"bodysmall" -> MaterialTheme.typography.bodySmall
|
|
935
|
-
"labellarge" -> MaterialTheme.typography.labelLarge
|
|
936
|
-
"labelmedium" -> MaterialTheme.typography.labelMedium
|
|
937
|
-
"labelsmall" -> MaterialTheme.typography.labelSmall
|
|
938
|
-
else -> MaterialTheme.typography.bodyMedium
|
|
846
|
+
Spacer(modifier = Modifier.height(24.dp))
|
|
847
|
+
|
|
848
|
+
Button(
|
|
849
|
+
onClick = { /* Handle click */ },
|
|
850
|
+
modifier = Modifier.fillMaxWidth()
|
|
851
|
+
) {
|
|
852
|
+
Text("Click Me!")
|
|
939
853
|
}
|
|
940
854
|
}
|
|
855
|
+
}`;
|
|
856
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(activityPath));
|
|
857
|
+
await fs_extra_1.default.writeFile(activityPath, content);
|
|
858
|
+
}
|
|
859
|
+
async function generateAndroidManifest(projectPath, options) {
|
|
860
|
+
const themeName = options.projectName.replace(/[^a-zA-Z0-9]/g, '');
|
|
861
|
+
const content = `<?xml version="1.0" encoding="utf-8"?>
|
|
862
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
941
863
|
|
|
942
|
-
|
|
943
|
-
* Parse color from string
|
|
944
|
-
*/
|
|
945
|
-
private fun parseColor(colorString: String?): Color? {
|
|
946
|
-
if (colorString == null) return null
|
|
864
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
947
865
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
866
|
+
<application
|
|
867
|
+
android:allowBackup="true"
|
|
868
|
+
android:label="@string/app_name"
|
|
869
|
+
android:theme="@style/Theme.${themeName}"
|
|
870
|
+
android:networkSecurityConfig="@xml/network_security_config">
|
|
871
|
+
<activity
|
|
872
|
+
android:name=".MainActivity"
|
|
873
|
+
android:exported="true">
|
|
874
|
+
<intent-filter>
|
|
875
|
+
<action android:name="android.intent.action.MAIN" />
|
|
876
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
877
|
+
</intent-filter>
|
|
878
|
+
</activity>
|
|
879
|
+
</application>
|
|
961
880
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
*/
|
|
965
|
-
private fun handleClick(action: String?) {
|
|
966
|
-
if (action != null) {
|
|
967
|
-
Log.d(TAG, "Button clicked: \$action")
|
|
968
|
-
// TODO: Implement action handling
|
|
969
|
-
}
|
|
970
|
-
}
|
|
881
|
+
</manifest>`;
|
|
882
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/AndroidManifest.xml'), content);
|
|
971
883
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
884
|
+
async function generateJetStartConfig(projectPath, options) {
|
|
885
|
+
const config = {
|
|
886
|
+
projectName: options.projectName,
|
|
887
|
+
packageName: options.packageName,
|
|
888
|
+
version: '1.0.0',
|
|
889
|
+
jetstart: {
|
|
890
|
+
version: '0.1.0',
|
|
891
|
+
enableHotReload: true,
|
|
892
|
+
enableLogs: true,
|
|
893
|
+
port: 8765,
|
|
894
|
+
},
|
|
895
|
+
};
|
|
896
|
+
await fs_extra_1.default.writeJSON(path_1.default.join(projectPath, 'jetstart.config.json'), config, { spaces: 2 });
|
|
975
897
|
}
|
|
976
|
-
async function
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
import org.json.JSONObject
|
|
898
|
+
async function generateGitignore(projectPath) {
|
|
899
|
+
const content = `# Build
|
|
900
|
+
/build
|
|
901
|
+
/app/build
|
|
902
|
+
.gradle
|
|
903
|
+
*.hprof
|
|
983
904
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
905
|
+
# IDE
|
|
906
|
+
.idea
|
|
907
|
+
*.iml
|
|
908
|
+
.vscode
|
|
909
|
+
.DS_Store
|
|
988
910
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
)
|
|
911
|
+
# Claude Code
|
|
912
|
+
.claude
|
|
913
|
+
.claude-worktrees
|
|
993
914
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
val text: String? = null,
|
|
997
|
-
val style: String? = null,
|
|
998
|
-
val color: String? = null,
|
|
999
|
-
val modifier: DSLModifier? = null,
|
|
1000
|
-
val horizontalAlignment: String? = null,
|
|
1001
|
-
val verticalArrangement: String? = null,
|
|
1002
|
-
val contentAlignment: String? = null,
|
|
1003
|
-
val height: Int? = null,
|
|
1004
|
-
val width: Int? = null,
|
|
1005
|
-
val onClick: String? = null,
|
|
1006
|
-
val enabled: Boolean? = true,
|
|
1007
|
-
val imageVector: String? = null,
|
|
1008
|
-
val tint: String? = null,
|
|
1009
|
-
val contentDescription: String? = null,
|
|
1010
|
-
val children: List<DSLElement>? = null
|
|
1011
|
-
)
|
|
915
|
+
# JetStart
|
|
916
|
+
.jetstart
|
|
1012
917
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
918
|
+
# Android
|
|
919
|
+
local.properties
|
|
920
|
+
*.apk
|
|
921
|
+
*.aab
|
|
922
|
+
*.ap_
|
|
923
|
+
*.dex
|
|
924
|
+
*.class
|
|
925
|
+
bin/
|
|
926
|
+
gen/
|
|
927
|
+
out/
|
|
928
|
+
captures/
|
|
929
|
+
.externalNativeBuild
|
|
930
|
+
.cxx
|
|
1025
931
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
*/
|
|
1029
|
-
fun parseUIDefinition(json: String): UIDefinition {
|
|
1030
|
-
val obj = JSONObject(json)
|
|
1031
|
-
val version = obj.optString("version", "1.0")
|
|
1032
|
-
val screenObj = obj.getJSONObject("screen")
|
|
932
|
+
# Log files
|
|
933
|
+
*.log
|
|
1033
934
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
)
|
|
935
|
+
# Keystore files
|
|
936
|
+
*.jks
|
|
937
|
+
*.keystore`;
|
|
938
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, '.gitignore'), content);
|
|
1038
939
|
}
|
|
940
|
+
async function generateReadme(projectPath, projectName) {
|
|
941
|
+
const content = `# ${projectName}
|
|
1039
942
|
|
|
1040
|
-
|
|
1041
|
-
* Parse JSONObject to DSLElement
|
|
1042
|
-
*/
|
|
1043
|
-
fun parseDSLElement(obj: JSONObject): DSLElement {
|
|
1044
|
-
val children = if (obj.has("children")) {
|
|
1045
|
-
val childrenArray = obj.getJSONArray("children")
|
|
1046
|
-
List(childrenArray.length()) { i ->
|
|
1047
|
-
parseDSLElement(childrenArray.getJSONObject(i))
|
|
1048
|
-
}
|
|
1049
|
-
} else null
|
|
943
|
+
A JetStart project with Kotlin and Jetpack Compose.
|
|
1050
944
|
|
|
1051
|
-
|
|
1052
|
-
val modObj = obj.getJSONObject("modifier")
|
|
1053
|
-
DSLModifier(
|
|
1054
|
-
fillMaxSize = modObj.optBoolean("fillMaxSize"),
|
|
1055
|
-
fillMaxWidth = modObj.optBoolean("fillMaxWidth"),
|
|
1056
|
-
fillMaxHeight = modObj.optBoolean("fillMaxHeight"),
|
|
1057
|
-
padding = if (modObj.has("padding")) modObj.getInt("padding") else null,
|
|
1058
|
-
paddingHorizontal = if (modObj.has("paddingHorizontal")) modObj.getInt("paddingHorizontal") else null,
|
|
1059
|
-
paddingVertical = if (modObj.has("paddingVertical")) modObj.getInt("paddingVertical") else null,
|
|
1060
|
-
size = if (modObj.has("size")) modObj.getInt("size") else null,
|
|
1061
|
-
height = if (modObj.has("height")) modObj.getInt("height") else null,
|
|
1062
|
-
width = if (modObj.has("width")) modObj.getInt("width") else null,
|
|
1063
|
-
weight = if (modObj.has("weight")) modObj.getDouble("weight").toFloat() else null
|
|
1064
|
-
)
|
|
1065
|
-
} else null
|
|
945
|
+
## Getting Started
|
|
1066
946
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
947
|
+
\`\`\`bash
|
|
948
|
+
# Start development server
|
|
949
|
+
jetstart dev
|
|
950
|
+
|
|
951
|
+
# Build production APK
|
|
952
|
+
jetstart build
|
|
953
|
+
|
|
954
|
+
# View logs
|
|
955
|
+
jetstart logs
|
|
956
|
+
\`\`\`
|
|
957
|
+
|
|
958
|
+
## Project Structure
|
|
959
|
+
|
|
960
|
+
\`\`\`
|
|
961
|
+
${projectName}/
|
|
962
|
+
├── app/
|
|
963
|
+
│ └── src/
|
|
964
|
+
│ └── main/
|
|
965
|
+
│ ├── java/ # Kotlin source files
|
|
966
|
+
│ └── res/ # Resources
|
|
967
|
+
├── jetstart.config.json # JetStart configuration
|
|
968
|
+
└── build.gradle # Gradle build file
|
|
969
|
+
\`\`\`
|
|
970
|
+
|
|
971
|
+
## Learn More
|
|
972
|
+
|
|
973
|
+
- [JetStart Documentation](https://github.com/phantom/jetstart)
|
|
974
|
+
- [Jetpack Compose](https://developer.android.com/jetpack/compose)
|
|
975
|
+
`;
|
|
976
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'README.md'), content);
|
|
977
|
+
}
|
|
978
|
+
async function generateRootBuildGradle(projectPath) {
|
|
979
|
+
const content = `// Top-level build file
|
|
980
|
+
buildscript {
|
|
981
|
+
ext {
|
|
982
|
+
kotlin_version = '1.9.21'
|
|
983
|
+
compose_version = '1.5.4'
|
|
984
|
+
}
|
|
985
|
+
repositories {
|
|
986
|
+
google()
|
|
987
|
+
mavenCentral()
|
|
988
|
+
}
|
|
989
|
+
dependencies {
|
|
990
|
+
classpath 'com.android.tools.build:gradle:8.2.0'
|
|
991
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
allprojects {
|
|
996
|
+
repositories {
|
|
997
|
+
google()
|
|
998
|
+
mavenCentral()
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
task clean(type: Delete) {
|
|
1003
|
+
delete rootProject.buildDir
|
|
1004
|
+
}`;
|
|
1005
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'build.gradle'), content);
|
|
1006
|
+
}
|
|
1007
|
+
async function generateGradleWrapper(projectPath) {
|
|
1008
|
+
// Use system Gradle to initialize proper wrapper
|
|
1009
|
+
// This generates:
|
|
1010
|
+
// - gradle/wrapper/gradle-wrapper.jar
|
|
1011
|
+
// - gradle/wrapper/gradle-wrapper.properties
|
|
1012
|
+
// - gradlew (Unix shell script)
|
|
1013
|
+
// - gradlew.bat (Windows batch script)
|
|
1014
|
+
return new Promise((resolve) => {
|
|
1015
|
+
// Try to use system gradle to generate wrapper
|
|
1016
|
+
const gradleCmd = process.platform === 'win32' ? 'gradle.bat' : 'gradle';
|
|
1017
|
+
const gradleProcess = (0, child_process_1.spawn)(gradleCmd, ['wrapper', '--gradle-version', '8.2'], {
|
|
1018
|
+
cwd: projectPath,
|
|
1019
|
+
shell: true,
|
|
1020
|
+
});
|
|
1021
|
+
gradleProcess.on('close', (code) => {
|
|
1022
|
+
// Continue regardless of success/failure
|
|
1023
|
+
// If gradle wrapper command fails, the build will fall back to system gradle
|
|
1024
|
+
resolve();
|
|
1025
|
+
});
|
|
1026
|
+
gradleProcess.on('error', () => {
|
|
1027
|
+
// Continue even if gradle command not found
|
|
1028
|
+
resolve();
|
|
1029
|
+
});
|
|
1030
|
+
// Timeout after 30 seconds
|
|
1031
|
+
setTimeout(() => {
|
|
1032
|
+
gradleProcess.kill();
|
|
1033
|
+
resolve();
|
|
1034
|
+
}, 30000);
|
|
1035
|
+
});
|
|
1085
1036
|
}
|
|
1037
|
+
async function generateResourceFiles(projectPath, projectName) {
|
|
1038
|
+
// Generate strings.xml
|
|
1039
|
+
const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
1040
|
+
<resources>
|
|
1041
|
+
<string name="app_name">${projectName}</string>
|
|
1042
|
+
</resources>`;
|
|
1043
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/res/values/strings.xml'), stringsXml);
|
|
1044
|
+
// Generate colors.xml
|
|
1045
|
+
const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
1046
|
+
<resources>
|
|
1047
|
+
<color name="purple_200">#FFBB86FC</color>
|
|
1048
|
+
<color name="purple_500">#FF6200EE</color>
|
|
1049
|
+
<color name="purple_700">#FF3700B3</color>
|
|
1050
|
+
<color name="teal_200">#FF03DAC5</color>
|
|
1051
|
+
<color name="teal_700">#FF018786</color>
|
|
1052
|
+
<color name="black">#FF000000</color>
|
|
1053
|
+
<color name="white">#FFFFFFFF</color>
|
|
1054
|
+
</resources>`;
|
|
1055
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/res/values/colors.xml'), colorsXml);
|
|
1056
|
+
// Generate themes.xml
|
|
1057
|
+
const themesXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
1058
|
+
<resources>
|
|
1059
|
+
<style name="Theme.${projectName.replace(/[^a-zA-Z0-9]/g, '')}" parent="android:Theme.Material.Light.NoActionBar" />
|
|
1060
|
+
</resources>`;
|
|
1061
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/res/values/themes.xml'), themesXml);
|
|
1062
|
+
// Generate network_security_config.xml for development (allows cleartext traffic)
|
|
1063
|
+
const networkSecurityConfig = `<?xml version="1.0" encoding="utf-8"?>
|
|
1064
|
+
<network-security-config>
|
|
1065
|
+
<base-config cleartextTrafficPermitted="true">
|
|
1066
|
+
<trust-anchors>
|
|
1067
|
+
<certificates src="system" />
|
|
1068
|
+
</trust-anchors>
|
|
1069
|
+
</base-config>
|
|
1070
|
+
</network-security-config>`;
|
|
1071
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(projectPath, 'app/src/main/res/xml'));
|
|
1072
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/res/xml/network_security_config.xml'), networkSecurityConfig);
|
|
1073
|
+
}
|
|
1074
|
+
async function generateLocalProperties(projectPath) {
|
|
1075
|
+
// Auto-detect Android SDK location
|
|
1076
|
+
let androidSdkPath;
|
|
1077
|
+
// Check environment variables first
|
|
1078
|
+
androidSdkPath = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
1079
|
+
// If not found, check common Windows locations
|
|
1080
|
+
if (!androidSdkPath && process.platform === 'win32') {
|
|
1081
|
+
const commonPaths = [
|
|
1082
|
+
'C:\\Android',
|
|
1083
|
+
path_1.default.join(require('os').homedir(), 'AppData', 'Local', 'Android', 'Sdk'),
|
|
1084
|
+
'C:\\Android\\Sdk',
|
|
1085
|
+
'C:\\Program Files (x86)\\Android\\android-sdk',
|
|
1086
|
+
];
|
|
1087
|
+
for (const p of commonPaths) {
|
|
1088
|
+
if (fs_extra_1.default.existsSync(p)) {
|
|
1089
|
+
androidSdkPath = p;
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
// If not found on macOS/Linux, check common paths
|
|
1095
|
+
if (!androidSdkPath && process.platform !== 'win32') {
|
|
1096
|
+
const commonPaths = [
|
|
1097
|
+
path_1.default.join(require('os').homedir(), 'Android', 'Sdk'),
|
|
1098
|
+
path_1.default.join(require('os').homedir(), 'Library', 'Android', 'sdk'),
|
|
1099
|
+
'/opt/android-sdk',
|
|
1100
|
+
];
|
|
1101
|
+
for (const p of commonPaths) {
|
|
1102
|
+
if (fs_extra_1.default.existsSync(p)) {
|
|
1103
|
+
androidSdkPath = p;
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (!androidSdkPath) {
|
|
1109
|
+
console.warn('[Warning] Android SDK not found. You may need to set ANDROID_HOME or create local.properties manually.');
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
// Create local.properties with SDK path
|
|
1113
|
+
const content = `# Auto-generated by JetStart
|
|
1114
|
+
sdk.dir=${androidSdkPath.replace(/\\/g, '\\\\')}
|
|
1086
1115
|
`;
|
|
1087
|
-
await fs_extra_1.default.
|
|
1088
|
-
|
|
1116
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'local.properties'), content);
|
|
1117
|
+
console.log(`[JetStart] Created local.properties with SDK: ${androidSdkPath}`);
|
|
1089
1118
|
}
|
|
1090
1119
|
//# sourceMappingURL=template.js.map
|