diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 892a374..d52b8d3 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -25,7 +25,7 @@ jobs: - name: Build run: | - ./gradlew build + ./gradlew build IdeaPlugin:verifyPlugin mv scripting-host/build/distributions/scripting-host-release*.tar.gz scripting-host-release.tar.gz - name: Upload build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f38f52..0ba0a23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: java-version: 17 - name: Build - run: ./gradlew assembleReleaseDist + run: ./gradlew scripting-host:assembleReleaseDist IdeaPlugin:buildPlugin - name: Create info variables id: variables @@ -36,10 +36,10 @@ jobs: - name: Get Changelog id: changelog - uses: mindsers/changelog-reader-action@v2.2.3 - with: - version: ${{ steps.variables.outputs.version }} - validation_level: error + run: | + echo 'changes<> $GITHUB_OUTPUT + ./gradlew -q getChangelog --project-version='${{ steps.variables.outputs.version }}' --no-links --no-header >> $GITHUB_OUTPUT + echo END_OF_CHANGELOG >> $GITHUB_OUTPUT - name: Create Release uses: softprops/action-gh-release@v2 @@ -49,5 +49,6 @@ jobs: files: | scripting-host/build/distributions/scripting-host-release*.tar.gz scripting-host/build/distributions/scripting-host-release*.zip + IdeaPlugin/build/distributions/IdeaPlugin-*.zip draft: false prerelease: false diff --git a/.gitignore b/.gitignore index 3a5ba96..60db1b9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ build/ ### IntelliJ IDEA ### .idea/ +.intellijPlatform/ ### Eclipse ### .apt_generated @@ -34,3 +35,6 @@ bin/ ### Project ### /kss.properties + +### Kotlin ### +.kotlin diff --git a/CHANGELOG.md b/CHANGELOG.md index ccbc4d1..5127bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # CHANGELOG -## [UNRELEASED] +## [Unreleased] + +### Added + +- IntelliJ Plugin ## [0.6.0] - 2025-01-25 diff --git a/IdeaPlugin/build.gradle.kts b/IdeaPlugin/build.gradle.kts new file mode 100644 index 0000000..aa77e2c --- /dev/null +++ b/IdeaPlugin/build.gradle.kts @@ -0,0 +1,96 @@ +import org.jetbrains.changelog.Changelog +import org.jetbrains.changelog.tasks.GetChangelogTask + +/* + * Copyright 2025 Eduard Wolf + * + * 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. + */ + +plugins { + alias(libs.plugins.changelog) + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.intellij.platform) +} + +group = "net.edwardday.kss" + +repositories { + mavenCentral() + intellijPlatform { + defaultRepositories() + } +} + +kotlin { + jvmToolchain(21) +} + +changelog { + versionPrefix.set("") + path.set(rootProject.file("CHANGELOG.md").canonicalPath) + +} + +intellijPlatform { + pluginConfiguration { + ideaVersion { + sinceBuild = "252" + untilBuild = "252.*" + } + val projectVersion = project.version.toString() + val changelog = tasks.getChangelog.flatMap(GetChangelogTask::changelog).map { changelog -> + val item = if (projectVersion.endsWith("SNAPSHOT")) changelog.unreleasedItem!! else changelog.getLatest() + changelog.renderItem(item, Changelog.OutputType.HTML) + } + changeNotes.set(changelog) + version = projectVersion + } + buildSearchableOptions = false + + pluginVerification { + ides { + recommended() + } + } + autoReload = false +} + +dependencies { + // Configure Gradle IntelliJ Plugin + // Read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin.html#setup + intellijPlatform { + // needed until 2025.3 can be targeted + @Suppress("DEPRECATION") + intellijIdeaCommunity("2025.2.2") + + bundledPlugins("org.jetbrains.kotlin") + + compileOnly(libs.kotlin.scripting.common) + compileOnly(libs.kotlinx.coroutines) + } + implementation(project(":scripting-definition")) { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-scripting-common") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-scripting-jvm") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-script-runtime") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-scripting-dependencies-maven") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core") + } + implementation(libs.kotlin.scripting.dependencies.maven.all) { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-scripting-common") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-scripting-jvm") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-script-runtime") + } +} diff --git a/IdeaPlugin/gradle.properties b/IdeaPlugin/gradle.properties new file mode 100644 index 0000000..24baa6d --- /dev/null +++ b/IdeaPlugin/gradle.properties @@ -0,0 +1,24 @@ +# +# Copyright 2025 Eduard Wolf +# +# 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. +# + +# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib +kotlin.stdlib.default.dependency = false + +# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html +org.gradle.configuration-cache = true + +# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html +org.gradle.caching = true diff --git a/IdeaPlugin/src/main/kotlin/net/edwardday/kss/ideaplugin/KssScriptDefinitionSource.kt b/IdeaPlugin/src/main/kotlin/net/edwardday/kss/ideaplugin/KssScriptDefinitionSource.kt new file mode 100644 index 0000000..00bdd6e --- /dev/null +++ b/IdeaPlugin/src/main/kotlin/net/edwardday/kss/ideaplugin/KssScriptDefinitionSource.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2025 Eduard Wolf + * + * 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. + */ + +package net.edwardday.kss.ideaplugin + +import com.intellij.util.PathUtil +import kotlinx.coroutines.Job +import net.edwardday.serverscript.scriptdefinition.ServerScriptDefinition +import net.edwardday.serverscript.scriptdefinition.script.ServerScript +import org.jetbrains.kotlin.idea.base.plugin.artifacts.KotlinArtifacts +import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition +import org.jetbrains.kotlin.scripting.definitions.ScriptDefinitionsSource +import java.io.File +import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver +import kotlin.script.experimental.host.ScriptingHostConfiguration +import kotlin.script.experimental.host.configurationDependencies +import kotlin.script.experimental.jvm.JvmDependency +import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration + +internal class KssScriptDefinitionSource : ScriptDefinitionsSource { + override val definitions: Sequence + get() { + val definition = ScriptDefinition.FromTemplate( + baseHostConfiguration = ScriptingHostConfiguration(defaultJvmScriptingHostConfiguration) { + val kotlinArtifacts = listOf(KotlinArtifacts.kotlinStdlib, KotlinArtifacts.kotlinScriptRuntime) + val kssArtifacts = + listOf(ServerScript::class, MavenDependenciesResolver::class, Job::class) + .map { File(PathUtil.getJarPathForClass(it.java)) } + val dependencies = (kssArtifacts + kotlinArtifacts).distinct() + configurationDependencies(listOf(JvmDependency(dependencies))) + }, + contextClass = ServerScriptDefinition::class, + template = ServerScriptDefinition::class, + ) + definition.order = Int.MIN_VALUE + return sequenceOf(definition) + } +} diff --git a/IdeaPlugin/src/main/resources/META-INF/plugin.xml b/IdeaPlugin/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..fa10fe6 --- /dev/null +++ b/IdeaPlugin/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,43 @@ + + + + + net.edwardday.kss.IdeaPlugin + + + Kotlin Server Scripts + + + Edwar D Day + + + Kotlin server scripts in the IDE. + ]]> + + + com.intellij.modules.platform + org.jetbrains.kotlin + + + + + + + diff --git a/IdeaPlugin/src/main/resources/META-INF/pluginIcon.svg b/IdeaPlugin/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000..85440e6 --- /dev/null +++ b/IdeaPlugin/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index f85c12c..8bd847b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![KSS Logo](IdeaPlugin/src/main/resources/META-INF/pluginIcon.svg) + # Kotlin Server Scripts Kotlin Server Scripts let you run kotlin scripts in response to server requests, without setting up a complete project. @@ -78,6 +80,16 @@ You can reference a [logback configuration file](https://logback.qos.ch/manual/c `kss.properties` configuration file via `logging.logback.configurationFile=`. This way you can change the log level from the default `info` or log to a file instead of the command line as it's done by default. +## Plugin + +You can use the IntelliJ plugin from the latest release to get proper code completion when writing scripts. + +### Install + +Download the plugin from the [latest release](https://github.com/EdwarDDay/kotlin-server-scripts/releases/latest), +check the [IDEA documentation](https://www.jetbrains.com/help/idea/managing-plugins.html#install_plugin_from_disk) to +get the latest information of how to install a plugin from disk. + ## Building To build the library just run `./gradlew scripting-host:build`. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1236200..6c2d9ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,7 @@ [versions] +changelog = "2.2.1" clikt = "5.0.3" +intellij-platform = "2.9.0" kotlin = "2.2.21" kotlinx-coroutines = "1.10.2" logback = "1.5.20" @@ -11,6 +13,7 @@ clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } kotlin-scripting-common = { module = "org.jetbrains.kotlin:kotlin-scripting-common", version.ref = "kotlin" } kotlin-scripting-dependencies = { module = "org.jetbrains.kotlin:kotlin-scripting-dependencies", version.ref = "kotlin" } kotlin-scripting-dependencies-maven = { module = "org.jetbrains.kotlin:kotlin-scripting-dependencies-maven", version.ref = "kotlin" } +kotlin-scripting-dependencies-maven-all = { module = "org.jetbrains.kotlin:kotlin-scripting-dependencies-maven-all", version.ref = "kotlin" } kotlin-scripting-jvm = { module = "org.jetbrains.kotlin:kotlin-scripting-jvm", version.ref = "kotlin" } kotlin-scripting-jvm-host = { module = "org.jetbrains.kotlin:kotlin-scripting-jvm-host", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -22,4 +25,6 @@ oshai-logging = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = " okio = { module = "com.squareup.okio:okio", version.ref = "okio" } [plugins] +changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +intellij-platform = { id = "org.jetbrains.intellij.platform", version.ref = "intellij-platform" } diff --git a/scripting-definition/src/main/kotlin/ScriptDefinition.kt b/scripting-definition/src/main/kotlin/ScriptDefinition.kt index 5685877..0f475db 100644 --- a/scripting-definition/src/main/kotlin/ScriptDefinition.kt +++ b/scripting-definition/src/main/kotlin/ScriptDefinition.kt @@ -16,158 +16,7 @@ package net.edwardday.serverscript.scriptdefinition -import kotlinx.coroutines.runBlocking -import net.edwardday.serverscript.scriptdefinition.annotation.Import -import net.edwardday.serverscript.scriptdefinition.script.ServerScript -import java.io.File import kotlin.script.experimental.annotations.KotlinScript -import kotlin.script.experimental.api.RefineScriptCompilationConfigurationHandler -import kotlin.script.experimental.api.ResultWithDiagnostics -import kotlin.script.experimental.api.ScriptCollectedData -import kotlin.script.experimental.api.ScriptCompilationConfiguration -import kotlin.script.experimental.api.ScriptConfigurationRefinementContext -import kotlin.script.experimental.api.ScriptDiagnostic -import kotlin.script.experimental.api.ScriptSourceAnnotation -import kotlin.script.experimental.api.asDiagnostics -import kotlin.script.experimental.api.asSuccess -import kotlin.script.experimental.api.collectedAnnotations -import kotlin.script.experimental.api.defaultImports -import kotlin.script.experimental.api.implicitReceivers -import kotlin.script.experimental.api.importScripts -import kotlin.script.experimental.api.onSuccess -import kotlin.script.experimental.api.refineConfiguration -import kotlin.script.experimental.dependencies.CompoundDependenciesResolver -import kotlin.script.experimental.dependencies.DependsOn -import kotlin.script.experimental.dependencies.FileSystemDependenciesResolver -import kotlin.script.experimental.dependencies.Repository -import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver -import kotlin.script.experimental.dependencies.resolveFromScriptSourceAnnotations -import kotlin.script.experimental.host.FileBasedScriptSource -import kotlin.script.experimental.host.toScriptSource -import kotlin.script.experimental.jvm.dependenciesFromClassContext -import kotlin.script.experimental.jvm.jvm -import kotlin.script.experimental.jvm.updateClasspath - -const val SERVER_SCRIPT_FILE_EXTENSION = "server.kts" - -@KotlinScript( - fileExtension = SERVER_SCRIPT_FILE_EXTENSION, - compilationConfiguration = ServerScriptConfiguration::class, -) +@KotlinScript(compilationConfiguration = ServerScriptConfiguration::class) abstract class ServerScriptDefinition - -class ServerScriptConfiguration : ScriptCompilationConfiguration( - body = { - defaultImports(DependsOn::class, Repository::class, Import::class) - defaultImports("net.edwardday.serverscript.scriptdefinition.script.*") - jvm { - dependenciesFromClassContext( - ServerScriptDefinition::class, - "kotlin-scripting-dependencies", - "scripting-definition", - ) - } - implicitReceivers(ServerScript::class) - refineConfiguration { - // the callback called when any of the listed file-level annotations are encountered in the compiled script - // the processing is defined by the `handler`, that may return refined configuration depending on the annotations - onAnnotations(DependsOn::class, Repository::class, handler = ServerScriptClasspathConfigurator()) - onAnnotations(Import::class, handler = ServerScriptImportsConfigurator()) - } - - } -) - -class ServerScriptImportsConfigurator : RefineScriptCompilationConfigurationHandler { - override fun invoke(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics { - val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations) - ?.takeIf(List>::isNotEmpty) - val importAnnotations = annotations?.filter { it.annotation is Import }?.takeIf(List<*>::isNotEmpty) - ?: return context.compilationConfiguration.asSuccess() - val workingDir = (context.script as? FileBasedScriptSource)?.file?.absoluteFile?.parentFile - val diagnostics = arrayListOf() - val scripts = importAnnotations.mapNotNull { (annotation, location) -> - val path = (annotation as Import).path.let { - if (it.endsWith(".$SERVER_SCRIPT_FILE_EXTENSION")) it else "$it.$SERVER_SCRIPT_FILE_EXTENSION" - } - val absolutePathFile = File(path).takeIf(File::isAbsolute) - when { - absolutePathFile != null -> absolutePathFile.takeIf(File::isFile).also { - if (it == null) { - diagnostics += ScriptDiagnostic( - code = ScriptDiagnostic.unspecifiedError, - message = "cannot find import script file at $path", - severity = ScriptDiagnostic.Severity.WARNING, - locationWithId = location, - ) - } - } - - workingDir != null -> File(workingDir, path).takeIf(File::isFile).also { - if (it == null) { - diagnostics += ScriptDiagnostic( - code = ScriptDiagnostic.unspecifiedError, - message = "cannot find import script file at ${workingDir.path}/$path", - severity = ScriptDiagnostic.Severity.WARNING, - locationWithId = location, - ) - } - } - - else -> { - diagnostics += ScriptDiagnostic( - code = ScriptDiagnostic.unspecifiedError, - message = "cannot find import script file with relative path ($path) from a script with unknown location", - severity = ScriptDiagnostic.Severity.ERROR, - locationWithId = location, - ) - null - } - }?.toScriptSource() - } - - return if (diagnostics.isNotEmpty()) { - ResultWithDiagnostics.Failure(diagnostics) - } else { - ScriptCompilationConfiguration(context.compilationConfiguration) { - importScripts(scripts) - }.asSuccess() - } - } - -} - -class ServerScriptClasspathConfigurator : RefineScriptCompilationConfigurationHandler { - private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver()) - - override operator fun invoke(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics = - processAnnotations(context) - - private fun processAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics { - val diagnostics = arrayListOf() - - val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations) - ?.takeIf(List<*>::isNotEmpty) - ?: return context.compilationConfiguration.asSuccess() - - val resolveResult = try { - runBlocking { - resolver.resolveFromScriptSourceAnnotations( - annotations.filter { it.annotation is DependsOn || it.annotation is Repository } - ) - } - } catch (e: Throwable) { - ResultWithDiagnostics.Failure( - *diagnostics.toTypedArray(), - e.asDiagnostics(path = context.script.locationId) - ) - } - - return resolveResult.onSuccess { resolvedClassPath -> - ScriptCompilationConfiguration(context.compilationConfiguration) { - updateClasspath(resolvedClassPath) - }.asSuccess() - } - } -} diff --git a/scripting-definition/src/main/kotlin/ServerScriptConfiguration.kt b/scripting-definition/src/main/kotlin/ServerScriptConfiguration.kt new file mode 100644 index 0000000..398a77b --- /dev/null +++ b/scripting-definition/src/main/kotlin/ServerScriptConfiguration.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2025 Eduard Wolf + * + * 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. + */ + +package net.edwardday.serverscript.scriptdefinition + +import kotlinx.coroutines.runBlocking +import net.edwardday.serverscript.scriptdefinition.annotation.Import +import net.edwardday.serverscript.scriptdefinition.script.ServerScript +import java.io.File +import kotlin.script.experimental.api.RefineScriptCompilationConfigurationHandler +import kotlin.script.experimental.api.ResultWithDiagnostics +import kotlin.script.experimental.api.ScriptCollectedData +import kotlin.script.experimental.api.ScriptCompilationConfiguration +import kotlin.script.experimental.api.ScriptConfigurationRefinementContext +import kotlin.script.experimental.api.ScriptDiagnostic +import kotlin.script.experimental.api.ScriptSourceAnnotation +import kotlin.script.experimental.api.asDiagnostics +import kotlin.script.experimental.api.asSuccess +import kotlin.script.experimental.api.collectedAnnotations +import kotlin.script.experimental.api.defaultImports +import kotlin.script.experimental.api.displayName +import kotlin.script.experimental.api.fileExtension +import kotlin.script.experimental.api.implicitReceivers +import kotlin.script.experimental.api.importScripts +import kotlin.script.experimental.api.onSuccess +import kotlin.script.experimental.api.refineConfiguration +import kotlin.script.experimental.dependencies.CompoundDependenciesResolver +import kotlin.script.experimental.dependencies.DependsOn +import kotlin.script.experimental.dependencies.ExternalDependenciesResolver +import kotlin.script.experimental.dependencies.FileSystemDependenciesResolver +import kotlin.script.experimental.dependencies.Repository +import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver +import kotlin.script.experimental.dependencies.resolveFromScriptSourceAnnotations +import kotlin.script.experimental.host.FileBasedScriptSource +import kotlin.script.experimental.host.toScriptSource +import kotlin.script.experimental.jvm.dependenciesFromClassContext +import kotlin.script.experimental.jvm.jvm +import kotlin.script.experimental.jvm.updateClasspath + +const val SERVER_SCRIPT_FILE_EXTENSION = "server.kts" +private const val SERVER_SCRIPT_DISPLAY_NAME = "KSS .server.kts" + +internal class ServerScriptConfiguration : ScriptCompilationConfiguration( + body = { + displayName(SERVER_SCRIPT_DISPLAY_NAME) + fileExtension(SERVER_SCRIPT_FILE_EXTENSION) + defaultImports(DependsOn::class, Repository::class, Import::class) + defaultImports("net.edwardday.serverscript.scriptdefinition.script.*") + jvm { + dependenciesFromClassContext( + ServerScriptDefinition::class, + "kotlin-scripting-dependencies", + "scripting-definition", + ) + } + implicitReceivers(ServerScript::class) + refineConfiguration { + // the callback called when any of the listed file-level annotations are encountered in the compiled script + // the processing is defined by the `handler`, that may return refined configuration depending on the annotations + onAnnotations( + DependsOn::class, + Repository::class, + handler = ServerScriptClasspathConfigurator(MavenDependenciesResolver()) + ) + onAnnotations(Import::class, handler = ServerScriptImportsConfigurator()) + } + + } +) + +internal class ServerScriptImportsConfigurator : RefineScriptCompilationConfigurationHandler { + override fun invoke(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics { + val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations) + ?.takeIf(List>::isNotEmpty) + val importAnnotations = annotations?.filter { it.annotation is Import }?.takeIf(List<*>::isNotEmpty) + ?: return context.compilationConfiguration.asSuccess() + val workingDir = (context.script as? FileBasedScriptSource)?.file?.absoluteFile?.parentFile + val diagnostics = arrayListOf() + val scripts = importAnnotations.mapNotNull { (annotation, location) -> + val path = (annotation as Import).path.let { + if (it.endsWith(".$SERVER_SCRIPT_FILE_EXTENSION")) it else "$it.$SERVER_SCRIPT_FILE_EXTENSION" + } + val absolutePathFile = File(path).takeIf(File::isAbsolute) + when { + absolutePathFile != null -> absolutePathFile.takeIf(File::isFile).also { + if (it == null) { + diagnostics += ScriptDiagnostic( + code = ScriptDiagnostic.unspecifiedError, + message = "cannot find import script file at $path", + severity = ScriptDiagnostic.Severity.WARNING, + locationWithId = location, + ) + } + } + + workingDir != null -> File(workingDir, path).takeIf(File::isFile).also { + if (it == null) { + diagnostics += ScriptDiagnostic( + code = ScriptDiagnostic.unspecifiedError, + message = "cannot find import script file at ${workingDir.path}/$path", + severity = ScriptDiagnostic.Severity.WARNING, + locationWithId = location, + ) + } + } + + else -> { + diagnostics += ScriptDiagnostic( + code = ScriptDiagnostic.unspecifiedError, + message = "cannot find import script file with relative path ($path) from a script with unknown location", + severity = ScriptDiagnostic.Severity.ERROR, + locationWithId = location, + ) + null + } + }?.toScriptSource() + } + + return if (diagnostics.isNotEmpty()) { + ResultWithDiagnostics.Failure(diagnostics) + } else { + ScriptCompilationConfiguration(context.compilationConfiguration) { + importScripts(scripts) + }.asSuccess() + } + } + +} + +class ServerScriptClasspathConfigurator(mavenResolver: ExternalDependenciesResolver) : + RefineScriptCompilationConfigurationHandler { + private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), mavenResolver) + + override operator fun invoke(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics = + processAnnotations(context) + + private fun processAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics { + val diagnostics = arrayListOf() + + val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations) + ?.takeIf(List<*>::isNotEmpty) + ?: return context.compilationConfiguration.asSuccess() + + val resolveResult = try { + runBlocking { + resolver.resolveFromScriptSourceAnnotations( + annotations.filter { it.annotation is DependsOn || it.annotation is Repository } + ) + } + } catch (e: Throwable) { + ResultWithDiagnostics.Failure( + *diagnostics.toTypedArray(), + e.asDiagnostics(path = context.script.locationId) + ) + } + + return resolveResult.onSuccess { resolvedClassPath -> + ScriptCompilationConfiguration(context.compilationConfiguration) { + updateClasspath(resolvedClassPath) + }.asSuccess() + } + } +} diff --git a/scripting-definition/src/main/kotlin/annotation/Import.kt b/scripting-definition/src/main/kotlin/annotation/Import.kt index a59b1c1..40e5c0c 100644 --- a/scripting-definition/src/main/kotlin/annotation/Import.kt +++ b/scripting-definition/src/main/kotlin/annotation/Import.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2025 Eduard Wolf + * + * 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. + */ + package net.edwardday.serverscript.scriptdefinition.annotation @Target(AnnotationTarget.FILE) diff --git a/scripting-host/src/main/kotlin/RequestState.kt b/scripting-host/src/main/kotlin/RequestState.kt index b8edcb3..a962cf2 100644 --- a/scripting-host/src/main/kotlin/RequestState.kt +++ b/scripting-host/src/main/kotlin/RequestState.kt @@ -47,6 +47,8 @@ import java.nio.file.Path as NioPath private val logger = KotlinLogging.logger {} +private val scriptingHost = BasicJvmScriptingHost() + class RequestState( record: FCGIRecord.BeginRequest, private val cache: Cache, @@ -242,7 +244,7 @@ class RequestState( } } - val result = BasicJvmScriptingHost().evalWithTemplate( + val result = scriptingHost.evalWithTemplate( script = scriptSource, evaluation = { refineConfigurationBeforeEvaluate { context -> diff --git a/settings.gradle.kts b/settings.gradle.kts index 8432ce2..cbaaf7d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,7 +14,15 @@ * limitations under the License. */ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + rootProject.name = "KotlinServerScripts" include("scripting-definition") include("scripting-host") +include("IdeaPlugin")