Skip to content
This repository was archived by the owner on Jul 29, 2026. It is now read-only.

Commit 24152a7

Browse files
authored
Add generated README to new projects (#14)
* Add generated project README * Verify generated README in integration tests * Use desktop hot reload in generated README * Remove explicit Compose Hot Reload plugin * Use auto hot reload command in README
1 parent dd6f588 commit 24152a7

6 files changed

Lines changed: 81 additions & 17 deletions

File tree

cli/src/integrationTest/kotlin/com/composables/cli/CliIntegrationTest.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class CliIntegrationTest {
4545
assertThat(initResult.finished).isTrue()
4646
assertThat(initResult.exitCode).isEqualTo(0)
4747
assertThat(initResult.output).contains("Success! Your new Compose app is ready")
48+
assertJvmReadme(projectDir)
4849

4950
val compileResult = runProcess(
5051
command = listOf(projectGradleScript(), ":shared:compileKotlinJvm"),
@@ -86,6 +87,7 @@ class CliIntegrationTest {
8687
assertThat(createResult.finished).isTrue()
8788
assertThat(createResult.exitCode).isEqualTo(0)
8889
assertThat(createResult.output).contains("Success! Your new Compose app is ready")
90+
assertJvmReadme(projectDir)
8991

9092
val compileResult = runProcess(
9193
command = listOf(projectGradleScript(), ":shared:compileKotlinJvm"),
@@ -118,6 +120,7 @@ class CliIntegrationTest {
118120
assertThat(createResult.finished).isTrue()
119121
assertThat(createResult.exitCode).isEqualTo(0)
120122
assertThat(createResult.output).contains("Success! Your new Compose app is ready")
123+
assertJvmReadme(projectDir)
121124

122125
val compileResult = runProcess(
123126
command = listOf(projectGradleScript(), ":shared:compileKotlinJvm"),
@@ -235,6 +238,19 @@ class CliIntegrationTest {
235238
"./gradlew"
236239
}
237240

241+
private fun assertJvmReadme(projectDir: File) {
242+
val readme = File(projectDir, "README.md")
243+
assertThat(readme.exists()).isTrue()
244+
245+
val content = readme.readText()
246+
assertThat(content).contains("# ${projectDir.name}")
247+
assertThat(content).contains("## Run")
248+
assertThat(content).contains("`./gradlew :desktopApp:hotRunJvm --auto`")
249+
assertThat(content).doesNotContain(":androidApp:installDebug")
250+
assertThat(content).doesNotContain("iosApp/iosApp.xcodeproj")
251+
assertThat(content).doesNotContain(":webApp:wasmJsBrowserDevelopmentRun")
252+
}
253+
238254
private fun runProcess(
239255
command: List<String>,
240256
workingDir: File,

cli/src/jvmMain/kotlin/Cli.kt

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,37 @@ private fun normalizeTargets(targets: Set<String>): LinkedHashSet<String> = link
3333
addAll(targets)
3434
}
3535

36+
private fun buildProjectReadme(
37+
projectName: String,
38+
targets: Set<String>,
39+
): String {
40+
val normalizedTargets = normalizeTargets(targets)
41+
val lines = mutableListOf<String>()
42+
43+
lines += "# $projectName"
44+
lines += ""
45+
lines += "## Run"
46+
lines += ""
47+
lines += "From the project root:"
48+
lines += ""
49+
50+
if (normalizedTargets.contains(JVM)) {
51+
lines += "- JVM: `./gradlew :$DESKTOP_APP_MODULE:hotRunJvm --auto`"
52+
}
53+
if (normalizedTargets.contains(ANDROID)) {
54+
lines += "- Android: open the project in Android Studio and run the `$ANDROID_APP_MODULE` app on a device or emulator"
55+
lines += "- Android install from terminal: `./gradlew :$ANDROID_APP_MODULE:installDebug`"
56+
}
57+
if (normalizedTargets.contains(IOS)) {
58+
lines += "- iOS: open `$IOS_APP_MODULE/$IOS_APP_MODULE.xcodeproj` in Xcode and run the app on a simulator or device"
59+
}
60+
if (normalizedTargets.contains(WASM)) {
61+
lines += "- Wasm: `./gradlew :$WEB_APP_MODULE:wasmJsBrowserDevelopmentRun`"
62+
}
63+
64+
return lines.joinToString("\n") + "\n"
65+
}
66+
3667
suspend fun main(args: Array<String>) {
3768
ComposablesCli()
3869
.subcommands(CreateApp(), Init(), Target())
@@ -1338,7 +1369,7 @@ fun cloneGradleProjectAndPrint(
13381369
infoln { "" }
13391370
infoln { "\tcd ${target.absolutePath}" }
13401371
val startCommand = when {
1341-
targets.contains(JVM) -> "$gradleScript :$DESKTOP_APP_MODULE:run"
1372+
targets.contains(JVM) -> "$gradleScript :$DESKTOP_APP_MODULE:hotRunJvm --auto"
13421373
targets.contains(WASM) -> "$gradleScript :$WEB_APP_MODULE:wasmJsBrowserDevelopmentRun"
13431374
else -> "$gradleScript build"
13441375
}
@@ -1517,6 +1548,13 @@ private fun cloneGradleProjectAt(
15171548
}
15181549
}
15191550
}
1551+
1552+
File(target, "README.md").writeText(
1553+
buildProjectReadme(
1554+
projectName = target.name,
1555+
targets = normalizedTargets,
1556+
),
1557+
)
15201558
}
15211559

15221560
private fun renderProjectTemplate(
@@ -1778,9 +1816,6 @@ fun updateRootBuildFile(
17781816
requiredPlugins.add(" alias(libs.plugins.jetbrains.compose.compiler) apply false")
17791817
}
17801818
}
1781-
if (!pluginsContent.contains("libs.plugins.jetbrains.compose.hotreload")) {
1782-
requiredPlugins.add(" alias(libs.plugins.jetbrains.compose.hotreload) apply false")
1783-
}
17841819
if (normalizedTargets.contains(ANDROID) && !pluginsContent.contains("libs.plugins.android.application")) {
17851820
requiredPlugins.add(" alias(libs.plugins.android.application) apply false")
17861821
}
@@ -1802,7 +1837,6 @@ fun updateRootBuildFile(
18021837
requiredPlugins.add(" alias(libs.plugins.jetbrains.kotlin.multiplatform) apply false")
18031838
requiredPlugins.add(" alias(libs.plugins.jetbrains.compose) apply false")
18041839
requiredPlugins.add(" alias(libs.plugins.jetbrains.compose.compiler) apply false")
1805-
requiredPlugins.add(" alias(libs.plugins.jetbrains.compose.hotreload) apply false")
18061840
if (normalizedTargets.contains(ANDROID)) {
18071841
requiredPlugins.add(" alias(libs.plugins.android.application) apply false")
18081842
requiredPlugins.add(" alias(libs.plugins.android.kotlin.multiplatform.library) apply false")
@@ -1852,9 +1886,6 @@ fun updateVersionCatalog(
18521886
if (!hasVersionVariable(versionsSection, "compose")) {
18531887
newVersions.add("compose = \"1.11.1\"")
18541888
}
1855-
if (!hasVersionVariable(versionsSection, "composeHotReload")) {
1856-
newVersions.add("composeHotReload = \"1.1.0\"")
1857-
}
18581889
if (!hasVersionVariable(versionsSection, "composablesUi")) {
18591890
newVersions.add("composablesUi = \"0.1.0\"")
18601891
}
@@ -1897,9 +1928,6 @@ fun updateVersionCatalog(
18971928
if (!hasPluginVariable(pluginsSection, "jetbrains-compose-compiler")) {
18981929
newPlugins.add("jetbrains-compose-compiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }")
18991930
}
1900-
if (!hasPluginVariable(pluginsSection, "jetbrains-compose-hotreload")) {
1901-
newPlugins.add("jetbrains-compose-hotreload = { id = \"org.jetbrains.compose.hot-reload\", version.ref = \"composeHotReload\" }")
1902-
}
19031931
if (targets.contains("android") && !hasPluginVariable(pluginsSection, "android-application")) {
19041932
newPlugins.add("android-application = { id = \"com.android.application\", version.ref = \"agp\" }")
19051933
}

cli/src/jvmMain/resources/project/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ plugins {
22
// this is necessary to avoid the plugins to be loaded multiple times
33
// in each subproject's classloader
44
alias(libs.plugins.jetbrains.kotlin.multiplatform) apply false
5-
alias(libs.plugins.jetbrains.compose.hotreload) apply false
65
alias(libs.plugins.jetbrains.compose) apply false
76
alias(libs.plugins.jetbrains.compose.compiler) apply false
87
{{android_root_plugins}}

cli/src/jvmMain/resources/project/desktopApp/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ plugins {
44
alias(libs.plugins.jetbrains.kotlin.multiplatform)
55
alias(libs.plugins.jetbrains.compose)
66
alias(libs.plugins.jetbrains.compose.compiler)
7-
alias(libs.plugins.jetbrains.compose.hotreload)
87
}
98

109
kotlin {

cli/src/jvmMain/resources/project/gradle/libs.versions.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
[versions]
22
# Compose
33
compose = "1.11.1"
4-
composeHotReload = "1.1.0"
54
composablesUi = "0.1.0"
65

76
# Kotlin
@@ -14,7 +13,6 @@ composables-ui = { group = "com.composables", name = "ui", version.ref = "compos
1413
{{android_libraries}}
1514
[plugins]
1615
{{android_plugins}}
17-
jetbrains-compose-hotreload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" }
1816
jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose" }
1917
jetbrains-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
2018
jetbrains-kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }

cli/src/jvmTest/kotlin/com/composables/cli/CliTest.kt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ class CliTest {
3131
val sharedBuildFile = File(projectDir, "shared/build.gradle.kts")
3232
val desktopBuildFile = File(projectDir, "desktopApp/build.gradle.kts")
3333
val rootBuildFile = File(projectDir, "build.gradle.kts")
34+
val readmeFile = File(projectDir, "README.md")
3435
val settingsFile = File(projectDir, "settings.gradle.kts")
3536
val appFile = File(projectDir, "shared/src/commonMain/kotlin/com/composables/demo/App.kt")
3637
val desktopMainFile = File(projectDir, "desktopApp/src/jvmMain/kotlin/com/composables/demo/main.kt")
3738

3839
assertThat(projectDir.isDirectory, "Generated project directory should exist").isTrue()
3940
assertThat(sharedBuildFile.exists(), "Generated shared module build file should exist").isTrue()
4041
assertThat(desktopBuildFile.exists(), "Generated desktop module build file should exist").isTrue()
42+
assertThat(readmeFile.exists(), "Generated README should exist").isTrue()
4143
assertThat(settingsFile.exists(), "Generated settings file should exist").isTrue()
4244
assertThat(appFile.exists(), "App source should be moved to the requested package").isTrue()
4345
assertThat(desktopMainFile.exists(), "Desktop launcher source should exist").isTrue()
@@ -50,6 +52,7 @@ class CliTest {
5052
val sharedBuildContent = sharedBuildFile.readText()
5153
val desktopBuildContent = desktopBuildFile.readText()
5254
val rootBuildContent = rootBuildFile.readText()
55+
val readmeContent = readmeFile.readText()
5356
val settingsContent = settingsFile.readText()
5457
val appContent = appFile.readText()
5558

@@ -72,6 +75,11 @@ class CliTest {
7275
assertThat(settingsContent).contains("""rootProject.name = "newApp"""")
7376
assertThat(settingsContent).contains("""include(":shared")""")
7477
assertThat(settingsContent).contains("""include(":desktopApp")""")
78+
assertThat(readmeContent).contains("# newApp")
79+
assertThat(readmeContent).contains("`./gradlew :desktopApp:hotRunJvm --auto`")
80+
assertThat(readmeContent).doesNotContain(":androidApp:installDebug")
81+
assertThat(readmeContent).doesNotContain("iosApp/iosApp.xcodeproj")
82+
assertThat(readmeContent).doesNotContain(":webApp:wasmJsBrowserDevelopmentRun")
7583

7684
assertThat(appContent).contains("package com.composables.demo")
7785
assertThat(appContent).contains("import androidx.compose.ui.tooling.preview.Preview")
@@ -111,16 +119,19 @@ class CliTest {
111119
val projectDir = File(targetDir, "newApp")
112120
val sharedBuildFile = File(projectDir, "sharedUi/build.gradle.kts")
113121
val androidAppBuildFile = File(projectDir, "androidApp/build.gradle.kts")
122+
val readmeFile = File(projectDir, "README.md")
114123
val settingsFile = File(projectDir, "settings.gradle.kts")
115124
val mainActivityFile = File(projectDir, "androidApp/src/main/kotlin/com/composables/demo/MainActivity.kt")
116125

117126
assertThat(sharedBuildFile).exists()
118127
assertThat(androidAppBuildFile).exists()
128+
assertThat(readmeFile).exists()
119129
assertThat(mainActivityFile).exists()
120130
assertThat(File(projectDir, "sharedUi/src/androidMain").exists()).isFalse()
121131

122132
val sharedBuildContent = sharedBuildFile.readText()
123133
val androidAppBuildContent = androidAppBuildFile.readText()
134+
val readmeContent = readmeFile.readText()
124135
val settingsContent = settingsFile.readText()
125136

126137
assertThat(sharedBuildContent).contains("alias(libs.plugins.android.kotlin.multiplatform.library)")
@@ -138,6 +149,15 @@ class CliTest {
138149
assertThat(settingsContent).contains("""include(":androidApp")""")
139150
assertThat(settingsContent).contains("""include(":desktopApp")""")
140151
assertThat(settingsContent).contains("""include(":webApp")""")
152+
153+
assertThat(readmeContent).contains("# newApp")
154+
assertThat(readmeContent).contains("`./gradlew :desktopApp:hotRunJvm --auto`")
155+
assertThat(readmeContent).contains("`./gradlew :androidApp:installDebug`")
156+
assertThat(readmeContent).contains("`iosApp/iosApp.xcodeproj`")
157+
assertThat(readmeContent).contains("`./gradlew :webApp:wasmJsBrowserDevelopmentRun`")
158+
assertThat(readmeContent.indexOf("- Android:") > readmeContent.indexOf("- JVM:")).isTrue()
159+
assertThat(readmeContent.indexOf("- iOS:") > readmeContent.indexOf("- Android install from terminal:")).isTrue()
160+
assertThat(readmeContent.indexOf("- Wasm:") > readmeContent.indexOf("- iOS:")).isTrue()
141161
}
142162
}
143163

@@ -173,6 +193,7 @@ class CliTest {
173193
val rootBuildContent = File(projectDir, "build.gradle.kts").readText()
174194
val sharedBuildContent = File(projectDir, "shared/build.gradle.kts").readText()
175195
val webAppBuildContent = File(projectDir, "webApp/build.gradle.kts").readText()
196+
val readmeContent = File(projectDir, "README.md").readText()
176197

177198
assertThat(rootBuildContent).contains("wasmJsBrowserDistribution")
178199
assertThat(rootBuildContent).contains("wasm-preloads")
@@ -184,6 +205,9 @@ class CliTest {
184205
assertThat(sharedBuildContent).doesNotContain("js {")
185206
assertThat(webAppBuildContent).contains("implementation(libs.compose.ui)")
186207
assertThat(webAppBuildContent).contains("wasmJs {")
208+
assertThat(readmeContent).contains("# newApp")
209+
assertThat(readmeContent).contains("`./gradlew :webApp:wasmJsBrowserDevelopmentRun`")
210+
assertThat(readmeContent).doesNotContain(":desktopApp:hotRunJvm --auto")
187211
}
188212
}
189213

@@ -206,7 +230,7 @@ class CliTest {
206230
assertThat(content.countOccurrences("alias(libs.plugins.jetbrains.kotlin.multiplatform) apply false")).isEqualTo(1)
207231
assertThat(content.countOccurrences("alias(libs.plugins.jetbrains.compose) apply false")).isEqualTo(1)
208232
assertThat(content.countOccurrences("alias(libs.plugins.jetbrains.compose.compiler) apply false")).isEqualTo(1)
209-
assertThat(content.countOccurrences("alias(libs.plugins.jetbrains.compose.hotreload) apply false")).isEqualTo(1)
233+
assertThat(content).doesNotContain("alias(libs.plugins.jetbrains.compose.hotreload)")
210234
assertThat(content.countOccurrences("alias(libs.plugins.android.application) apply false")).isEqualTo(1)
211235
assertThat(content.countOccurrences("alias(libs.plugins.android.kotlin.multiplatform.library) apply false")).isEqualTo(1)
212236
}
@@ -240,7 +264,7 @@ class CliTest {
240264
assertThat(content.countOccurrences("""android-application = { id = "com.android.application", version.ref = "agp" }""")).isEqualTo(1)
241265
assertThat(content.countOccurrences("""android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }""")).isEqualTo(1)
242266
assertThat(content).contains("""compose = "1.11.1"""")
243-
assertThat(content).contains("""composeHotReload = "1.1.0"""")
267+
assertThat(content).doesNotContain("composeHotReload")
244268
assertThat(content).contains("""composablesUi = "0.1.0"""")
245269
}
246270
}

0 commit comments

Comments
 (0)