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 627d89a9a83..e488769e248 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 @@ -17,6 +17,9 @@ import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProjectModules import org.jetbrains.kotlin.gradle.targets.js.npm.PackageJson import org.jetbrains.kotlin.gradle.targets.js.npm.fromSrcPackageJson +import org.jetbrains.kotlin.gradle.targets.js.yarn.YARN_LOCK_MISMATCH_MESSAGE +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask.Companion.STORE_YARN_LOCK_NAME +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask.Companion.UPGRADE_YARN_LOCK import org.jetbrains.kotlin.gradle.tasks.USING_JS_INCREMENTAL_COMPILATION_MESSAGE import org.jetbrains.kotlin.gradle.tasks.USING_JS_IR_BACKEND_MESSAGE import org.jetbrains.kotlin.gradle.testbase.* @@ -25,7 +28,6 @@ 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.io.File import java.nio.file.Files import java.nio.file.Paths import java.util.zip.ZipFile @@ -33,7 +35,6 @@ import kotlin.io.path.* import kotlin.streams.toList import kotlin.test.assertEquals import kotlin.test.assertFalse -import kotlin.test.assertNotEquals import kotlin.test.assertTrue @JsGradlePluginTests @@ -867,6 +868,10 @@ abstract class AbstractKotlin2JsGradlePluginIT(protected val irBackend: Boolean) } } + build(UPGRADE_YARN_LOCK) { + assertTasksExecuted(":$UPGRADE_YARN_LOCK") + } + build("jsJar") { val archive = Files.list(projectPath.resolve("build").resolve("libs")).use { files -> files @@ -1178,6 +1183,202 @@ abstract class AbstractKotlin2JsGradlePluginIT(protected val irBackend: Boolean) } } + @DisplayName("Failing with yarn.lock update") + @GradleTest + fun testFailingWithYarnLockUpdate(gradleVersion: GradleVersion) { + project("kotlin-js-yarn-lock-project", gradleVersion) { + build("compileKotlinJs") { + assertTasksExecuted(":$STORE_YARN_LOCK_NAME") + } + + projectPath.resolve("kotlin-js-store").deleteRecursively() + + buildGradleKts.modify { + it + "\n" + + """ + rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin::class.java) { + rootProject.the().reportNewYarnLock = true + } + """.trimIndent() + } + + buildAndFail("compileKotlinJs") { + assertTasksFailed(":$STORE_YARN_LOCK_NAME") + } + + buildGradleKts.modify { + it + "\n" + + """ + dependencies { + implementation(npm("decamelize", "6.0.0")) + } + + rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin::class.java) { + rootProject.the().yarnLockMismatchReport = + org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport.FAIL + } + """.trimIndent() + } + + buildAndFail("compileKotlinJs") { + assertTasksFailed(":$STORE_YARN_LOCK_NAME") + } + + // yarn.lock was not updated + buildAndFail("compileKotlinJs") { + assertTasksFailed(":$STORE_YARN_LOCK_NAME") + } + + build(UPGRADE_YARN_LOCK) { + assertTasksExecuted(":$UPGRADE_YARN_LOCK") + } + + buildGradleKts.modify { + val replaced = it.replace("implementation(npm(\"decamelize\", \"6.0.0\"))", "") + replaced + "\n" + + """ + rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin::class.java) { + rootProject.the().yarnLockMismatchReport = + org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport.WARNING + } + """.trimIndent() + } + + build("compileKotlinJs") { + assertTasksExecuted(":$STORE_YARN_LOCK_NAME") + + assertOutputContains(YARN_LOCK_MISMATCH_MESSAGE) + } + + build(UPGRADE_YARN_LOCK) { + assertTasksExecuted(":$UPGRADE_YARN_LOCK") + } + + buildGradleKts.modify { + it + "\n" + + """ + dependencies { + implementation(npm("decamelize", "6.0.0")) + } + + rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin::class.java) { + rootProject.the().yarnLockMismatchReport = + org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport.NONE + } + """.trimIndent() + } + + build("compileKotlinJs") { + assertTasksExecuted(":$STORE_YARN_LOCK_NAME") + + assertOutputDoesNotContain(YARN_LOCK_MISMATCH_MESSAGE) + } + + build(UPGRADE_YARN_LOCK) { + assertTasksExecuted(":$UPGRADE_YARN_LOCK") + } + + buildGradleKts.modify { + val replaced = it.replace("implementation(npm(\"decamelize\", \"6.0.0\"))", "") + replaced + "\n" + + """ + rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin::class.java) { + rootProject.the().yarnLockMismatchReport = + org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport.FAIL_AFTER_BUILD + } + """.trimIndent() + } + + buildAndFail("compileKotlinJs") { + assertTasksExecuted(":$STORE_YARN_LOCK_NAME") + + assertOutputContains(YARN_LOCK_MISMATCH_MESSAGE) + } + + buildAndFail("compileKotlinJs") { + assertTasksUpToDate(":$STORE_YARN_LOCK_NAME") + + assertOutputContains(YARN_LOCK_MISMATCH_MESSAGE) + } + + projectPath.resolve("kotlin-js-store").deleteRecursively() + + buildGradleKts.modify { + it + "\n" + + """ + rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin::class.java) { + rootProject.the().yarnLockMismatchReport = + org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport.NONE + rootProject.the().reportNewYarnLock = + true + } + """.trimIndent() + } + + build("compileKotlinJs") { + assertTasksExecuted(":$STORE_YARN_LOCK_NAME") + + assertOutputDoesNotContain(YARN_LOCK_MISMATCH_MESSAGE) + } + + buildGradleKts.modify { + it + "\n" + + """ + dependencies { + implementation(npm("decamelize", "6.0.0")) + } + + rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin::class.java) { + rootProject.the().yarnLockMismatchReport = + org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport.FAIL + rootProject.the().yarnLockAutoReplace = + true + } + """.trimIndent() + } + + buildAndFail("compileKotlinJs") { + assertTasksFailed(":$STORE_YARN_LOCK_NAME") + } + + //yarn.lock was updated + build("compileKotlinJs") { + assertTasksExecuted(":$STORE_YARN_LOCK_NAME") + } + + buildGradleKts.modify { + val replaced = it.replace("implementation(npm(\"decamelize\", \"6.0.0\"))", "") + replaced + "\n" + + """ + rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin::class.java) { + rootProject.the().yarnLockMismatchReport = + org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport.FAIL_AFTER_BUILD + } + """.trimIndent() + } + + buildAndFail("compileKotlinJs") { + assertTasksExecuted(":$STORE_YARN_LOCK_NAME") + + assertOutputContains(YARN_LOCK_MISMATCH_MESSAGE) + } + + //yarn.lock was updated + build("compileKotlinJs") { + assertTasksUpToDate(":$STORE_YARN_LOCK_NAME") + } + + // check if everything ok without build/js/yarn.lock + build("clean") { + assertTasksExecuted(":clean") + } + + build("clean") { + assertTasksUpToDate(":clean") + } + } + } + @DisplayName("webpack configuration is valid") @GradleTest fun testWebpackConfig(gradleVersion: GradleVersion) { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-yarn-lock-project/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-yarn-lock-project/build.gradle.kts new file mode 100644 index 00000000000..8105e2909dd --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-yarn-lock-project/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("org.jetbrains.kotlin.js") +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-js") + testImplementation("org.jetbrains.kotlin:kotlin-test-js") +} + +repositories { + mavenLocal() + mavenCentral() +} + +kotlin { + js { + useCommonJs() + binaries.executable() + nodejs { + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-yarn-lock-project/gradle.properties b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-yarn-lock-project/gradle.properties new file mode 100644 index 00000000000..a4a181c3de2 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-yarn-lock-project/gradle.properties @@ -0,0 +1 @@ +kotlin.tests.individualTaskReports=true \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-yarn-lock-project/src/main/kotlin/Base.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-yarn-lock-project/src/main/kotlin/Base.kt new file mode 100644 index 00000000000..e69d852c3e0 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-yarn-lock-project/src/main/kotlin/Base.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2010-2019 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 com.example + +fun best(): Int { + return 42 +} + +fun simpleBest(): Int { + return 73 +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/dukat/DukatCompilationResolverPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/dukat/DukatCompilationResolverPlugin.kt index 6e006a41401..3e6bfccff43 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/dukat/DukatCompilationResolverPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/dukat/DukatCompilationResolverPlugin.kt @@ -52,7 +52,11 @@ internal class DukatCompilationResolverPlugin( it.group = DUKAT_TASK_GROUP it.description = "Integrated generation Kotlin/JS external declarations for .d.ts files in $compilation" it.externalsOutputFormat = externalsOutputFormat - it.dependsOn(nodeJs_.npmInstallTaskProvider, npmProject.packageJsonTask) + it.dependsOn( + nodeJs_.npmInstallTaskProvider, + nodeJs_.storeYarnLockTaskProvider, + npmProject.packageJsonTask + ) } } @@ -82,7 +86,11 @@ internal class DukatCompilationResolverPlugin( ) { it.group = DUKAT_TASK_GROUP it.description = "Generate Kotlin/JS external declarations for .d.ts files of all NPM dependencies in ${compilation}" - it.dependsOn(nodeJs_.npmInstallTaskProvider, npmProject.packageJsonTask) + it.dependsOn( + nodeJs_.npmInstallTaskProvider, + nodeJs_.storeYarnLockTaskProvider, + npmProject.packageJsonTask + ) } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinBrowserJsIr.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinBrowserJsIr.kt index c19ac710f0a..83782e3772e 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinBrowserJsIr.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinBrowserJsIr.kt @@ -47,7 +47,11 @@ abstract class KotlinBrowserJsIr @Inject constructor(target: KotlinJsIrTarget) : get() = "Run all ${target.name} tests inside browser using karma and webpack" override fun configureTestDependencies(test: KotlinJsTest) { - test.dependsOn(nodeJs.npmInstallTaskProvider, nodeJs.nodeJsSetupTaskProvider) + test.dependsOn( + nodeJs.npmInstallTaskProvider, + nodeJs.storeYarnLockTaskProvider, + nodeJs.nodeJsSetupTaskProvider + ) } override fun configureDefaultTestFramework(test: KotlinJsTest) { @@ -256,6 +260,7 @@ abstract class KotlinBrowserJsIr @Inject constructor(target: KotlinJsIrTarget) : ) { dependsOn( nodeJs.npmInstallTaskProvider, + nodeJs.storeYarnLockTaskProvider, target.project.tasks.named(compilation.processResourcesTaskName) ) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinNodeJsIr.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinNodeJsIr.kt index dcf5d0520fa..d3c05a4607a 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinNodeJsIr.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinNodeJsIr.kt @@ -47,7 +47,11 @@ abstract class KotlinNodeJsIr @Inject constructor(target: KotlinJsIrTarget) : } override fun configureTestDependencies(test: KotlinJsTest) { - test.dependsOn(nodeJs.npmInstallTaskProvider, nodeJs.nodeJsSetupTaskProvider) + test.dependsOn( + nodeJs.npmInstallTaskProvider, + nodeJs.storeYarnLockTaskProvider, + nodeJs.nodeJsSetupTaskProvider + ) } override fun configureDefaultTestFramework(test: KotlinJsTest) { diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsExec.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsExec.kt index 305a4f74fde..7d51b6eb020 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsExec.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsExec.kt @@ -100,7 +100,10 @@ constructor( it.nodeJs = nodeJs it.executable = nodeJs.requireConfigured().nodeExecutable it.workingDir = npmProject.dir - it.dependsOn(nodeJs.npmInstallTaskProvider) + it.dependsOn( + nodeJs.npmInstallTaskProvider, + nodeJs.storeYarnLockTaskProvider, + ) it.dependsOn(compilation.compileKotlinTaskProvider) if (compilation.platformType == KotlinPlatformType.wasm) { it.nodeArgs.addWasmExperimentalArguments() diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt index 29abb8eb6bb..570723e7673 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt @@ -19,6 +19,9 @@ import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmCachesSetup import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.RootPackageJsonTask import org.jetbrains.kotlin.gradle.targets.js.yarn.Yarn +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask.Companion.RESTORE_YARN_LOCK_NAME +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask.Companion.STORE_YARN_LOCK_NAME import org.jetbrains.kotlin.gradle.tasks.internal.CleanableStore import java.io.File import java.io.Serializable @@ -73,6 +76,14 @@ open class NodeJsRootExtension(@Transient val rootProject: Project) : Configurat val npmCachesSetupTaskProvider: TaskProvider? get() = rootProject?.tasks?.withType(KotlinNpmCachesSetup::class.java)?.named(KotlinNpmCachesSetup.NAME) + @Suppress("UNNECESSARY_SAFE_CALL", "SAFE_CALL_WILL_CHANGE_NULLABILITY") // TODO: investigate this warning; fixing it breaks integration tests. + val storeYarnLockTaskProvider: TaskProvider? + get() = rootProject?.tasks?.withType(YarnLockCopyTask::class.java)?.named(STORE_YARN_LOCK_NAME) + + @Suppress("UNNECESSARY_SAFE_CALL", "SAFE_CALL_WILL_CHANGE_NULLABILITY") // TODO: investigate this warning; fixing it breaks integration tests. + val restoreYarnLockTaskProvider: TaskProvider? + get() = rootProject?.tasks?.withType(YarnLockCopyTask::class.java)?.named(RESTORE_YARN_LOCK_NAME) + val rootPackageDir: File by lazy { rootProject.buildDir.resolve("js") } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/resolver/KotlinCompilationNpmResolver.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/resolver/KotlinCompilationNpmResolver.kt index 2ae344b7d12..229e01ec2de 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/resolver/KotlinCompilationNpmResolver.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/resolver/KotlinCompilationNpmResolver.kt @@ -80,7 +80,10 @@ internal class KotlinCompilationNpmResolver( npmProject.publicPackageJsonTaskName, listOf(compilation) ) { - it.dependsOn(nodeJs_.npmInstallTaskProvider) + it.dependsOn( + nodeJs_.npmInstallTaskProvider, + nodeJs_.storeYarnLockTaskProvider, + ) it.dependsOn(packageJsonTaskHolder) }.also { packageJsonTask -> if (compilation.isMain()) { diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/resolver/KotlinProjectNpmResolver.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/resolver/KotlinProjectNpmResolver.kt index 36796a77b06..ed080536da6 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/resolver/KotlinProjectNpmResolver.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/resolver/KotlinProjectNpmResolver.kt @@ -68,7 +68,10 @@ internal class KotlinProjectNpmResolver( nodeJs.taskRequirements.addTaskRequirements(task) } task.dependsOn(packageJsonTaskHolder) - task.dependsOn(nodeJs.npmInstallTaskProvider) + task.dependsOn( + nodeJs.npmInstallTaskProvider, + nodeJs.storeYarnLockTaskProvider, + ) } } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/tasks/KotlinPackageJsonTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/tasks/KotlinPackageJsonTask.kt index a4302895b61..519661ab1a0 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/tasks/KotlinPackageJsonTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/tasks/KotlinPackageJsonTask.kt @@ -133,6 +133,7 @@ abstract class KotlinPackageJsonTask : DefaultTask() { nodeJs.rootPackageJsonTaskProvider!!.configure { it.mustRunAfter(packageJsonTask) } compilation.compileKotlinTaskProvider.dependsOn(npmInstallTask!!) + compilation.compileKotlinTaskProvider.dependsOn(nodeJs.storeYarnLockTaskProvider!!) compilation.compileKotlinTaskProvider.dependsOn(packageJsonTask) return packageJsonTask diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/subtargets/KotlinBrowserJs.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/subtargets/KotlinBrowserJs.kt index 3ca68ba2b7a..b75b8473eed 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/subtargets/KotlinBrowserJs.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/subtargets/KotlinBrowserJs.kt @@ -278,6 +278,7 @@ abstract class KotlinBrowserJs @Inject constructor(target: KotlinJsTarget) : ) { dependsOn( nodeJs.npmInstallTaskProvider, + nodeJs.storeYarnLockTaskProvider, target.project.tasks.named(compilation.processResourcesTaskName) ) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/subtargets/KotlinJsSubTarget.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/subtargets/KotlinJsSubTarget.kt index 5fecac274ef..9bbf7f9a20c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/subtargets/KotlinJsSubTarget.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/subtargets/KotlinJsSubTarget.kt @@ -106,7 +106,12 @@ abstract class KotlinJsSubTarget( val compileOutputFile = compileTask.flatMap { it.outputFileProperty } testJs.inputFileProperty.fileProvider(compileOutputFile) - testJs.dependsOn(nodeJs.npmInstallTaskProvider, compileTask, nodeJs.nodeJsSetupTaskProvider) + testJs.dependsOn( + nodeJs.npmInstallTaskProvider, + nodeJs.storeYarnLockTaskProvider, + compileTask, + nodeJs.nodeJsSetupTaskProvider + ) testJs.onlyIf { compileOutputFile.get().exists() diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnEnv.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnEnv.kt index 1b0daf5cfeb..f386036b24c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnEnv.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnEnv.kt @@ -17,4 +17,7 @@ data class YarnEnv( val ivyDependency: String, val standalone: Boolean, val ignoreScripts: Boolean, + val yarnLockMismatchReport: YarnLockMismatchReport, + val reportNewYarnLock: Boolean, + val yarnLockAutoReplace: Boolean, ) : Serializable diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnLockCopyTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnLockCopyTask.kt index f8dbcd64bdc..872f16bc34b 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnLockCopyTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnLockCopyTask.kt @@ -6,15 +6,18 @@ package org.jetbrains.kotlin.gradle.targets.js.yarn import org.gradle.api.DefaultTask +import org.gradle.api.GradleException import org.gradle.api.file.FileSystemOperations import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.* +import org.jetbrains.kotlin.gradle.utils.contentEquals import java.io.File import javax.inject.Inject abstract class YarnLockCopyTask : DefaultTask() { + @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE) abstract val inputFile: RegularFileProperty @@ -35,11 +38,66 @@ abstract class YarnLockCopyTask : DefaultTask() { abstract val fs: FileSystemOperations @TaskAction - fun copy() { + open fun copy() { fs.copy { copy -> copy.from(inputFile) copy.rename { fileName.get() } copy.into(outputDirectory) } } + + companion object { + val STORE_YARN_LOCK_NAME = "kotlinStoreYarnLock" + val RESTORE_YARN_LOCK_NAME = "kotlinRestoreYarnLock" + val UPGRADE_YARN_LOCK = "kotlinUpgradeYarnLock" + } +} + +abstract class YarnLockStoreTask : YarnLockCopyTask() { + @get:Internal + abstract val yarnLockMismatchReportService: Property + + @Input + lateinit var yarnLockMismatchReport: Provider + + @Input + lateinit var reportNewYarnLock: Provider + + @Input + lateinit var yarnLockAutoReplace: Provider + + override fun copy() { + val outputFile = outputDirectory.get().asFile.resolve(fileName.get()) + + val shouldReportMismatch = if (!outputFile.exists()) { + reportNewYarnLock.get() + } else { + yarnLockMismatchReport.get() != YarnLockMismatchReport.NONE && !contentEquals(inputFile.get().asFile, outputFile) + } + + if (!outputFile.exists() || yarnLockAutoReplace.get()) { + super.copy() + } + + if (shouldReportMismatch) { + when (yarnLockMismatchReport.get()) { + YarnLockMismatchReport.NONE -> {} + YarnLockMismatchReport.WARNING -> { + logger.warn(YARN_LOCK_MISMATCH_MESSAGE) + } + YarnLockMismatchReport.FAIL -> throw GradleException(YARN_LOCK_MISMATCH_MESSAGE) + YarnLockMismatchReport.FAIL_AFTER_BUILD -> { + yarnLockMismatchReportService.get().failOnClose() + } + else -> error("Unknown yarn.lock mismatch report kind") + } + } + } +} + +enum class YarnLockMismatchReport { + NONE, + WARNING, + FAIL, + FAIL_AFTER_BUILD, } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnLockMismatchReportService.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnLockMismatchReportService.kt new file mode 100644 index 00000000000..e708407a7b6 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnLockMismatchReportService.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2022 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.yarn + +import org.gradle.api.GradleException +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters +import org.gradle.tooling.events.FinishEvent +import org.gradle.tooling.events.OperationCompletionListener +import org.jetbrains.kotlin.gradle.utils.contentEquals + +abstract class YarnLockMismatchReportService : BuildService, AutoCloseable, + OperationCompletionListener { + @Volatile + private var shouldFailOnClose: Boolean = false + + // Some parameters for the web server + internal interface Params : BuildServiceParameters { + val inputFile: RegularFileProperty + val outputFile: RegularFileProperty + val shouldFailOnClose: Property + val reportNewYarnLock: Property + } + + fun failOnClose() { + shouldFailOnClose = true + } + + override fun onFinish(event: FinishEvent?) { + // noop + } + + override fun close() { + if (!parameters.inputFile.get().asFile.exists()) { + return + } + + if (parameters.reportNewYarnLock.get() && !parameters.outputFile.get().asFile.exists()) { + throw GradleException(YARN_LOCK_MISMATCH_MESSAGE) + } + + if (shouldFailOnClose || parameters.shouldFailOnClose.get() && !contentEquals(parameters.inputFile.get().asFile, parameters.outputFile.get().asFile)) { + throw GradleException(YARN_LOCK_MISMATCH_MESSAGE) + } + } +} + +val YARN_LOCK_MISMATCH_MESSAGE = "yarn.lock was changed. Run `kotlinActualizeYarnLock` task to actualize yarn.lock file" \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnPlugin.kt index 3f9519f7b69..d5fe980c97c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnPlugin.kt @@ -9,12 +9,16 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.BasePlugin import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.gradle.plugin.BuildEventsListenerRegistryHolder import org.jetbrains.kotlin.gradle.targets.js.MultiplePluginDeclarationDetector import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin import org.jetbrains.kotlin.gradle.targets.js.npm.RequiresNpmDependencies import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.implementing import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.RootPackageJsonTask +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask.Companion.RESTORE_YARN_LOCK_NAME +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask.Companion.STORE_YARN_LOCK_NAME +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask.Companion.UPGRADE_YARN_LOCK import org.jetbrains.kotlin.gradle.tasks.CleanDataTask import org.jetbrains.kotlin.gradle.tasks.registerTask @@ -69,14 +73,42 @@ open class YarnPlugin : Plugin { it.dependsOn(packageJsonUmbrella) } - val storeYarnLock = tasks.register("kotlinStoreYarnLock", YarnLockCopyTask::class.java) { - it.dependsOn(kotlinNpmInstall) - it.inputFile.set(nodeJs.rootPackageDir.resolve("yarn.lock")) - it.outputDirectory.set(yarnRootExtension.lockFileDirectory) - it.fileName.set(yarnRootExtension.lockFileName) + val service = project.gradle.sharedServices + .registerIfAbsent("kotlin-store-yarn-lock-mismatch-reporter", YarnLockMismatchReportService::class.java) { + it.parameters.inputFile.set(nodeJs.rootPackageDir.resolve("yarn.lock")) + it.parameters.outputFile.set(yarnRootExtension.lockFileDirectory.resolve(yarnRootExtension.lockFileName)) + it.parameters.shouldFailOnClose.set( + provider { yarnRootExtension.requireConfigured().yarnLockMismatchReport == YarnLockMismatchReport.FAIL_AFTER_BUILD } + ) + it.parameters.reportNewYarnLock.set( + provider { yarnRootExtension.requireConfigured().reportNewYarnLock } + ) + } + + BuildEventsListenerRegistryHolder.getInstance(project).listenerRegistry + .onTaskCompletion(service) + + val storeYarnLock = tasks.register(STORE_YARN_LOCK_NAME, YarnLockStoreTask::class.java) { task -> + task.dependsOn(kotlinNpmInstall) + task.inputFile.set(nodeJs.rootPackageDir.resolve("yarn.lock")) + task.outputDirectory.set(yarnRootExtension.lockFileDirectory) + task.fileName.set(yarnRootExtension.lockFileName) + + task.yarnLockMismatchReportService.set(service) + + task.yarnLockMismatchReport = provider { yarnRootExtension.requireConfigured().yarnLockMismatchReport } + task.reportNewYarnLock = provider { yarnRootExtension.requireConfigured().reportNewYarnLock } + task.yarnLockAutoReplace = provider { yarnRootExtension.requireConfigured().yarnLockAutoReplace } } - val restoreYarnLock = tasks.register("kotlinRestoreYarnLock", YarnLockCopyTask::class.java) { + tasks.register(UPGRADE_YARN_LOCK, YarnLockCopyTask::class.java) { task -> + task.dependsOn(kotlinNpmInstall) + task.inputFile.set(nodeJs.rootPackageDir.resolve("yarn.lock")) + task.outputDirectory.set(yarnRootExtension.lockFileDirectory) + task.fileName.set(yarnRootExtension.lockFileName) + } + + val restoreYarnLock = tasks.register(RESTORE_YARN_LOCK_NAME, YarnLockCopyTask::class.java) { val lockFile = yarnRootExtension.lockFileDirectory.resolve(yarnRootExtension.lockFileName) it.inputFile.set(yarnRootExtension.lockFileDirectory.resolve(yarnRootExtension.lockFileName)) it.outputDirectory.set(nodeJs.rootPackageDir) @@ -88,7 +120,6 @@ open class YarnPlugin : Plugin { kotlinNpmInstall.configure { it.dependsOn(restoreYarnLock) - it.finalizedBy(storeYarnLock) } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnRootExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnRootExtension.kt index 9dd89c0241c..2f1424a29d8 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnRootExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnRootExtension.kt @@ -44,6 +44,12 @@ open class YarnRootExtension( var ignoreScripts by Property(true) + var yarnLockMismatchReport: YarnLockMismatchReport by Property(YarnLockMismatchReport.FAIL) + + var reportNewYarnLock: Boolean by Property(false) + + var yarnLockAutoReplace: Boolean by Property(false) + val yarnSetupTaskProvider: TaskProvider get() = project.tasks .withType(YarnSetupTask::class.java) @@ -107,6 +113,9 @@ open class YarnRootExtension( standalone = !download, ivyDependency = "com.yarnpkg:yarn:$version@tar.gz", ignoreScripts = ignoreScripts, + yarnLockMismatchReport = yarnLockMismatchReport, + reportNewYarnLock = reportNewYarnLock, + yarnLockAutoReplace = yarnLockAutoReplace, ) } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/fileUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/fileUtils.kt index 5b043d2a0c4..cc7f24a7f8d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/fileUtils.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/fileUtils.kt @@ -95,3 +95,23 @@ internal inline fun withTemporaryDirectory(prefix: String, action: (director directory.deleteRecursively() } } + +fun contentEquals(file1: File, file2: File): Boolean { + file1.useLines { seq1 -> + file2.useLines { seq2 -> + val iterator1 = seq1.iterator() + val iterator2 = seq2.iterator() + + while(iterator1.hasNext() == iterator2.hasNext()) { + + if (!iterator1.hasNext()) return true + + if (iterator1.next() != iterator2.next()) { + return false + } + } + + return true + } + } +}