-
Notifications
You must be signed in to change notification settings - Fork 37
feat: add dynamic URL plugin, Supabase config, and PlatformBuildConfig #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
f1f7c04
29fd85b
c8edd9a
beeba2d
645abc8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| /* | ||
| * Copyright 2025 Mifos Initiative | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| * | ||
| * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE | ||
| */ | ||
|
|
||
| import org.gradle.api.Plugin | ||
| import org.gradle.api.Project | ||
| import org.gradle.kotlin.dsl.configure | ||
| import org.gradle.kotlin.dsl.dependencies | ||
| import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension | ||
| import org.convention.libs | ||
|
|
||
| /** | ||
| * Convention plugin that generates Supabase credentials from a JSON secrets file. | ||
| * | ||
| * This plugin: | ||
| * 1. Reads credentials from `secrets/supabaseCredentialsFile.json` | ||
| * 2. Generates a `SupabaseCredentials` object implementing `template.core.base.network.SupabaseCredentials` | ||
| * 3. Adds the generated source to the commonMain source set | ||
| * 4. Adds the supabase-postgrest dependency | ||
| * | ||
| * ## Usage | ||
| * | ||
| * Apply the plugin to your module's build.gradle.kts: | ||
| * ```kotlin | ||
| * plugins { | ||
| * alias(libs.plugins.kmp.supabase.config) | ||
| * } | ||
| * | ||
| * // Optional: Configure the package name (defaults to module namespace + ".config") | ||
| * supabaseConfig { | ||
| * packageName = "com.example.myapp.network.config" | ||
| * } | ||
| * ``` | ||
| * | ||
| * ## Secrets File Format | ||
| * | ||
| * Create `secrets/supabaseCredentialsFile.json` in your project root: | ||
| * ```json | ||
| * { | ||
| * "url": "https://your-project.supabase.co", | ||
| * "anonKey": "your-anon-key" | ||
| * } | ||
| * ``` | ||
| * | ||
| * ## Generated Code | ||
| * | ||
| * The plugin generates: | ||
| * ```kotlin | ||
| * object SupabaseCredentials : template.core.base.network.SupabaseCredentials { | ||
| * override val url = "https://your-project.supabase.co" | ||
| * override val anonKey = "your-anon-key" | ||
| * } | ||
| * ``` | ||
| */ | ||
| class SupabaseConfigConventionPlugin : Plugin<Project> { | ||
| override fun apply(target: Project) { | ||
| with(target) { | ||
| // Create extension for configuration | ||
| val extension = extensions.create("supabaseConfig", SupabaseConfigExtension::class.java) | ||
|
|
||
| val generatedDir = layout.buildDirectory.dir("generated/supabase") | ||
| val secretsFile = rootProject.file("secrets/supabaseCredentialsFile.json") | ||
|
|
||
| // Register the code generation task | ||
| val generateTask = tasks.register("generateSupabaseConfig") { | ||
| // Determine package name from extension or derive from namespace | ||
| val packageName = extension.packageName | ||
| ?: project.findProperty("android.namespace")?.toString()?.let { "$it.config" } | ||
| ?: "${project.group}.config" | ||
|
|
||
| val packagePath = packageName.replace(".", "/") | ||
| val outputFile = generatedDir.get().asFile | ||
| .resolve("$packagePath/SupabaseCredentials.kt") | ||
|
|
||
| // Track file content as input property for proper up-to-date checking | ||
| val fileContent = if (secretsFile.exists() && secretsFile.length() > 0) { | ||
| secretsFile.readText() | ||
| } else { | ||
| "" | ||
| } | ||
| inputs.property("credentialsContent", fileContent) | ||
| inputs.property("packageName", packageName) | ||
| outputs.file(outputFile) | ||
|
|
||
| doLast { | ||
| val currentContent = inputs.properties["credentialsContent"] as String | ||
| val currentPackage = inputs.properties["packageName"] as String | ||
| val (url, anonKey) = if (currentContent.isNotEmpty()) { | ||
| try { | ||
| parseCredentials(currentContent) | ||
| } catch (e: Exception) { | ||
| logger.warn("Failed to parse Supabase credentials file: ${e.message}") | ||
| Pair("", "") | ||
| } | ||
| } else { | ||
| logger.warn("Supabase credentials file not found at: ${secretsFile.absolutePath}") | ||
| logger.warn("Create secrets/supabaseCredentialsFile.json with 'url' and 'anonKey' fields") | ||
| Pair("", "") | ||
| } | ||
|
|
||
| outputFile.parentFile.mkdirs() | ||
|
|
||
| outputFile.writeText( | ||
| """ | ||
| |/* | ||
| | * Copyright 2025 Mifos Initiative | ||
| | * | ||
| | * This Source Code Form is subject to the terms of the Mozilla Public | ||
| | * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| | * file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| | * | ||
| | * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE | ||
| | */ | ||
| |package $currentPackage | ||
| | | ||
| |import template.core.base.network.SupabaseCredentials as BaseSupabaseCredentials | ||
| | | ||
| |/** | ||
| | * Generated Supabase credentials from secrets/supabaseCredentialsFile.json | ||
| | * DO NOT EDIT - This file is generated by SupabaseConfigConventionPlugin | ||
| | */ | ||
| |object SupabaseCredentials : BaseSupabaseCredentials { | ||
| | override val url: String = "$url" | ||
| | override val anonKey: String = "$anonKey" | ||
| |} | ||
| """.trimMargin() | ||
| ) | ||
|
Comment on lines
+109
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parsed If the JSON values contain characters like Proposed fix: escape special characters before interpolation+ private fun escapeKotlinString(value: String): String =
+ value.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("\$", "\\\$")
+
// In the doLast block:
- | override val url: String = "$url"
- | override val anonKey: String = "$anonKey"
+ | override val url: String = "${escapeKotlinString(url)}"
+ | override val anonKey: String = "${escapeKotlinString(anonKey)}"🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| // Configure the Kotlin Multiplatform extension | ||
| extensions.configure<KotlinMultiplatformExtension> { | ||
| sourceSets.named("commonMain") { | ||
| kotlin.srcDir(generatedDir) | ||
| dependencies { | ||
| implementation(libs.findLibrary("supabase-postgrest").get()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Make compile and KSP tasks depend on the generate task | ||
| tasks.matching { | ||
| it.name.startsWith("compileKotlin") || it.name.startsWith("ksp") | ||
| }.configureEach { | ||
| dependsOn(generateTask) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Simple JSON parser for extracting url and anonKey fields. | ||
| * Avoids dependency on kotlinx-serialization in build-logic. | ||
| */ | ||
| private fun parseCredentials(jsonContent: String): Pair<String, String> { | ||
| val urlPattern = """"url"\s*:\s*"([^"]*)"""".toRegex() | ||
| val anonKeyPattern = """"anonKey"\s*:\s*"([^"]*)"""".toRegex() | ||
|
|
||
| val url = urlPattern.find(jsonContent)?.groupValues?.getOrNull(1) ?: "" | ||
| val anonKey = anonKeyPattern.find(jsonContent)?.groupValues?.getOrNull(1) ?: "" | ||
|
|
||
| return Pair(url, anonKey) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Extension for configuring SupabaseConfigConventionPlugin. | ||
| */ | ||
| open class SupabaseConfigExtension { | ||
| /** | ||
| * The package name for the generated SupabaseCredentials object. | ||
| * If not set, defaults to the module's android namespace + ".config" | ||
| */ | ||
| var packageName: String? = null | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2074,6 +2074,35 @@ | |
| | | | +--- io.ktor:ktor-client-auth:3.3.3 (*) | ||
| | | | +--- de.jensklingenberg.ktorfit:ktorfit-lib:2.7.1 (*) | ||
| | | | +--- co.touchlab:kermit:2.0.8 (*) | ||
| | | | +--- io.github.jan-tennert.supabase:postgrest-kt:3.1.1 | ||
| | | | | \--- io.github.jan-tennert.supabase:postgrest-kt-android:3.1.1 | ||
| | | | | +--- io.github.jan-tennert.supabase:auth-kt:3.1.1 | ||
| | | | | | \--- io.github.jan-tennert.supabase:auth-kt-android:3.1.1 | ||
| | | | | | +--- androidx.startup:startup-runtime:1.2.0 (*) | ||
| | | | | | +--- androidx.browser:browser:1.8.0 -> 1.9.0 (*) | ||
| | | | | | +--- io.github.jan-tennert.supabase:supabase-kt:3.1.1 | ||
| | | | | | | \--- io.github.jan-tennert.supabase:supabase-kt-android:3.1.1 | ||
| | | | | | | +--- androidx.lifecycle:lifecycle-process:2.8.7 -> 2.9.4 (*) | ||
| | | | | | | +--- org.jetbrains.kotlinx:kotlinx-datetime:0.6.1 -> 0.7.1 (*) | ||
| | | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1 -> 1.10.2 (*) | ||
| | | | | | | +--- co.touchlab:kermit:2.0.5 -> 2.0.8 (*) | ||
| | | | | | | +--- io.ktor:ktor-client-core:3.0.3 -> 3.3.3 (*) | ||
| | | | | | | +--- io.ktor:ktor-client-content-negotiation:3.0.3 -> 3.3.3 (*) | ||
| | | | | | | +--- io.ktor:ktor-serialization-kotlinx-json:3.0.3 -> 3.3.3 (*) | ||
| | | | | | | +--- org.jetbrains.kotlinx:atomicfu:0.27.0 | ||
| | | | | | | | \--- org.jetbrains.kotlinx:atomicfu-jvm:0.27.0 | ||
| | | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:{prefer 2.1.0} -> 2.2.21 (*) | ||
| | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.10 -> 2.2.21 (*) | ||
| | | | | | +--- com.squareup.okio:okio:3.10.2 -> 3.16.4 (*) | ||
| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.1.10 -> 2.2.21 (*) | ||
| | | | | | +--- com.russhwolf:multiplatform-settings-no-arg:1.3.0 (*) | ||
| | | | | | +--- com.russhwolf:multiplatform-settings-coroutines:1.3.0 (*) | ||
| | | | | | \--- org.kotlincrypto:secure-random:0.3.2 | ||
| | | | | | \--- org.kotlincrypto:secure-random-jvm:0.3.2 | ||
| | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.2.21 (*) | ||
| | | | | +--- org.jetbrains.kotlin:kotlin-reflect:2.1.10 | ||
| | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.10 -> 2.2.21 (*) | ||
| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.10 -> 2.2.21 (*) | ||
|
Comment on lines
+2077
to
+2105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, verify the file exists and understand the repo structure
find . -name "build.gradle.kts" -o -name "build.gradle" -o -name "libs.versions.toml" | head -20Repository: openMF/kmp-project-template Length of output: 744 🏁 Script executed: # Look for the cmp-android module structure
fd -t d "cmp-android" | head -5Repository: openMF/kmp-project-template Length of output: 83 🏁 Script executed: # Check the dependency tree file itself
wc -l cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt 2>/dev/null || echo "File path may vary"Repository: openMF/kmp-project-template Length of output: 137 🏁 Script executed: # Search for where these dependencies are declared
rg "postgrest-kt|supabase-kt" --type gradle --type toml -A 2 -B 2Repository: openMF/kmp-project-template Length of output: 504 🏁 Script executed: # Read the versions file to see actual version values
cat gradle/libs.versions.toml | head -100Repository: openMF/kmp-project-template Length of output: 2205 🏁 Script executed: # Find where Ktor, Kotlin, Coroutines versions are defined
rg "ktor|kotlin|coroutines|supabase" gradle/libs.versions.toml -iRepository: openMF/kmp-project-template Length of output: 5989 🏁 Script executed: # Check the cmp-android build file
cat cmp-android/build.gradle.ktsRepository: openMF/kmp-project-template Length of output: 5137 🌐 Web query:
💡 Result:
What this means
Recommended options
Sources: [1] [2] [3] [4] 🌐 Web query:
💡 Result: No public CVE (or GitHub Security Advisory / GHSA) specifically targeting A CVE that sometimes gets cited in “Supabase + PostgREST” searches—CVE‑2024‑24213—is disputed and concerns a SQL-query endpoint in the Supabase dashboard/pg_meta context, not the Kotlin client library References
Citations:
🌐 Web query:
💡 Result: For Jan Tennert’s So, for best Ktor client compatibility, use Ktor Sources: [1] [2] Resolve Ktor version compatibility with PostgREST dependency. The dependency tree shows Ktor 3.3.3, but No known CVEs exist for these library versions. 🤖 Prompt for AI Agents |
||
| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.2.21 (*) | ||
| | | | +--- io.insert-koin:koin-bom:4.1.1 (*) | ||
| | | | +--- io.insert-koin:koin-core:4.1.1 (*) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extension properties are evaluated eagerly at configuration time —
packageNameset after plugin application will be ignored.Lines 73-75 read
extension.packageNameduring task registration (configuration phase). If the user configures the extension after applying the plugin (which is the standard Gradle pattern),packageNamewill still benull.Similarly, the
outputFilepath (line 78-79) andsecretsFilecontent (lines 82-86) are computed eagerly. This violates Gradle's configuration avoidance principle and breaks up-to-date checking when inputs change.The idiomatic fix is to use
Property<String>in the extension and providers in the task:Suggested approach — use lazy evaluation
open class SupabaseConfigExtension { - var packageName: String? = null + val packageName: Property<String> = objects.property(String::class.java) }In the task registration, defer evaluation to
doLastor use task inputs with providers:val generateTask = tasks.register("generateSupabaseConfig") { - val packageName = extension.packageName - ?: project.findProperty("android.namespace")?.toString()?.let { "$it.config" } - ?: "${project.group}.config" - val packagePath = packageName.replace(".", "/") - val outputFile = generatedDir.get().asFile - .resolve("$packagePath/SupabaseCredentials.kt") - val fileContent = if (secretsFile.exists() && secretsFile.length() > 0) { - secretsFile.readText() - } else { - "" - } - inputs.property("credentialsContent", fileContent) - inputs.property("packageName", packageName) - outputs.file(outputFile) + val resolvedPackageName = extension.packageName.orElse( + provider { + project.findProperty("android.namespace")?.toString()?.let { "$it.config" } + ?: "${project.group}.config" + } + ) + inputs.property("packageName", resolvedPackageName) + inputs.file(secretsFile).optional() + outputs.dir(generatedDir) doLast { - val currentContent = inputs.properties["credentialsContent"] as String - val currentPackage = inputs.properties["packageName"] as String + val currentPackage = resolvedPackageName.get() + val currentContent = if (secretsFile.exists()) secretsFile.readText() else "" // ... rest of doLast } }🤖 Prompt for AI Agents