diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt index e791632e334..f39b6ab9eda 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt @@ -25,10 +25,10 @@ import org.jetbrains.kotlin.gradle.tasks.USING_JS_IR_BACKEND_MESSAGE import org.jetbrains.kotlin.gradle.testbase.* import org.jetbrains.kotlin.gradle.util.jsCompilerType import org.jetbrains.kotlin.gradle.util.normalizePath -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.condition.DisabledIf import java.nio.file.Files +import java.nio.file.Path import java.nio.file.Paths import java.util.zip.ZipFile import kotlin.io.path.* @@ -110,7 +110,6 @@ class Kotlin2JsIrGradlePluginIT : AbstractKotlin2JsGradlePluginIT(true) { } @GradleTest - @Disabled // Persistent IR is no longer supported fun testJsIrIncrementalInParallel(gradleVersion: GradleVersion) { project("kotlin-js-browser-project", gradleVersion) { buildGradleKts.modify(::transformBuildScriptWithPluginsDsl) @@ -126,6 +125,95 @@ class Kotlin2JsIrGradlePluginIT : AbstractKotlin2JsGradlePluginIT(true) { } } + @DisplayName("Only changed files synced during JS IR build") + @GradleTest + fun testJsIrOnlyChangedFilesSynced(gradleVersion: GradleVersion) { + project("kotlin-js-browser-project", gradleVersion) { + buildGradleKts.modify(::transformBuildScriptWithPluginsDsl) + + val filesModified: MutableMap = mutableMapOf() + + build("compileDevelopmentExecutableKotlinJs") { + assertTasksExecuted(":app:developmentExecutableCompileSync") + + projectPath.resolve("build/js/packages/kotlin-js-browser-app") + .resolve("kotlin") + .toFile() + .walkTopDown() + .associateByTo(filesModified, { it.path }) { + it.lastModified() + } + } + + projectPath.resolve("base/src/main/kotlin/Base.kt").modify { + it.replace("73", "37") + } + + val fooTxt = projectPath.resolve("app/src/main/resources/foo/foo.txt") + fooTxt.parent.toFile().mkdirs() + fooTxt.createFile().writeText("foo") + + build("compileDevelopmentExecutableKotlinJs") { + assertTasksExecuted(":app:developmentExecutableCompileSync") + + val modified = projectPath.resolve("build/js/packages/kotlin-js-browser-app") + .resolve("kotlin") + .toFile() + .walkTopDown() + .filter { it.isFile } + .filterNot { filesModified[it.path] == it.lastModified() } + .toSet() + + assertEquals( + setOf( + projectPath.resolve("build/js/packages/kotlin-js-browser-app/kotlin/kotlin-js-browser-base-js-ir.js").toFile(), + projectPath.resolve("build/js/packages/kotlin-js-browser-app/kotlin/foo/foo.txt").toFile(), + ), + modified.toSet() + ) + } + + projectPath.resolve("build/js/packages/kotlin-js-browser-app") + .resolve("kotlin") + .toFile() + .walkTopDown() + .associateByTo(filesModified, { it.path }) { + it.lastModified() + } + + fooTxt.writeText("bar") + + build("compileDevelopmentExecutableKotlinJs") { + assertTasksExecuted(":app:developmentExecutableCompileSync") + + val modified = projectPath.resolve("build/js/packages/kotlin-js-browser-app") + .resolve("kotlin") + .toFile() + .walkTopDown() + .filter { it.isFile } + .filterNot { filesModified[it.path] == it.lastModified() } + .toSet() + + assertEquals( + setOf( + projectPath.resolve("build/js/packages/kotlin-js-browser-app/kotlin/foo/foo.txt").toFile(), + ), + modified.toSet() + ) + } + + fooTxt.deleteExisting() + + + build("compileDevelopmentExecutableKotlinJs") { + assertTasksExecuted(":app:developmentExecutableCompileSync") + + assertFileInProjectNotExists("build/js/packages/kotlin-js-browser-app/kotlin/foo/foo.txt") + assertFileInProjectNotExists("build/js/packages/kotlin-js-browser-app/sync-hashes/foo/foo.txt.hash") + } + } + } + @DisplayName("falsify kotlin js compiler args") @GradleTest fun testFalsifyKotlinJsCompilerArgs(gradleVersion: GradleVersion) { diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt index 9f6553f01ad..a8340456098 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt @@ -8,7 +8,6 @@ package org.jetbrains.kotlin.gradle.targets.js.ir import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Project import org.gradle.api.Task -import org.gradle.api.tasks.Copy import org.gradle.api.tasks.TaskProvider import org.jetbrains.kotlin.gradle.dsl.KotlinJsOptions import org.jetbrains.kotlin.gradle.plugin.* @@ -22,7 +21,6 @@ import org.jetbrains.kotlin.gradle.targets.js.KotlinJsReportAggregatingTestRun import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget import org.jetbrains.kotlin.gradle.targets.js.binaryen.BinaryenExec import org.jetbrains.kotlin.gradle.targets.js.dsl.* -import org.jetbrains.kotlin.gradle.targets.js.internal.RewriteSourceMapFilterReader import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject import org.jetbrains.kotlin.gradle.targets.js.typescript.TypeScriptValidationTask import org.jetbrains.kotlin.gradle.tasks.locateOrRegisterTask @@ -131,10 +129,11 @@ constructor( } } - private fun registerCompileSync(binary: JsIrBinary): TaskProvider { + private fun registerCompileSync(binary: JsIrBinary): TaskProvider { val compilation = binary.compilation val npmProject = compilation.npmProject - return project.registerTask( + + return project.registerTask( binary.linkSyncTaskName ) { task -> task.from( @@ -145,18 +144,8 @@ constructor( task.from(project.tasks.named(compilation.processResourcesTaskName)) - // Rewrite relative paths in sourcemaps in the target directory - task.eachFile { - if (it.name.endsWith(".js.map")) { - it.filter( - mapOf( - "srcSourceRoot" to it.file.parentFile, - "targetSourceRoot" to npmProject.dist - ), - RewriteSourceMapFilterReader::class.java - ) - } - } + val hashDir = npmProject.dir.resolve("sync-hashes") + task.hashDir.set(hashDir) task.into(npmProject.dist) } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/SyncExecutableTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/SyncExecutableTask.kt new file mode 100644 index 00000000000..10c02f95bbb --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/SyncExecutableTask.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.targets.js.ir + +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.OutputDirectory +import org.gradle.internal.hash.FileHasher +import org.jetbrains.kotlin.gradle.targets.js.internal.RewriteSourceMapFilterReader +import org.jetbrains.kotlin.gradle.targets.js.toHex +import java.io.File +import javax.inject.Inject + +abstract class SyncExecutableTask : Copy() { + + @get:Inject + abstract val fileHasher: FileHasher + + @get:Inject + abstract val objects: ObjectFactory + + @get:OutputDirectory + abstract val hashDir: Property + + override fun copy() { + val actualFiles = mutableSetOf() + + val hashDirFile = hashDir.get() + eachFile { + actualFiles.add(it.relativeSourcePath.getFile(destinationDir)) + + val hashFile = hashDirFile.resolve(it.relativeSourcePath.pathString + ".$HASH_EXTENSION") + + val currentHash = fileHasher.hash(it.file) + val currentHashHex = currentHash.toByteArray().toHex() + + if (hashFile.exists()) { + val previousHash = hashFile.readText() + + if (previousHash == currentHashHex) { + it.exclude() + return@eachFile + } + } else { + hashFile.parentFile.mkdirs() + } + + hashFile.writeText(currentHashHex) + } + + // Rewrite relative paths in sourcemaps in the target directory + eachFile { + if (it.name.endsWith(".js.map")) { + it.filter( + mapOf( + RewriteSourceMapFilterReader::srcSourceRoot.name to it.file.parentFile, + RewriteSourceMapFilterReader::targetSourceRoot.name to destinationDir + ), + RewriteSourceMapFilterReader::class.java + ) + } + } + + super.copy() + + objects.fileTree() + .from(hashDirFile) + .files + .forEach { + if (destinationDir.resolve(it.relativeTo(hashDirFile).path.removeSuffix(".$HASH_EXTENSION")) !in actualFiles) { + it.delete() + } + } + + objects.fileTree() + .from(destinationDir) + .files + .forEach { + if (it !in actualFiles) { + it.delete() + } + } + } + + companion object { + const val HASH_EXTENSION = "hash" + } +} \ No newline at end of file