From ee1c7c0b0026ba3b52f26f17d6007f80d8460ffe Mon Sep 17 00:00:00 2001 From: Ilya Goncharov Date: Tue, 11 Nov 2025 13:27:34 +0100 Subject: [PATCH] Adopt Compose wasm multi module compilation. Use stable versions of compose --- build.gradle.kts | 26 +- buildSrc/src/main/kotlin/CacheAttribute.kt | 10 - buildSrc/src/main/kotlin/PropertiesUpdater.kt | 34 ++- cache-maker/build.gradle.kts | 282 +++++++++++++++--- cache-maker/src/wasmJsMain/kotlin/main.kt | 5 +- .../server/common/components/CliUtils.kt | 5 +- .../common/components/KotlinEnvironment.kt | 3 +- gradle/libs.versions.toml | 12 +- resource-server/build.gradle.kts | 119 +++++--- .../controllers/ResourceRestController.kt | 64 ---- .../com/compiler/server/SkikoResourceTest.kt | 83 ------ .../compiler/components/KotlinEnvironment.kt | 3 + .../components/KotlinToJSTranslator.kt | 28 +- .../configuration/ApplicationConfiguration.kt | 14 +- .../controllers/ResourceRestController.kt | 23 -- .../server/model/bean/Dependencies.kt | 5 + 16 files changed, 389 insertions(+), 327 deletions(-) delete mode 100644 buildSrc/src/main/kotlin/CacheAttribute.kt delete mode 100644 resource-server/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt delete mode 100644 resource-server/src/test/kotlin/com/compiler/server/SkikoResourceTest.kt delete mode 100644 src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt create mode 100644 src/main/kotlin/com/compiler/server/model/bean/Dependencies.kt diff --git a/build.gradle.kts b/build.gradle.kts index 430dc0796..3ee2fdb92 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -39,7 +39,7 @@ setOf( } } -val kotlinComposeWasmStdlibFile: Configuration by configurations.creating { +val kotlinComposeWasmRuntime: Configuration by configurations.creating { isTransitive = false isCanBeResolved = true isCanBeConsumed = false @@ -48,10 +48,6 @@ val kotlinComposeWasmStdlibFile: Configuration by configurations.creating { Category.CATEGORY_ATTRIBUTE, objects.categoryComposeCache ) - attribute( - CacheAttribute.cacheAttribute, - CacheAttribute.WASM - ) } } @@ -93,7 +89,7 @@ dependencies { } testImplementation(libs.kotlinx.coroutines.test) - kotlinComposeWasmStdlibFile(project(":cache-maker")) + kotlinComposeWasmRuntime(project(":cache-maker")) composeWasmStaticResources(project(":resource-server")) } @@ -121,12 +117,10 @@ fun Project.generateProperties( ) val propertiesGenerator by tasks.registering(PropertiesGenerator::class) { - dependsOn(kotlinComposeWasmStdlibFile) + dependsOn(kotlinComposeWasmRuntime) propertiesFile.fileValue(rootDir.resolve("src/main/resources/${propertyFile}")) - hashableFile.fileProvider( - provider { - kotlinComposeWasmStdlibFile.singleFile - } + hashableDir.from( + kotlinComposeWasmRuntime ) generateProperties().forEach { (name, value) -> propertiesMap.put(name, value) @@ -134,13 +128,9 @@ val propertiesGenerator by tasks.registering(PropertiesGenerator::class) { } val lambdaPropertiesGenerator by tasks.registering(PropertiesGenerator::class) { - dependsOn(kotlinComposeWasmStdlibFile) + dependsOn(kotlinComposeWasmRuntime) propertiesFile.set(layout.buildDirectory.file("tmp/propertiesGenerator/${propertyFile}")) - hashableFile.fileProvider( - provider { - kotlinComposeWasmStdlibFile.singleFile - } - ) + hashableDir.from(kotlinComposeWasmRuntime) generateProperties(lambdaPrefix).forEach { (name, value) -> propertiesMap.put(name, value) @@ -186,7 +176,7 @@ val buildLambda by tasks.creating(Zip::class) { from(libJVMFolder) { into(libJVM) } from(compilerPluginsForJVMFolder) { into(compilerPluginsForJVM) } from(libComposeWasmCompilerPluginsFolder) { into(libComposeWasmCompilerPlugins) } - dependsOn(kotlinComposeWasmStdlibFile) + dependsOn(kotlinComposeWasmRuntime) into("lib") { from(configurations.compileClasspath) { exclude("tomcat-embed-*") } } diff --git a/buildSrc/src/main/kotlin/CacheAttribute.kt b/buildSrc/src/main/kotlin/CacheAttribute.kt deleted file mode 100644 index f78422bc4..000000000 --- a/buildSrc/src/main/kotlin/CacheAttribute.kt +++ /dev/null @@ -1,10 +0,0 @@ -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/PropertiesUpdater.kt b/buildSrc/src/main/kotlin/PropertiesUpdater.kt index b93405e8a..90b4913b0 100644 --- a/buildSrc/src/main/kotlin/PropertiesUpdater.kt +++ b/buildSrc/src/main/kotlin/PropertiesUpdater.kt @@ -1,18 +1,17 @@ import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection 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 org.gradle.api.tasks.* import java.io.File import java.io.FileInputStream import java.security.MessageDigest abstract class PropertiesGenerator : DefaultTask() { - @get:InputFile - abstract val hashableFile: RegularFileProperty + @get:InputFiles + @get:Classpath + abstract val hashableDir: ConfigurableFileCollection @get:Input abstract val propertiesMap: MapProperty @@ -34,23 +33,26 @@ abstract class PropertiesGenerator : DefaultTask() { } file.appendText( - "\ndependencies.compose.wasm=${hashFileContent(hashableFile.get().asFile.absolutePath)}" + "\ndependencies.compose-wasm=${hashFileContent(hashableDir.singleFile)}" ) } } -fun hashFileContent(filePath: String, hashAlgorithm: String = "SHA-256"): String { - val file = File(filePath) +fun hashFileContent(files: File, hashAlgorithm: String = "SHA-256"): String { 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) + files.listFiles() + .filter { it.isFile } + .forEach { file -> + // 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) } diff --git a/cache-maker/build.gradle.kts b/cache-maker/build.gradle.kts index fbd7510c7..2dcbfbed6 100644 --- a/cache-maker/build.gradle.kts +++ b/cache-maker/build.gradle.kts @@ -1,31 +1,35 @@ +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") @file:OptIn(ExperimentalWasmDsl::class) +import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments +import org.jetbrains.kotlin.compilerRunner.* import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.targets.js.binaryen.BinaryenRootEnvSpec +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion +import org.jetbrains.kotlin.gradle.plugin.COMPILER_CLASSPATH_CONFIGURATION_NAME +import org.jetbrains.kotlin.gradle.plugin.categoryByName +import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages +import org.jetbrains.kotlin.gradle.plugin.usesPlatformOf +import org.jetbrains.kotlin.gradle.report.ReportingSettings +import org.jetbrains.kotlin.gradle.targets.js.internal.LibraryFilterCachingService 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 +import org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsRootExtension +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy +import org.jetbrains.kotlin.gradle.utils.kotlinSessionsDir +import org.jetbrains.kotlin.library.impl.isKotlinLibrary plugins { id("base-kotlin-multiplatform-conventions") + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) } kotlin { wasmJs { - outputModuleName.set("stdlib") + nodejs() 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") - } - ) + compilerOptions.freeCompilerArgs.add("-Xwasm-included-module-only") } } } @@ -40,19 +44,109 @@ kotlin { } } -val compileProductionExecutableKotlinWasmJs: TaskProvider by tasks.existing(KotlinJsIrLink::class) { +val allRuntimes: Configuration by configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false + usesPlatformOf(kotlin.wasmJs()) + attributes.attribute(Usage.USAGE_ATTRIBUTE, KotlinUsages.consumerRuntimeUsage(kotlin.wasmJs())) + attributes.attribute(Category.CATEGORY_ATTRIBUTE, project.categoryByName(Category.LIBRARY)) + attributes { + attribute( + ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, + "compiled-wasm" + ) + } +} + +// we don't need to build cache-maker +tasks.named("build") { + dependsOn.clear() +} + +val mainCompilation = kotlin.wasmJs().compilations.getByName("main") +val compileTask = mainCompilation.compileTaskProvider + +dependencies { + allRuntimes(libs.bundles.compose) + allRuntimes(libs.kotlinx.coroutines.core.compose.wasm) + allRuntimes(libs.kotlin.stdlib.wasm.js) + + registerTransform(WasmBinaryTransform::class.java) { + from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "klib") + to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "compiled-wasm") + + val libraryFilterService = LibraryFilterCachingService.registerIfAbsent(project) + + + parameters { + currentJvmJdkToolsJar.set( + compileTask.flatMap { it.defaultKotlinJavaToolchain } + .flatMap { it.currentJvmJdkToolsJar } + ) + defaultCompilerClasspath.setFrom(project.configurations.named(COMPILER_CLASSPATH_CONFIGURATION_NAME)) + kotlinPluginVersion.set( + compileTask.map { getKotlinPluginVersion(it.logger) } + ) + pathProvider.set( + compileTask.map { it.path } + ) + projectRootFile.set( + project.projectDir + ) + val projectName = project.name + clientIsAliveFlagFile.set( + compileTask.map { GradleCompilerRunner.getOrCreateClientFlagFile(it.logger, projectName) } + + ) + val projectSessionsDir = project.kotlinSessionsDir + sessionFlagFile.set( + compileTask.map { GradleCompilerRunner.getOrCreateSessionFlagFile(it.logger, projectSessionsDir) } + + ) + buildDir.set(project.layout.buildDirectory.asFile) + + libraryFilterCacheService.set(libraryFilterService) + } + } + + attributesSchema { + attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE) { + compatibilityRules.add(JarToKlibRule::class) + } + } } -val composeWasmStdlib: Provider = compileProductionExecutableKotlinWasmJs - .flatMap { it.destinationDirectory.locationOnly } -val composeWasmStdlibFile: Provider = composeWasmStdlib - .map { it.file("stdlib_master.wasm") } +val prepareRuntime by tasks.registering(Copy::class) { + val allRuntimes: FileCollection = allRuntimes + + from(allRuntimes) { + include { + it.name.endsWith(".uninstantiated.mjs") || it.name.endsWith(".wasm") || it.name.endsWith("skiko.mjs") || it.name.endsWith("skiko.wasm") + } + + filesMatching(listOf("**/*.uninstantiated.mjs")) { + filter { line: String -> + line.replace("%3", "%253") + } + } + } + + val npmInstallTaskProvider = rootProject.the().npmInstallTaskProvider + + dependsOn(npmInstallTaskProvider) + + val nodeModulesDir: Provider = npmInstallTaskProvider.flatMap { + it.nodeModules + } + + from(nodeModulesDir.map { it.dir("@js-joda") }) { + into("@js-joda") + } -rootProject.plugins.withType { - rootProject.the().version = "122" + into(layout.buildDirectory.dir("all-libs")) } -val kotlinComposeWasmStdlibFile: Configuration by configurations.creating { +val kotlinComposeWasmRuntime: Configuration by configurations.creating { isTransitive = false isCanBeResolved = false isCanBeConsumed = true @@ -62,35 +156,131 @@ val kotlinComposeWasmStdlibFile: Configuration by configurations.creating { objects.categoryComposeCache ) } + + outgoing.variants.create("all", Action { + artifact(prepareRuntime) + }) } -kotlinComposeWasmStdlibFile.outgoing.variants.create("stdlib") { - attributes { - attribute( - CacheAttribute.cacheAttribute, - CacheAttribute.FULL - ) - } +class JarToKlibRule : AttributeCompatibilityRule { + // Implement the execute method which will check compatibility + override fun execute(details: CompatibilityCheckDetails) { + // Switch case to check the consumer value for supported Java versions - artifact(composeWasmStdlib) { - builtBy(compileProductionExecutableKotlinWasmJs) + if (details.producerValue == "jar" && details.consumerValue == "klib") { + details.compatible() + } } } -kotlinComposeWasmStdlibFile.outgoing.variants.create("wasm-file") { - attributes { - attribute( - CacheAttribute.cacheAttribute, - CacheAttribute.WASM - ) - } +abstract class WasmBinaryTransform : TransformAction { + abstract class Parameters : TransformParameters { + @get:Internal + abstract val currentJvmJdkToolsJar: Property - artifact(composeWasmStdlibFile) { - builtBy(compileProductionExecutableKotlinWasmJs) + @get:Classpath + abstract val defaultCompilerClasspath: ConfigurableFileCollection + + @get:Internal + abstract val kotlinPluginVersion: Property + + @get:Internal + abstract val pathProvider: Property + + @get:Internal + abstract val projectRootFile: Property + + @get:Internal + abstract val clientIsAliveFlagFile: Property + + @get:Internal + abstract val sessionFlagFile: Property + + @get:Internal + abstract val buildDir: Property + + @get:Internal + internal abstract val libraryFilterCacheService: Property } -} -// we don't need to build cache-maker -tasks.named("build") { - dependsOn.clear() -} + @get:Inject + abstract val fs: FileSystemOperations + + @get:Inject + abstract val archiveOperations: ArchiveOperations + + @get:InputArtifact + abstract val inputArtifact: Provider + + @get:CompileClasspath + @get:InputArtifactDependencies + abstract val dependencies: FileCollection + + override fun transform(outputs: TransformOutputs) { + val inputFile = inputArtifact.get().asFile + val outputDir = outputs.dir(inputFile.name.replace(".klib", "-transformed")) + + val isKotlinLibrary = parameters.libraryFilterCacheService.get().getOrCompute( + LibraryFilterCachingService.LibraryFilterCacheKey( + inputFile + ), + ::isKotlinLibrary + ) + + if (!isKotlinLibrary) { + fs.copy { + from(archiveOperations.zipTree(inputFile)) + into(outputDir) + } + return + } + + val args = K2JSCompilerArguments().apply { + multiPlatform = true + this.outputDir = outputDir.absolutePath + libraries = dependencies.files.plus(inputFile).joinToString(File.pathSeparator) { it.absolutePath } + moduleName = inputFile.nameWithoutExtension + includes = inputFile.absolutePath + wasm = true + wasmTarget = "wasm-js" + wasmIncludedModuleOnly = true + irProduceJs = true + forceDebugFriendlyCompilation = true + } + + val workArgs = GradleKotlinCompilerWorkArguments( + projectFiles = ProjectFilesForCompilation( + parameters.projectRootFile.get(), + parameters.clientIsAliveFlagFile.get(), + parameters.sessionFlagFile.get(), + parameters.buildDir.get(), + ), + compilerFullClasspath = (parameters.defaultCompilerClasspath.files + parameters.currentJvmJdkToolsJar.orNull).filterNotNull(), + compilerClassName = KotlinCompilerClass.JS, + compilerArgs = ArgumentUtils.convertArgumentsToStringList(args).toTypedArray(), + isVerbose = false, + incrementalCompilationEnvironment = null, + incrementalModuleInfo = null, + outputFiles = emptyList(), + taskPath = parameters.pathProvider.get(), + reportingSettings = ReportingSettings(), + kotlinScriptExtensions = emptyArray(), + allWarningsAsErrors = false, + compilerExecutionSettings = CompilerExecutionSettings( + null, + KotlinCompilerExecutionStrategy.DAEMON, + true, +// generateCompilerRefIndex = false, + ), + errorsFiles = null, + kotlinPluginVersion = parameters.kotlinPluginVersion.get(), + //no need to log warnings in MessageCollector hear it will be logged by compiler + kotlinLanguageVersion = KotlinVersion.DEFAULT, + compilerArgumentsLogLevel = KotlinCompilerArgumentsLogLevel.DEFAULT, + ) + + GradleKotlinCompilerWork( + workArgs + ).run() + } +} \ No newline at end of file diff --git a/cache-maker/src/wasmJsMain/kotlin/main.kt b/cache-maker/src/wasmJsMain/kotlin/main.kt index a5ba81202..9b79c640c 100644 --- a/cache-maker/src/wasmJsMain/kotlin/main.kt +++ b/cache-maker/src/wasmJsMain/kotlin/main.kt @@ -12,12 +12,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.window.ComposeViewport import kotlinx.browser.document -//sampleStart @OptIn(ExperimentalComposeUiApi::class) fun main() { - ComposeViewport(document.body!!) { + ComposeViewport(viewportContainer = document.body!!, content = { App() - } + }) } @Composable 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 index 00db05bf2..17aa526a2 100644 --- a/common/src/main/kotlin/com/compiler/server/common/components/CliUtils.kt +++ b/common/src/main/kotlin/com/compiler/server/common/components/CliUtils.kt @@ -66,14 +66,15 @@ fun linkWasmArgs( "-Xwasm", "-Xir-produce-js", "-Xinclude=$klibPath", - "-libraries=${dependencies.joinToString(PATH_SEPARATOR)}", + "-libraries=${(dependencies + klibPath).joinToString(PATH_SEPARATOR)}", "-ir-output-dir=${(outputDir / "wasm").toFile().canonicalPath}", "-ir-output-name=$moduleName", + "-Xwasm-debug-friendly", ).also { if (debugInfo) it.add("-Xwasm-generate-wat") if (multiModule) { - it.add("-Xwasm-multimodule-mode=slave") + it.add("-Xwasm-included-module-only") } else { it.add("-Xir-dce") } diff --git a/common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironment.kt b/common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironment.kt index b06ed0f0f..b4cd6767f 100644 --- a/common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironment.kt +++ b/common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironment.kt @@ -17,10 +17,10 @@ import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.config.JVMConfigurationKeys import org.jetbrains.kotlin.config.languageVersionSettings import org.jetbrains.kotlin.js.config.JSConfigurationKeys +import org.jetbrains.kotlin.js.config.ModuleKind import org.jetbrains.kotlin.library.impl.isKotlinLibrary import org.jetbrains.kotlin.serialization.js.JsModuleDescriptor import org.jetbrains.kotlin.serialization.js.KotlinJavascriptSerializationUtil -import org.jetbrains.kotlin.serialization.js.ModuleKind import org.jetbrains.kotlin.utils.KotlinJavascriptMetadataUtils import org.jetbrains.kotlin.wasm.config.WasmConfigurationKeys import java.io.File @@ -34,6 +34,7 @@ class KotlinEnvironment( composeWasmCompilerPlugins: List, val compilerPlugins: List = emptyList(), composeWasmCompilerPluginsOptions: List, + val dependenciesComposeWasm: String = "", ) { companion object { /** diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b6fc7d67..8e9fb0643 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "2.2.21-RC-343" +kotlin = "2.3.0-Beta2" kotlinIdeVersion = "1.9.20-506" kotlinIdeVersionWithSuffix = "231-1.9.20-506-IJ8109.175" spring-boot = "3.5.5" @@ -10,7 +10,7 @@ junit = "4.13.2" logstash-logback-encoder = "8.1" trove4j = "1.0.20221201" kotlinx-coroutines = "1.7.3" -kotlinx-coroutines-compose-wasm = "1.8.1" +kotlinx-coroutines-compose-wasm = "1.10.2" kotlinx-coroutines-test = "1.6.4" kotlinx-datetime = "0.7.1-0.6.x-compat" kotlinx-io = "0.8.0" @@ -19,8 +19,8 @@ skiko = "0.9.22.2" # don't forget to update jackson version in `executor.policy` file. jackson = "2.19.2" hamcrest = "3.0" -compose = "1.9.0-rc02" -compose-material3 = "1.9.0-beta05" +compose = "1.9.3" +compose-material3 = "1.9.0" gradle-develocity = "3.17.5" [libraries] @@ -90,4 +90,6 @@ compose = [ kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-plugin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } -spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-managment" } \ No newline at end of file +spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-managment" } +composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose" } +composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file diff --git a/resource-server/build.gradle.kts b/resource-server/build.gradle.kts index 7920fad4d..dccdae884 100644 --- a/resource-server/build.gradle.kts +++ b/resource-server/build.gradle.kts @@ -1,6 +1,7 @@ -import org.gradle.kotlin.dsl.support.serviceOf +import org.apache.tools.ant.filters.ConcatFilter import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.FileInputStream +import java.io.StringReader import java.util.* plugins { @@ -15,7 +16,7 @@ val resourceDependency: Configuration by configurations.creating { isCanBeConsumed = false } -val kotlinComposeWasmStdlibFile: Configuration by configurations.creating { +val kotlinComposeWasmRuntime: Configuration by configurations.creating { isTransitive = false isCanBeResolved = true isCanBeConsumed = false @@ -24,26 +25,6 @@ val kotlinComposeWasmStdlibFile: Configuration by configurations.creating { 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 - ) } } @@ -54,12 +35,11 @@ dependencies { } resourceDependency(libs.skiko.js.wasm.runtime) - kotlinComposeWasmStdlib(project(":cache-maker")) - kotlinComposeWasmStdlibFile(project(":cache-maker")) + kotlinComposeWasmRuntime(project(":cache-maker")) } val propertiesGenerator by tasks.registering(PropertiesGenerator::class) { - dependsOn(kotlinComposeWasmStdlibFile) + dependsOn(kotlinComposeWasmRuntime) propertiesMap.put("spring.mvc.pathmatch.matching-strategy", "ant_path_matcher") propertiesMap.put("server.port", "8081") propertiesMap.put("skiko.version", libs.versions.skiko.get()) @@ -68,40 +48,28 @@ val propertiesGenerator by tasks.registering(PropertiesGenerator::class) { propertiesFile.fileValue(applicationPropertiesPath) - val composeWasmStdlibFile: FileCollection = kotlinComposeWasmStdlibFile + val composeWasmStdlibFile: FileCollection = kotlinComposeWasmRuntime - hashableFile.fileProvider( - provider { - composeWasmStdlibFile.singleFile - } - ) + hashableDir.from(composeWasmStdlibFile) } tasks.withType { - dependsOn(kotlinComposeWasmStdlibFile) + dependsOn(kotlinComposeWasmRuntime) dependsOn(propertiesGenerator) } val skikoVersion = libs.versions.skiko val prepareComposeWasmResources by tasks.registering(Sync::class) { - dependsOn(kotlinComposeWasmStdlibFile) + dependsOn(kotlinComposeWasmRuntime) 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") + from(kotlinComposeWasmRuntime) { + include("**/*.uninstantiated.mjs", "skiko.mjs", "**/*.wasm", "@js-joda/**") rename { original -> val properties = FileInputStream(propertiesFile.get().asFile).use { @@ -109,19 +77,76 @@ val prepareComposeWasmResources by tasks.registering(Sync::class) { load(it) } } - val regex = Regex("stdlib_master(\\.uninstantiated)*\\.(.*)") - regex.find(original)?.groupValues?.get(2)?.let { extension -> - "stdlib-${properties["dependencies.compose.wasm"]}.$extension" + val regex = Regex("^(.+?)(\\.uninstantiated)*\\.(mjs|wasm)\$") + regex.find(original)?.groupValues?.let { groups -> + val name = groups[1] + val uninst: String = groups[2] + val extension = groups[3] + "$name-${properties["dependencies.compose-wasm"]}$uninst.$extension" } ?: original } + + includeEmptyDirs = false + + filesMatching("@js-joda/**") { + val properties = FileInputStream(propertiesFile.get().asFile).use { + Properties().apply { + load(it) + } + } + path = path.replace("@js-joda", "@js-joda-${properties["dependencies.compose-wasm"]}") + } + + filesMatching(listOf("_kotlin_.uninstantiated.mjs")) { + val header = """ + class BufferedOutput { + constructor() { + this.buffer = "" + } + } + globalThis.bufferedOutput = new BufferedOutput() + """.trimIndent() + + filter(mapOf("prependReader" to StringReader(header)), ConcatFilter::class.java) + + filter { line: String -> + line.replace( + "const importObject = {", + "js_code['kotlin.io.printImpl'] = (message) => globalThis.bufferedOutput.buffer += message\n" + + "js_code['kotlin.io.printlnImpl'] = (message) => {globalThis.bufferedOutput.buffer += message;bufferedOutput.buffer += \"\\n\"}\n" + + "const importObject = {" + ) + } + } + + filesMatching(listOf("**/*.uninstantiated.mjs", "skiko.mjs")) { + filter { line: String -> + val properties = FileInputStream(propertiesFile.get().asFile).use { + Properties().apply { + load(it) + } + } + + val composeWasmHash = properties["dependencies.compose-wasm"] + line + .replace(".wasm\'", "-$composeWasmHash.wasm\'") + .replace(".uninstantiated.mjs\')", "-$composeWasmHash.uninstantiated.mjs\')") + .replace("skiko.mjs\')", "skiko-$composeWasmHash.mjs\')") + .replace("skiko.wasm\"", "skiko-$composeWasmHash.wasm\"") + .replace( + "import('@js-joda/core')", + "import('./@js-joda-${composeWasmHash}/core/dist/js-joda.esm.js')" + ) + } + } } } tasks.named("processResources") { dependsOn(prepareComposeWasmResources) from(prepareComposeWasmResources) { - into("com/compiler/server") + into("static") } } 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 deleted file mode 100644 index 5f6a454d3..000000000 --- a/resource-server/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index cde6a5e7d..000000000 --- a/resource-server/src/test/kotlin/com/compiler/server/SkikoResourceTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -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/src/main/kotlin/com/compiler/server/compiler/components/KotlinEnvironment.kt b/src/main/kotlin/com/compiler/server/compiler/components/KotlinEnvironment.kt index 5704082c2..0215f0ffa 100644 --- a/src/main/kotlin/com/compiler/server/compiler/components/KotlinEnvironment.kt +++ b/src/main/kotlin/com/compiler/server/compiler/components/KotlinEnvironment.kt @@ -1,6 +1,7 @@ package com.compiler.server.compiler.components import com.compiler.server.common.components.KotlinEnvironment +import com.compiler.server.model.bean.Dependencies import com.compiler.server.model.bean.LibrariesFile import component.CompilerPluginOption import org.springframework.context.annotation.Bean @@ -9,6 +10,7 @@ import org.springframework.context.annotation.Configuration @Configuration class KotlinEnvironmentConfiguration( val librariesFile: LibrariesFile, + val dependencies: Dependencies, ) { @Bean fun kotlinEnvironment(): KotlinEnvironment { @@ -39,6 +41,7 @@ class KotlinEnvironmentConfiguration( "false" ), ), + dependencies.composeWasm ) } } 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 73d328396..034efe45a 100644 --- a/src/main/kotlin/com/compiler/server/compiler/components/KotlinToJSTranslator.kt +++ b/src/main/kotlin/com/compiler/server/compiler/components/KotlinToJSTranslator.kt @@ -159,6 +159,12 @@ class KotlinToJSTranslator( val filePaths = ioFiles.map { it.toFile().canonicalPath } val klibPath = (outputDir / "klib").toFile().canonicalPath + val fixedModuleName = if (!multiModule) { + moduleName + } else { + "_${moduleName}_" + } + K2JSCompiler().tryCompilation( inputDir, ioFiles, @@ -186,19 +192,25 @@ class KotlinToJSTranslator( } .map { WasmTranslationSuccessfulOutput( - jsCode = (outputDir / "wasm" / "$moduleName.uninstantiated.mjs").readText().fixSkikoImport(), - jsInstantiated = (outputDir / "wasm" / "$moduleName.mjs").readText(), - wasm = (outputDir / "wasm" / "$moduleName.wasm").readBytes(), - wat = if (debugInfo) (outputDir / "wasm" / "$moduleName.wat").readText() else null, + jsCode = (outputDir / "wasm" / "$fixedModuleName.uninstantiated.mjs").readText().fixWasmImports().fixSkikoImports(), + jsInstantiated = (outputDir / "wasm" / "$fixedModuleName.mjs").readText(), + wasm = (outputDir / "wasm" / "$fixedModuleName.wasm").readBytes(), + wat = if (debugInfo) (outputDir / "wasm" / "$fixedModuleName.wat").readText() else null, ) } } } -} -private fun String.fixSkikoImport(): String = lineSequence() - .filterNot { it.contains("imports['./skiko.mjs'].skikoApi") } - .joinToString("\n") + private fun String.fixWasmImports(): String = replace( + ".uninstantiated.mjs", + "-${kotlinEnvironment.dependenciesComposeWasm}.uninstantiated.mjs" + ) + + private fun String.fixSkikoImports(): String = replace( + "skiko.mjs", + "skiko-${kotlinEnvironment.dependenciesComposeWasm}.mjs" + ) +} private fun String.withMainArgumentsIr(arguments: List): String { val mainIrFunction = """ diff --git a/src/main/kotlin/com/compiler/server/configuration/ApplicationConfiguration.kt b/src/main/kotlin/com/compiler/server/configuration/ApplicationConfiguration.kt index 09c7ecaac..989020285 100644 --- a/src/main/kotlin/com/compiler/server/configuration/ApplicationConfiguration.kt +++ b/src/main/kotlin/com/compiler/server/configuration/ApplicationConfiguration.kt @@ -1,5 +1,6 @@ package com.compiler.server.configuration +import com.compiler.server.model.bean.Dependencies import com.compiler.server.model.bean.LibrariesFile import com.compiler.server.model.bean.VersionInfo import org.springframework.beans.factory.annotation.Value @@ -12,10 +13,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import java.io.File @Configuration -@EnableConfigurationProperties(value = [LibrariesFolderProperties::class]) +@EnableConfigurationProperties(value = [LibrariesFolderProperties::class, DependenciesProperties::class]) class ApplicationConfiguration( @Value("\${kotlin.version}") private val version: String, private val librariesFolderProperties: LibrariesFolderProperties, + private val dependenciesProperties: DependenciesProperties, ) : WebMvcConfigurer { override fun addFormatters(registry: FormatterRegistry) { registry.addConverter(ProjectConverter()) @@ -27,6 +29,11 @@ class ApplicationConfiguration( stdlibVersion = version ) + @Bean + fun dependencies() = Dependencies( + dependenciesProperties.composeWasm, + ) + @Bean fun librariesFiles() = LibrariesFile( File(librariesFolderProperties.jvm), @@ -46,4 +53,9 @@ class LibrariesFolderProperties { lateinit var composeWasm: String lateinit var composeWasmCompilerPlugins: String lateinit var compilerPlugins: String +} + +@ConfigurationProperties(prefix = "dependencies") +class DependenciesProperties { + lateinit var composeWasm: String } \ No newline at end of file diff --git a/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt b/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt deleted file mode 100644 index fcb609bc1..000000000 --- a/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.compiler.server.controllers - -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 - - -@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("/compose-wasm-versions") - fun getVersions(): Map { - return mapOf( - "skiko" to skikoVersion, - "stdlib" to dependenciesComposeWasm - ) - } -} diff --git a/src/main/kotlin/com/compiler/server/model/bean/Dependencies.kt b/src/main/kotlin/com/compiler/server/model/bean/Dependencies.kt new file mode 100644 index 000000000..46f5e251b --- /dev/null +++ b/src/main/kotlin/com/compiler/server/model/bean/Dependencies.kt @@ -0,0 +1,5 @@ +package com.compiler.server.model.bean + +data class Dependencies( + val composeWasm: String, +) \ No newline at end of file