diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt index c39ea711d0..9527898c8a 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt @@ -2,6 +2,8 @@ package org.jetbrains.compose.resources import com.squareup.kotlinpoet.* import java.nio.file.Path +import java.util.SortedMap +import java.util.TreeMap import kotlin.io.path.invariantSeparatorsPathString internal enum class ResourceType(val typeName: String) { @@ -26,12 +28,14 @@ internal data class ResourceItem( val path: Path ) -private fun ResourceItem.getClassName(): ClassName = when (type) { +private fun ResourceType.getClassName(): ClassName = when (this) { ResourceType.DRAWABLE -> ClassName("org.jetbrains.compose.resources", "DrawableResource") ResourceType.STRING -> ClassName("org.jetbrains.compose.resources", "StringResource") ResourceType.FONT -> ClassName("org.jetbrains.compose.resources", "FontResource") } +private val resourceItemClass = ClassName("org.jetbrains.compose.resources", "ResourceItem") + private fun CodeBlock.Builder.addQualifiers(resourceItem: ResourceItem): CodeBlock.Builder { val languageQualifier = ClassName("org.jetbrains.compose.resources", "LanguageQualifier") val regionQualifier = ClassName("org.jetbrains.compose.resources", "RegionQualifier") @@ -101,85 +105,118 @@ internal fun getResFileSpec( //type -> id -> items resources: Map>>, packageName: String -): FileSpec = FileSpec.builder(packageName, "Res").apply { - addType(TypeSpec.objectBuilder("Res").apply { - addModifiers(KModifier.INTERNAL) +): FileSpec = + FileSpec.builder(packageName, "Res").apply { addAnnotation( AnnotationSpec.builder(ClassName("kotlin", "OptIn")) .addMember("org.jetbrains.compose.resources.InternalResourceApi::class") - .build() - ) - addAnnotation( - AnnotationSpec.builder(ClassName("org.jetbrains.compose.resources", "ExperimentalResourceApi")) + .addMember("org.jetbrains.compose.resources.ExperimentalResourceApi::class") .build() ) - //readFileBytes - val readResourceBytes = MemberName("org.jetbrains.compose.resources", "readResourceBytes") - addFunction( - FunSpec.builder("readBytes") - .addKdoc(""" + //we need to sort it to generate the same code on different platforms + val sortedResources = sortResources(resources) + + addType(TypeSpec.objectBuilder("Res").apply { + addModifiers(KModifier.INTERNAL) + addAnnotation( + AnnotationSpec.builder( + ClassName("org.jetbrains.compose.resources", "ExperimentalResourceApi") + ).build() + ) + + //readFileBytes + val readResourceBytes = MemberName("org.jetbrains.compose.resources", "readResourceBytes") + addFunction( + FunSpec.builder("readBytes") + .addKdoc( + """ Reads the content of the resource file at the specified path and returns it as a byte array. Example: `val bytes = Res.readBytes("files/key.bin")` @param path The path of the file to read in the compose resource's directory. @return The content of the file as a byte array. - """.trimIndent()) - .addParameter("path", String::class) - .addModifiers(KModifier.SUSPEND) - .returns(ByteArray::class) - .addStatement("return %M(path)", readResourceBytes) //todo: add module ID here - .build() - ) + """.trimIndent() + ) + .addParameter("path", String::class) + .addModifiers(KModifier.SUSPEND) + .returns(ByteArray::class) + .addStatement("return %M(path)", readResourceBytes) //todo: add module ID here + .build() + ) + val types = sortedResources.map { (type, idToResources) -> + getResourceTypeObject(type, idToResources) + } + addTypes(types) + }.build()) - val types = resources.map { (type, idToResources) -> - getResourceTypeObject(type, idToResources) - }.sortedBy { it.name } - addTypes(types) - }.build()) -}.build() + sortedResources + .flatMap { (type, idToResources) -> + idToResources.map { (name, items) -> + getResourceInitializer(name, type, items) + } + } + .forEach { addFunction(it) } + }.build() private fun getResourceTypeObject(type: ResourceType, nameToResources: Map>) = TypeSpec.objectBuilder(type.typeName).apply { - nameToResources.entries - .sortedBy { it.key } - .forEach { (name, items) -> - addResourceProperty(name, items.sortedBy { it.path }) + nameToResources.keys + .forEach { name -> + addProperty( + PropertySpec + .builder(name, type.getClassName()) + .initializer("get_$name()") + .build() + ) } }.build() -private fun TypeSpec.Builder.addResourceProperty(name: String, items: List) { - val resourceItemClass = ClassName("org.jetbrains.compose.resources", "ResourceItem") - - val first = items.first() - val propertyClassName = first.getClassName() - val resourceId = first.let { "${it.type}:${it.name}" } - - val initializer = CodeBlock.builder() - .add("%T(\n", propertyClassName).withIndent { - add("\"$resourceId\",\n") - if (first.type == ResourceType.STRING) { - add("\"${first.name}\",\n") - } - add("setOf(\n").withIndent { - items.forEach { item -> - add("%T(\n", resourceItemClass).withIndent { - add("setOf(").addQualifiers(item).add("),\n") - //file separator should be '/' on all platforms - add("\"${item.path.invariantSeparatorsPathString}\"\n") //todo: add module ID here +private fun getResourceInitializer(name: String, type: ResourceType, items: List): FunSpec { + val propertyTypeName = type.getClassName() + val resourceId = "${type}:${name}" + return FunSpec.builder("get_$name") + .addModifiers(KModifier.PRIVATE) + .returns(propertyTypeName) + .addStatement( + CodeBlock.builder() + .add("return %T(\n", propertyTypeName).withIndent { + add("\"$resourceId\",") + if (type == ResourceType.STRING) add(" \"$name\",") + withIndent { + add("\nsetOf(\n").withIndent { + items.forEach { item -> + add("%T(", resourceItemClass) + add("setOf(").addQualifiers(item).add("), ") + //file separator should be '/' on all platforms + add("\"${item.path.invariantSeparatorsPathString}\"") //todo: add module ID here + add("),\n") + } + } + add(")\n") } - add("),\n") } - } - add(")\n") - } - .add(")") + .add(")") + .build().toString() + ) .build() +} - addProperty( - PropertySpec.builder(name, propertyClassName) - .initializer(initializer) - .build() - ) +private fun sortResources( + resources: Map>> +): TreeMap>> { + val result = TreeMap>>() + resources + .entries + .forEach { (type, items) -> + val typeResult = TreeMap>() + items + .entries + .forEach { (name, resItems) -> + typeResult[name] = resItems.sortedBy { it.path } + } + result[type] = typeResult + } + return result } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt index e9ce9be344..18e08731cb 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt @@ -9,7 +9,7 @@ import kotlin.io.path.Path class ResourcesTest : GradlePluginTestBase() { @Test - fun testGeneratedAccessorsAndCopiedFonts(): Unit = with(testProject("misc/commonResources")) { + fun testGeneratedAccessors(): Unit = with(testProject("misc/commonResources")) { //check generated resource's accessors gradle("generateComposeResClass").checks { assertEqualTextFiles( @@ -137,4 +137,89 @@ class ResourcesTest : GradlePluginTestBase() { } gradle("jar") } + + //https://github.com/JetBrains/compose-multiplatform/issues/4194 + @Test + fun testHugeNumberOfStrings(): Unit = with( + //disable cache for the test because the generateStringFiles task doesn't support it + testProject("misc/commonResources", defaultTestEnvironment.copy(useGradleConfigurationCache = false)) + ) { + file("build.gradle.kts").let { f -> + val originText = f.readText() + f.writeText( + buildString { + appendLine("import java.util.Locale") + append(originText) + appendLine() + append(""" + val template = ""${'"'} + + Compose Resources App + 😊 Hello world! + Lorem ipsum dolor sit amet, + consectetur adipiscing elit. + Donec eget turpis ac sem ultricies consequat. + Hello, %1${'$'}{"$"}s! You have %2${'$'}{"$"}d new messages. + + item 1 + item 2 + item 3 + + [ADDITIONAL_STRINGS] + + ""${'"'}.trimIndent() + + val generateStringFiles = tasks.register("generateStringFiles") { + val numberOfLanguages = 20 + val numberOfStrings = 500 + val langs = Locale.getAvailableLocales() + .map { it.language } + .filter { it.count() == 2 } + .sorted() + .distinct() + .take(numberOfLanguages) + .toList() + + val resourcesFolder = project.file("src/commonMain/composeResources") + + doLast { + // THIS REMOVES THE `values` FOLDER IN `composeResources` + // THIS REMOVES THE `values` FOLDER IN `composeResources` + // Necessary when reducing the number of languages. + resourcesFolder.listFiles()?.filter { it.name.startsWith("values") }?.forEach { + it.deleteRecursively() + } + + langs.forEachIndexed { langIndex, lang -> + val additionalStrings = + (0 until numberOfStrings).joinToString(System.lineSeparator()) { index -> + ""${'"'} + String ${'$'}index in lang ${'$'}lang + ""${'"'}.trimIndent() + } + + val langFile = if (langIndex == 0) { + File(resourcesFolder, "values/strings.xml") + } else { + File(resourcesFolder, "values-${'$'}lang/strings.xml") + } + langFile.parentFile.mkdirs() + langFile.writeText(template.replace("[ADDITIONAL_STRINGS]", additionalStrings)) + } + } + } + + tasks.named("generateComposeResClass") { + dependsOn(generateStringFiles) + } + """.trimIndent()) + } + ) + } + gradle("desktopJar").checks { + check.taskSuccessful(":generateStringFiles") + check.taskSuccessful(":generateComposeResClass") + assert(file("src/commonMain/composeResources/values/strings.xml").readLines().size == 513) + } + } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt index 22e3aa7d47..27a8abb979 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt @@ -1,3 +1,8 @@ +@file:OptIn( + org.jetbrains.compose.resources.InternalResourceApi::class, + org.jetbrains.compose.resources.ExperimentalResourceApi::class, +) + package app.group.resources_test.generated.resources import kotlin.ByteArray @@ -6,14 +11,9 @@ import kotlin.String import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.FontResource -import org.jetbrains.compose.resources.LanguageQualifier -import org.jetbrains.compose.resources.RegionQualifier -import org.jetbrains.compose.resources.ResourceItem import org.jetbrains.compose.resources.StringResource -import org.jetbrains.compose.resources.ThemeQualifier import org.jetbrains.compose.resources.readResourceBytes -@OptIn(org.jetbrains.compose.resources.InternalResourceApi::class) @ExperimentalResourceApi internal object Res { /** @@ -27,115 +27,99 @@ internal object Res { public suspend fun readBytes(path: String): ByteArray = readResourceBytes(path) public object drawable { - public val _3_strange_name: DrawableResource = DrawableResource( - "drawable:_3_strange_name", - setOf( - ResourceItem( - setOf(), - "drawable/3-strange-name.xml" - ), - ) - ) + public val _3_strange_name: DrawableResource = get__3_strange_name() - public val vector: DrawableResource = DrawableResource( - "drawable:vector", - setOf( - ResourceItem( - setOf(LanguageQualifier("au"), RegionQualifier("US"), ), - "drawable-au-rUS/vector.xml" - ), - ResourceItem( - setOf(ThemeQualifier.DARK, LanguageQualifier("ge"), ), - "drawable-dark-ge/vector.xml" - ), - ResourceItem( - setOf(LanguageQualifier("en"), ), - "drawable-en/vector.xml" - ), - ResourceItem( - setOf(), - "drawable/vector.xml" - ), - ) - ) + public val vector: DrawableResource = get_vector() - public val vector_2: DrawableResource = DrawableResource( - "drawable:vector_2", - setOf( - ResourceItem( - setOf(), - "drawable/vector_2.xml" - ), - ) - ) + public val vector_2: DrawableResource = get_vector_2() + } + + public object string { + public val app_name: StringResource = get_app_name() + + public val hello: StringResource = get_hello() + + public val multi_line: StringResource = get_multi_line() + + public val str_arr: StringResource = get_str_arr() + + public val str_template: StringResource = get_str_template() } public object font { - public val emptyfont: FontResource = FontResource( - "font:emptyfont", - setOf( - ResourceItem( - setOf(), - "font/emptyFont.otf" - ), - ) - ) + public val emptyfont: FontResource = get_emptyfont() } +} - public object string { - public val app_name: StringResource = StringResource( - "string:app_name", - "app_name", - setOf( - ResourceItem( - setOf(), - "values/strings.xml" - ), - ) +private fun get__3_strange_name(): DrawableResource = + org.jetbrains.compose.resources.DrawableResource( + "drawable:_3_strange_name", + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/3-strange-name.xml"), ) + ) - public val hello: StringResource = StringResource( - "string:hello", - "hello", - setOf( - ResourceItem( - setOf(), - "values/strings.xml" - ), - ) - ) +private fun get_vector(): DrawableResource = org.jetbrains.compose.resources.DrawableResource( + "drawable:vector", + setOf( - public val multi_line: StringResource = StringResource( - "string:multi_line", - "multi_line", - setOf( - ResourceItem( - setOf(), - "values/strings.xml" - ), - ) - ) + org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("au"), + org.jetbrains.compose.resources.RegionQualifier("US"), ), "drawable-au-rUS/vector.xml"), - public val str_arr: StringResource = StringResource( - "string:str_arr", - "str_arr", - setOf( - ResourceItem( - setOf(), - "values/strings.xml" - ), - ) - ) + org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.ThemeQualifier.DARK, + org.jetbrains.compose.resources.LanguageQualifier("ge"), ), "drawable-dark-ge/vector.xml"), - public val str_template: StringResource = StringResource( - "string:str_template", - "str_template", - setOf( - ResourceItem( - setOf(), - "values/strings.xml" - ), - ) - ) - } -} \ No newline at end of file + org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("en"), + ), "drawable-en/vector.xml"), + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector.xml"), + ) +) + +private fun get_vector_2(): DrawableResource = org.jetbrains.compose.resources.DrawableResource( + "drawable:vector_2", + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector_2.xml"), + ) +) + +private fun get_app_name(): StringResource = org.jetbrains.compose.resources.StringResource( + "string:app_name", "app_name", + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + ) +) + +private fun get_hello(): StringResource = org.jetbrains.compose.resources.StringResource( + "string:hello", "hello", + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + ) +) + +private fun get_multi_line(): StringResource = org.jetbrains.compose.resources.StringResource( + "string:multi_line", "multi_line", + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + ) +) + +private fun get_str_arr(): StringResource = org.jetbrains.compose.resources.StringResource( + "string:str_arr", "str_arr", + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + ) +) + +private fun get_str_template(): StringResource = org.jetbrains.compose.resources.StringResource( + "string:str_template", "str_template", + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"), + ) +) + +private fun get_emptyfont(): FontResource = org.jetbrains.compose.resources.FontResource( + "font:emptyfont", + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "font/emptyFont.otf"), + ) +) \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt index 8a954170b1..2a3770e9ae 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt @@ -1,3 +1,8 @@ +@file:OptIn( + org.jetbrains.compose.resources.InternalResourceApi::class, + org.jetbrains.compose.resources.ExperimentalResourceApi::class, +) + package app.group.empty_res.generated.resources import kotlin.ByteArray @@ -6,7 +11,6 @@ import kotlin.String import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.readResourceBytes -@OptIn(org.jetbrains.compose.resources.InternalResourceApi::class) @ExperimentalResourceApi internal object Res { /** diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt index fda4811df5..83a5295364 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt @@ -1,3 +1,8 @@ +@file:OptIn( + org.jetbrains.compose.resources.InternalResourceApi::class, + org.jetbrains.compose.resources.ExperimentalResourceApi::class, +) + package me.app.jvmonlyresources.generated.resources import kotlin.ByteArray @@ -5,10 +10,8 @@ import kotlin.OptIn import kotlin.String import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.ExperimentalResourceApi -import org.jetbrains.compose.resources.ResourceItem import org.jetbrains.compose.resources.readResourceBytes -@OptIn(org.jetbrains.compose.resources.InternalResourceApi::class) @ExperimentalResourceApi internal object Res { /** @@ -22,14 +25,13 @@ internal object Res { public suspend fun readBytes(path: String): ByteArray = readResourceBytes(path) public object drawable { - public val vector: DrawableResource = DrawableResource( - "drawable:vector", - setOf( - ResourceItem( - setOf(), - "drawable/vector.xml" - ), - ) - ) + public val vector: DrawableResource = get_vector() } -} \ No newline at end of file +} + +private fun get_vector(): DrawableResource = org.jetbrains.compose.resources.DrawableResource( + "drawable:vector", + setOf( + org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector.xml"), + ) +) \ No newline at end of file