diff --git a/.gitignore b/.gitignore index ac787777e..36cb4e424 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ !**/src/main/** !**/src/test/** src/main/resources/application.properties +resource-server/src/main/resources/application.properties # compile artifacts .kotlin/ diff --git a/build.gradle.kts b/build.gradle.kts index 315e08a4d..ecdf5dcfa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,3 @@ -import org.gradle.kotlin.dsl.support.serviceOf import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -32,6 +31,7 @@ allprojects { maven("https://repo.spring.io/snapshot") maven("https://repo.spring.io/milestone") maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-ide") + maven("https://packages.jetbrains.team/maven/p/kt/dev/") maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev") maven("https://cache-redirector.jetbrains.com/jetbrains.bintray.com/intellij-third-party-dependencies") maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-ide-plugin-dependencies") @@ -41,9 +41,17 @@ allprojects { maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } - afterEvaluate { - dependencies { - dependencies { +} + +setOf( + rootProject, + project(":common"), + project(":executors"), + project(":indexation"), +).forEach { project -> + project.afterEvaluate { + project.dependencies { + project.dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2") implementation(libs.kotlin.idea) { isTransitive = false @@ -53,9 +61,32 @@ allprojects { } } -val resourceDependency: Configuration by configurations.creating { +val kotlinComposeWasmStdlibFile: Configuration by configurations.creating { + isTransitive = false + isCanBeResolved = true + isCanBeConsumed = false + attributes { + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.categoryComposeCache + ) + attribute( + CacheAttribute.cacheAttribute, + CacheAttribute.WASM + ) + } +} + +val composeWasmStaticResources: Configuration by configurations.creating { + isTransitive = false isCanBeResolved = true isCanBeConsumed = false + attributes { + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.categoryComposeWasmResources + ) + } } dependencies { @@ -84,35 +115,57 @@ dependencies { } testImplementation(libs.kotlinx.coroutines.test) - resourceDependency(libs.skiko.js.wasm.runtime) + kotlinComposeWasmStdlibFile(project(":cache-maker")) + composeWasmStaticResources(project(":resource-server")) } -fun buildPropertyFile() { - rootDir.resolve("src/main/resources/${propertyFile}").apply { - println("Generate properties into $absolutePath") - parentFile.mkdirs() - writeText(generateProperties()) +fun Project.generateProperties( + prefix: String = "", +): Map = mapOf( + "kotlin.version" to kotlinVersion, + "policy.file" to prefix + policy, + "indexes.file" to prefix + indexes, + "indexesJs.file" to prefix + indexesJs, + "indexesWasm.file" to prefix + indexesWasm, + "indexesComposeWasm.file" to prefix + indexesComposeWasm, + "libraries.folder.jvm" to prefix + libJVM, + "libraries.folder.js" to prefix + libJS, + "libraries.folder.wasm" to prefix + libWasm, + "libraries.folder.compose-wasm" to prefix + libComposeWasm, + "libraries.folder.compose-wasm-compiler-plugins" to prefix + libComposeWasmCompilerPlugins, + "libraries.folder.compiler-plugins" to prefix + compilerPluginsForJVM, + "spring.mvc.pathmatch.matching-strategy" to "ant_path_matcher", + "server.compression.enabled" to "true", + "server.compression.mime-types" to "application/json,text/javascript,application/wasm", + "skiko.version" to libs.versions.skiko.get(), +) + +val propertiesGenerator by tasks.registering(PropertiesGenerator::class) { + dependsOn(kotlinComposeWasmStdlibFile) + propertiesFile.fileValue(rootDir.resolve("src/main/resources/${propertyFile}")) + hashableFile.fileProvider( + provider { + kotlinComposeWasmStdlibFile.singleFile + } + ) + generateProperties().forEach { (name, value) -> + propertiesMap.put(name, value) } } -fun generateProperties(prefix: String = "") = """ - # this file is autogenerated by build.gradle.kts - kotlin.version=${kotlinVersion} - policy.file=${prefix + policy} - indexes.file=${prefix + indexes} - indexesJs.file=${prefix + indexesJs} - indexesWasm.file=${prefix + indexesWasm} - indexesComposeWasm.file=${prefix + indexesComposeWasm} - libraries.folder.jvm=${prefix + libJVM} - libraries.folder.js=${prefix + libJS} - libraries.folder.wasm=${prefix + libWasm} - libraries.folder.compose-wasm=${prefix + libComposeWasm} - libraries.folder.compose-wasm-compiler-plugins=${prefix + libComposeWasmCompilerPlugins} - libraries.folder.compiler-plugins=${prefix + compilerPluginsForJVM} - spring.mvc.pathmatch.matching-strategy=ant_path_matcher - server.compression.enabled=true - server.compression.mime-types=application/json,text/javascript,application/wasm -""".trimIndent() +val lambdaPropertiesGenerator by tasks.registering(PropertiesGenerator::class) { + dependsOn(kotlinComposeWasmStdlibFile) + propertiesFile.set(layout.buildDirectory.file("tmp/propertiesGenerator/${propertyFile}")) + hashableFile.fileProvider( + provider { + kotlinComposeWasmStdlibFile.singleFile + } + ) + + generateProperties(lambdaPrefix).forEach { (name, value) -> + propertiesMap.put(name, value) + } +} tasks.withType { compilerOptions { @@ -120,7 +173,7 @@ tasks.withType { } dependsOn(":executors:jar") dependsOn(":indexation:run") - buildPropertyFile() + dependsOn(propertiesGenerator) } println("Using Kotlin compiler ${libs.versions.kotlin.get()}") @@ -129,18 +182,19 @@ tasks.withType { requiresUnpack("**/kotlinx-*.jar") } +val prepareComposeWasmResources by tasks.registering(Sync::class) { + from(composeWasmStaticResources) + into(layout.buildDirectory.dir("compose-wasm-resources")) +} + val buildLambda by tasks.creating(Zip::class) { val propertyFile = propertyFile - val propertyFileContent = generateProperties("/var/task/") from(tasks.compileKotlin) from(tasks.processResources) { - eachFile { - if (name == propertyFile) { - file.writeText(propertyFileContent) - } - } + exclude(propertyFile) } + from(lambdaPropertiesGenerator) from(policy) from(indexes) from(indexesJs) @@ -150,20 +204,18 @@ val buildLambda by tasks.creating(Zip::class) { from(libWasmFolder) { into(libWasm) } from(libComposeWasmFolder) { into(libComposeWasm) } from(libJVMFolder) { into(libJVM) } - from(compilerPluginsForJVMFolder) {into(compilerPluginsForJVM)} + from(compilerPluginsForJVMFolder) { into(compilerPluginsForJVM) } from(libComposeWasmCompilerPluginsFolder) { into(libComposeWasmCompilerPlugins) } + dependsOn(kotlinComposeWasmStdlibFile) into("lib") { from(configurations.compileClasspath) { exclude("tomcat-embed-*") } } + + dependsOn(prepareComposeWasmResources) } tasks.named("processResources") { - val archiveOperation = project.serviceOf() - from(resourceDependency.map { - archiveOperation.zipTree(it) - }) { - into("com/compiler/server") - } + dependsOn(propertiesGenerator) } tasks.withType { diff --git a/buildSrc/src/main/kotlin/CacheAttribute.kt b/buildSrc/src/main/kotlin/CacheAttribute.kt new file mode 100644 index 000000000..f78422bc4 --- /dev/null +++ b/buildSrc/src/main/kotlin/CacheAttribute.kt @@ -0,0 +1,10 @@ +import org.gradle.api.attributes.Attribute + +enum class CacheAttribute { + FULL, + WASM; + + companion object { + val cacheAttribute = Attribute.of("org.jetbrains.kotlin-compiler-server.cache", CacheAttribute::class.java) + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/CategoryAttribute.kt b/buildSrc/src/main/kotlin/CategoryAttribute.kt new file mode 100644 index 000000000..e4f1847eb --- /dev/null +++ b/buildSrc/src/main/kotlin/CategoryAttribute.kt @@ -0,0 +1,8 @@ +import org.gradle.api.attributes.Category +import org.gradle.api.model.ObjectFactory + +val ObjectFactory.categoryComposeCache + get() = named(Category::class.java, "compose-cache") + +val ObjectFactory.categoryComposeWasmResources + get() = named(Category::class.java, "compose-wasm-resources") \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/PropertiesUpdater.kt b/buildSrc/src/main/kotlin/PropertiesUpdater.kt new file mode 100644 index 000000000..b93405e8a --- /dev/null +++ b/buildSrc/src/main/kotlin/PropertiesUpdater.kt @@ -0,0 +1,57 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.FileInputStream +import java.security.MessageDigest + +abstract class PropertiesGenerator : DefaultTask() { + + @get:InputFile + abstract val hashableFile: RegularFileProperty + + @get:Input + abstract val propertiesMap: MapProperty + + @get:OutputFile + abstract val propertiesFile: RegularFileProperty + + @TaskAction + fun updateProperties() { + val file = propertiesFile.get().asFile + + propertiesMap.get().let { + if (it.isNotEmpty()) { + file.writeText("") + it.forEach { (key, value) -> + file.appendText("$key=$value\n") + } + } + } + + file.appendText( + "\ndependencies.compose.wasm=${hashFileContent(hashableFile.get().asFile.absolutePath)}" + ) + } +} + +fun hashFileContent(filePath: String, hashAlgorithm: String = "SHA-256"): String { + val file = File(filePath) + val digest = MessageDigest.getInstance(hashAlgorithm) + + // Read the file content in chunks and update the digest + FileInputStream(file).use { fileInputStream -> + val buffer = ByteArray(1024) + var bytesRead: Int + while (fileInputStream.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + } + + // Convert the resulting byte array to a readable hex string + return digest.digest().joinToString("") { "%02x".format(it) } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/lambda.kt b/buildSrc/src/main/kotlin/lambda.kt new file mode 100644 index 000000000..fbb233586 --- /dev/null +++ b/buildSrc/src/main/kotlin/lambda.kt @@ -0,0 +1 @@ +val lambdaPrefix = "/var/task/" \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/properties.kt b/buildSrc/src/main/kotlin/properties.kt index 36e5a2539..3dbab4598 100644 --- a/buildSrc/src/main/kotlin/properties.kt +++ b/buildSrc/src/main/kotlin/properties.kt @@ -1,6 +1,6 @@ +import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.api.Project import org.gradle.kotlin.dsl.provideDelegate -import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.kotlin.dsl.the val indexes: String by System.getProperties() diff --git a/cache-maker/build.gradle.kts b/cache-maker/build.gradle.kts new file mode 100644 index 000000000..950b1c4d7 --- /dev/null +++ b/cache-maker/build.gradle.kts @@ -0,0 +1,97 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.targets.js.binaryen.BinaryenExec +import org.jetbrains.kotlin.gradle.targets.js.binaryen.BinaryenRootEnvSpec +import org.jetbrains.kotlin.gradle.targets.js.ir.JsIrBinary +import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrLink +import org.jetbrains.kotlin.gradle.targets.js.ir.WasmBinary + +plugins { + kotlin("multiplatform") +} + +kotlin { + wasmJs { + outputModuleName.set("stdlib") + binaries.executable().forEach { binary: JsIrBinary -> + binary.linkTask.configure { + compilerOptions.freeCompilerArgs.add("-Xir-dce=false") + compilerOptions.freeCompilerArgs.add("-Xwasm-multimodule-mode=master") + } + + (binary as WasmBinary).optimizeTask.configure { + inputFileProperty.fileProvider( + binary.mainFile.map { + val file = it.asFile + file.resolveSibling("${file.nameWithoutExtension}_master.wasm") + } + ) + } + } + } + + sourceSets { + wasmJsMain { + dependencies { + implementation(libs.bundles.compose) + implementation(libs.kotlinx.coroutines.core.compose.wasm) + } + } + } +} + +val compileProductionExecutableKotlinWasmJs: TaskProvider by tasks.existing(KotlinJsIrLink::class) { +} + +val composeWasmStdlib: Provider = compileProductionExecutableKotlinWasmJs + .flatMap { it.destinationDirectory.locationOnly } +val composeWasmStdlibFile: Provider = composeWasmStdlib + .map { it.file("stdlib_master.wasm") } + +rootProject.plugins.withType { + rootProject.the().version = "122" +} + +val kotlinComposeWasmStdlibFile: Configuration by configurations.creating { + isTransitive = false + isCanBeResolved = false + isCanBeConsumed = true + attributes { + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.categoryComposeCache + ) + } +} + +kotlinComposeWasmStdlibFile.outgoing.variants.create("stdlib") { + attributes { + attribute( + CacheAttribute.cacheAttribute, + CacheAttribute.FULL + ) + } + + artifact(composeWasmStdlib) { + builtBy(compileProductionExecutableKotlinWasmJs) + } +} + +kotlinComposeWasmStdlibFile.outgoing.variants.create("wasm-file") { + attributes { + attribute( + CacheAttribute.cacheAttribute, + CacheAttribute.WASM + ) + } + + artifact(composeWasmStdlibFile) { + builtBy(compileProductionExecutableKotlinWasmJs) + } +} + +// we don't need to build cache-maker +tasks.named("build") { + dependsOn.clear() +} diff --git a/cache-maker/src/wasmJsMain/kotlin/main.kt b/cache-maker/src/wasmJsMain/kotlin/main.kt new file mode 100644 index 000000000..3ea082c28 --- /dev/null +++ b/cache-maker/src/wasmJsMain/kotlin/main.kt @@ -0,0 +1,63 @@ +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.CanvasBasedWindow +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +//sampleStart +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + CanvasBasedWindow { App() } +} + +@Composable +fun App() { + MaterialTheme { + var greetingText by remember { mutableStateOf("Hello World!") } + var showImage by remember { mutableStateOf(false) } + var counter by remember { mutableStateOf(0) } + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = { + counter++ + greetingText = "Compose: ${Greeting().greet()}" + showImage = !showImage + }) { + Text(greetingText) + } + AnimatedVisibility(showImage) { + Text(counter.toString()) + } + } + } +} + +private val platform = object : Platform { + + override val name: String + get() = "Web with Kotlin/Wasm" +} + +fun getPlatform(): Platform = platform + +class Greeting { + private val platform = getPlatform() + + fun greet(): String { + return "Hello, ${platform.name}!" + } +} + +interface Platform { + val name: String +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/compiler/server/common/components/CliUtils.kt b/common/src/main/kotlin/com/compiler/server/common/components/CliUtils.kt new file mode 100644 index 000000000..00db05bf2 --- /dev/null +++ b/common/src/main/kotlin/com/compiler/server/common/components/CliUtils.kt @@ -0,0 +1,83 @@ +package com.compiler.server.common.components + +import java.io.File +import java.nio.file.Path +import java.util.* +import kotlin.io.path.* + +@OptIn(ExperimentalPathApi::class) +fun usingTempDirectory(action: (path: Path) -> T): T { + val path = getTempDirectory() + path.createDirectories() + return try { + action(path) + } finally { + path.deleteRecursively() + } +} + +private fun getTempDirectory(): Path { + val dir = System.getProperty("java.io.tmpdir") + val sessionId = UUID.randomUUID().toString().replace("-", "") + return File(dir).canonicalFile.resolve(sessionId).toPath() +} + +fun compileWasmArgs( + moduleName: String, + filePaths: List, + klibPath: String, + compilerPlugins: List, + compilerPluginOptions: List, + dependencies: List, +): List { + val compilerPluginsArgs: List = compilerPlugins + .takeIf { it.isNotEmpty() } + ?.let { plugins -> + plugins.map { + "-Xplugin=$it" + } + compilerPluginOptions.map { + "-P=$it" + } + } ?: emptyList() + val additionalCompilerArgumentsForKLib: List = mutableListOf( + "-Xreport-all-warnings", + "-Wextra", + "-Xwasm", + "-Xir-produce-klib-dir", + "-libraries=${dependencies.joinToString(PATH_SEPARATOR)}", + "-ir-output-dir=$klibPath", + "-ir-output-name=$moduleName", + ) + compilerPluginsArgs + + return filePaths + additionalCompilerArgumentsForKLib +} + +fun linkWasmArgs( + moduleName: String, + klibPath: String, + dependencies: List, + multiModule: Boolean, + outputDir: Path, + debugInfo: Boolean, +): List { + return mutableListOf( + "-Xreport-all-warnings", + "-Wextra", + "-Xwasm", + "-Xir-produce-js", + "-Xinclude=$klibPath", + "-libraries=${dependencies.joinToString(PATH_SEPARATOR)}", + "-ir-output-dir=${(outputDir / "wasm").toFile().canonicalPath}", + "-ir-output-name=$moduleName", + ).also { + if (debugInfo) it.add("-Xwasm-generate-wat") + + if (multiModule) { + it.add("-Xwasm-multimodule-mode=slave") + } else { + it.add("-Xir-dce") + } + } +} + +val PATH_SEPARATOR: String = File.pathSeparator diff --git a/common/src/main/kotlin/component/KotlinEnvironment.kt b/common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironment.kt similarity index 96% rename from common/src/main/kotlin/component/KotlinEnvironment.kt rename to common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironment.kt index b3a995b2a..685157edf 100644 --- a/common/src/main/kotlin/component/KotlinEnvironment.kt +++ b/common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironment.kt @@ -1,6 +1,7 @@ -package component +package com.compiler.server.common.components import com.intellij.openapi.util.Disposer +import component.CompilerPluginOption import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments import org.jetbrains.kotlin.cli.common.messages.MessageCollector @@ -85,6 +86,8 @@ class KotlinEnvironment( return f(environment) } + val disposable = Disposer.newDisposable() + private val configuration = createConfiguration() val jsConfiguration: CompilerConfiguration = configuration.copy().apply { put(CommonConfigurationKeys.MODULE_NAME, "playground") @@ -109,12 +112,13 @@ class KotlinEnvironment( COMPOSE_WASM_COMPILER_PLUGINS, composeWasmCompilerPluginOptions, emptyList(), - this + this, + disposable ) } private val environment = KotlinCoreEnvironment.createForProduction( - projectDisposable = Disposer.newDisposable(), + projectDisposable = disposable, configuration = configuration.copy(), configFiles = EnvironmentConfigFiles.JVM_CONFIG_FILES ) diff --git a/indexation/src/main/kotlin/KotlinEnvironmentConfiguration.kt b/common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironmentConfiguration.kt similarity index 85% rename from indexation/src/main/kotlin/KotlinEnvironmentConfiguration.kt rename to common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironmentConfiguration.kt index 14ce88197..b93fb7c9d 100644 --- a/indexation/src/main/kotlin/KotlinEnvironmentConfiguration.kt +++ b/common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironmentConfiguration.kt @@ -1,7 +1,6 @@ -package indexation +package com.compiler.server.common.components import component.CompilerPluginOption -import component.KotlinEnvironment import java.io.File class KotlinEnvironmentConfiguration( @@ -39,12 +38,7 @@ class KotlinEnvironmentConfiguration( "generateDecoys", "false" ), - CompilerPluginOption( - "androidx.compose.compiler.plugins.kotlin", - "suppressKotlinVersionCompatibilityCheck", - version - ), - ) + ), ) } } diff --git a/dependencies/build.gradle.kts b/dependencies/build.gradle.kts index 5e9358ece..7bde4e2ac 100644 --- a/dependencies/build.gradle.kts +++ b/dependencies/build.gradle.kts @@ -122,6 +122,7 @@ dependencies { // compose kotlinComposeWasmDependency(libs.kotlin.stdlib.wasm.js) kotlinComposeWasmDependency(libs.bundles.compose) + kotlinComposeWasmDependency(libs.kotlinx.coroutines.core.compose.wasm) composeWasmCompilerPlugins(libs.kotlin.compose.compiler.plugin) } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 116440fc1..861f7a8de 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,4 +4,5 @@ systemProp.indexesJs=indexesJs.json systemProp.indexesWasm=indexesWasm.json systemProp.indexesComposeWasm=indexesComposeWasm.json -org.gradle.configuration-cache=true \ No newline at end of file +org.gradle.configuration-cache=true +org.gradle.jvmargs=-Xmx6048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx6048M" \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac9ca3a1f..9da20cca2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "2.1.0" +kotlin = "2.2.0-dev-8507" kotlinIdeVersion = "1.9.20-506" kotlinIdeVersionWithSuffix = "231-1.9.20-506-IJ8109.175" spring-boot = "2.7.10" @@ -10,6 +10,7 @@ junit = "4.13.2" logstash-logback-encoder = "7.3" trove4j = "1.0.20221201" kotlinx-coroutines = "1.7.3" +kotlinx-coroutines-compose-wasm = "1.8.1" kotlinx-coroutines-test = "1.6.4" kotlinx-datetime = "0.6.0-RC.2" kotlinx-io = "0.6.0" @@ -39,6 +40,7 @@ kotlin-compiler-ide = { group = "org.jetbrains.kotlin", name = "kotlin-compiler- kotlin-idea = { group = "org.jetbrains.kotlin", name = "idea", version.ref = "kotlinIdeVersionWithSuffix" } kotlin-core = { group = "org.jetbrains.kotlin", name = "core", version.ref = "kotlinIdeVersionWithSuffix" } kotlinx-coroutines-core-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-jvm", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-core-compose-wasm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-wasm-js", version.ref = "kotlinx-coroutines-compose-wasm" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" } kotlinx-io-bytestring = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-bytestring", version.ref = "kotlinx-io" } diff --git a/indexation/src/main/kotlin/JvmIndexationBuilder.kt b/indexation/src/main/kotlin/JvmIndexationBuilder.kt index 153a89aca..dc603946d 100644 --- a/indexation/src/main/kotlin/JvmIndexationBuilder.kt +++ b/indexation/src/main/kotlin/JvmIndexationBuilder.kt @@ -1,7 +1,7 @@ package indexation +import com.compiler.server.common.components.KotlinEnvironment import model.ImportInfo -import component.KotlinEnvironment import org.jetbrains.kotlin.cli.jvm.compiler.CliBindingTrace import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM import org.jetbrains.kotlin.container.getService diff --git a/indexation/src/main/kotlin/Main.kt b/indexation/src/main/kotlin/Main.kt index 3e3efa90d..f0bb1d7f5 100644 --- a/indexation/src/main/kotlin/Main.kt +++ b/indexation/src/main/kotlin/Main.kt @@ -1,5 +1,7 @@ package indexation +import com.compiler.server.common.components.KotlinEnvironmentConfiguration + /** * First argument is path to folder with jars * Second argument is path to output file for jvm indexes diff --git a/indexation/src/main/kotlin/WebIndexationBuilder.kt b/indexation/src/main/kotlin/WebIndexationBuilder.kt index a0b409cf1..064658c23 100644 --- a/indexation/src/main/kotlin/WebIndexationBuilder.kt +++ b/indexation/src/main/kotlin/WebIndexationBuilder.kt @@ -1,7 +1,7 @@ package indexation +import com.compiler.server.common.components.KotlinEnvironment import model.ImportInfo -import component.KotlinEnvironment import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport import org.jetbrains.kotlin.cli.jvm.plugins.PluginCliParser import org.jetbrains.kotlin.config.CompilerConfiguration @@ -29,7 +29,8 @@ class WebIndexationBuilder( compilerPlugins, compilerPluginOptions, emptyList(), - configuration + configuration, + kotlinEnvironment.disposable ) } val sourceModule = prepareAnalyzedSourceModule( diff --git a/resource-server/build.gradle.kts b/resource-server/build.gradle.kts new file mode 100644 index 000000000..f7b4df08e --- /dev/null +++ b/resource-server/build.gradle.kts @@ -0,0 +1,155 @@ +import org.gradle.kotlin.dsl.support.serviceOf +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.io.FileInputStream +import java.util.* + +plugins { + alias(libs.plugins.spring.dependency.management) + alias(libs.plugins.spring.boot) + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.plugin.spring) +} + +kotlin.jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + vendor.set(JvmVendorSpec.AMAZON) +} + +val resourceDependency: Configuration by configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false +} + +val kotlinComposeWasmStdlibFile: Configuration by configurations.creating { + isTransitive = false + isCanBeResolved = true + isCanBeConsumed = false + attributes { + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.categoryComposeCache + ) + attribute( + CacheAttribute.cacheAttribute, + CacheAttribute.WASM + ) + } +} + +val kotlinComposeWasmStdlib: Configuration by configurations.creating { + isTransitive = false + isCanBeResolved = true + isCanBeConsumed = false + attributes { + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.categoryComposeCache + ) + attribute( + CacheAttribute.cacheAttribute, + CacheAttribute.FULL + ) + } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude(group = "org.junit.vintage", module = "junit-vintage-engine") + } + + resourceDependency(libs.skiko.js.wasm.runtime) + kotlinComposeWasmStdlib(project(":cache-maker")) + kotlinComposeWasmStdlibFile(project(":cache-maker")) +} + +val propertiesGenerator by tasks.registering(PropertiesGenerator::class) { + dependsOn(kotlinComposeWasmStdlibFile) + propertiesMap.put("spring.mvc.pathmatch.matching-strategy", "ant_path_matcher") + propertiesMap.put("server.port", "8081") + propertiesMap.put("skiko.version", libs.versions.skiko.get()) + + val applicationPropertiesPath = projectDir.resolve("src/main/resources/application.properties") + + propertiesFile.fileValue(applicationPropertiesPath) + + val composeWasmStdlibFile: FileCollection = kotlinComposeWasmStdlibFile + + hashableFile.fileProvider( + provider { + composeWasmStdlibFile.singleFile + } + ) +} + +tasks.withType { + dependsOn(kotlinComposeWasmStdlibFile) + dependsOn(propertiesGenerator) +} + +val skikoVersion = libs.versions.skiko + +val prepareComposeWasmResources by tasks.registering(Sync::class) { + dependsOn(kotlinComposeWasmStdlibFile) + dependsOn(propertiesGenerator) + val archiveOperation = project.serviceOf() + + into(layout.buildDirectory.dir("tmp/prepareResources")) + + from(resourceDependency.map { + archiveOperation.zipTree(it) + }) { + rename("skiko\\.(.*)", "skiko-${skikoVersion.get()}.\$1") + include("skiko.mjs", "skiko.wasm") + } + + val propertiesFile = propertiesGenerator.flatMap { it.propertiesFile } + + from(kotlinComposeWasmStdlib) { + include("stdlib_master.uninstantiated.mjs", "stdlib_master.wasm") + + rename { original -> + val properties = FileInputStream(propertiesFile.get().asFile).use { + Properties().apply { + load(it) + } + } + val regex = Regex("stdlib_master(\\.uninstantiated)*\\.(.*)") + regex.find(original)?.groupValues?.get(2)?.let { extension -> + "stdlib-${properties["dependencies.compose.wasm"]}.$extension" + } ?: original + + } + } +} + +tasks.named("processResources") { + dependsOn(prepareComposeWasmResources) + from(prepareComposeWasmResources) { + into("com/compiler/server") + } +} + +tasks.withType { + useJUnitPlatform() + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(17)) + vendor.set(JvmVendorSpec.AMAZON) + }) +} + +val composeWasmStaticResources: Configuration by configurations.creating { + isTransitive = false + isCanBeResolved = false + isCanBeConsumed = true + attributes { + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.categoryComposeWasmResources + ) + } + + outgoing.artifact(prepareComposeWasmResources) { + builtBy(prepareComposeWasmResources) + } +} \ No newline at end of file diff --git a/resource-server/src/main/kotlin/com/compiler/server/ResourceApplication.kt b/resource-server/src/main/kotlin/com/compiler/server/ResourceApplication.kt new file mode 100644 index 000000000..55362542a --- /dev/null +++ b/resource-server/src/main/kotlin/com/compiler/server/ResourceApplication.kt @@ -0,0 +1,11 @@ +package com.compiler.server + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class ResourceApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/resource-server/src/main/kotlin/com/compiler/server/configuration/CorsConfiguration.kt b/resource-server/src/main/kotlin/com/compiler/server/configuration/CorsConfiguration.kt new file mode 100644 index 000000000..237ad05f5 --- /dev/null +++ b/resource-server/src/main/kotlin/com/compiler/server/configuration/CorsConfiguration.kt @@ -0,0 +1,26 @@ +package com.compiler.server.configuration + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.UrlBasedCorsConfigurationSource +import org.springframework.web.filter.CorsFilter + +val ACCESS_CONTROL_ALLOW_ORIGIN_VALUE: String = System.getenv("ACCESS_CONTROL_ALLOW_ORIGIN_VALUE") ?: "*" +val ACCESS_CONTROL_ALLOW_HEADER_VALUE: String = System.getenv("ACCESS_CONTROL_ALLOW_HEADER_VALUE") ?: "*" + +@Configuration +class CorsConfiguration { + @Bean + fun corsFilter(): CorsFilter { + val source = UrlBasedCorsConfigurationSource() + val config = CorsConfiguration().apply { + addAllowedHeader(ACCESS_CONTROL_ALLOW_HEADER_VALUE) + addAllowedOrigin(ACCESS_CONTROL_ALLOW_ORIGIN_VALUE) + addAllowedMethod("GET") + addAllowedMethod("POST") + } + source.registerCorsConfiguration("/**", config) + return CorsFilter(source) + } +} diff --git a/resource-server/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt b/resource-server/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt new file mode 100644 index 000000000..5f6a454d3 --- /dev/null +++ b/resource-server/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt @@ -0,0 +1,64 @@ +package com.compiler.server.controllers + +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.io.FileSystemResource +import org.springframework.core.io.Resource +import org.springframework.http.* +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.concurrent.TimeUnit + + +@RestController +@RequestMapping(value = ["/api/resource", "/api/**/resource"]) +class ResourceRestController( + @Value("\${skiko.version}") private val skikoVersion: String, + @Value("\${dependencies.compose.wasm}") private val dependenciesComposeWasm: String, +) { + @Suppress("unused") + @GetMapping("/skiko-{version}.mjs") + fun getSkikoMjs(@PathVariable version: String): ResponseEntity { + return cacheableResource("/com/compiler/server/skiko-$version.mjs", MediaType("text", "javascript")) + } + + @Suppress("unused") + @GetMapping("/skiko-{version}.wasm") + fun getSkikoWasm(@PathVariable version: String): ResponseEntity { + return cacheableResource("/com/compiler/server/skiko-$version.wasm", MediaType("application", "wasm")) + } + + @GetMapping("/stdlib-{hash}.mjs") + fun getStdlibMjs(@PathVariable hash: String): ResponseEntity { + return cacheableResource("/com/compiler/server/stdlib-$hash.mjs", MediaType("text", "javascript")) + } + + @GetMapping("/stdlib-{hash}.wasm") + fun getStdlibWasm(@PathVariable hash: String): ResponseEntity { + return cacheableResource("/com/compiler/server/stdlib-$hash.wasm", MediaType("application", "wasm")) + } + + private fun cacheableResource(path: String, mediaType: MediaType): ResponseEntity { + return resource(path, mediaType) { + cacheControl = CacheControl.maxAge(365, TimeUnit.DAYS).headerValue + } + } + + private fun resource( + path: String, + mediaType: MediaType, + headers: HttpHeaders.() -> Unit = {}, + ): ResponseEntity { + val resourcePath = javaClass.getResource(path)?.path + ?: return ResponseEntity.internalServerError().build() + + val resource = FileSystemResource(resourcePath) + val headers = HttpHeaders().apply { + contentType = mediaType + headers() + } + + return ResponseEntity(resource, headers, HttpStatus.OK) + } +} diff --git a/resource-server/src/test/kotlin/com/compiler/server/SkikoResourceTest.kt b/resource-server/src/test/kotlin/com/compiler/server/SkikoResourceTest.kt new file mode 100644 index 000000000..cde6a5e7d --- /dev/null +++ b/resource-server/src/test/kotlin/com/compiler/server/SkikoResourceTest.kt @@ -0,0 +1,83 @@ +package com.compiler.server + +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.HttpHeaders +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import java.util.concurrent.TimeUnit + +@SpringBootTest +@AutoConfigureMockMvc +class SkikoResourceTest { + @Autowired + private lateinit var mockMvc: MockMvc + + @Value("\${skiko.version}") + private lateinit var skikoVersion: String + + @Value("\${dependencies.compose.wasm}") + private lateinit var stdlibHash: String + + @Test + fun `test caching headers for skiko mjs resource`() { + testCachingHeadersForResource( + "/api/resource/skiko-$skikoVersion.mjs", + "text/javascript" + ) + } + + @Test + fun `test caching headers for skiko wasm resource`() { + testCachingHeadersForResource( + "/api/resource/skiko-$skikoVersion.wasm", + "application/wasm" + ) + } + + @Test + fun `test caching headers for stdlib mjs resource`() { + testCachingHeadersForResource( + "/api/resource/stdlib-$stdlibHash.mjs", + "text/javascript" + ) + } + + @Test + fun `test caching headers for stdlib wasm resource`() { + testCachingHeadersForResource( + "/api/resource/stdlib-$stdlibHash.wasm", + "application/wasm" + ) + } + + private fun testCachingHeadersForResource( + resourceUrl: String, + contentType: String + ) { + val expectedCacheControl = "max-age=${TimeUnit.DAYS.toSeconds(365)}" + + mockMvc + .perform(MockMvcRequestBuilders.get(resourceUrl)) + .andExpect(MockMvcResultMatchers.status().isOk) // HTTP 200 status + .andExpect( + MockMvcResultMatchers.header().exists(HttpHeaders.CACHE_CONTROL) + ) + .andExpect( + MockMvcResultMatchers.header().string( + HttpHeaders.CACHE_CONTROL, + expectedCacheControl + ) + ) + .andExpect( + MockMvcResultMatchers.header().string( + HttpHeaders.CONTENT_TYPE, + contentType + ) + ) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 60d3b651a..ceecfaf11 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,10 @@ pluginManagement { gradlePluginPortal() mavenCentral() maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap") + + maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev") + + maven("https://packages.jetbrains.team/maven/p/kt/dev/") } } plugins { @@ -16,4 +20,6 @@ plugins { include(":executors") include(":indexation") include(":common") -include(":dependencies") \ No newline at end of file +include(":dependencies") +include(":cache-maker") +include(":resource-server") \ No newline at end of file diff --git a/src/main/kotlin/com/compiler/server/compiler/components/CliUtils.kt b/src/main/kotlin/com/compiler/server/compiler/components/CliUtils.kt index 7edd8710f..6b3d7b042 100644 --- a/src/main/kotlin/com/compiler/server/compiler/components/CliUtils.kt +++ b/src/main/kotlin/com/compiler/server/compiler/components/CliUtils.kt @@ -12,10 +12,11 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.* import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation import org.jetbrains.kotlin.cli.common.messages.MessageRenderer import org.jetbrains.kotlin.psi.KtFile -import java.io.File import java.nio.file.Path -import java.util.* -import kotlin.io.path.* +import kotlin.io.path.Path +import kotlin.io.path.div +import kotlin.io.path.pathString +import kotlin.io.path.writeText private fun minusOne(value: Int) = if (value > 0) value - 1 else value @@ -111,23 +112,6 @@ fun CLICompiler<*>.tryCompilation(inputDirectory: Path, inputFiles: List usingTempDirectory(action: (path: Path) -> T): T { - val path = getTempDirectory() - path.createDirectories() - return try { - action(path) - } finally { - path.deleteRecursively() - } -} - -private fun getTempDirectory(): Path { - val dir = System.getProperty("java.io.tmpdir") - val sessionId = UUID.randomUUID().toString().replace("-", "") - return File(dir).canonicalFile.resolve(sessionId).toPath() -} - fun List.writeToIoFiles(inputDir: Path): List { val ioFiles = map { inputDir / it.name } for ((ioFile, ktFile) in ioFiles zip this) { @@ -135,5 +119,3 @@ fun List.writeToIoFiles(inputDir: Path): List { } return ioFiles } - -val PATH_SEPARATOR: String = File.pathSeparator diff --git a/src/main/kotlin/com/compiler/server/compiler/components/ErrorAnalyzer.kt b/src/main/kotlin/com/compiler/server/compiler/components/ErrorAnalyzer.kt index 8591b19e0..cae2b53ad 100644 --- a/src/main/kotlin/com/compiler/server/compiler/components/ErrorAnalyzer.kt +++ b/src/main/kotlin/com/compiler/server/compiler/components/ErrorAnalyzer.kt @@ -1,12 +1,12 @@ package com.compiler.server.compiler.components +import com.compiler.server.common.components.KotlinEnvironment import com.compiler.server.model.* import com.intellij.openapi.util.Pair import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiErrorElement import com.intellij.psi.PsiFile -import component.KotlinEnvironment import model.Completion import org.jetbrains.kotlin.analyzer.AnalysisResult import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport diff --git a/src/main/kotlin/com/compiler/server/compiler/components/KotlinCompiler.kt b/src/main/kotlin/com/compiler/server/compiler/components/KotlinCompiler.kt index 47c8e66b2..f2a76d0b3 100644 --- a/src/main/kotlin/com/compiler/server/compiler/components/KotlinCompiler.kt +++ b/src/main/kotlin/com/compiler/server/compiler/components/KotlinCompiler.kt @@ -1,12 +1,14 @@ package com.compiler.server.compiler.components +import com.compiler.server.common.components.KotlinEnvironment +import com.compiler.server.common.components.PATH_SEPARATOR +import com.compiler.server.common.components.usingTempDirectory import com.compiler.server.executor.CommandLineArgument import com.compiler.server.executor.JavaExecutor import com.compiler.server.model.JvmExecutionResult import com.compiler.server.model.OutputDirectory import com.compiler.server.model.bean.LibrariesFile import com.compiler.server.model.toExceptionDescriptor -import component.KotlinEnvironment import executors.JUnitExecutors import executors.JavaRunnerExecutor import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler diff --git a/src/main/kotlin/com/compiler/server/compiler/components/KotlinEnvironment.kt b/src/main/kotlin/com/compiler/server/compiler/components/KotlinEnvironment.kt index 4858bb6c7..5704082c2 100644 --- a/src/main/kotlin/com/compiler/server/compiler/components/KotlinEnvironment.kt +++ b/src/main/kotlin/com/compiler/server/compiler/components/KotlinEnvironment.kt @@ -1,15 +1,14 @@ package com.compiler.server.compiler.components +import com.compiler.server.common.components.KotlinEnvironment import com.compiler.server.model.bean.LibrariesFile -import com.compiler.server.model.bean.VersionInfo import component.CompilerPluginOption -import component.KotlinEnvironment import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class KotlinEnvironmentConfiguration( - val librariesFile: LibrariesFile + val librariesFile: LibrariesFile, ) { @Bean fun kotlinEnvironment(): KotlinEnvironment { @@ -39,7 +38,7 @@ class KotlinEnvironmentConfiguration( "generateDecoys", "false" ), - ) + ), ) } } diff --git a/src/main/kotlin/com/compiler/server/compiler/components/KotlinToJSTranslator.kt b/src/main/kotlin/com/compiler/server/compiler/components/KotlinToJSTranslator.kt index d90c983a7..dcaf39588 100644 --- a/src/main/kotlin/com/compiler/server/compiler/components/KotlinToJSTranslator.kt +++ b/src/main/kotlin/com/compiler/server/compiler/components/KotlinToJSTranslator.kt @@ -1,11 +1,14 @@ package com.compiler.server.compiler.components +import com.compiler.server.common.components.* import com.compiler.server.model.* import com.fasterxml.jackson.databind.ObjectMapper -import component.KotlinEnvironment import org.jetbrains.kotlin.cli.js.K2JSCompiler import org.jetbrains.kotlin.psi.KtFile +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service +import java.io.File +import java.nio.file.Path import kotlin.io.path.div import kotlin.io.path.readBytes import kotlin.io.path.readText @@ -14,6 +17,8 @@ import kotlin.io.path.readText class KotlinToJSTranslator( private val kotlinEnvironment: KotlinEnvironment, ) { + private val log = LoggerFactory.getLogger(KotlinToJSTranslator::class.java) + companion object { private const val JS_IR_CODE_BUFFER = "playground.output?.buffer_1;\n" @@ -46,29 +51,38 @@ class KotlinToJSTranslator( fun translateWasm( files: List, debugInfo: Boolean, + multiModule: Boolean, projectType: ProjectType, - translate: (List, List, List, List, Boolean) -> CompilationResult + translate: ( + List, + List, + List, + List, + Boolean, + Boolean, + ) -> CompilationResult ): TranslationResultWithJsCode { return try { - val (dependencies, compilerPlugins, compilerPluginOptions) = when (projectType) { - ProjectType.WASM -> listOf( + val parameters: WasmParameters = when (projectType) { + ProjectType.WASM -> WasmParameters( kotlinEnvironment.WASM_LIBRARIES, emptyList(), - emptyList() + emptyList(), ) - ProjectType.COMPOSE_WASM -> listOf( + ProjectType.COMPOSE_WASM -> WasmParameters( kotlinEnvironment.COMPOSE_WASM_LIBRARIES, kotlinEnvironment.COMPOSE_WASM_COMPILER_PLUGINS, - kotlinEnvironment.composeWasmCompilerPluginOptions + kotlinEnvironment.composeWasmCompilerPluginOptions, ) else -> throw IllegalStateException("Wasm should have wasm or compose-wasm project type") } val compilationResult = translate( files, - dependencies, - compilerPlugins, - compilerPluginOptions, - debugInfo + parameters.dependencies, + parameters.plugins, + parameters.pluginOptions, + multiModule, + debugInfo, ) val wasmCompilationOutput = when (compilationResult) { is Compiled -> compilationResult.result @@ -137,6 +151,7 @@ class KotlinToJSTranslator( dependencies: List, compilerPlugins: List, compilerPluginOptions: List, + multiModule: Boolean, debugInfo: Boolean, ): CompilationResult = usingTempDirectory { inputDir -> @@ -146,38 +161,31 @@ class KotlinToJSTranslator( val k2JSCompiler = K2JSCompiler() val filePaths = ioFiles.map { it.toFile().canonicalPath } val klibPath = (outputDir / "klib").toFile().canonicalPath - val compilerPluginsArgs: List = compilerPlugins - .takeIf { it.isNotEmpty() } - ?.let { plugins -> - plugins.map { - "-Xplugin=$it" - } + compilerPluginOptions.map { - "-P=$it" - } - } ?: emptyList() - val additionalCompilerArgumentsForKLib: List = listOf( - "-Xreport-all-warnings", - "-Wextra", - "-Xwasm", - "-Xir-produce-klib-dir", - "-libraries=${dependencies.joinToString(PATH_SEPARATOR)}", - "-ir-output-dir=$klibPath", - "-ir-output-name=$moduleName", - ) + compilerPluginsArgs - k2JSCompiler.tryCompilation(inputDir, ioFiles, filePaths + additionalCompilerArgumentsForKLib) + k2JSCompiler.tryCompilation( + inputDir, + ioFiles, + compileWasmArgs( + moduleName, + filePaths, + klibPath, + compilerPlugins, + compilerPluginOptions, + dependencies, + ) + ) .flatMap { - k2JSCompiler.tryCompilation(inputDir, ioFiles, mutableListOf( - "-Xreport-all-warnings", - "-Wextra", - "-Xwasm", - "-Xir-produce-js", - "-Xir-dce", - "-Xinclude=$klibPath", - "-libraries=${dependencies.joinToString(PATH_SEPARATOR)}", - "-ir-output-dir=${(outputDir / "wasm").toFile().canonicalPath}", - "-ir-output-name=$moduleName", - ).also { if (debugInfo) it.add("-Xwasm-generate-wat") }) + k2JSCompiler.tryCompilation( + inputDir, ioFiles, + linkWasmArgs( + moduleName, + klibPath, + dependencies, + multiModule, + outputDir, + debugInfo, + ) + ) } .map { WasmTranslationSuccessfulOutput( @@ -209,4 +217,27 @@ data class WasmTranslationSuccessfulOutput( val jsInstantiated: String, val wasm: ByteArray, val wat: String?, -) +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as WasmTranslationSuccessfulOutput + + if (jsCode != other.jsCode) return false + if (jsInstantiated != other.jsInstantiated) return false + if (!wasm.contentEquals(other.wasm)) return false + if (wat != other.wat) return false + + return true + } + + override fun hashCode(): Int { + var result = jsCode.hashCode() + result = 31 * result + jsInstantiated.hashCode() + result = 31 * result + wasm.contentHashCode() + result = 31 * result + (wat?.hashCode() ?: 0) + return result + } + +} diff --git a/src/main/kotlin/com/compiler/server/compiler/components/WasmParameters.kt b/src/main/kotlin/com/compiler/server/compiler/components/WasmParameters.kt new file mode 100644 index 000000000..4c7f43610 --- /dev/null +++ b/src/main/kotlin/com/compiler/server/compiler/components/WasmParameters.kt @@ -0,0 +1,7 @@ +package com.compiler.server.compiler.components + +data class WasmParameters( + val dependencies: List, + val plugins: List, + val pluginOptions: List, +) \ No newline at end of file diff --git a/src/main/kotlin/com/compiler/server/configuration/ApplicationConfiguration.kt b/src/main/kotlin/com/compiler/server/configuration/ApplicationConfiguration.kt index 0a3426694..09c7ecaac 100644 --- a/src/main/kotlin/com/compiler/server/configuration/ApplicationConfiguration.kt +++ b/src/main/kotlin/com/compiler/server/configuration/ApplicationConfiguration.kt @@ -15,7 +15,7 @@ import java.io.File @EnableConfigurationProperties(value = [LibrariesFolderProperties::class]) class ApplicationConfiguration( @Value("\${kotlin.version}") private val version: String, - private val librariesFolderProperties: LibrariesFolderProperties + private val librariesFolderProperties: LibrariesFolderProperties, ) : WebMvcConfigurer { override fun addFormatters(registry: FormatterRegistry) { registry.addConverter(ProjectConverter()) diff --git a/src/main/kotlin/com/compiler/server/controllers/CompilerRestController.kt b/src/main/kotlin/com/compiler/server/controllers/CompilerRestController.kt index 92c219c54..422d66171 100644 --- a/src/main/kotlin/com/compiler/server/controllers/CompilerRestController.kt +++ b/src/main/kotlin/com/compiler/server/controllers/CompilerRestController.kt @@ -32,8 +32,16 @@ class CompilerRestController(private val kotlinProjectExecutor: KotlinProjectExe ): TranslationResultWithJsCode { return when (KotlinTranslatableCompiler.valueOf(compiler.uppercase().replace("-", "_"))) { KotlinTranslatableCompiler.JS -> kotlinProjectExecutor.convertToJsIr(project) - KotlinTranslatableCompiler.WASM -> kotlinProjectExecutor.convertToWasm(project, debugInfo) - KotlinTranslatableCompiler.COMPOSE_WASM -> kotlinProjectExecutor.convertToWasm(project, debugInfo) + KotlinTranslatableCompiler.WASM -> kotlinProjectExecutor.convertToWasm( + project, + debugInfo, + multiModule = false, + ) + KotlinTranslatableCompiler.COMPOSE_WASM -> kotlinProjectExecutor.convertToWasm( + project, + debugInfo, + multiModule = true, + ) } } diff --git a/src/main/kotlin/com/compiler/server/controllers/KotlinPlaygroundRestController.kt b/src/main/kotlin/com/compiler/server/controllers/KotlinPlaygroundRestController.kt index ffcc3d6f2..32e659e43 100644 --- a/src/main/kotlin/com/compiler/server/controllers/KotlinPlaygroundRestController.kt +++ b/src/main/kotlin/com/compiler/server/controllers/KotlinPlaygroundRestController.kt @@ -46,9 +46,15 @@ class KotlinPlaygroundRestController(private val kotlinProjectExecutor: KotlinPr kotlinProjectExecutor.convertToJsIr( project, ) - ProjectType.WASM, ProjectType.COMPOSE_WASM -> kotlinProjectExecutor.convertToWasm( + ProjectType.WASM -> kotlinProjectExecutor.convertToWasm( project, debugInfo = false, + multiModule = false, + ) + ProjectType.COMPOSE_WASM -> kotlinProjectExecutor.convertToWasm( + project, + debugInfo = false, + multiModule = true, ) ProjectType.JUNIT -> kotlinProjectExecutor.test(project, addByteCode) } diff --git a/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt b/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt index 93a2bc1fb..fcb609bc1 100644 --- a/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt +++ b/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt @@ -1,37 +1,23 @@ package com.compiler.server.controllers -import org.springframework.core.io.FileSystemResource -import org.springframework.core.io.Resource -import org.springframework.http.* +import org.springframework.beans.factory.annotation.Value import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -import java.util.concurrent.TimeUnit @RestController @RequestMapping(value = ["/api/resource", "/api/**/resource"]) -class ResourceRestController { - @GetMapping("/skiko.mjs") - fun getSkikoMjs(): ResponseEntity { - return cacheableResource("/com/compiler/server/skiko.mjs", MediaType("text", "javascript")) - } - - @GetMapping("/skiko.wasm") - fun getSkikoWasm(): ResponseEntity { - return cacheableResource("/com/compiler/server/skiko.wasm", MediaType("application", "wasm")) - } - - private fun cacheableResource(path: String, mediaType: MediaType): ResponseEntity { - val resourcePath = javaClass.getResource(path)?.path - ?: return ResponseEntity.internalServerError().build() - - val resource = FileSystemResource(resourcePath) - val headers = HttpHeaders().apply { - contentType = mediaType - cacheControl = CacheControl.maxAge(365, TimeUnit.DAYS).headerValue +class ResourceRestController( + @Value("\${skiko.version}") private val skikoVersion: String, + @Value("\${dependencies.compose.wasm}") private val dependenciesComposeWasm: String, +) { + @Suppress("unused") + @GetMapping("/compose-wasm-versions") + fun getVersions(): Map { + return mapOf( + "skiko" to skikoVersion, + "stdlib" to dependenciesComposeWasm + ) } - - return ResponseEntity(resource, headers, HttpStatus.OK) - } } diff --git a/src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt b/src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt index d0177c4ff..f5a5dbbc0 100644 --- a/src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt +++ b/src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt @@ -1,15 +1,16 @@ package com.compiler.server.service +import com.compiler.server.common.components.KotlinEnvironment import com.compiler.server.compiler.KotlinFile import com.compiler.server.compiler.components.* import com.compiler.server.model.* import com.compiler.server.model.bean.VersionInfo -import component.KotlinEnvironment import model.Completion import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.psi.KtFile import org.slf4j.LoggerFactory import org.springframework.stereotype.Component +import java.io.File @Component class KotlinProjectExecutor( @@ -48,8 +49,17 @@ class KotlinProjectExecutor( return kotlinCompiler.compile(files) } - fun convertToWasm(project: Project, debugInfo: Boolean): TranslationResultWithJsCode { - return convertWasmWithConverter(project, debugInfo, kotlinToJSTranslator::doTranslateWithWasm) + fun convertToWasm( + project: Project, + debugInfo: Boolean, + multiModule: Boolean, + ): TranslationResultWithJsCode { + return convertWasmWithConverter( + project, + debugInfo, + multiModule, + kotlinToJSTranslator::doTranslateWithWasm + ) } fun complete(project: Project, line: Int, character: Int): List { @@ -71,11 +81,16 @@ class KotlinProjectExecutor( convertToJsIr( project, ).compilerDiagnostics - ProjectType.WASM, ProjectType.COMPOSE_WASM -> - convertToWasm( - project, - debugInfo = false, - ).compilerDiagnostics + ProjectType.WASM -> convertToWasm( + project, + debugInfo = false, + multiModule = false, + ).compilerDiagnostics + ProjectType.COMPOSE_WASM -> convertToWasm( + project, + debugInfo = false, + multiModule = true, + ).compilerDiagnostics } } catch (e: Exception) { log.warn("Exception in getting highlight. Project: $project", e) @@ -101,13 +116,22 @@ class KotlinProjectExecutor( private fun convertWasmWithConverter( project: Project, debugInfo: Boolean, - converter: (List, List, List, List, Boolean) -> CompilationResult + multiModule: Boolean, + converter: ( + List, + List, + List, + List, + Boolean, + Boolean, + ) -> CompilationResult ): TranslationResultWithJsCode { return kotlinEnvironment.environment { environment -> val files = getFilesFrom(project, environment).map { it.kotlinFile } kotlinToJSTranslator.translateWasm( files, debugInfo, + multiModule, project.confType, converter ) diff --git a/src/test/kotlin/com/compiler/server/generator/TestProjectRunner.kt b/src/test/kotlin/com/compiler/server/generator/TestProjectRunner.kt index 16991de67..d9451a84c 100644 --- a/src/test/kotlin/com/compiler/server/generator/TestProjectRunner.kt +++ b/src/test/kotlin/com/compiler/server/generator/TestProjectRunner.kt @@ -188,6 +188,7 @@ class TestProjectRunner { val result = kotlinProjectExecutor.convertToWasm( project, debugInfo = true, + multiModule = false, ) if (result !is TranslationWasmResult) {