From 30fba0067fdb513a45159bcd3668d383cd3d2c0f Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Thu, 8 May 2025 23:01:53 +0700 Subject: [PATCH 01/22] add :modules:preview-processor --- build.gradle.kts | 6 ++++++ modules/preview-processor/.gitignore | 1 + modules/preview-processor/build.gradle.kts | 21 +++++++++++++++++++++ settings.gradle.kts | 1 + 4 files changed, 29 insertions(+) create mode 100644 modules/preview-processor/.gitignore create mode 100644 modules/preview-processor/build.gradle.kts diff --git a/build.gradle.kts b/build.gradle.kts index 507998a..8dd64ce 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,12 @@ plugins { alias(libs.plugins.spotless) apply false } +buildscript { + dependencies { + classpath(kotlin("gradle-plugin", version = libs.versions.kotlin.asProvider().get())) + } +} + subprojects { version = findProperty("storytale.deploy.version") ?: error("'storytale.deploy.version' was not set") diff --git a/modules/preview-processor/.gitignore b/modules/preview-processor/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/modules/preview-processor/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/modules/preview-processor/build.gradle.kts b/modules/preview-processor/build.gradle.kts new file mode 100644 index 0000000..bfba8bb --- /dev/null +++ b/modules/preview-processor/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(libs.plugins.kotlin.jvm) +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21 + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} +dependencies{ + implementation("com.google.devtools.ksp:symbol-processing-api:2.1.20-2.0.1") + testImplementation(kotlin("test")) + testImplementation(libs.assertj.core) + testImplementation(libs.junit) + testImplementation(libs.kotlinCompileTesting.core) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index f160878..261d953 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -59,5 +59,6 @@ include(":modules:gradle-plugin") include(":modules:compiler-plugin") include(":modules:dokka-plugin") include(":modules:runtime-api") +include(":modules:preview-processor") include(":gallery-demo") From e343aa55c0c3a77b9ae9f87940ca0d1deb834ad6 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Fri, 9 May 2025 00:06:41 +0700 Subject: [PATCH 02/22] Add PreviewProcessor and first good test --- gradle/libs.versions.toml | 3 + modules/preview-processor/build.gradle.kts | 21 ++++++- .../src/main/kotlin/PreviewProcessor.kt | 25 ++++++++ .../src/test/kotlin/PreviewProcessorTest.kt | 57 +++++++++++++++++++ 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 modules/preview-processor/src/main/kotlin/PreviewProcessor.kt create mode 100644 modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 17b54db..365abb8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,10 +21,12 @@ dokka = "1.9.10" kotlinx-html = "0.7.3" junit = "5.10.1" jsoup = "1.16.1" +ksp = "2.1.20-2.0.1" [libraries] assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } kotlinCompileTesting-core = { module = "dev.zacsweers.kctfork:core", version.ref = "kotlinCompileTesting" } +kotlinCompileTesting-ksp = { module = "dev.zacsweers.kctfork:ksp", version.ref = "kotlinCompileTesting" } junit = { module = "junit:junit", version.ref = "junitVersion" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } @@ -45,6 +47,7 @@ junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "jun jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } ktlint = "com.pinterest.ktlint:ktlint-cli:1.5.0" composeRules = "io.nlopez.compose.rules:ktlint:0.4.22" +ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/modules/preview-processor/build.gradle.kts b/modules/preview-processor/build.gradle.kts index bfba8bb..84f3c4d 100644 --- a/modules/preview-processor/build.gradle.kts +++ b/modules/preview-processor/build.gradle.kts @@ -1,5 +1,12 @@ +import org.gradle.kotlin.dsl.withType +import org.jetbrains.compose.ComposePlugin.CommonComponentsDependencies +import org.jetbrains.compose.ComposePlugin.CommonComponentsDependencies.uiToolingPreview +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + plugins { alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose.compiler) } kotlin { @@ -12,10 +19,18 @@ java { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } -dependencies{ - implementation("com.google.devtools.ksp:symbol-processing-api:2.1.20-2.0.1") + +dependencies { + compileOnly(compose.runtime) + implementation(libs.ksp.api) + testImplementation(compose.components.uiToolingPreview) + testImplementation(compose.runtime) testImplementation(kotlin("test")) testImplementation(libs.assertj.core) testImplementation(libs.junit) - testImplementation(libs.kotlinCompileTesting.core) + testImplementation(libs.kotlinCompileTesting.ksp) +} + +tasks.withType>().configureEach { + compilerOptions.optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") } diff --git a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt new file mode 100644 index 0000000..882b4ac --- /dev/null +++ b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt @@ -0,0 +1,25 @@ +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.symbol.KSAnnotated + +class PreviewProcessor( + private val logger: KSPLogger, + private val codeGenerator: CodeGenerator, +) : SymbolProcessor { + override fun process(resolver: Resolver): List { + return emptyList() + } + + class Provider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return PreviewProcessor( + environment.logger, + environment.codeGenerator, + ) + } + } +} diff --git a/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt b/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt new file mode 100644 index 0000000..8177308 --- /dev/null +++ b/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt @@ -0,0 +1,57 @@ +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import com.tschuchort.compiletesting.symbolProcessorProviders +import com.tschuchort.compiletesting.useKsp2 +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class PreviewProcessorTest { + @Test + fun `first good test`() { + val group1Kt = SourceFile.kotlin( + "KmpButton.kt", + """ + package storytale.gallery.demo + + @androidx.compose.runtime.Composable + fun KmpButton() { } + + @org.jetbrains.compose.ui.tooling.preview.Preview + @androidx.compose.runtime.Composable + fun PreviewKmpButton() { + KmpButton() + } + """, + ) + val group2Kt = SourceFile.kotlin( + "AndroidButton.kt", + """package storytale.gallery.demo + + @androidx.compose.runtime.Composable + fun AndroidButton() { } + + @androidx.compose.ui.tooling.preview.Preview + @androidx.compose.runtime.Composable + fun PreviewAndroidButton() { + AndroidButton() + } + """, + ) + + val result = KotlinCompilation().apply { + sources = listOf(group1Kt) + + useKsp2() + symbolProcessorProviders.add(PreviewProcessor.Provider())// = mutableListOf(PreviewProcessor.Provider()) + + // magic + inheritClassPath = true + messageOutputStream = System.out // see diagnostics in real time + jvmTarget = "21" + verbose = false + } + .compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + } +} From b89fdcf22a2fbaa0100899cd7e6fd32194e0de7e Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sat, 10 May 2025 22:56:00 +0700 Subject: [PATCH 03/22] Implement PreviewProcessor --- modules/preview-processor/build.gradle.kts | 4 ++ .../src/main/kotlin/PreviewProcessor.kt | 63 ++++++++++++++++++- .../src/test/kotlin/PreviewProcessorTest.kt | 38 ++++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/modules/preview-processor/build.gradle.kts b/modules/preview-processor/build.gradle.kts index 84f3c4d..bc26d9d 100644 --- a/modules/preview-processor/build.gradle.kts +++ b/modules/preview-processor/build.gradle.kts @@ -22,13 +22,17 @@ java { dependencies { compileOnly(compose.runtime) + implementation(libs.kotlin.poet) implementation(libs.ksp.api) testImplementation(compose.components.uiToolingPreview) testImplementation(compose.runtime) + testImplementation(kotlin("compiler-embeddable")) + testImplementation(kotlin("compose-compiler-plugin-embeddable")) testImplementation(kotlin("test")) testImplementation(libs.assertj.core) testImplementation(libs.junit) testImplementation(libs.kotlinCompileTesting.ksp) + testImplementation(project(":modules:runtime-api")) } tasks.withType>().configureEach { diff --git a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt index 882b4ac..9a0d6e6 100644 --- a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt +++ b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt @@ -1,17 +1,78 @@ import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorProvider import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSVisitorVoid +import com.google.devtools.ksp.validate +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.Dynamic +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.MemberName +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.buildCodeBlock +/** + * https://github.com/google/ksp/blob/1db41bc678a1d0e86f71ca3b052a147675769691/examples/playground/test-processor/src/main/kotlin/BuilderProcessor.kt + */ class PreviewProcessor( private val logger: KSPLogger, private val codeGenerator: CodeGenerator, ) : SymbolProcessor { override fun process(resolver: Resolver): List { - return emptyList() + val symbols = + resolver.getSymbolsWithAnnotation("org.jetbrains.compose.ui.tooling.preview.Preview") + val ret = symbols.filter { !it.validate() }.toList() + symbols + .filter { it is KSFunctionDeclaration && it.validate() } + .forEach { it.accept(BuilderVisitor(), Unit) } + return ret + } + + inner class BuilderVisitor : KSVisitorVoid() { + override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) { + val packageName = function.packageName.asString() + val fileSpecBuilder = FileSpec.builder( + MemberName(packageName, "Preview.story"), + ).apply { + indent(" ") + + addImport("org.jetbrains.compose.storytale", "story") + + addProperty( + PropertySpec + .builder( + function.simpleName.asString() + // because removeSurrounding doesn't work + .removePrefix("Preview").removeSuffix("Preview"), + ClassName("org.jetbrains.compose.storytale", "Story"), + ) + .delegate( + buildCodeBlock { + beginControlFlow("story") + addStatement("%N()", function.simpleName.asString()) + endControlFlow() + }, + ) + .build(), + ) + } + + val file = codeGenerator.createNewFile( + Dependencies(true, function.containingFile!!), + packageName, + "Previews.story", + ) + fileSpecBuilder.build().toJavaFileObject().openInputStream().copyTo(file) + } } class Provider : SymbolProcessorProvider { diff --git a/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt b/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt index 8177308..efdcda7 100644 --- a/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt +++ b/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt @@ -1,8 +1,16 @@ +import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile +import com.tschuchort.compiletesting.kspSourcesDir +import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor import com.tschuchort.compiletesting.symbolProcessorProviders import com.tschuchort.compiletesting.useKsp2 +import java.io.File +import java.nio.charset.Charset import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.util.Files +import org.intellij.lang.annotations.Language +import org.jetbrains.kotlin.utils.fileUtils.descendantRelativeTo import org.junit.Test class PreviewProcessorTest { @@ -38,9 +46,10 @@ class PreviewProcessorTest { """, ) - val result = KotlinCompilation().apply { + val compilation = KotlinCompilation().apply { sources = listOf(group1Kt) + compilerPluginRegistrars = listOf(ComposePluginRegistrar()) useKsp2() symbolProcessorProviders.add(PreviewProcessor.Provider())// = mutableListOf(PreviewProcessor.Provider()) @@ -50,8 +59,35 @@ class PreviewProcessorTest { jvmTarget = "21" verbose = false } + val result = compilation .compile() assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + assertThat(result.sourcesGeneratedBySymbolProcessor.toList()).hasSize(1) + + assertThat( + result.sourcesGeneratedBySymbolProcessor.toList().map { + it.descendantRelativeTo(compilation.kspSourcesDir).path to + Files.contentOf(it, Charsets.UTF_8).trim() + }, + ) + .containsExactlyInAnyOrder( + "kotlin/storytale/gallery/demo/Previews.story.kt" hasContent """ + |package storytale.gallery.demo + | + |import org.jetbrains.compose.storytale.Story + |import org.jetbrains.compose.storytale.story + | + |public val KmpButton: Story by story { + | PreviewKmpButton() + |} + """.trimMargin(), + ) } } + +@Suppress("NOTHING_TO_INLINE") +inline infix fun String.hasContent(@Language("kotlin") content: String): Pair { + return this to content +} From 909d4d7a955a6fe319dbe3f6b33fe713efcdb6fe Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sat, 10 May 2025 23:18:56 +0700 Subject: [PATCH 04/22] Add missing SymbolProcessorProvider metadata --- build.gradle.kts | 1 + gallery-demo/build.gradle.kts | 6 +++++ .../storytale/gallery/demo/PreviewButton.kt | 25 +++++++++++++++++++ gradle/libs.versions.toml | 2 +- ...ols.ksp.processing.SymbolProcessorProvider | 1 + 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt create mode 100644 modules/preview-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider diff --git a/build.gradle.kts b/build.gradle.kts index 8dd64ce..c6e7631 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,6 +11,7 @@ plugins { alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.dokka) apply false alias(libs.plugins.spotless) apply false + alias(libs.plugins.ksp) apply false } buildscript { diff --git a/gallery-demo/build.gradle.kts b/gallery-demo/build.gradle.kts index 1036ff7..e3c9b00 100644 --- a/gallery-demo/build.gradle.kts +++ b/gallery-demo/build.gradle.kts @@ -12,6 +12,7 @@ plugins { alias(libs.plugins.compose.compiler) alias(libs.plugins.serialization) id("org.jetbrains.compose.hot-reload") version "1.0.0-alpha03" + alias(libs.plugins.ksp) } class StorytaleCompilerPlugin : KotlinCompilerPluginSupportPlugin { @@ -107,6 +108,11 @@ kotlin { } } +dependencies { + add("kspCommonMainMetadata", project(":modules:preview-processor")) + add("ksp", project(":modules:preview-processor")) +} + compose.desktop { application { mainClass = "storytale.gallery.demo.MainKt" diff --git a/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt b/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt new file mode 100644 index 0000000..18160b9 --- /dev/null +++ b/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt @@ -0,0 +1,25 @@ +package storytale.gallery.demo + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddCircle +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@org.jetbrains.compose.ui.tooling.preview.Preview +@Composable +fun PreviewExtendedFAB() { + val bgColor = MaterialTheme.colorScheme.primary + + ExtendedFloatingActionButton(onClick = {}, containerColor = bgColor) { + Icon(imageVector = Icons.Default.AddCircle, contentDescription = null) + Spacer(Modifier.padding(4.dp)) + Text("Extended") + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 365abb8..84dd986 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,6 +60,6 @@ buildTimeConfig = { id = "dev.limebeck.build-time-config", version.ref = "build- storytale = { id = "org.jetbrains.compose.storytale", version.ref = "storytale" } kotlinDsl = { id = "kotlin-dsl" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } - +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } diff --git a/modules/preview-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/modules/preview-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000..3c9e451 --- /dev/null +++ b/modules/preview-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +PreviewProcessor$Provider From ec79a93844a521228cabb87ae800f465e98023e9 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sat, 10 May 2025 23:29:10 +0700 Subject: [PATCH 05/22] generated stories are in org.jetbrains.compose.storytale.generated --- modules/preview-processor/build.gradle.kts | 1 + .../src/main/kotlin/PreviewProcessor.kt | 4 +++- .../src/test/kotlin/PreviewProcessorTest.kt | 11 +++++------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/modules/preview-processor/build.gradle.kts b/modules/preview-processor/build.gradle.kts index bc26d9d..ee9deeb 100644 --- a/modules/preview-processor/build.gradle.kts +++ b/modules/preview-processor/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { compileOnly(compose.runtime) implementation(libs.kotlin.poet) implementation(libs.ksp.api) + implementation(project(":modules:gradle-plugin")) testImplementation(compose.components.uiToolingPreview) testImplementation(compose.runtime) testImplementation(kotlin("compiler-embeddable")) diff --git a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt index 9a0d6e6..489f071 100644 --- a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt +++ b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt @@ -19,6 +19,7 @@ import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.buildCodeBlock +import org.jetbrains.compose.storytale.plugin.StorytaleGradlePlugin /** * https://github.com/google/ksp/blob/1db41bc678a1d0e86f71ca3b052a147675769691/examples/playground/test-processor/src/main/kotlin/BuilderProcessor.kt @@ -39,13 +40,14 @@ class PreviewProcessor( inner class BuilderVisitor : KSVisitorVoid() { override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) { - val packageName = function.packageName.asString() + val packageName = StorytaleGradlePlugin.STORYTALE_PACKAGE val fileSpecBuilder = FileSpec.builder( MemberName(packageName, "Preview.story"), ).apply { indent(" ") addImport("org.jetbrains.compose.storytale", "story") + addImport(function.packageName.asString(), function.simpleName.asString()) addProperty( PropertySpec diff --git a/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt b/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt index efdcda7..8c429b1 100644 --- a/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt +++ b/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt @@ -5,8 +5,6 @@ import com.tschuchort.compiletesting.kspSourcesDir import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor import com.tschuchort.compiletesting.symbolProcessorProviders import com.tschuchort.compiletesting.useKsp2 -import java.io.File -import java.nio.charset.Charset import org.assertj.core.api.Assertions.assertThat import org.assertj.core.util.Files import org.intellij.lang.annotations.Language @@ -15,7 +13,7 @@ import org.junit.Test class PreviewProcessorTest { @Test - fun `first good test`() { + fun `generates story for single preview function`() { val group1Kt = SourceFile.kotlin( "KmpButton.kt", """ @@ -38,7 +36,7 @@ class PreviewProcessorTest { @androidx.compose.runtime.Composable fun AndroidButton() { } - @androidx.compose.ui.tooling.preview.Preview + @org.jetbrains.compose.ui.tooling.preview.Preview @androidx.compose.runtime.Composable fun PreviewAndroidButton() { AndroidButton() @@ -73,11 +71,12 @@ class PreviewProcessorTest { }, ) .containsExactlyInAnyOrder( - "kotlin/storytale/gallery/demo/Previews.story.kt" hasContent """ - |package storytale.gallery.demo + "kotlin/org/jetbrains/compose/storytale/generated/Previews.story.kt" hasContent """ + |package org.jetbrains.compose.storytale.generated | |import org.jetbrains.compose.storytale.Story |import org.jetbrains.compose.storytale.story + |import storytale.gallery.demo.PreviewKmpButton | |public val KmpButton: Story by story { | PreviewKmpButton() From fba031be71544954de1a8eff173dbb9eac80f92b Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sat, 10 May 2025 23:42:51 +0700 Subject: [PATCH 06/22] Add previewParameter functions for non Story scopes --- .../storytale/gallery/demo/PreviewButton.kt | 3 ++- .../compose/storytale/PreviewParameter.kt | 22 +++++++++++++++++++ .../compose/storytale/StoryDelegate.kt | 13 ++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/PreviewParameter.kt diff --git a/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt b/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt index 18160b9..ce397f0 100644 --- a/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt +++ b/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt @@ -11,11 +11,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import org.jetbrains.compose.storytale.previewParameter @org.jetbrains.compose.ui.tooling.preview.Preview @Composable fun PreviewExtendedFAB() { - val bgColor = MaterialTheme.colorScheme.primary + val bgColor by previewParameter(MaterialTheme.colorScheme.primary) ExtendedFloatingActionButton(onClick = {}, containerColor = bgColor) { Icon(imageVector = Icons.Default.AddCircle, contentDescription = null) diff --git a/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/PreviewParameter.kt b/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/PreviewParameter.kt new file mode 100644 index 0000000..a4e36f1 --- /dev/null +++ b/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/PreviewParameter.kt @@ -0,0 +1,22 @@ +package org.jetbrains.compose.storytale + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.staticCompositionLocalOf + +@Composable +inline fun previewParameter(defaultValue: T) = + LocalStory.current.parameter(defaultValue) + +@Composable +inline fun previewParameter( + values: List, + defaultValueIndex: Int = 0, + label: String? = null, +) = LocalStory.current.parameter(values, defaultValueIndex, label) + +@Composable +inline fun > previewParameter(defaultValue: T, label: String? = null) = + LocalStory.current.parameter(defaultValue, label) + +@PublishedApi +internal val LocalStory = staticCompositionLocalOf { Story(-1, "DefaultPreviewStory", "", "", {}) } diff --git a/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/StoryDelegate.kt b/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/StoryDelegate.kt index 477e575..638680a 100644 --- a/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/StoryDelegate.kt +++ b/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/StoryDelegate.kt @@ -1,6 +1,7 @@ package org.jetbrains.compose.storytale import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import kotlin.reflect.KProperty val storiesStorage = mutableListOf() @@ -23,7 +24,17 @@ class StoryDelegate( } operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): StoryDelegate { - instance = Story(storiesStorage.size, property.name, group, code, content).also(storiesStorage::add) + val wrappedContent: @Composable Story.() -> Unit = { + val story = this + val delegate = this@StoryDelegate + + CompositionLocalProvider(LocalStory provides story) { + delegate.content(story) + } + } + + instance = Story(storiesStorage.size, property.name, group, code, wrappedContent) + .also(storiesStorage::add) return this } } From 704cb2319ffc3114ee8c3fc6b186a4e66d5fc110 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sat, 10 May 2025 23:54:32 +0700 Subject: [PATCH 07/22] apply spotless --- .../kotlin/storytale/gallery/demo/PreviewButton.kt | 1 + modules/preview-processor/build.gradle.kts | 2 -- .../preview-processor/src/main/kotlin/PreviewProcessor.kt | 5 ----- .../src/test/kotlin/PreviewProcessorTest.kt | 2 +- .../org/jetbrains/compose/storytale/PreviewParameter.kt | 6 ++---- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt b/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt index ce397f0..d396a5c 100644 --- a/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt +++ b/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt @@ -15,6 +15,7 @@ import org.jetbrains.compose.storytale.previewParameter @org.jetbrains.compose.ui.tooling.preview.Preview @Composable +@Suppress("ktlint") fun PreviewExtendedFAB() { val bgColor by previewParameter(MaterialTheme.colorScheme.primary) diff --git a/modules/preview-processor/build.gradle.kts b/modules/preview-processor/build.gradle.kts index ee9deeb..f904584 100644 --- a/modules/preview-processor/build.gradle.kts +++ b/modules/preview-processor/build.gradle.kts @@ -1,6 +1,4 @@ import org.gradle.kotlin.dsl.withType -import org.jetbrains.compose.ComposePlugin.CommonComponentsDependencies -import org.jetbrains.compose.ComposePlugin.CommonComponentsDependencies.uiToolingPreview import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask plugins { diff --git a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt index 489f071..8538ed9 100644 --- a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt +++ b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt @@ -10,14 +10,9 @@ import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.google.devtools.ksp.symbol.KSVisitorVoid import com.google.devtools.ksp.validate import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.CodeBlock -import com.squareup.kotlinpoet.Dynamic import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.PropertySpec -import com.squareup.kotlinpoet.TypeName -import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.buildCodeBlock import org.jetbrains.compose.storytale.plugin.StorytaleGradlePlugin diff --git a/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt b/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt index 8c429b1..93bc402 100644 --- a/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt +++ b/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt @@ -49,7 +49,7 @@ class PreviewProcessorTest { compilerPluginRegistrars = listOf(ComposePluginRegistrar()) useKsp2() - symbolProcessorProviders.add(PreviewProcessor.Provider())// = mutableListOf(PreviewProcessor.Provider()) + symbolProcessorProviders.add(PreviewProcessor.Provider()) // magic inheritClassPath = true diff --git a/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/PreviewParameter.kt b/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/PreviewParameter.kt index a4e36f1..859b14e 100644 --- a/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/PreviewParameter.kt +++ b/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/PreviewParameter.kt @@ -4,8 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.staticCompositionLocalOf @Composable -inline fun previewParameter(defaultValue: T) = - LocalStory.current.parameter(defaultValue) +inline fun previewParameter(defaultValue: T) = LocalStory.current.parameter(defaultValue) @Composable inline fun previewParameter( @@ -15,8 +14,7 @@ inline fun previewParameter( ) = LocalStory.current.parameter(values, defaultValueIndex, label) @Composable -inline fun > previewParameter(defaultValue: T, label: String? = null) = - LocalStory.current.parameter(defaultValue, label) +inline fun > previewParameter(defaultValue: T, label: String? = null) = LocalStory.current.parameter(defaultValue, label) @PublishedApi internal val LocalStory = staticCompositionLocalOf { Story(-1, "DefaultPreviewStory", "", "", {}) } From 9ba6ec28fe000e5b0e75028be334167f3e18efd3 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sun, 11 May 2025 00:07:05 +0700 Subject: [PATCH 08/22] add :modules:preview-processor-test --- modules/preview-processor-test/.gitignore | 1 + .../preview-processor-test/build.gradle.kts | 27 +++++++++++++++++++ settings.gradle.kts | 1 + 3 files changed, 29 insertions(+) create mode 100644 modules/preview-processor-test/.gitignore create mode 100644 modules/preview-processor-test/build.gradle.kts diff --git a/modules/preview-processor-test/.gitignore b/modules/preview-processor-test/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/modules/preview-processor-test/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/modules/preview-processor-test/build.gradle.kts b/modules/preview-processor-test/build.gradle.kts new file mode 100644 index 0000000..facf907 --- /dev/null +++ b/modules/preview-processor-test/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) +} + +kotlin { + jvm("desktop") + androidTarget() + + sourceSets { + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val androidUnitTest by getting { + dependencies { + + } + } + val desktopTest by getting { + dependencies { + + } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 261d953..6e7dddc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -60,5 +60,6 @@ include(":modules:compiler-plugin") include(":modules:dokka-plugin") include(":modules:runtime-api") include(":modules:preview-processor") +include(":modules:preview-processor-test") include(":gallery-demo") From 885c84e222807ccc5c920c215adf58d33e3729d2 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sun, 11 May 2025 13:02:08 +0700 Subject: [PATCH 09/22] move PreviewProcessorTest to :modules:preview-processor-test --- .../preview-processor-test/build.gradle.kts | 53 ++++++- .../kotlin/PreviewProcessorAndroidTest.kt} | 42 ++--- .../src/commonMain/kotlin/util/HasContent.kt | 8 + .../jvmTest/kotlin/PreviewProcessorTest.kt | 147 ++++++++++++++++++ modules/preview-processor/build.gradle.kts | 19 --- .../src/main/kotlin/PreviewProcessor.kt | 16 +- 6 files changed, 227 insertions(+), 58 deletions(-) rename modules/{preview-processor/src/test/kotlin/PreviewProcessorTest.kt => preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt} (68%) create mode 100644 modules/preview-processor-test/src/commonMain/kotlin/util/HasContent.kt create mode 100644 modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt diff --git a/modules/preview-processor-test/build.gradle.kts b/modules/preview-processor-test/build.gradle.kts index facf907..1f7c62a 100644 --- a/modules/preview-processor-test/build.gradle.kts +++ b/modules/preview-processor-test/build.gradle.kts @@ -1,27 +1,70 @@ +import org.gradle.kotlin.dsl.compileOnly +import org.gradle.kotlin.dsl.kotlin +import org.gradle.kotlin.dsl.project +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose.compiler) } kotlin { - jvm("desktop") + jvm() androidTarget() sourceSets { - val commonTest by getting { + val commonMain by getting { + dependencies { + compileOnly(compose.runtime) + } + } + val jvmMain by getting { dependencies { + implementation(compose.components.uiToolingPreview) + implementation(project(":modules:preview-processor")) implementation(kotlin("test")) + + implementation(compose.runtime) + implementation(kotlin("compiler-embeddable")) + implementation(kotlin("compose-compiler-plugin-embeddable")) + implementation(kotlin("test")) + implementation(libs.assertj.core) + implementation(libs.junit) + implementation(libs.kotlinCompileTesting.ksp) + implementation(project(":modules:runtime-api")) } } val androidUnitTest by getting { - dependencies { + dependsOn(jvmMain) + dependencies { + implementation("androidx.compose.ui:ui-tooling-preview-android:1.7.0") } } - val desktopTest by getting { + val jvmTest by getting { dependencies { - + implementation("androidx.compose.ui:ui-tooling-preview-desktop:1.7.0") } } } } + +android { + compileSdk = 35 + namespace = "org.jetbrains.compose.storytale.preview.processor.test" + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + defaultConfig { + minSdk = 24 + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } +} + +tasks.withType>().configureEach { + compilerOptions.optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") +} diff --git a/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt b/modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt similarity index 68% rename from modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt rename to modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt index 93bc402..d3617ad 100644 --- a/modules/preview-processor/src/test/kotlin/PreviewProcessorTest.kt +++ b/modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt @@ -7,45 +7,32 @@ import com.tschuchort.compiletesting.symbolProcessorProviders import com.tschuchort.compiletesting.useKsp2 import org.assertj.core.api.Assertions.assertThat import org.assertj.core.util.Files -import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.utils.fileUtils.descendantRelativeTo import org.junit.Test +import util.hasContent + +class PreviewProcessorAndroidTest { -class PreviewProcessorTest { @Test - fun `generates story for single preview function`() { - val group1Kt = SourceFile.kotlin( - "KmpButton.kt", + fun `generates story for Androidx Android Preview function`() { + val sourceFile = SourceFile.kotlin( + "AndroidButton.kt", """ package storytale.gallery.demo - @androidx.compose.runtime.Composable - fun KmpButton() { } - - @org.jetbrains.compose.ui.tooling.preview.Preview - @androidx.compose.runtime.Composable - fun PreviewKmpButton() { - KmpButton() - } - """, - ) - val group2Kt = SourceFile.kotlin( - "AndroidButton.kt", - """package storytale.gallery.demo - @androidx.compose.runtime.Composable fun AndroidButton() { } - @org.jetbrains.compose.ui.tooling.preview.Preview + @androidx.compose.ui.tooling.preview.Preview @androidx.compose.runtime.Composable fun PreviewAndroidButton() { AndroidButton() } - """, + """, ) val compilation = KotlinCompilation().apply { - sources = listOf(group1Kt) + sources = listOf(sourceFile) compilerPluginRegistrars = listOf(ComposePluginRegistrar()) useKsp2() @@ -76,17 +63,12 @@ class PreviewProcessorTest { | |import org.jetbrains.compose.storytale.Story |import org.jetbrains.compose.storytale.story - |import storytale.gallery.demo.PreviewKmpButton + |import storytale.gallery.demo.PreviewAndroidButton | - |public val KmpButton: Story by story { - | PreviewKmpButton() + |public val AndroidButton: Story by story { + | PreviewAndroidButton() |} """.trimMargin(), ) } } - -@Suppress("NOTHING_TO_INLINE") -inline infix fun String.hasContent(@Language("kotlin") content: String): Pair { - return this to content -} diff --git a/modules/preview-processor-test/src/commonMain/kotlin/util/HasContent.kt b/modules/preview-processor-test/src/commonMain/kotlin/util/HasContent.kt new file mode 100644 index 0000000..ae086b5 --- /dev/null +++ b/modules/preview-processor-test/src/commonMain/kotlin/util/HasContent.kt @@ -0,0 +1,8 @@ +package util + +import org.intellij.lang.annotations.Language + +@Suppress("NOTHING_TO_INLINE") +inline infix fun String.hasContent(@Language("kotlin") content: String): Pair { + return this to content +} diff --git a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt new file mode 100644 index 0000000..732f48c --- /dev/null +++ b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt @@ -0,0 +1,147 @@ +import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import com.tschuchort.compiletesting.kspSourcesDir +import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor +import com.tschuchort.compiletesting.symbolProcessorProviders +import com.tschuchort.compiletesting.useKsp2 +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.util.Files +import org.jetbrains.kotlin.utils.fileUtils.descendantRelativeTo +import org.junit.Test +import util.hasContent + +class PreviewProcessorTest { + @Test + fun `generates story for Jetbrains Compose Preview function`() { + val group1Kt = SourceFile.kotlin( + "KmpButton.kt", + """ + package storytale.gallery.demo + + @androidx.compose.runtime.Composable + fun KmpButton() { } + + @org.jetbrains.compose.ui.tooling.preview.Preview + @androidx.compose.runtime.Composable + fun PreviewKmpButton() { + KmpButton() + } + """, + ) + val group2Kt = SourceFile.kotlin( + "AndroidButton.kt", + """package storytale.gallery.demo + + @androidx.compose.runtime.Composable + fun AndroidButton() { } + + @org.jetbrains.compose.ui.tooling.preview.Preview + @androidx.compose.runtime.Composable + fun PreviewAndroidButton() { + AndroidButton() + } + """, + ) + + val compilation = KotlinCompilation().apply { + sources = listOf(group1Kt) + + compilerPluginRegistrars = listOf(ComposePluginRegistrar()) + useKsp2() + symbolProcessorProviders.add(PreviewProcessor.Provider()) + + // magic + inheritClassPath = true + messageOutputStream = System.out // see diagnostics in real time + jvmTarget = "21" + verbose = false + } + val result = compilation + .compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + assertThat(result.sourcesGeneratedBySymbolProcessor.toList()).hasSize(1) + + assertThat( + result.sourcesGeneratedBySymbolProcessor.toList().map { + it.descendantRelativeTo(compilation.kspSourcesDir).path to + Files.contentOf(it, Charsets.UTF_8).trim() + }, + ) + .containsExactlyInAnyOrder( + "kotlin/org/jetbrains/compose/storytale/generated/Previews.story.kt" hasContent """ + |package org.jetbrains.compose.storytale.generated + | + |import org.jetbrains.compose.storytale.Story + |import org.jetbrains.compose.storytale.story + |import storytale.gallery.demo.PreviewKmpButton + | + |public val KmpButton: Story by story { + | PreviewKmpButton() + |} + """.trimMargin(), + ) + } + + @Test + fun `generates story for Androidx Desktop Preview function`() { + val sourceFile = SourceFile.kotlin( + "DesktopButton.kt", + """ + package storytale.gallery.demo + + @androidx.compose.runtime.Composable + fun DesktopButton() { } + + @androidx.compose.desktop.ui.tooling.preview.Preview + @androidx.compose.runtime.Composable + fun PreviewDesktopButton() { + DesktopButton() + } + """, + ) + + val compilation = KotlinCompilation().apply { + sources = listOf(sourceFile) + + compilerPluginRegistrars = listOf(ComposePluginRegistrar()) + useKsp2() + symbolProcessorProviders.add(PreviewProcessor.Provider()) + + // magic + inheritClassPath = true + messageOutputStream = System.out // see diagnostics in real time + jvmTarget = "21" + verbose = false + } + val result = compilation + .compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + assertThat(result.sourcesGeneratedBySymbolProcessor.toList()).hasSize(1) + + assertThat( + result.sourcesGeneratedBySymbolProcessor.toList().map { + it.descendantRelativeTo(compilation.kspSourcesDir).path to + Files.contentOf(it, Charsets.UTF_8).trim() + }, + ) + .containsExactlyInAnyOrder( + "kotlin/org/jetbrains/compose/storytale/generated/Previews.story.kt" hasContent """ + |package org.jetbrains.compose.storytale.generated + | + |import org.jetbrains.compose.storytale.Story + |import org.jetbrains.compose.storytale.story + |import storytale.gallery.demo.PreviewDesktopButton + | + |public val DesktopButton: Story by story { + | PreviewDesktopButton() + |} + """.trimMargin(), + ) + } + +} diff --git a/modules/preview-processor/build.gradle.kts b/modules/preview-processor/build.gradle.kts index f904584..457d943 100644 --- a/modules/preview-processor/build.gradle.kts +++ b/modules/preview-processor/build.gradle.kts @@ -1,10 +1,5 @@ -import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask - plugins { alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.jetbrainsCompose) - alias(libs.plugins.compose.compiler) } kotlin { @@ -19,21 +14,7 @@ java { } dependencies { - compileOnly(compose.runtime) implementation(libs.kotlin.poet) implementation(libs.ksp.api) implementation(project(":modules:gradle-plugin")) - testImplementation(compose.components.uiToolingPreview) - testImplementation(compose.runtime) - testImplementation(kotlin("compiler-embeddable")) - testImplementation(kotlin("compose-compiler-plugin-embeddable")) - testImplementation(kotlin("test")) - testImplementation(libs.assertj.core) - testImplementation(libs.junit) - testImplementation(libs.kotlinCompileTesting.ksp) - testImplementation(project(":modules:runtime-api")) -} - -tasks.withType>().configureEach { - compilerOptions.optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") } diff --git a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt index 8538ed9..67668aa 100644 --- a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt +++ b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt @@ -14,6 +14,7 @@ import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.buildCodeBlock +import org.gradle.internal.impldep.org.bouncycastle.pqc.legacy.math.linearalgebra.PolynomialRingGF2.rest import org.jetbrains.compose.storytale.plugin.StorytaleGradlePlugin /** @@ -24,13 +25,20 @@ class PreviewProcessor( private val codeGenerator: CodeGenerator, ) : SymbolProcessor { override fun process(resolver: Resolver): List { - val symbols = + val (jetbrains, discarded1) = resolver.getSymbolsWithAnnotation("org.jetbrains.compose.ui.tooling.preview.Preview") - val ret = symbols.filter { !it.validate() }.toList() - symbols + .partition { it.validate() } + val (androidxDesktop, discarded2) = resolver.getSymbolsWithAnnotation("androidx.compose.desktop.ui.tooling.preview.Preview") + .partition { it.validate() } + + val (androidxAndroid, discarded3) = resolver.getSymbolsWithAnnotation("androidx.compose.ui.tooling.preview.Preview") + .partition { it.validate() } + + (jetbrains + androidxDesktop + androidxAndroid) .filter { it is KSFunctionDeclaration && it.validate() } .forEach { it.accept(BuilderVisitor(), Unit) } - return ret + + return discarded1 + discarded2 + discarded3 } inner class BuilderVisitor : KSVisitorVoid() { From 1ac4019855a8a415939d6885db60ad32992e656d Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sun, 11 May 2025 13:31:38 +0700 Subject: [PATCH 10/22] add AssertableFile.kt and Compilation.kt --- .../kotlin/PreviewProcessorAndroidTest.kt | 27 ++------- .../src/commonMain/kotlin/util/HasContent.kt | 8 --- .../src/jvmMain/kotlin/util/AssertableFile.kt | 20 +++++++ .../src/jvmMain/kotlin/util/Compilation.kt | 45 ++++++++++++++ .../jvmTest/kotlin/PreviewProcessorTest.kt | 60 ++----------------- 5 files changed, 75 insertions(+), 85 deletions(-) delete mode 100644 modules/preview-processor-test/src/commonMain/kotlin/util/HasContent.kt create mode 100644 modules/preview-processor-test/src/jvmMain/kotlin/util/AssertableFile.kt create mode 100644 modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt diff --git a/modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt b/modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt index d3617ad..ec961ce 100644 --- a/modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt +++ b/modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt @@ -1,14 +1,10 @@ -import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile -import com.tschuchort.compiletesting.kspSourcesDir import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor -import com.tschuchort.compiletesting.symbolProcessorProviders -import com.tschuchort.compiletesting.useKsp2 import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.util.Files -import org.jetbrains.kotlin.utils.fileUtils.descendantRelativeTo import org.junit.Test +import util.assertableGeneratedKspSources +import util.createCompilation import util.hasContent class PreviewProcessorAndroidTest { @@ -31,18 +27,8 @@ class PreviewProcessorAndroidTest { """, ) - val compilation = KotlinCompilation().apply { + val compilation = createCompilation { sources = listOf(sourceFile) - - compilerPluginRegistrars = listOf(ComposePluginRegistrar()) - useKsp2() - symbolProcessorProviders.add(PreviewProcessor.Provider()) - - // magic - inheritClassPath = true - messageOutputStream = System.out // see diagnostics in real time - jvmTarget = "21" - verbose = false } val result = compilation .compile() @@ -51,12 +37,7 @@ class PreviewProcessorAndroidTest { assertThat(result.sourcesGeneratedBySymbolProcessor.toList()).hasSize(1) - assertThat( - result.sourcesGeneratedBySymbolProcessor.toList().map { - it.descendantRelativeTo(compilation.kspSourcesDir).path to - Files.contentOf(it, Charsets.UTF_8).trim() - }, - ) + assertThat(result.assertableGeneratedKspSources(compilation)) .containsExactlyInAnyOrder( "kotlin/org/jetbrains/compose/storytale/generated/Previews.story.kt" hasContent """ |package org.jetbrains.compose.storytale.generated diff --git a/modules/preview-processor-test/src/commonMain/kotlin/util/HasContent.kt b/modules/preview-processor-test/src/commonMain/kotlin/util/HasContent.kt deleted file mode 100644 index ae086b5..0000000 --- a/modules/preview-processor-test/src/commonMain/kotlin/util/HasContent.kt +++ /dev/null @@ -1,8 +0,0 @@ -package util - -import org.intellij.lang.annotations.Language - -@Suppress("NOTHING_TO_INLINE") -inline infix fun String.hasContent(@Language("kotlin") content: String): Pair { - return this to content -} diff --git a/modules/preview-processor-test/src/jvmMain/kotlin/util/AssertableFile.kt b/modules/preview-processor-test/src/jvmMain/kotlin/util/AssertableFile.kt new file mode 100644 index 0000000..1b3b076 --- /dev/null +++ b/modules/preview-processor-test/src/jvmMain/kotlin/util/AssertableFile.kt @@ -0,0 +1,20 @@ +package util + +import java.io.File +import org.assertj.core.util.Files +import org.intellij.lang.annotations.Language + +data class AssertableFile( + val path: String, + val content: String, +) + +fun File.assertable(): AssertableFile { + val it = this + return path hasContent Files.contentOf(it, Charsets.UTF_8).trim() +} + +@Suppress("NOTHING_TO_INLINE") +inline infix fun String.hasContent(@Language("kotlin") content: String): AssertableFile { + return AssertableFile(this, content) +} diff --git a/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt b/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt new file mode 100644 index 0000000..60346f3 --- /dev/null +++ b/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt @@ -0,0 +1,45 @@ +package util + +import PreviewProcessor +import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar +import com.tschuchort.compiletesting.JvmCompilationResult +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.kspSourcesDir +import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor +import com.tschuchort.compiletesting.symbolProcessorProviders +import com.tschuchort.compiletesting.useKsp2 +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import org.jetbrains.kotlin.utils.fileUtils.descendantRelativeTo + +@OptIn(ExperimentalContracts::class) +fun createCompilation(builder: KotlinCompilation.() -> Unit): KotlinCompilation { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + return KotlinCompilation().apply { + + compilerPluginRegistrars = listOf(ComposePluginRegistrar()) + useKsp2() + symbolProcessorProviders.add(PreviewProcessor.Provider()) + + // magic + inheritClassPath = true + messageOutputStream = System.out // see diagnostics in real time + jvmTarget = "21" + verbose = false + + builder() + } +} + +fun JvmCompilationResult.assertableGeneratedKspSources( + compilation: KotlinCompilation, +): List { + return sourcesGeneratedBySymbolProcessor.toList().map { + it.descendantRelativeTo(compilation.kspSourcesDir).path hasContent + org.assertj.core.util.Files.contentOf(it, Charsets.UTF_8).trim() + } +} diff --git a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt index 732f48c..c63c004 100644 --- a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt +++ b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt @@ -1,14 +1,10 @@ -import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile -import com.tschuchort.compiletesting.kspSourcesDir import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor -import com.tschuchort.compiletesting.symbolProcessorProviders -import com.tschuchort.compiletesting.useKsp2 import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.util.Files -import org.jetbrains.kotlin.utils.fileUtils.descendantRelativeTo import org.junit.Test +import util.assertableGeneratedKspSources +import util.createCompilation import util.hasContent class PreviewProcessorTest { @@ -29,33 +25,9 @@ class PreviewProcessorTest { } """, ) - val group2Kt = SourceFile.kotlin( - "AndroidButton.kt", - """package storytale.gallery.demo - @androidx.compose.runtime.Composable - fun AndroidButton() { } - - @org.jetbrains.compose.ui.tooling.preview.Preview - @androidx.compose.runtime.Composable - fun PreviewAndroidButton() { - AndroidButton() - } - """, - ) - - val compilation = KotlinCompilation().apply { + val compilation = createCompilation { sources = listOf(group1Kt) - - compilerPluginRegistrars = listOf(ComposePluginRegistrar()) - useKsp2() - symbolProcessorProviders.add(PreviewProcessor.Provider()) - - // magic - inheritClassPath = true - messageOutputStream = System.out // see diagnostics in real time - jvmTarget = "21" - verbose = false } val result = compilation .compile() @@ -64,12 +36,7 @@ class PreviewProcessorTest { assertThat(result.sourcesGeneratedBySymbolProcessor.toList()).hasSize(1) - assertThat( - result.sourcesGeneratedBySymbolProcessor.toList().map { - it.descendantRelativeTo(compilation.kspSourcesDir).path to - Files.contentOf(it, Charsets.UTF_8).trim() - }, - ) + assertThat(result.assertableGeneratedKspSources(compilation)) .containsExactlyInAnyOrder( "kotlin/org/jetbrains/compose/storytale/generated/Previews.story.kt" hasContent """ |package org.jetbrains.compose.storytale.generated @@ -103,18 +70,8 @@ class PreviewProcessorTest { """, ) - val compilation = KotlinCompilation().apply { + val compilation = createCompilation { sources = listOf(sourceFile) - - compilerPluginRegistrars = listOf(ComposePluginRegistrar()) - useKsp2() - symbolProcessorProviders.add(PreviewProcessor.Provider()) - - // magic - inheritClassPath = true - messageOutputStream = System.out // see diagnostics in real time - jvmTarget = "21" - verbose = false } val result = compilation .compile() @@ -123,12 +80,7 @@ class PreviewProcessorTest { assertThat(result.sourcesGeneratedBySymbolProcessor.toList()).hasSize(1) - assertThat( - result.sourcesGeneratedBySymbolProcessor.toList().map { - it.descendantRelativeTo(compilation.kspSourcesDir).path to - Files.contentOf(it, Charsets.UTF_8).trim() - }, - ) + assertThat(result.assertableGeneratedKspSources(compilation)) .containsExactlyInAnyOrder( "kotlin/org/jetbrains/compose/storytale/generated/Previews.story.kt" hasContent """ |package org.jetbrains.compose.storytale.generated From 0eb1388f2b7190da144c74db8137f56b8c9e7ec1 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sun, 11 May 2025 13:32:48 +0700 Subject: [PATCH 11/22] apply spotless --- .../src/jvmMain/kotlin/util/Compilation.kt | 1 - .../src/jvmTest/kotlin/PreviewProcessorTest.kt | 1 - modules/preview-processor/src/main/kotlin/PreviewProcessor.kt | 1 - 3 files changed, 3 deletions(-) diff --git a/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt b/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt index 60346f3..e460614 100644 --- a/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt +++ b/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt @@ -20,7 +20,6 @@ fun createCompilation(builder: KotlinCompilation.() -> Unit): KotlinCompilation } return KotlinCompilation().apply { - compilerPluginRegistrars = listOf(ComposePluginRegistrar()) useKsp2() symbolProcessorProviders.add(PreviewProcessor.Provider()) diff --git a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt index c63c004..0e5e7f9 100644 --- a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt +++ b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt @@ -95,5 +95,4 @@ class PreviewProcessorTest { """.trimMargin(), ) } - } diff --git a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt index 67668aa..5229228 100644 --- a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt +++ b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt @@ -14,7 +14,6 @@ import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.buildCodeBlock -import org.gradle.internal.impldep.org.bouncycastle.pqc.legacy.math.linearalgebra.PolynomialRingGF2.rest import org.jetbrains.compose.storytale.plugin.StorytaleGradlePlugin /** From c0ce7837b7486316216c617f18373f27d6b06197 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sun, 11 May 2025 13:57:19 +0700 Subject: [PATCH 12/22] bump JDK version v21 in github workflows --- .github/workflows/ci.yaml | 4 ++-- .github/workflows/smokebuild.yaml | 2 +- .github/workflows/spotless.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 30f64d2..2b1a971 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: uses: actions/setup-java@v2 with: distribution: 'adopt' - java-version: '17' + java-version: '21' - name: Checkout code uses: actions/checkout@v3 @@ -54,4 +54,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/smokebuild.yaml b/.github/workflows/smokebuild.yaml index f1499ff..430daa4 100644 --- a/.github/workflows/smokebuild.yaml +++ b/.github/workflows/smokebuild.yaml @@ -16,7 +16,7 @@ jobs: uses: actions/setup-java@v2 with: distribution: 'adopt' - java-version: '17' + java-version: '21' - name: Checkout code uses: actions/checkout@v3 diff --git a/.github/workflows/spotless.yaml b/.github/workflows/spotless.yaml index 83cd7ad..997bfe7 100644 --- a/.github/workflows/spotless.yaml +++ b/.github/workflows/spotless.yaml @@ -16,7 +16,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - java-version: '17' + java-version: '21' distribution: 'temurin' cache: gradle From 2f1cd84c8f4d76986b964f0e6f806d5d30346d82 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sun, 11 May 2025 21:02:40 +0700 Subject: [PATCH 13/22] disable proguard for :gallery-demo desktop target --- gallery-demo/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gallery-demo/build.gradle.kts b/gallery-demo/build.gradle.kts index 1036ff7..3db0855 100644 --- a/gallery-demo/build.gradle.kts +++ b/gallery-demo/build.gradle.kts @@ -110,6 +110,9 @@ kotlin { compose.desktop { application { mainClass = "storytale.gallery.demo.MainKt" + buildTypes.release.proguard { + isEnabled.set(false) + } } } From a49870987ef99efc4f7ee390d8c73416ce493303 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sun, 11 May 2025 21:36:50 +0700 Subject: [PATCH 14/22] pretty PreviewProcessorTest --- .../kotlin/PreviewProcessorAndroidTest.kt | 14 +++-------- .../src/jvmMain/kotlin/util/Compilation.kt | 24 +++++++++++++++--- .../jvmTest/kotlin/PreviewProcessorTest.kt | 25 ++++++------------- 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt b/modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt index ec961ce..7577342 100644 --- a/modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt +++ b/modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt @@ -1,19 +1,17 @@ import com.tschuchort.compiletesting.KotlinCompilation -import com.tschuchort.compiletesting.SourceFile import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor import org.assertj.core.api.Assertions.assertThat import org.junit.Test import util.assertableGeneratedKspSources -import util.createCompilation import util.hasContent +import util.storytaleTest class PreviewProcessorAndroidTest { @Test fun `generates story for Androidx Android Preview function`() { - val sourceFile = SourceFile.kotlin( - "AndroidButton.kt", - """ + val compilation = storytaleTest { + "AndroidButton.kt" hasContent """ package storytale.gallery.demo @androidx.compose.runtime.Composable @@ -24,11 +22,7 @@ class PreviewProcessorAndroidTest { fun PreviewAndroidButton() { AndroidButton() } - """, - ) - - val compilation = createCompilation { - sources = listOf(sourceFile) + """ } val result = compilation .compile() diff --git a/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt b/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt index e460614..df8c933 100644 --- a/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt +++ b/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt @@ -4,6 +4,7 @@ import PreviewProcessor import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile import com.tschuchort.compiletesting.kspSourcesDir import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor import com.tschuchort.compiletesting.symbolProcessorProviders @@ -11,12 +12,23 @@ import com.tschuchort.compiletesting.useKsp2 import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.utils.fileUtils.descendantRelativeTo @OptIn(ExperimentalContracts::class) -fun createCompilation(builder: KotlinCompilation.() -> Unit): KotlinCompilation { +fun storytaleTest( + compilationBuilder: KotlinCompilation.() -> Unit = {}, + testSourceBuilder: StorytaleTestSourceScope.() -> Unit, +): KotlinCompilation { contract { - callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + callsInPlace(compilationBuilder, InvocationKind.EXACTLY_ONCE) + } + + val testSourceScope = object : StorytaleTestSourceScope { + val sources = mutableListOf() + override fun String.hasContent(content: String) { + sources.add(SourceFile.Companion.kotlin(this, content)) + } } return KotlinCompilation().apply { @@ -30,10 +42,16 @@ fun createCompilation(builder: KotlinCompilation.() -> Unit): KotlinCompilation jvmTarget = "21" verbose = false - builder() + compilationBuilder() + testSourceScope.testSourceBuilder() + sources = testSourceScope.sources } } +interface StorytaleTestSourceScope { + infix fun String.hasContent(@Language("kotlin") content: String) +} + fun JvmCompilationResult.assertableGeneratedKspSources( compilation: KotlinCompilation, ): List { diff --git a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt index 0e5e7f9..bd0bed4 100644 --- a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt +++ b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt @@ -1,18 +1,16 @@ import com.tschuchort.compiletesting.KotlinCompilation -import com.tschuchort.compiletesting.SourceFile import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor import org.assertj.core.api.Assertions.assertThat import org.junit.Test import util.assertableGeneratedKspSources -import util.createCompilation import util.hasContent +import util.storytaleTest class PreviewProcessorTest { @Test fun `generates story for Jetbrains Compose Preview function`() { - val group1Kt = SourceFile.kotlin( - "KmpButton.kt", - """ + val compilation = storytaleTest { + "KmpButton.kt" hasContent """ package storytale.gallery.demo @androidx.compose.runtime.Composable @@ -23,11 +21,7 @@ class PreviewProcessorTest { fun PreviewKmpButton() { KmpButton() } - """, - ) - - val compilation = createCompilation { - sources = listOf(group1Kt) + """ } val result = compilation .compile() @@ -54,9 +48,8 @@ class PreviewProcessorTest { @Test fun `generates story for Androidx Desktop Preview function`() { - val sourceFile = SourceFile.kotlin( - "DesktopButton.kt", - """ + val compilation = storytaleTest { + "DesktopButton.kt" hasContent """ package storytale.gallery.demo @androidx.compose.runtime.Composable @@ -67,11 +60,7 @@ class PreviewProcessorTest { fun PreviewDesktopButton() { DesktopButton() } - """, - ) - - val compilation = createCompilation { - sources = listOf(sourceFile) + """ } val result = compilation .compile() From 2350e9a59fdee926b0669756173497b7ec75726b Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sun, 11 May 2025 21:52:01 +0700 Subject: [PATCH 15/22] Support multiple @Preview composables --- .../src/jvmMain/kotlin/util/AssertableFile.kt | 12 +- .../jvmTest/kotlin/PreviewProcessorTest.kt | 119 ++++++++++++++++++ .../src/main/kotlin/PreviewProcessor.kt | 61 +++++---- 3 files changed, 166 insertions(+), 26 deletions(-) diff --git a/modules/preview-processor-test/src/jvmMain/kotlin/util/AssertableFile.kt b/modules/preview-processor-test/src/jvmMain/kotlin/util/AssertableFile.kt index 1b3b076..9d80d81 100644 --- a/modules/preview-processor-test/src/jvmMain/kotlin/util/AssertableFile.kt +++ b/modules/preview-processor-test/src/jvmMain/kotlin/util/AssertableFile.kt @@ -7,7 +7,17 @@ import org.intellij.lang.annotations.Language data class AssertableFile( val path: String, val content: String, -) +) { + override fun toString(): String { + return buildString { + append('\"') + append(path) + appendLine("\" has content \"\"\"") + appendLine(content) + appendLine("\"\"\"") + } + } +} fun File.assertable(): AssertableFile { val it = this diff --git a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt index bd0bed4..45e5f35 100644 --- a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt +++ b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt @@ -84,4 +84,123 @@ class PreviewProcessorTest { """.trimMargin(), ) } + + @Test + fun `generates stories for multiple Preview functions in the same file`() { + + val compilation = storytaleTest { + "MultiplePreviews.kt" hasContent """ + package storytale.gallery.demo + + import androidx.compose.runtime.Composable + import org.jetbrains.compose.ui.tooling.preview.Preview + + @Composable + fun Button() { } + + @Preview + @Composable + fun PreviewButton1() { + Button() + } + @Preview + @Composable + fun PreviewButton2() { + Button() + } + """.trimIndent() + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + assertThat(result.sourcesGeneratedBySymbolProcessor.toList()).hasSize(1) + + assertThat(result.assertableGeneratedKspSources(compilation)) + .containsExactlyInAnyOrder( + "kotlin/org/jetbrains/compose/storytale/generated/Previews.story.kt" hasContent """ + |package org.jetbrains.compose.storytale.generated + | + |import org.jetbrains.compose.storytale.Story + |import org.jetbrains.compose.storytale.story + |import storytale.gallery.demo.PreviewButton1 + |import storytale.gallery.demo.PreviewButton2 + | + |public val Button1: Story by story { + | PreviewButton1() + |} + | + |public val Button2: Story by story { + | PreviewButton2() + |} + """.trimMargin() + ) + } + + @Test + fun `generates stories for Preview functions in different files calling same component`() { + val compilation = storytaleTest { + "CommonButton.kt" hasContent """ + package storytale.gallery.demo + + import androidx.compose.runtime.Composable + + @Composable + fun Button() { } + """.trimIndent() + + "FirstPreview.kt" hasContent """ + package storytale.gallery.demo.first + + import androidx.compose.runtime.Composable + import org.jetbrains.compose.ui.tooling.preview.Preview + import storytale.gallery.demo.Button + + @Preview + @Composable + fun PreviewButtonPrimary() { + Button() + } + """.trimIndent() + + "SecondPreview.kt" hasContent """ + package storytale.gallery.demo.second + + import androidx.compose.runtime.Composable + import org.jetbrains.compose.ui.tooling.preview.Preview + import storytale.gallery.demo.Button + + @Preview + @Composable + fun PreviewButtonSecondary() { + Button() + } + """.trimIndent() + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + assertThat(result.sourcesGeneratedBySymbolProcessor.toList()).hasSize(1) + + assertThat(result.assertableGeneratedKspSources(compilation)) + .containsExactlyInAnyOrder( + "kotlin/org/jetbrains/compose/storytale/generated/Previews.story.kt" hasContent """ + |package org.jetbrains.compose.storytale.generated + | + |import org.jetbrains.compose.storytale.Story + |import org.jetbrains.compose.storytale.story + |import storytale.gallery.demo.first.PreviewButtonPrimary + |import storytale.gallery.demo.second.PreviewButtonSecondary + | + |public val ButtonPrimary: Story by story { + | PreviewButtonPrimary() + |} + | + |public val ButtonSecondary: Story by story { + | PreviewButtonSecondary() + |} + """.trimMargin() + ) + } } diff --git a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt index 5229228..95ca8bc 100644 --- a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt +++ b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt @@ -7,18 +7,13 @@ import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorProvider import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSFunctionDeclaration -import com.google.devtools.ksp.symbol.KSVisitorVoid import com.google.devtools.ksp.validate import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.buildCodeBlock import org.jetbrains.compose.storytale.plugin.StorytaleGradlePlugin -/** - * https://github.com/google/ksp/blob/1db41bc678a1d0e86f71ca3b052a147675769691/examples/playground/test-processor/src/main/kotlin/BuilderProcessor.kt - */ class PreviewProcessor( private val logger: KSPLogger, private val codeGenerator: CodeGenerator, @@ -33,50 +28,66 @@ class PreviewProcessor( val (androidxAndroid, discarded3) = resolver.getSymbolsWithAnnotation("androidx.compose.ui.tooling.preview.Preview") .partition { it.validate() } - (jetbrains + androidxDesktop + androidxAndroid) + val validPreviewFunctions = (jetbrains + androidxDesktop + androidxAndroid) .filter { it is KSFunctionDeclaration && it.validate() } - .forEach { it.accept(BuilderVisitor(), Unit) } + .map { it as KSFunctionDeclaration } + + + generatePreviewFile(validPreviewFunctions) return discarded1 + discarded2 + discarded3 } - inner class BuilderVisitor : KSVisitorVoid() { - override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) { - val packageName = StorytaleGradlePlugin.STORYTALE_PACKAGE - val fileSpecBuilder = FileSpec.builder( - MemberName(packageName, "Preview.story"), - ).apply { - indent(" ") + private fun generatePreviewFile(previewFunctions: List) { + if (previewFunctions.isEmpty()) return - addImport("org.jetbrains.compose.storytale", "story") + val packageName = StorytaleGradlePlugin.STORYTALE_PACKAGE + val fileSpecBuilder = FileSpec.builder( + packageName, "Previews.story", + ).apply { + indent(" ") + addImport("org.jetbrains.compose.storytale", "story") + + previewFunctions.forEach { function -> addImport(function.packageName.asString(), function.simpleName.asString()) + val functionName = function.simpleName.asString() + val storyName = functionName.removePrefix("Preview").removeSuffix("Preview") + addProperty( PropertySpec .builder( - function.simpleName.asString() - // because removeSurrounding doesn't work - .removePrefix("Preview").removeSuffix("Preview"), + storyName.ifEmpty { functionName }, ClassName("org.jetbrains.compose.storytale", "Story"), ) .delegate( buildCodeBlock { beginControlFlow("story") - addStatement("%N()", function.simpleName.asString()) + addStatement("%N()", functionName) endControlFlow() }, ) .build(), ) } + } - val file = codeGenerator.createNewFile( - Dependencies(true, function.containingFile!!), - packageName, - "Previews.story", - ) - fileSpecBuilder.build().toJavaFileObject().openInputStream().copyTo(file) + val containingFiles = previewFunctions + .mapNotNull { it.containingFile } + .distinct() + + val dependencies = if (containingFiles.isNotEmpty()) { + Dependencies(true, containingFiles.first()) + } else { + Dependencies.ALL_FILES } + + val file = codeGenerator.createNewFile( + dependencies, + packageName, + "Previews.story", + ) + fileSpecBuilder.build().toJavaFileObject().openInputStream().copyTo(file) } class Provider : SymbolProcessorProvider { From b84713dbe4d4cf4a6341436c3a1e7d033605865a Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sun, 11 May 2025 21:56:49 +0700 Subject: [PATCH 16/22] Add @Preview composables samples in :gallery-demo --- .../storytale/gallery/demo/PreviewCheckbox.kt | 27 +++++++++++++++++++ .../storytale/gallery/demo/PreviewButton.kt | 24 +++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt diff --git a/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt b/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt new file mode 100644 index 0000000..cadf453 --- /dev/null +++ b/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt @@ -0,0 +1,27 @@ +package storytale.gallery.demo + +import androidx.compose.material3.TriStateCheckbox +import androidx.compose.runtime.Composable +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.state.ToggleableState.Indeterminate +import androidx.compose.ui.state.ToggleableState.Off +import androidx.compose.ui.state.ToggleableState.On +import org.jetbrains.compose.storytale.previewParameter +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Preview +@Composable +fun PreviewCheckbox() { + var state by previewParameter(ToggleableState.entries) + + TriStateCheckbox( + state = state, + onClick = { + state = when (state) { + On -> Indeterminate + Off -> On + Indeterminate -> Off + } + }, + ) +} diff --git a/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt b/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt index d396a5c..ac2bff9 100644 --- a/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt +++ b/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt @@ -7,8 +7,13 @@ import androidx.compose.material.icons.filled.AddCircle import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.jetbrains.compose.storytale.previewParameter @@ -25,3 +30,22 @@ fun PreviewExtendedFAB() { Text("Extended") } } + +@androidx.compose.desktop.ui.tooling.preview.Preview +@Composable +@Suppress("ktlint") +fun PreviewSegmentedButton() { + val selectedIndex = remember { mutableIntStateOf(0) } + + SingleChoiceSegmentedButtonRow { + repeat(3) { index -> + SegmentedButton( + selected = index == selectedIndex.value, + onClick = { selectedIndex.value = index }, + shape = SegmentedButtonDefaults.itemShape(index, 3), + ) { + Text("Button $index", modifier = Modifier.padding(4.dp)) + } + } + } +} From 6df79a676fa593ddc6fd5d468ca6476af86cb5b4 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sun, 11 May 2025 22:01:48 +0700 Subject: [PATCH 17/22] apply spotless --- .../kotlin/storytale/gallery/demo/PreviewCheckbox.kt | 1 + .../src/jvmTest/kotlin/PreviewProcessorTest.kt | 5 ++--- .../preview-processor/src/main/kotlin/PreviewProcessor.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt b/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt index cadf453..1effaba 100644 --- a/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt +++ b/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt @@ -11,6 +11,7 @@ import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable +@Suppress("ktlint") fun PreviewCheckbox() { var state by previewParameter(ToggleableState.entries) diff --git a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt index 45e5f35..2d128f6 100644 --- a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt +++ b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt @@ -87,7 +87,6 @@ class PreviewProcessorTest { @Test fun `generates stories for multiple Preview functions in the same file`() { - val compilation = storytaleTest { "MultiplePreviews.kt" hasContent """ package storytale.gallery.demo @@ -133,7 +132,7 @@ class PreviewProcessorTest { |public val Button2: Story by story { | PreviewButton2() |} - """.trimMargin() + """.trimMargin(), ) } @@ -200,7 +199,7 @@ class PreviewProcessorTest { |public val ButtonSecondary: Story by story { | PreviewButtonSecondary() |} - """.trimMargin() + """.trimMargin(), ) } } diff --git a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt index 95ca8bc..427568d 100644 --- a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt +++ b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt @@ -32,7 +32,6 @@ class PreviewProcessor( .filter { it is KSFunctionDeclaration && it.validate() } .map { it as KSFunctionDeclaration } - generatePreviewFile(validPreviewFunctions) return discarded1 + discarded2 + discarded3 @@ -43,7 +42,8 @@ class PreviewProcessor( val packageName = StorytaleGradlePlugin.STORYTALE_PACKAGE val fileSpecBuilder = FileSpec.builder( - packageName, "Previews.story", + packageName, + "Previews.story", ).apply { indent(" ") addImport("org.jetbrains.compose.storytale", "story") From 2d1634a22ddb30e443e547ef3eb548d1d49a9256 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sat, 17 May 2025 11:19:41 +0700 Subject: [PATCH 18/22] Sort validPreviewFunctions before generatePreviewFile in PreviewProcessor --- .../jvmTest/kotlin/PreviewProcessorTest.kt | 77 +++++++++++++++++++ .../src/main/kotlin/PreviewProcessor.kt | 1 + 2 files changed, 78 insertions(+) diff --git a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt index 2d128f6..0c85264 100644 --- a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt +++ b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt @@ -202,4 +202,81 @@ class PreviewProcessorTest { """.trimMargin(), ) } + + @Test + fun `verifies stories are generated in alphabetical order`() { + val compilation = storytaleTest { + "ZAButton.kt" hasContent """ + package storytale.gallery.demo.z + + import androidx.compose.runtime.Composable + import org.jetbrains.compose.ui.tooling.preview.Preview + + @Composable + fun ZAButton() { } + + @Preview + @Composable + fun PreviewZAButton() { + ZAButton() + } + """.trimIndent() + + "BButton.kt" hasContent """ + package storytale.gallery.demo.b + + import androidx.compose.runtime.Composable + import org.jetbrains.compose.ui.tooling.preview.Preview + + @Composable + fun BButton() { } + + @Preview + @Composable + fun PreviewBButton() { + BButton() + } + """.trimIndent() + + "AButton.kt" hasContent """ + package storytale.gallery.demo.a + + import androidx.compose.runtime.Composable + import org.jetbrains.compose.ui.tooling.preview.Preview + + @Composable + fun AButton() { } + + @Preview + @Composable + fun PreviewAButton() { + AButton() + } + """.trimIndent() + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + assertThat(result.sourcesGeneratedBySymbolProcessor.toList()).hasSize(1) + + // Verify the stories are generated in alphabetical order based on qualified name + // The package name affects the sorting order: a.PreviewAButton comes before b.PreviewBButton before z.PreviewZAButton + val generatedSource = result.assertableGeneratedKspSources(compilation).first() + val content = generatedSource.content + + val importLines = content.lines().filter { it.startsWith("import storytale.gallery.demo") } + assertThat(importLines).containsExactly( + "import storytale.gallery.demo.a.PreviewAButton", + "import storytale.gallery.demo.b.PreviewBButton", + "import storytale.gallery.demo.z.PreviewZAButton" + ) + + val storyLines = content.lines().filter { it.startsWith("public val") } + assertThat(storyLines).containsExactly( + "public val AButton: Story by story {", + "public val BButton: Story by story {", + "public val ZAButton: Story by story {" + ) + } } diff --git a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt index 427568d..1dadd6b 100644 --- a/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt +++ b/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt @@ -31,6 +31,7 @@ class PreviewProcessor( val validPreviewFunctions = (jetbrains + androidxDesktop + androidxAndroid) .filter { it is KSFunctionDeclaration && it.validate() } .map { it as KSFunctionDeclaration } + .sortedBy { (it.qualifiedName ?: it.simpleName).asString() } generatePreviewFile(validPreviewFunctions) From a233b23a96adc2b9ed65e5cb32a2a71618161b8f Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sat, 17 May 2025 11:20:23 +0700 Subject: [PATCH 19/22] apply spotless --- .../src/jvmTest/kotlin/PreviewProcessorTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt index 0c85264..c118742 100644 --- a/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt +++ b/modules/preview-processor-test/src/jvmTest/kotlin/PreviewProcessorTest.kt @@ -269,14 +269,14 @@ class PreviewProcessorTest { assertThat(importLines).containsExactly( "import storytale.gallery.demo.a.PreviewAButton", "import storytale.gallery.demo.b.PreviewBButton", - "import storytale.gallery.demo.z.PreviewZAButton" + "import storytale.gallery.demo.z.PreviewZAButton", ) val storyLines = content.lines().filter { it.startsWith("public val") } assertThat(storyLines).containsExactly( "public val AButton: Story by story {", "public val BButton: Story by story {", - "public val ZAButton: Story by story {" + "public val ZAButton: Story by story {", ) } } From 53680fb2189e780e5c43771b0a1a3dbca80dcf39 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sat, 17 May 2025 22:07:31 +0700 Subject: [PATCH 20/22] Add PublicPreviewComposableRegistrar --- .../src/jvmMain/kotlin/util/Compilation.kt | 6 +- .../PublicPreviewComposableRegistrarTest.kt | 157 ++++++++++++++++++ modules/preview-processor/build.gradle.kts | 1 + .../PublicPreviewComposableRegistrar.kt | 12 ++ ...in.compiler.plugin.CompilerPluginRegistrar | 17 ++ 5 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 modules/preview-processor-test/src/jvmTest/kotlin/PublicPreviewComposableRegistrarTest.kt create mode 100644 modules/preview-processor/src/main/kotlin/PublicPreviewComposableRegistrar.kt create mode 100644 modules/preview-processor/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar diff --git a/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt b/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt index df8c933..27a3b42 100644 --- a/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt +++ b/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt @@ -1,6 +1,7 @@ package util import PreviewProcessor +import PublicPreviewComposableRegistrar import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation @@ -32,7 +33,10 @@ fun storytaleTest( } return KotlinCompilation().apply { - compilerPluginRegistrars = listOf(ComposePluginRegistrar()) + compilerPluginRegistrars = listOf( + ComposePluginRegistrar(), + PublicPreviewComposableRegistrar(), + ) useKsp2() symbolProcessorProviders.add(PreviewProcessor.Provider()) diff --git a/modules/preview-processor-test/src/jvmTest/kotlin/PublicPreviewComposableRegistrarTest.kt b/modules/preview-processor-test/src/jvmTest/kotlin/PublicPreviewComposableRegistrarTest.kt new file mode 100644 index 0000000..362a60a --- /dev/null +++ b/modules/preview-processor-test/src/jvmTest/kotlin/PublicPreviewComposableRegistrarTest.kt @@ -0,0 +1,157 @@ +import com.tschuchort.compiletesting.KotlinCompilation +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import util.storytaleTest +import java.lang.reflect.Modifier + +class PublicPreviewComposableRegistrarTest { + + @Test + fun `ensure that private Preview functions are made public`() { + val compilation = storytaleTest { + "PrivatePreview.kt" hasContent """ + package test + + import androidx.compose.runtime.Composable + import org.jetbrains.compose.ui.tooling.preview.Preview + + @Composable + private fun PrivateComponent() {} + + @Preview + @Composable + private fun PrivatePreviewFunction() { + PrivateComponent() + } + """ + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + // Load the compiled class + val classLoader = result.classLoader + val previewClass = classLoader.loadClass("test.PrivatePreviewKt") + + // Check that the preview function is public + val previewFunction = previewClass.declaredMethods.find { it.name == "PrivatePreviewFunction" } + assertThat(previewFunction).isNotNull + assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() + + // Regular private function should remain private + val privateComponent = previewClass.declaredMethods.find { it.name == "PrivateComponent" } + assertThat(privateComponent).isNotNull + assertThat(Modifier.isPrivate(privateComponent!!.modifiers)).isTrue() + } + + @Test + fun `ensure that internal Preview functions are made public`() { + val compilation = storytaleTest { + "InternalPreview.kt" hasContent """ + package test + + import androidx.compose.runtime.Composable + import org.jetbrains.compose.ui.tooling.preview.Preview + + @Composable + internal fun InternalComponent() {} + + @Preview + @Composable + internal fun InternalPreviewFunction() { + InternalComponent() + } + """ + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + // Load the compiled class + val classLoader = result.classLoader + val previewClass = classLoader.loadClass("test.InternalPreviewKt") + + // Check that the preview function is public + val previewFunction = previewClass.declaredMethods.find { it.name == "InternalPreviewFunction" } + assertThat(previewFunction).isNotNull + assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() + + // Internal component should remain internal + val internalComponent = previewClass.declaredMethods.find { it.name == "InternalComponent" } + assertThat(internalComponent).isNotNull + assertThat(!Modifier.isPrivate(internalComponent!!.modifiers) && !Modifier.isPublic(internalComponent.modifiers)).isTrue() + } + + @Test + fun `ensure that protected Preview functions are made public in classes`() { + val compilation = storytaleTest { + "ProtectedPreview.kt" hasContent """ + package test + + import androidx.compose.runtime.Composable + import org.jetbrains.compose.ui.tooling.preview.Preview + + open class PreviewContainer { + @Composable + protected fun ProtectedComponent() {} + + @Preview + @Composable + protected fun ProtectedPreviewFunction() { + ProtectedComponent() + } + } + """ + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + // Load the compiled class + val classLoader = result.classLoader + val containerClass = classLoader.loadClass("test.PreviewContainer") + + // Check that the preview function is public + val previewFunction = containerClass.declaredMethods.find { it.name == "ProtectedPreviewFunction" } + assertThat(previewFunction).isNotNull + assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() + + // Protected component should remain protected + val protectedComponent = containerClass.declaredMethods.find { it.name == "ProtectedComponent" } + assertThat(protectedComponent).isNotNull + assertThat(Modifier.isProtected(protectedComponent!!.modifiers)).isTrue() + } + + @Test + fun `ensure that public Preview functions remain public`() { + val compilation = storytaleTest { + "PublicPreview.kt" hasContent """ + package test + + import androidx.compose.runtime.Composable + import org.jetbrains.compose.ui.tooling.preview.Preview + + @Composable + fun PublicComponent() {} + + @Preview + @Composable + fun PublicPreviewFunction() { + PublicComponent() + } + """ + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + // Load the compiled class + val classLoader = result.classLoader + val previewClass = classLoader.loadClass("test.PublicPreviewKt") + + // Check that the preview function is public + val previewFunction = previewClass.declaredMethods.find { it.name == "PublicPreviewFunction" } + assertThat(previewFunction).isNotNull + assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() + } +} diff --git a/modules/preview-processor/build.gradle.kts b/modules/preview-processor/build.gradle.kts index 457d943..2f9e98f 100644 --- a/modules/preview-processor/build.gradle.kts +++ b/modules/preview-processor/build.gradle.kts @@ -16,5 +16,6 @@ java { dependencies { implementation(libs.kotlin.poet) implementation(libs.ksp.api) + implementation(kotlin("compiler-embeddable")) implementation(project(":modules:gradle-plugin")) } diff --git a/modules/preview-processor/src/main/kotlin/PublicPreviewComposableRegistrar.kt b/modules/preview-processor/src/main/kotlin/PublicPreviewComposableRegistrar.kt new file mode 100644 index 0000000..58b5d7a --- /dev/null +++ b/modules/preview-processor/src/main/kotlin/PublicPreviewComposableRegistrar.kt @@ -0,0 +1,12 @@ +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration + +@OptIn(ExperimentalCompilerApi::class) +class PublicPreviewComposableRegistrar : CompilerPluginRegistrar() { + override val supportsK2: Boolean get() = true + + override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { + + } +} diff --git a/modules/preview-processor/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar b/modules/preview-processor/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar new file mode 100644 index 0000000..b1d3cda --- /dev/null +++ b/modules/preview-processor/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar @@ -0,0 +1,17 @@ +# +# Copyright 2010-2023 JetBrains s.r.o. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +PublicPreviewComposableRegistrar From 146268d8965a0e8c307145ca7ddaca214ea73ea4 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sun, 18 May 2025 11:02:44 +0700 Subject: [PATCH 21/22] Add MakePreviewPublicFirExtensionRegistrar --- gallery-demo/build.gradle.kts | 23 ++ .../storytale/gallery/demo/PreviewCheckbox.kt | 2 +- .../storytale/gallery/demo/PreviewButton.kt | 4 +- ...PublicFirExtensionRegistrarAndroidTest.kt} | 98 +++--- .../src/jvmMain/kotlin/util/Compilation.kt | 4 +- ...ePreviewPublicFirExtensionRegistrarTest.kt | 280 ++++++++++++++++++ .../MakePreviewPublicFirExtensionRegistrar.kt | 47 +++ ...istrar.kt => PreviewComponentRegistrar.kt} | 5 +- ...in.compiler.plugin.CompilerPluginRegistrar | 2 +- 9 files changed, 403 insertions(+), 62 deletions(-) rename modules/preview-processor-test/src/{jvmTest/kotlin/PublicPreviewComposableRegistrarTest.kt => androidUnitTest/kotlin/MakePreviewPublicFirExtensionRegistrarAndroidTest.kt} (52%) create mode 100644 modules/preview-processor-test/src/jvmTest/kotlin/MakePreviewPublicFirExtensionRegistrarTest.kt create mode 100644 modules/preview-processor/src/main/kotlin/MakePreviewPublicFirExtensionRegistrar.kt rename modules/preview-processor/src/main/kotlin/{PublicPreviewComposableRegistrar.kt => PreviewComponentRegistrar.kt} (62%) diff --git a/gallery-demo/build.gradle.kts b/gallery-demo/build.gradle.kts index 0d5aa9c..1dd3c2d 100644 --- a/gallery-demo/build.gradle.kts +++ b/gallery-demo/build.gradle.kts @@ -35,13 +35,36 @@ class StorytaleCompilerPlugin : KotlinCompilerPluginSupportPlugin { ) } } +class MakePreviewPublicCompilerPlugin : KotlinCompilerPluginSupportPlugin { + override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider> { + return kotlinCompilation.project.provider { emptyList() } + } + + override fun getCompilerPluginId(): String { + return "org.jetbrains.compose.compiler.plugins.storytale.preview.public" + } + + override fun getPluginArtifact(): SubpluginArtifact { + return SubpluginArtifact("org.jetbrains.compose.storytale.preview.public", "local-compiler-plugin") + } + + override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean { + return kotlinCompilation.target.platformType in setOf( + org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.jvm, + org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.wasm, + ) + } +} apply() +apply() configurations.all { resolutionStrategy.dependencySubstitution { substitute(module("org.jetbrains.compose.storytale:local-compiler-plugin")) .using(project(":modules:compiler-plugin")) + substitute(module("org.jetbrains.compose.storytale.preview.public:local-compiler-plugin")) + .using(project(":modules:preview-processor")) } } diff --git a/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt b/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt index 1effaba..541a0e7 100644 --- a/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt +++ b/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt @@ -12,7 +12,7 @@ import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable @Suppress("ktlint") -fun PreviewCheckbox() { +private fun PreviewCheckbox() { var state by previewParameter(ToggleableState.entries) TriStateCheckbox( diff --git a/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt b/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt index ac2bff9..9c55710 100644 --- a/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt +++ b/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt @@ -21,7 +21,7 @@ import org.jetbrains.compose.storytale.previewParameter @org.jetbrains.compose.ui.tooling.preview.Preview @Composable @Suppress("ktlint") -fun PreviewExtendedFAB() { +private fun PreviewExtendedFAB() { val bgColor by previewParameter(MaterialTheme.colorScheme.primary) ExtendedFloatingActionButton(onClick = {}, containerColor = bgColor) { @@ -34,7 +34,7 @@ fun PreviewExtendedFAB() { @androidx.compose.desktop.ui.tooling.preview.Preview @Composable @Suppress("ktlint") -fun PreviewSegmentedButton() { +private fun PreviewSegmentedButton() { val selectedIndex = remember { mutableIntStateOf(0) } SingleChoiceSegmentedButtonRow { diff --git a/modules/preview-processor-test/src/jvmTest/kotlin/PublicPreviewComposableRegistrarTest.kt b/modules/preview-processor-test/src/androidUnitTest/kotlin/MakePreviewPublicFirExtensionRegistrarAndroidTest.kt similarity index 52% rename from modules/preview-processor-test/src/jvmTest/kotlin/PublicPreviewComposableRegistrarTest.kt rename to modules/preview-processor-test/src/androidUnitTest/kotlin/MakePreviewPublicFirExtensionRegistrarAndroidTest.kt index 362a60a..9b20000 100644 --- a/modules/preview-processor-test/src/jvmTest/kotlin/PublicPreviewComposableRegistrarTest.kt +++ b/modules/preview-processor-test/src/androidUnitTest/kotlin/MakePreviewPublicFirExtensionRegistrarAndroidTest.kt @@ -1,27 +1,30 @@ +package com.storytale + +import androidx.compose.runtime.Composable import com.tschuchort.compiletesting.KotlinCompilation import org.assertj.core.api.Assertions.assertThat import org.junit.Test import util.storytaleTest import java.lang.reflect.Modifier +import kotlin.test.Ignore -class PublicPreviewComposableRegistrarTest { +class MakePreviewPublicFirExtensionRegistrarAndroidTest { @Test - fun `ensure that private Preview functions are made public`() { + fun `ensure that private androidx Preview functions are made public`() { val compilation = storytaleTest { - "PrivatePreview.kt" hasContent """ + "PrivateAndroidPreview.kt" hasContent """ package test import androidx.compose.runtime.Composable - import org.jetbrains.compose.ui.tooling.preview.Preview @Composable - private fun PrivateComponent() {} + private fun PrivateAndroidComponent() {} - @Preview + @androidx.compose.ui.tooling.preview.Preview @Composable - private fun PrivatePreviewFunction() { - PrivateComponent() + private fun PrivateAndroidPreviewFunction() { + PrivateAndroidComponent() } """ } @@ -29,37 +32,33 @@ class PublicPreviewComposableRegistrarTest { assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) - // Load the compiled class val classLoader = result.classLoader - val previewClass = classLoader.loadClass("test.PrivatePreviewKt") + val previewClass = classLoader.loadClass("test.PrivateAndroidPreviewKt") - // Check that the preview function is public - val previewFunction = previewClass.declaredMethods.find { it.name == "PrivatePreviewFunction" } + val previewFunction = previewClass.declaredMethods.find { it.name == "PrivateAndroidPreviewFunction" } assertThat(previewFunction).isNotNull assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() - // Regular private function should remain private - val privateComponent = previewClass.declaredMethods.find { it.name == "PrivateComponent" } + val privateComponent = previewClass.declaredMethods.find { it.name == "PrivateAndroidComponent" } assertThat(privateComponent).isNotNull assertThat(Modifier.isPrivate(privateComponent!!.modifiers)).isTrue() } @Test - fun `ensure that internal Preview functions are made public`() { + fun `ensure that internal androidx Preview functions are made public`() { val compilation = storytaleTest { - "InternalPreview.kt" hasContent """ + "InternalAndroidPreview.kt" hasContent """ package test import androidx.compose.runtime.Composable - import org.jetbrains.compose.ui.tooling.preview.Preview @Composable - internal fun InternalComponent() {} + internal fun InternalAndroidComponent() {} - @Preview + @androidx.compose.ui.tooling.preview.Preview @Composable - internal fun InternalPreviewFunction() { - InternalComponent() + internal fun InternalAndroidPreviewFunction() { + InternalAndroidComponent() } """ } @@ -67,38 +66,35 @@ class PublicPreviewComposableRegistrarTest { assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) - // Load the compiled class val classLoader = result.classLoader - val previewClass = classLoader.loadClass("test.InternalPreviewKt") + val previewClass = classLoader.loadClass("test.InternalAndroidPreviewKt") - // Check that the preview function is public - val previewFunction = previewClass.declaredMethods.find { it.name == "InternalPreviewFunction" } + val previewFunction = previewClass.declaredMethods.find { it.name == "InternalAndroidPreviewFunction" } assertThat(previewFunction).isNotNull assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() - // Internal component should remain internal - val internalComponent = previewClass.declaredMethods.find { it.name == "InternalComponent" } + val internalComponent = previewClass.declaredMethods.find { it.name == "InternalAndroidComponent" } assertThat(internalComponent).isNotNull - assertThat(!Modifier.isPrivate(internalComponent!!.modifiers) && !Modifier.isPublic(internalComponent.modifiers)).isTrue() + assertThat(Modifier.isPublic(internalComponent!!.modifiers)).isTrue() } + @Ignore("PreviewProcessor hasn't support protected Preview functions in classes yet") @Test - fun `ensure that protected Preview functions are made public in classes`() { + fun `ensure that protected androidx Preview functions are made public in classes`() { val compilation = storytaleTest { - "ProtectedPreview.kt" hasContent """ + "ProtectedAndroidPreview.kt" hasContent """ package test import androidx.compose.runtime.Composable - import org.jetbrains.compose.ui.tooling.preview.Preview - open class PreviewContainer { + open class PreviewAndroidContainer { @Composable - protected fun ProtectedComponent() {} + protected fun ProtectedAndroidComponent() {} - @Preview + @androidx.compose.ui.tooling.preview.Preview @Composable - protected fun ProtectedPreviewFunction() { - ProtectedComponent() + protected fun ProtectedAndroidPreviewFunction() { + ProtectedAndroidComponent() } } """ @@ -107,37 +103,33 @@ class PublicPreviewComposableRegistrarTest { assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) - // Load the compiled class val classLoader = result.classLoader - val containerClass = classLoader.loadClass("test.PreviewContainer") + val containerClass = classLoader.loadClass("test.PreviewAndroidContainer") - // Check that the preview function is public - val previewFunction = containerClass.declaredMethods.find { it.name == "ProtectedPreviewFunction" } + val previewFunction = containerClass.declaredMethods.find { it.name == "ProtectedAndroidPreviewFunction" } assertThat(previewFunction).isNotNull assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() - // Protected component should remain protected - val protectedComponent = containerClass.declaredMethods.find { it.name == "ProtectedComponent" } + val protectedComponent = containerClass.declaredMethods.find { it.name == "ProtectedAndroidComponent" } assertThat(protectedComponent).isNotNull - assertThat(Modifier.isProtected(protectedComponent!!.modifiers)).isTrue() + assertThat(Modifier.isPublic(protectedComponent!!.modifiers)).isTrue() } @Test - fun `ensure that public Preview functions remain public`() { + fun `ensure that public androidx Preview functions remain public`() { val compilation = storytaleTest { - "PublicPreview.kt" hasContent """ + "PublicAndroidPreview.kt" hasContent """ package test import androidx.compose.runtime.Composable - import org.jetbrains.compose.ui.tooling.preview.Preview @Composable - fun PublicComponent() {} + fun PublicAndroidComponent() {} - @Preview + @androidx.compose.ui.tooling.preview.Preview @Composable - fun PublicPreviewFunction() { - PublicComponent() + fun PublicAndroidPreviewFunction() { + PublicAndroidComponent() } """ } @@ -145,12 +137,10 @@ class PublicPreviewComposableRegistrarTest { assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) - // Load the compiled class val classLoader = result.classLoader - val previewClass = classLoader.loadClass("test.PublicPreviewKt") + val previewClass = classLoader.loadClass("test.PublicAndroidPreviewKt") - // Check that the preview function is public - val previewFunction = previewClass.declaredMethods.find { it.name == "PublicPreviewFunction" } + val previewFunction = previewClass.declaredMethods.find { it.name == "PublicAndroidPreviewFunction" } assertThat(previewFunction).isNotNull assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() } diff --git a/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt b/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt index 27a3b42..1d5cd66 100644 --- a/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt +++ b/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt @@ -1,7 +1,7 @@ package util import PreviewProcessor -import PublicPreviewComposableRegistrar +import PreviewComponentRegistrar import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation @@ -35,7 +35,7 @@ fun storytaleTest( return KotlinCompilation().apply { compilerPluginRegistrars = listOf( ComposePluginRegistrar(), - PublicPreviewComposableRegistrar(), + PreviewComponentRegistrar(), ) useKsp2() symbolProcessorProviders.add(PreviewProcessor.Provider()) diff --git a/modules/preview-processor-test/src/jvmTest/kotlin/MakePreviewPublicFirExtensionRegistrarTest.kt b/modules/preview-processor-test/src/jvmTest/kotlin/MakePreviewPublicFirExtensionRegistrarTest.kt new file mode 100644 index 0000000..a33f50a --- /dev/null +++ b/modules/preview-processor-test/src/jvmTest/kotlin/MakePreviewPublicFirExtensionRegistrarTest.kt @@ -0,0 +1,280 @@ +import com.tschuchort.compiletesting.KotlinCompilation +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import util.storytaleTest +import java.lang.reflect.Modifier +import kotlin.test.Ignore + +class MakePreviewPublicFirExtensionRegistrarTest { + + @Test + fun `ensure that private Jetbrains Compose Preview functions are made public`() { + val compilation = storytaleTest { + "PrivatePreview.kt" hasContent """ + package test + + import androidx.compose.runtime.Composable + import org.jetbrains.compose.ui.tooling.preview.Preview + + @Composable + private fun PrivateComponent() {} + + @org.jetbrains.compose.ui.tooling.preview.Preview + @Composable + private fun PrivatePreviewFunction() { + PrivateComponent() + } + """ + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + val classLoader = result.classLoader + val previewClass = classLoader.loadClass("test.PrivatePreviewKt") + + val previewFunction = previewClass.declaredMethods.find { it.name == "PrivatePreviewFunction" } + assertThat(previewFunction).isNotNull + assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() + + val privateComponent = previewClass.declaredMethods.find { it.name == "PrivateComponent" } + assertThat(privateComponent).isNotNull + assertThat(Modifier.isPrivate(privateComponent!!.modifiers)).isTrue() + } + + @Test + fun `ensure that internal Jetbrains Compose Preview functions are made public`() { + val compilation = storytaleTest { + "InternalPreview.kt" hasContent """ + package test + + import androidx.compose.runtime.Composable + + @Composable + internal fun InternalComponent() {} + + @org.jetbrains.compose.ui.tooling.preview.Preview + @Composable + internal fun InternalPreviewFunction() { + InternalComponent() + } + """ + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + val classLoader = result.classLoader + val previewClass = classLoader.loadClass("test.InternalPreviewKt") + + val previewFunction = previewClass.declaredMethods.find { it.name == "InternalPreviewFunction" } + assertThat(previewFunction).isNotNull + assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() + + val internalComponent = previewClass.declaredMethods.find { it.name == "InternalComponent" } + assertThat(internalComponent).isNotNull + assertThat(Modifier.isPublic(internalComponent!!.modifiers)).isTrue() + } + + @Ignore("PreviewProcessor hasn't support protected Preview functions in classes yet") + @Test + fun `ensure that protected Jetbrains Compose Preview functions are made public in classes`() { + val compilation = storytaleTest { + "ProtectedPreview.kt" hasContent """ + package test + + import androidx.compose.runtime.Composable + + open class PreviewContainer { + @Composable + protected fun ProtectedComponent() {} + + @org.jetbrains.compose.ui.tooling.preview.Preview + @Composable + protected fun ProtectedPreviewFunction() { + ProtectedComponent() + } + } + """ + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + val classLoader = result.classLoader + val containerClass = classLoader.loadClass("test.PreviewContainer") + + val previewFunction = containerClass.declaredMethods.find { it.name == "ProtectedPreviewFunction" } + assertThat(previewFunction).isNotNull + assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() + + val protectedComponent = containerClass.declaredMethods.find { it.name == "ProtectedComponent" } + assertThat(protectedComponent).isNotNull + assertThat(Modifier.isPublic(protectedComponent!!.modifiers)).isTrue() + } + + @Test + fun `ensure that public Jetbrains Compose Preview functions remain public`() { + val compilation = storytaleTest { + "PublicPreview.kt" hasContent """ + package test + + import androidx.compose.runtime.Composable + + @Composable + fun PublicComponent() {} + + @org.jetbrains.compose.ui.tooling.preview.Preview + @Composable + fun PublicPreviewFunction() { + PublicComponent() + } + """ + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + val classLoader = result.classLoader + val previewClass = classLoader.loadClass("test.PublicPreviewKt") + + val previewFunction = previewClass.declaredMethods.find { it.name == "PublicPreviewFunction" } + assertThat(previewFunction).isNotNull + assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() + } + + @Test + fun `ensure that private androidx desktop Preview functions are made public`() { + val compilation = storytaleTest { + "PrivateDesktopPreview.kt" hasContent """ + package test + + import androidx.compose.runtime.Composable + + @Composable + private fun PrivateDesktopComponent() {} + + @androidx.compose.desktop.ui.tooling.preview.Preview + @Composable + private fun PrivateDesktopPreviewFunction() { + PrivateDesktopComponent() + } + """ + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + val classLoader = result.classLoader + val previewClass = classLoader.loadClass("test.PrivateDesktopPreviewKt") + + val previewFunction = previewClass.declaredMethods.find { it.name == "PrivateDesktopPreviewFunction" } + assertThat(previewFunction).isNotNull + assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() + + val privateComponent = previewClass.declaredMethods.find { it.name == "PrivateDesktopComponent" } + assertThat(privateComponent).isNotNull + assertThat(Modifier.isPrivate(privateComponent!!.modifiers)).isTrue() + } + + @Test + fun `ensure that internal androidx desktop Preview functions are made public`() { + val compilation = storytaleTest { + "InternalDesktopPreview.kt" hasContent """ + package test + + import androidx.compose.runtime.Composable + + @Composable + internal fun InternalDesktopComponent() {} + + @androidx.compose.desktop.ui.tooling.preview.Preview + @Composable + internal fun InternalDesktopPreviewFunction() { + InternalDesktopComponent() + } + """ + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + val classLoader = result.classLoader + val previewClass = classLoader.loadClass("test.InternalDesktopPreviewKt") + + val previewFunction = previewClass.declaredMethods.find { it.name == "InternalDesktopPreviewFunction" } + assertThat(previewFunction).isNotNull + assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() + + val internalComponent = previewClass.declaredMethods.find { it.name == "InternalDesktopComponent" } + assertThat(internalComponent).isNotNull + assertThat(Modifier.isPublic(internalComponent!!.modifiers)).isTrue() + } + + @Ignore("PreviewProcessor hasn't support protected Preview functions in classes yet") + @Test + fun `ensure that protected androidx desktop Preview functions are made public in classes`() { + val compilation = storytaleTest { + "ProtectedDesktopPreview.kt" hasContent """ + package test + + import androidx.compose.runtime.Composable + + open class PreviewDesktopContainer { + @Composable + protected fun ProtectedDesktopComponent() {} + + @androidx.compose.desktop.ui.tooling.preview.Preview + @Composable + protected fun ProtectedDesktopPreviewFunction() { + ProtectedDesktopComponent() + } + } + """ + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + val classLoader = result.classLoader + val containerClass = classLoader.loadClass("test.PreviewDesktopContainer") + + val previewFunction = containerClass.declaredMethods.find { it.name == "ProtectedDesktopPreviewFunction" } + assertThat(previewFunction).isNotNull + assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() + + val protectedComponent = containerClass.declaredMethods.find { it.name == "ProtectedDesktopComponent" } + assertThat(protectedComponent).isNotNull + assertThat(Modifier.isPublic(protectedComponent!!.modifiers)).isTrue() + } + + @Test + fun `ensure that public androidx desktop Preview functions remain public`() { + val compilation = storytaleTest { + "PublicDesktopPreview.kt" hasContent """ + package test + + import androidx.compose.runtime.Composable + + @Composable + fun PublicDesktopComponent() {} + + @androidx.compose.desktop.ui.tooling.preview.Preview + @Composable + fun PublicDesktopPreviewFunction() { + PublicDesktopComponent() + } + """ + } + val result = compilation.compile() + + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + + val classLoader = result.classLoader + val previewClass = classLoader.loadClass("test.PublicDesktopPreviewKt") + + val previewFunction = previewClass.declaredMethods.find { it.name == "PublicDesktopPreviewFunction" } + assertThat(previewFunction).isNotNull + assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() + } +} diff --git a/modules/preview-processor/src/main/kotlin/MakePreviewPublicFirExtensionRegistrar.kt b/modules/preview-processor/src/main/kotlin/MakePreviewPublicFirExtensionRegistrar.kt new file mode 100644 index 0000000..ca71484 --- /dev/null +++ b/modules/preview-processor/src/main/kotlin/MakePreviewPublicFirExtensionRegistrar.kt @@ -0,0 +1,47 @@ +import org.jetbrains.kotlin.descriptors.Visibilities +import org.jetbrains.kotlin.fir.FirSession +import org.jetbrains.kotlin.fir.declarations.FirDeclaration +import org.jetbrains.kotlin.fir.declarations.FirDeclarationStatus +import org.jetbrains.kotlin.fir.declarations.FirSimpleFunction +import org.jetbrains.kotlin.fir.declarations.hasAnnotation +import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar +import org.jetbrains.kotlin.fir.extensions.FirStatusTransformerExtension +import org.jetbrains.kotlin.fir.extensions.transform +import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName + + +class MakePreviewPublicFirExtensionRegistrar : FirExtensionRegistrar() { + companion object { + private val PREVIEW_ANNOTATION_FQ_NAME = FqName("org.jetbrains.compose.ui.tooling.preview.Preview") + private val ANDROIDX_PREVIEW_ANNOTATION_FQ_NAME = FqName("androidx.compose.ui.tooling.preview.Preview") + private val DESKTOP_PREVIEW_ANNOTATION_FQ_NAME = FqName("androidx.compose.desktop.ui.tooling.preview.Preview") + + private val PREVIEW_CLASS_IDS = setOf( + ClassId.topLevel(PREVIEW_ANNOTATION_FQ_NAME), + ClassId.topLevel(ANDROIDX_PREVIEW_ANNOTATION_FQ_NAME), + ClassId.topLevel(DESKTOP_PREVIEW_ANNOTATION_FQ_NAME) + ) + } + + override fun ExtensionRegistrarContext.configurePlugin() { + +FirStatusTransformerExtension.Factory { session -> Extension(session) } + } + + class Extension(session: FirSession) : FirStatusTransformerExtension(session) { + override fun needTransformStatus(declaration: FirDeclaration): Boolean { + if (declaration !is FirSimpleFunction) return false + return PREVIEW_CLASS_IDS.any { declaration.hasAnnotation(it, session) } + } + + override fun transformStatus( + status: FirDeclarationStatus, + function: FirSimpleFunction, + containingClass: FirClassLikeSymbol<*>?, + isLocal: Boolean, + ): FirDeclarationStatus { + return status.transform(visibility = Visibilities.Public) + } + } +} diff --git a/modules/preview-processor/src/main/kotlin/PublicPreviewComposableRegistrar.kt b/modules/preview-processor/src/main/kotlin/PreviewComponentRegistrar.kt similarity index 62% rename from modules/preview-processor/src/main/kotlin/PublicPreviewComposableRegistrar.kt rename to modules/preview-processor/src/main/kotlin/PreviewComponentRegistrar.kt index 58b5d7a..71d1f33 100644 --- a/modules/preview-processor/src/main/kotlin/PublicPreviewComposableRegistrar.kt +++ b/modules/preview-processor/src/main/kotlin/PreviewComponentRegistrar.kt @@ -1,12 +1,13 @@ import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrarAdapter @OptIn(ExperimentalCompilerApi::class) -class PublicPreviewComposableRegistrar : CompilerPluginRegistrar() { +class PreviewComponentRegistrar : CompilerPluginRegistrar() { override val supportsK2: Boolean get() = true override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { - + FirExtensionRegistrarAdapter.registerExtension(MakePreviewPublicFirExtensionRegistrar()) } } diff --git a/modules/preview-processor/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar b/modules/preview-processor/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar index b1d3cda..a67d468 100644 --- a/modules/preview-processor/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +++ b/modules/preview-processor/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar @@ -14,4 +14,4 @@ # limitations under the License. # -PublicPreviewComposableRegistrar +PreviewComponentRegistrar From 2fa23cbb4b480e0e54a05abaa2249cfbf9c53101 Mon Sep 17 00:00:00 2001 From: Vu Chi Cong Date: Sun, 18 May 2025 11:03:24 +0700 Subject: [PATCH 22/22] apply spotless --- .../MakePreviewPublicFirExtensionRegistrarAndroidTest.kt | 5 ++--- .../src/jvmMain/kotlin/util/Compilation.kt | 2 +- .../kotlin/MakePreviewPublicFirExtensionRegistrarTest.kt | 4 ++-- .../main/kotlin/MakePreviewPublicFirExtensionRegistrar.kt | 3 +-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/modules/preview-processor-test/src/androidUnitTest/kotlin/MakePreviewPublicFirExtensionRegistrarAndroidTest.kt b/modules/preview-processor-test/src/androidUnitTest/kotlin/MakePreviewPublicFirExtensionRegistrarAndroidTest.kt index 9b20000..dd9d7ab 100644 --- a/modules/preview-processor-test/src/androidUnitTest/kotlin/MakePreviewPublicFirExtensionRegistrarAndroidTest.kt +++ b/modules/preview-processor-test/src/androidUnitTest/kotlin/MakePreviewPublicFirExtensionRegistrarAndroidTest.kt @@ -1,12 +1,11 @@ package com.storytale -import androidx.compose.runtime.Composable import com.tschuchort.compiletesting.KotlinCompilation +import java.lang.reflect.Modifier +import kotlin.test.Ignore import org.assertj.core.api.Assertions.assertThat import org.junit.Test import util.storytaleTest -import java.lang.reflect.Modifier -import kotlin.test.Ignore class MakePreviewPublicFirExtensionRegistrarAndroidTest { diff --git a/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt b/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt index 1d5cd66..b5854c1 100644 --- a/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt +++ b/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt @@ -1,7 +1,7 @@ package util -import PreviewProcessor import PreviewComponentRegistrar +import PreviewProcessor import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation diff --git a/modules/preview-processor-test/src/jvmTest/kotlin/MakePreviewPublicFirExtensionRegistrarTest.kt b/modules/preview-processor-test/src/jvmTest/kotlin/MakePreviewPublicFirExtensionRegistrarTest.kt index a33f50a..580a800 100644 --- a/modules/preview-processor-test/src/jvmTest/kotlin/MakePreviewPublicFirExtensionRegistrarTest.kt +++ b/modules/preview-processor-test/src/jvmTest/kotlin/MakePreviewPublicFirExtensionRegistrarTest.kt @@ -1,9 +1,9 @@ import com.tschuchort.compiletesting.KotlinCompilation +import java.lang.reflect.Modifier +import kotlin.test.Ignore import org.assertj.core.api.Assertions.assertThat import org.junit.Test import util.storytaleTest -import java.lang.reflect.Modifier -import kotlin.test.Ignore class MakePreviewPublicFirExtensionRegistrarTest { diff --git a/modules/preview-processor/src/main/kotlin/MakePreviewPublicFirExtensionRegistrar.kt b/modules/preview-processor/src/main/kotlin/MakePreviewPublicFirExtensionRegistrar.kt index ca71484..2e6a505 100644 --- a/modules/preview-processor/src/main/kotlin/MakePreviewPublicFirExtensionRegistrar.kt +++ b/modules/preview-processor/src/main/kotlin/MakePreviewPublicFirExtensionRegistrar.kt @@ -11,7 +11,6 @@ import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName - class MakePreviewPublicFirExtensionRegistrar : FirExtensionRegistrar() { companion object { private val PREVIEW_ANNOTATION_FQ_NAME = FqName("org.jetbrains.compose.ui.tooling.preview.Preview") @@ -21,7 +20,7 @@ class MakePreviewPublicFirExtensionRegistrar : FirExtensionRegistrar() { private val PREVIEW_CLASS_IDS = setOf( ClassId.topLevel(PREVIEW_ANNOTATION_FQ_NAME), ClassId.topLevel(ANDROIDX_PREVIEW_ANNOTATION_FQ_NAME), - ClassId.topLevel(DESKTOP_PREVIEW_ANNOTATION_FQ_NAME) + ClassId.topLevel(DESKTOP_PREVIEW_ANNOTATION_FQ_NAME), ) }