diff --git a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/SupabaseFunctionExtension.kt b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/SupabaseFunctionExtension.kt index 6796380..864b2da 100644 --- a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/SupabaseFunctionExtension.kt +++ b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/SupabaseFunctionExtension.kt @@ -101,4 +101,8 @@ internal fun SupabaseFunctionExtension.setupConvention(project: Project) { functionName.convention(project.name) verifyJwt.convention(true) importMap.convention(true) + + packageName.convention(project.provider { + error("packageName is not set, please provide the package name of the kotlin main function.") + }) } \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/kmp/Kmp.kt b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/kmp/Kmp.kt index 54985c0..d0a0fb9 100644 --- a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/kmp/Kmp.kt +++ b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/kmp/Kmp.kt @@ -24,9 +24,12 @@ package io.github.manriif.supabase.functions.kmp import io.github.manriif.supabase.functions.COROUTINES_VERSION import io.github.manriif.supabase.functions.COROUTINES_VERSION_GRADLE_PROPERTY import io.github.manriif.supabase.functions.SUPABASE_FUNCTION_PLUGIN_NAME +import io.github.manriif.supabase.functions.task.SupabaseFunctionCopyKotlinTask import io.github.manriif.supabase.functions.task.TASK_GENERATE_BRIDGE +import io.github.manriif.supabase.functions.util.postponeErrorOnTaskInvocation import org.gradle.api.Project import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.JsModuleKind import org.jetbrains.kotlin.gradle.dsl.JsSourceMapEmbedMode import org.jetbrains.kotlin.gradle.dsl.JsSourceMapNamesPolicy @@ -36,7 +39,6 @@ import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget internal fun Project.setupKotlinMultiplatform(kmpExtension: KotlinMultiplatformExtension) { kmpExtension.targets.withType().configureEach { - ensureMeetRequirements() configureCompilation() } @@ -48,57 +50,66 @@ internal fun Project.setupKotlinMultiplatform(kmpExtension: KotlinMultiplatformE implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}") } } + + afterEvaluate { + kmpExtension.targets.withType().forEach { target -> + target.checkUserConfiguration() + } + } +} + +private fun KotlinJsIrTarget.configureCompilation() { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + // [KT-47968](https://youtrack.jetbrains.com/issue/KT-47968/KJS-IR-Debug-in-external-tool-cant-step-into-library-function-with-available-sources) + // [KT-49757](https://youtrack.jetbrains.com/issue/KT-49757/Kotlin-JS-support-sourceMapEmbedSources-setting-by-IR-backend) + sourceMap.set(true) + sourceMapNamesPolicy.set(JsSourceMapNamesPolicy.SOURCE_MAP_NAMES_POLICY_FQ_NAMES) + sourceMapEmbedSources.set(JsSourceMapEmbedMode.SOURCE_MAP_SOURCE_CONTENT_ALWAYS) + } + + compilations.named(KotlinCompilation.MAIN_COMPILATION_NAME) { + compileTaskProvider.configure { + dependsOn(TASK_GENERATE_BRIDGE) + } + } } -private fun KotlinJsIrTarget.ensureMeetRequirements() { +private fun KotlinJsIrTarget.checkUserConfiguration() { + if (isBrowserConfigured) { + project.logger.warn( + "Browser execution environment is not supported by " + + "`$SUPABASE_FUNCTION_PLUGIN_NAME` plugin." + ) + } + val granularity = project.findProperty("kotlin.js.ir.output.granularity")?.toString() if (!(granularity.isNullOrBlank() || granularity == "per-module")) { - error( + project.postponeErrorOnTaskInvocation( "Only `per-module` JS IR output granularity is supported " + "by `$SUPABASE_FUNCTION_PLUGIN_NAME` plugin. " + "Current granularity is `$granularity`." ) } - if (isBrowserConfigured) { - error( - "Browser execution environment is not supported by " + - "`$SUPABASE_FUNCTION_PLUGIN_NAME` plugin." + val compilation = compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME) + + // Module kind is not set when using new compiler option DSL, fallback to deprecated one + val options = @Suppress("DEPRECATION") compilation?.compilerOptions?.options + val moduleKind = options?.moduleKind?.orNull + + if (moduleKind != JsModuleKind.MODULE_ES) { + project.postponeErrorOnTaskInvocation( + "Plugin `supabase-function` only supports ES module kind. " + + "Current module kind is `$moduleKind`." ) } if (!isNodejsConfigured) { - error( + project.postponeErrorOnTaskInvocation( "Node.js execution environment is a requirement " + "for `$SUPABASE_FUNCTION_PLUGIN_NAME` plugin." ) } -} - -private fun KotlinJsIrTarget.configureCompilation() { - compilations.named(KotlinCompilation.MAIN_COMPILATION_NAME) { - // Module kind is not set when using new compiler option DSL, fallback to deprecated one - @Suppress("DEPRECATION") - compilerOptions.configure { - val kind = moduleKind.orNull - - if (kind != JsModuleKind.MODULE_ES) { - error( - "Plugin `supabase-function` only supports ES module kind. " + - "Current module kind is $kind." - ) - } - - // [KT-47968](https://youtrack.jetbrains.com/issue/KT-47968/KJS-IR-Debug-in-external-tool-cant-step-into-library-function-with-available-sources) - // [KT-49757](https://youtrack.jetbrains.com/issue/KT-49757/Kotlin-JS-support-sourceMapEmbedSources-setting-by-IR-backend) - sourceMap.set(true) - sourceMapNamesPolicy.set(JsSourceMapNamesPolicy.SOURCE_MAP_NAMES_POLICY_FQ_NAMES) - sourceMapEmbedSources.set(JsSourceMapEmbedMode.SOURCE_MAP_SOURCE_CONTENT_ALWAYS) - } - - compileTaskProvider.configure { - dependsOn(TASK_GENERATE_BRIDGE) - } - } } \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionAggregateImportMapTask.kt b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionAggregateImportMapTask.kt index b66d5a7..0bcd696 100644 --- a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionAggregateImportMapTask.kt +++ b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionAggregateImportMapTask.kt @@ -28,12 +28,14 @@ import io.github.manriif.supabase.functions.IMPORT_MAP_TEMPLATE_FILE_NAME import io.github.manriif.supabase.functions.error.SupabaseFunctionImportMapTemplateException import io.github.manriif.supabase.functions.supabase.supabaseAllFunctionsDirFile import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.CacheableTask -import org.gradle.api.tasks.IgnoreEmptyDirectories -import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity @@ -49,18 +51,17 @@ import java.io.File @CacheableTask abstract class SupabaseFunctionAggregateImportMapTask : DefaultTask() { - @get:InputDirectory - @get:IgnoreEmptyDirectories + @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) - internal abstract val importMapsDir: DirectoryProperty + internal abstract val importMapDirs: ConfigurableFileCollection @get:Internal internal abstract val supabaseDir: DirectoryProperty @get:InputFile + @get:Optional @get:PathSensitive(PathSensitivity.RELATIVE) - internal val importMapTemplateFile: File - get() = supabaseAllFunctionsDirFile(supabaseDir, IMPORT_MAP_TEMPLATE_FILE_NAME) + internal abstract val importMapTemplateFile: RegularFileProperty @get:OutputFile internal val aggregatedImportMapFile: File @@ -72,9 +73,11 @@ abstract class SupabaseFunctionAggregateImportMapTask : DefaultTask() { .setPrettyPrinting() .create() - val importMap = if (importMapTemplateFile.exists() && importMapTemplateFile.isFile) { + val template = importMapTemplateFile.orNull?.asFile + + val importMap = if (template?.exists() == true && template.isFile) { try { - JsonParser.parseReader(importMapTemplateFile.reader()).asJsonObject + JsonParser.parseReader(template.reader()).asJsonObject } catch (throwable: Throwable) { throw SupabaseFunctionImportMapTemplateException( message = "Failed to load $IMPORT_MAP_TEMPLATE_FILE_NAME", @@ -96,7 +99,7 @@ abstract class SupabaseFunctionAggregateImportMapTask : DefaultTask() { val imports = importMap.getAsJsonObject(IMPORT_MAP_JSON_IMPORTS) val scopes = importMap.getAsJsonObject(IMPORT_MAP_JSON_SCOPES) - importMapsDir.get().asFileTree + importMapDirs.asFileTree .matching { include { file -> !file.isDirectory && file.name.endsWith(".json") diff --git a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionCopyJsTask.kt b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionCopyJsTask.kt index 592c422..2282e1a 100644 --- a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionCopyJsTask.kt +++ b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionCopyJsTask.kt @@ -40,7 +40,7 @@ import java.io.File import javax.inject.Inject /** - * Task responsible for copying generated js code into supabase function directory. + * Task responsible for copying js sources into supabase function directory. */ @CacheableTask abstract class SupabaseFunctionCopyJsTask : DefaultTask() { diff --git a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionCopyKotlinTask.kt b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionCopyKotlinTask.kt index 5cd2be9..6d60fed 100644 --- a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionCopyKotlinTask.kt +++ b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionCopyKotlinTask.kt @@ -32,6 +32,7 @@ import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity diff --git a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionGenerateImportMapTask.kt b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionGenerateImportMapTask.kt index 6d74ea9..ec91d54 100644 --- a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionGenerateImportMapTask.kt +++ b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/SupabaseFunctionGenerateImportMapTask.kt @@ -29,13 +29,15 @@ import io.github.manriif.supabase.functions.JS_SOURCES_INPUT_DIR import io.github.manriif.supabase.functions.kmp.JsDependency import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity @@ -64,9 +66,10 @@ abstract class SupabaseFunctionGenerateImportMapTask : DefaultTask() { @get:Internal internal abstract val importMapsDir: DirectoryProperty - @get:InputDirectory + @get:InputFile + @get:Optional @get:PathSensitive(PathSensitivity.RELATIVE) - internal abstract val packageJsonDir: DirectoryProperty + internal abstract val packageJsonFile: RegularFileProperty @get:Input internal abstract val functionName: Property @@ -94,8 +97,7 @@ abstract class SupabaseFunctionGenerateImportMapTask : DefaultTask() { private fun createFunctionImports(): JsonObject { val imports = JsonObject() - val packageJsonFile = packageJsonDir.file("package.json").get().asFile - val packageJson = fromSrcPackageJson(packageJsonFile) ?: return imports + val packageJson = fromSrcPackageJson(packageJsonFile.orNull?.asFile) ?: return imports packageJson.dependencies.forEach { (packageName, version) -> imports.addProperty(packageName, "npm:$packageName@$version") diff --git a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/Tasks.kt b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/Tasks.kt index 6307971..d2d2fa1 100644 --- a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/Tasks.kt +++ b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/task/Tasks.kt @@ -21,9 +21,11 @@ */ package io.github.manriif.supabase.functions.task +import io.github.manriif.supabase.functions.IMPORT_MAP_TEMPLATE_FILE_NAME import io.github.manriif.supabase.functions.KOTLIN_MAIN_FUNCTION_NAME import io.github.manriif.supabase.functions.REQUEST_CONFIG_FILE_NAME import io.github.manriif.supabase.functions.SUPABASE_FUNCTION_OUTPUT_DIR +import io.github.manriif.supabase.functions.SUPABASE_FUNCTION_PLUGIN_NAME import io.github.manriif.supabase.functions.SUPABASE_FUNCTION_TASK_GROUP import io.github.manriif.supabase.functions.SupabaseFunctionExtension import io.github.manriif.supabase.functions.kmp.JsDependency @@ -41,6 +43,7 @@ import org.gradle.kotlin.dsl.support.uppercaseFirstChar import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension internal const val PREPARE_KOTLIN_BUILD_SCRIPT_MODEL_TASK = "prepareKotlinBuildScriptModel" +internal const val JS_PUBLIC_PACKAGE_JSON_TASK = "jsPublicPackageJson" internal const val TASK_PREFIX = "supabaseFunction" internal const val TASK_GENERATE_ENVIRONMENT_TEMPLATE = "${TASK_PREFIX}CopyKotlin%s" @@ -68,7 +71,7 @@ internal fun Project.configurePluginTasks( val jsDependenciesProvider = jsDependencies() registerGenerateImportMapTask(extension, jsDependenciesProvider) - registerGenerateBridgeTask(extension, kmpExtension) + registerGenerateKotlinBridgeTask(extension, kmpExtension) registerCopyJsTask(extension, jsDependenciesProvider) registerCopyKotlinTask(extension, "development") registerCopyKotlinTask(extension, "production") @@ -93,8 +96,11 @@ private fun Project.registerAggregateImportMapTask(extension: SupabaseFunctionEx group = SUPABASE_FUNCTION_TASK_GROUP description = "Aggregate functions import maps." - importMapsDir.convention(layout.buildDirectory.dir("${SUPABASE_FUNCTION_OUTPUT_DIR}/importMaps")) supabaseDir.convention(extension.supabaseDir) + + importMapTemplateFile.convention( + extension.supabaseDir.file("functions/$IMPORT_MAP_TEMPLATE_FILE_NAME").orNone() + ) } tasks.named(PREPARE_KOTLIN_BUILD_SCRIPT_MODEL_TASK) { @@ -111,16 +117,11 @@ private val Project.aggregateTaskProvider: TaskProvider(TASK_GENERATE_BRIDGE) { group = SUPABASE_FUNCTION_TASK_GROUP @@ -128,13 +129,19 @@ private fun Project.registerGenerateBridgeTask( description = "Generate a kotlin function that acts as a bridge between " + "the `Deno.serve` and the kotlin main function." + packageName.convention(extension.packageName) supabaseDir.convention(extension.supabaseDir) generatedSourceOutputDir.convention(outputDir) - packageName.convention(extension.packageName) jsOutputName.convention(jsOutputName(kmpExtension)) functionName.convention(extension.functionName) mainFunctionName.convention(KOTLIN_MAIN_FUNCTION_NAME) } + + afterEvaluate { + kmpExtension.sourceSets.named { it == "jsMain" }.configureEach { + kotlin.srcDir(outputDir) + } + } } private fun Project.registerCopyJsTask( @@ -151,34 +158,52 @@ private fun Project.registerCopyJsTask( } } +private fun missingJsCompileTaskError(compileTaskName: String): Nothing { + error( + """ + Task `$compileTaskName` was not found during project sync, common reasons for this error are: + + - The `$SUPABASE_FUNCTION_PLUGIN_NAME` plugin was applied on a build script where the kotlin multiplatform plugin was not applied (e.g., root build script) + - The kotlin multiplatform plugin was not applied on this project + - JS target was not initialized on this project + - JS target is missing `binaries.library()` + """.trimIndent() + ) +} + private fun Project.registerCopyKotlinTask( extension: SupabaseFunctionExtension, environment: String, ) { val uppercaseEnvironment = environment.uppercaseFirstChar() val compileSyncTaskName = "js${uppercaseEnvironment}LibraryCompileSync" - - if (tasks.names.none { it == compileSyncTaskName }) { - error( - "Could not locate task `$compileSyncTaskName`, " + - "be sure you add `binaries.library()` to the js node target." - ) - } - val taskName = TASK_GENERATE_ENVIRONMENT_TEMPLATE.format(uppercaseEnvironment) tasks.register(taskName) { group = SUPABASE_FUNCTION_TASK_GROUP - description = "Copy Kotlin generated sources into supabase function directory." - - compiledSourceDir.convention( - layout.buildDirectory.dir("compileSync/js/main/${environment}Library/kotlin") - ) + description = "Copy Kotlin generated $environment sources into supabase function directory." supabaseDir.convention(extension.supabaseDir) functionName.convention(extension.functionName) - dependsOn(compileSyncTaskName) + val compileTaskFound = tasks.names.any { it == compileSyncTaskName } + + if (compileTaskFound) { + compiledSourceDir.convention( + layout.buildDirectory.dir("compileSync/js/main/${environment}Library/kotlin") + ) + + dependsOn(compileSyncTaskName) + } else { + compiledSourceDir.convention(provider { + missingJsCompileTaskError(compileSyncTaskName) + }) + + doFirst { + missingJsCompileTaskError(compileSyncTaskName) + } + } + dependsOn(TASK_COPY_JS) } } @@ -247,26 +272,30 @@ private fun Project.registerGenerateImportMapTask( extension: SupabaseFunctionExtension, jsDependenciesProvider: Provider> ) { + val importMapDir = layout.buildDirectory + .dir("generated/${SUPABASE_FUNCTION_OUTPUT_DIR}/importMap") + val generateTaskProvider = tasks.register( name = TASK_GENERATE_IMPORT_MAP ) { group = SUPABASE_FUNCTION_TASK_GROUP description = "Generate import map." - packageJsonDir.convention(layout.buildDirectory.dir("tmp/jsPublicPackageJson")) + packageJsonFile.convention( + layout.buildDirectory.file("tmp/jsPublicPackageJson/package.json").orNone() + ) + + importMapsDir.convention(importMapDir) functionName.convention(extension.functionName) jsDependencies.convention(jsDependenciesProvider) - dependsOn("jsPublicPackageJson") + if (tasks.names.any { it == JS_PUBLIC_PACKAGE_JSON_TASK }) { + dependsOn(JS_PUBLIC_PACKAGE_JSON_TASK) + } } aggregateTaskProvider.configure { - val aggregateTask = apply { - dependsOn(generateTaskProvider) - } - - generateTaskProvider.configure { - importMapsDir.convention(aggregateTask.importMapsDir) - } + importMapDirs.from(importMapDir) + dependsOn(generateTaskProvider) } } \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/util/Error.kt b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/util/Error.kt new file mode 100644 index 0000000..f08fcba --- /dev/null +++ b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/util/Error.kt @@ -0,0 +1,13 @@ +package io.github.manriif.supabase.functions.util + +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.kotlin.dsl.withType + +internal inline fun Project.postponeErrorOnTaskInvocation(errorMessage: String) { + tasks.withType().configureEach { + doFirst { + error(errorMessage) + } + } +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/util/Files.kt b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/util/Files.kt index 567034f..0fbc2b2 100644 --- a/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/util/Files.kt +++ b/gradle-plugin/src/main/kotlin/io/github/manriif/supabase/functions/util/Files.kt @@ -22,13 +22,13 @@ package io.github.manriif.supabase.functions.util import org.gradle.api.Project -import org.gradle.api.file.RegularFile +import org.gradle.api.file.FileSystemLocation import org.gradle.api.provider.Provider /** * Returns a [Provider] that will provides the file ony if it exists. */ -internal fun Provider.orNone(): Provider { +internal fun Provider.orNone(): Provider { @Suppress("UnstableApiUsage") return filter { it.asFile.exists() } } @@ -36,6 +36,6 @@ internal fun Provider.orNone(): Provider { /** * Returns a [Provider] that will provides the file ony if it exists. */ -internal fun RegularFile.orNone(project: Project): Provider { +internal fun T.orNone(project: Project): Provider { return project.provider { this }.orNone() } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc87d67..a1d1726 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # Projct version -supabase-functions = "0.0.2" +supabase-functions = "0.0.4" jvm-target = "11"