[Gradle, JS] Add npm support but default Yarn package manager left

Co-authored-by: Alexander Likhachev <Alexander.Likhachev@jetbrains.com>


^KT-64119 fixed
This commit is contained in:
Ilya Goncharov
2024-01-26 13:38:23 +00:00
committed by Space Team
parent f9348c69ba
commit d18660c8f1
57 changed files with 1609 additions and 717 deletions
@@ -112,10 +112,30 @@ class JsIrConfigurationCacheIT : KGPBaseTest() {
@DisplayName("KT-48241: configuration cache works with test dependencies")
@GradleTest
fun testTestDependencies(gradleVersion: GradleVersion) {
project("kotlin-js-project-with-test-dependencies", gradleVersion) {
assertSimpleConfigurationCacheScenarioWorks(
"assemble", "kotlinStorePackageLock",
buildOptions = defaultBuildOptions.copy(
jsOptions = defaultBuildOptions.jsOptions?.copy(
yarn = false
)
),
executedTaskNames = listOf(":rootPackageJson")
)
}
}
@DisplayName("KT-48241: configuration cache works with test dependencies for yarn.lock")
@GradleTest
fun testTestDependenciesYarnLock(gradleVersion: GradleVersion) {
project("kotlin-js-project-with-test-dependencies", gradleVersion) {
assertSimpleConfigurationCacheScenarioWorks(
"assemble", "kotlinStoreYarnLock",
buildOptions = defaultBuildOptions,
buildOptions = defaultBuildOptions.copy(
jsOptions = defaultBuildOptions.jsOptions?.copy(
yarn = true
)
),
executedTaskNames = listOf(":rootPackageJson")
)
}
@@ -28,16 +28,47 @@ class JsSetupConfigurationCacheIT : KGPBaseTest() {
@DisplayName("Check Node.JS setup on different platforms")
@GradleTest
fun checkNodeJsSetup(gradleVersion: GradleVersion) {
project("kotlin-js-browser-project", gradleVersion) {
build("kotlinUpgradeYarnLock") {
assertTasksExecuted(":kotlinUpgradeYarnLock")
assertConfigurationCacheStored()
}
project(
"kotlin-js-browser-project",
gradleVersion,
buildOptions = defaultBuildOptions.copy(
jsOptions = defaultBuildOptions.jsOptions?.copy(
yarn = false
)
)
) {
checkNodeJsSetup("kotlinUpgradePackageLock")
}
}
build("kotlinUpgradeYarnLock") {
assertTasksUpToDate(":kotlinUpgradeYarnLock")
assertConfigurationCacheReused()
}
// hack to be run on Mac m*
@DisplayName("Check Node.JS setup on different platforms with Yarn")
@GradleTest
fun checkNodeJsSetupYarn(gradleVersion: GradleVersion) {
project(
"kotlin-js-browser-project",
gradleVersion,
buildOptions = defaultBuildOptions.copy(
jsOptions = defaultBuildOptions.jsOptions?.copy(
yarn = true
)
)
) {
checkNodeJsSetup("kotlinUpgradeYarnLock")
}
}
private fun TestProject.checkNodeJsSetup(
upgradeTask: String
) {
build(upgradeTask) {
assertTasksExecuted(":$upgradeTask")
assertConfigurationCacheStored()
}
build(upgradeTask) {
assertTasksUpToDate(":$upgradeTask")
assertConfigurationCacheReused()
}
}
}
@@ -12,12 +12,15 @@ import org.gradle.testkit.runner.BuildResult
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.targets.js.dsl.Distribution
import org.jetbrains.kotlin.gradle.targets.js.ir.KLIB_TYPE
import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask
import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask.Companion.PACKAGE_LOCK_MISMATCH_MESSAGE
import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask.Companion.RESTORE_PACKAGE_LOCK_NAME
import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask.Companion.STORE_PACKAGE_LOCK_NAME
import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask.Companion.UPGRADE_PACKAGE_LOCK
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject
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.YarnLockCopyTask.Companion.STORE_YARN_LOCK_NAME
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask.Companion.UPGRADE_YARN_LOCK
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask.Companion.YARN_LOCK_MISMATCH_MESSAGE
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin
import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.testbase.TestVersions.Gradle.G_7_6
import org.jetbrains.kotlin.gradle.util.replaceText
@@ -34,6 +37,7 @@ import kotlin.test.assertTrue
@JsGradlePluginTests
class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
@DisplayName("TS type declarations are generated")
@GradleTest
fun generateDts(gradleVersion: GradleVersion) {
@@ -171,37 +175,6 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
}
}
@DisplayName("js composite build works with yarn.lock persistence")
@GradleTest
@GradleTestVersions(minVersion = G_7_6)
fun testJsCompositeBuildWithUpgradeYarnLock(gradleVersion: GradleVersion) {
project("js-composite-build", gradleVersion) {
buildGradleKts.modify(::transformBuildScriptWithPluginsDsl)
subProject("lib").apply {
buildGradleKts.modify(::transformBuildScriptWithPluginsDsl)
}
subProject("base").apply {
buildGradleKts.modify(::transformBuildScriptWithPluginsDsl)
}
build("kotlinUpgradeYarnLock") {
assertTasksExecuted(":base:publicPackageJson")
assertTasksExecuted(":lib:lib-2:publicPackageJson")
assertTasksExecuted(":kotlinNpmInstall")
assertTasksExecuted(":kotlinUpgradeYarnLock")
}
build(":nodeTest") {
assertTasksUpToDate(":base:publicPackageJson")
assertTasksUpToDate(":lib:lib-2:publicPackageJson")
assertTasksUpToDate(":kotlinNpmInstall")
assertTasksExecuted(":kotlinStoreYarnLock")
}
}
}
@GradleTest
fun testJsIrIncrementalInParallel(gradleVersion: GradleVersion) {
project("kotlin-js-browser-project", gradleVersion) {
@@ -1029,7 +1002,15 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
@DisplayName("public NPM dependencies are included into package.json")
@GradleTest
fun testPackageJsonWithPublicNpmDependencies(gradleVersion: GradleVersion) {
project("npm-dependencies", gradleVersion) {
project(
"npm-dependencies",
gradleVersion,
buildOptions = defaultBuildOptions.copy(
jsOptions = defaultBuildOptions.jsOptions?.copy(
yarn = false
)
)
) {
build("jsJar") {
val archive = projectPath
.resolve("build/libs")
@@ -1097,8 +1078,8 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
}
}
build(UPGRADE_YARN_LOCK) {
assertTasksExecuted(":$UPGRADE_YARN_LOCK")
build(UPGRADE_PACKAGE_LOCK) {
assertTasksExecuted(":$UPGRADE_PACKAGE_LOCK")
}
build("jsJar") {
@@ -1216,10 +1197,18 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
}
}
@DisplayName("yarn resolutions works")
@DisplayName("npm overrides works")
@GradleTest
fun testYarnResolution(gradleVersion: GradleVersion) {
project("kotlin-js-yarn-resolutions", gradleVersion) {
fun testNpmOverrides(gradleVersion: GradleVersion) {
project(
"kotlin-js-npm-overrides",
gradleVersion,
buildOptions = defaultBuildOptions.copy(
jsOptions = defaultBuildOptions.jsOptions?.copy(
yarn = false
)
)
) {
build("packageJson", "rootPackageJson", "kotlinNpmInstall") {
fun getPackageJson() =
projectPath.resolve("build/js")
@@ -1229,16 +1218,16 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
}
val name = "lodash"
val version = getPackageJson().resolutions?.get(name)
val version = getPackageJson().overrides?.get(name)
val requiredVersion = ">=1.0.0 <1.2.1 || >1.4.0 <2.0.0"
assertTrue("Root package.json must have resolution $name with version $requiredVersion, but $version found") {
assertTrue("Root package.json must override $name with version $requiredVersion, but $version found") {
version == requiredVersion
}
val react = "react"
val reactVersion = getPackageJson().resolutions?.get(react)
val reactVersion = getPackageJson().overrides?.get(react)
val requiredReactVersion = "16.0.0"
assertTrue("Root package.json must have resolution $react with version $requiredReactVersion, but $reactVersion found") {
assertTrue("Root package.json must override $react with version $requiredReactVersion, but $reactVersion found") {
reactVersion == requiredReactVersion
}
}
@@ -1374,176 +1363,6 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
}
}
@DisplayName("Failing with yarn.lock update")
@GradleTest
fun testFailingWithYarnLockUpdate(gradleVersion: GradleVersion) {
project("kotlin-js-yarn-lock-project", gradleVersion) {
build(STORE_YARN_LOCK_NAME) {
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<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().reportNewYarnLock = true
}
""".trimIndent()
}
buildAndFail(STORE_YARN_LOCK_NAME) {
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<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().yarnLockMismatchReport =
org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport.FAIL
}
""".trimIndent()
}
buildAndFail(STORE_YARN_LOCK_NAME) {
assertTasksFailed(":$STORE_YARN_LOCK_NAME")
}
// yarn.lock was not updated
buildAndFail(STORE_YARN_LOCK_NAME) {
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<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().yarnLockMismatchReport =
org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport.WARNING
}
""".trimIndent()
}
build(STORE_YARN_LOCK_NAME) {
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<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().yarnLockMismatchReport =
org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport.NONE
}
""".trimIndent()
}
build(STORE_YARN_LOCK_NAME) {
assertTasksExecuted(":$STORE_YARN_LOCK_NAME")
assertOutputDoesNotContain(YARN_LOCK_MISMATCH_MESSAGE)
}
build(UPGRADE_YARN_LOCK) {
assertTasksExecuted(":$UPGRADE_YARN_LOCK")
}
buildGradleKts.modify {
it.replace("implementation(npm(\"decamelize\", \"6.0.0\"))", "")
}
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<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().yarnLockMismatchReport =
org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport.NONE
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().reportNewYarnLock =
true
}
""".trimIndent()
}
build(STORE_YARN_LOCK_NAME) {
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<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().yarnLockMismatchReport =
org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport.FAIL
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().yarnLockAutoReplace =
true
}
""".trimIndent()
}
buildAndFail(STORE_YARN_LOCK_NAME) {
assertTasksFailed(":$STORE_YARN_LOCK_NAME")
}
//yarn.lock was updated
build(STORE_YARN_LOCK_NAME) {
assertTasksExecuted(":$STORE_YARN_LOCK_NAME")
}
buildGradleKts.modify {
it.replace("implementation(npm(\"decamelize\", \"6.0.0\"))", "")
}
// check if everything ok without build/js/yarn.lock
build("clean") {
assertTasksExecuted(":clean")
}
build("clean") {
assertTasksUpToDate(":clean")
}
buildAndFail(STORE_YARN_LOCK_NAME) {
assertTasksFailed(":$STORE_YARN_LOCK_NAME")
}
projectPath.resolve("kotlin-js-store").deleteRecursively()
//check if independent tasks can be executed
build("help") {
assertTasksExecuted(":help")
}
}
}
@DisplayName("webpack configuration is valid")
@GradleTest
fun testWebpackConfig(gradleVersion: GradleVersion) {
@@ -1622,7 +1441,15 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
@DisplayName("Disable download should not download Node.JS and Yarn")
@GradleTest
fun testNodeJsAndYarnNotDownloaded(gradleVersion: GradleVersion) {
project("nodeJsDownload", gradleVersion) {
project(
"nodeJsDownload",
gradleVersion,
buildOptions = defaultBuildOptions.copy(
jsOptions = defaultBuildOptions.jsOptions?.copy(
yarn = true
)
)
) {
buildGradleKts.modify {
it + "\n" +
"""
@@ -1640,71 +1467,6 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
}
}
@DisplayName("Yarn.lock persistence")
@GradleTest
fun testYarnLockStore(gradleVersion: GradleVersion) {
project("nodeJsDownload", gradleVersion) {
build("assemble", "kotlinStoreYarnLock") {
assertFileExists(projectPath.resolve("kotlin-js-store").resolve("yarn.lock"))
assert(
projectPath
.resolve("kotlin-js-store")
.resolve("yarn.lock")
.readText() == projectPath.resolve("build/js/yarn.lock").readText()
)
}
}
}
@DisplayName("Yarn ignore scripts")
@GradleTest
fun testYarnIgnoreScripts(gradleVersion: GradleVersion) {
project("nodeJsDownload", gradleVersion) {
buildGradleKts.modify {
it + "\n" +
"""
dependencies {
implementation(npm("puppeteer", "11.0.0"))
}
""".trimIndent()
}
build("assemble", "kotlinNpmInstall") {
assert(
projectPath
.resolve("build")
.resolve("js")
.resolve("node_modules")
.resolve("puppeteer")
.resolve(".local-chromium")
.notExists()
) {
"Chromium should not be installed with --ignore-scripts"
}
}
buildGradleKts.modify {
it + "\n" +
"""
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().ignoreScripts = false
}
""".trimIndent()
}
build("clean")
build("assemble", "kotlinNpmInstall") {
assertDirectoryExists(
projectPath
.resolve("build")
.resolve("js")
.resolve("node_modules")
.resolve("puppeteer")
.resolve(".local-chromium")
)
}
}
}
@DisplayName("js files from dependency are installed")
@GradleTest
fun testKotlinJsDependencyWithJsFiles(gradleVersion: GradleVersion) {
@@ -1754,7 +1516,7 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
}
}
@DisplayName("test package.jsom skipped with main package.json was up-to-date")
@DisplayName("test package.json skipped with main package.json was up-to-date")
@GradleTest
fun testPackageJsonSkippedWithUpToDatePackageJsons(gradleVersion: GradleVersion) {
project("kotlin-js-browser-library-project", gradleVersion) {
@@ -0,0 +1,464 @@
/*
* Copyright 2010-2020 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
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask
import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask.Companion.PACKAGE_LOCK_MISMATCH_MESSAGE
import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask.Companion.RESTORE_PACKAGE_LOCK_NAME
import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask.Companion.STORE_PACKAGE_LOCK_NAME
import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask.Companion.UPGRADE_PACKAGE_LOCK
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin
import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.testbase.TestVersions.Gradle.G_7_6
import org.junit.jupiter.api.DisplayName
import kotlin.io.path.deleteRecursively
import kotlin.io.path.notExists
import kotlin.io.path.readText
class NpmGradlePluginIT : PackageManagerGradlePluginIT() {
override val yarn: Boolean = false
override val plugin = "nodejs.NodeJsRootPlugin"
override val extension = "npm.NpmExtension"
override val upgradeTaskName: String = UPGRADE_PACKAGE_LOCK
override val storeTaskName = STORE_PACKAGE_LOCK_NAME
override val restoreTaskName = RESTORE_PACKAGE_LOCK_NAME
override val reportNewLockFile = "reportNewPackageLock"
override val lockMismatchReport = "packageLockMismatchReport"
override val lockFileAutoReplace = "packageLockAutoReplace"
override val mismatchReportType = "npm.LockFileMismatchReport"
override val lockFileName: String = LockCopyTask.PACKAGE_LOCK
override val setProperty: (String) -> String = { ".set($it)" }
}
class YarnGradlePluginIT : PackageManagerGradlePluginIT() {
override val yarn: Boolean = true
override val plugin: String = "yarn.YarnPlugin"
override val extension: String = "yarn.YarnRootExtension"
override val upgradeTaskName: String = YarnPlugin.UPGRADE_YARN_LOCK
override val storeTaskName = YarnPlugin.STORE_YARN_LOCK_NAME
override val restoreTaskName: String = YarnPlugin.RESTORE_YARN_LOCK_NAME
override val reportNewLockFile: String = "reportNewYarnLock"
override val lockMismatchReport: String = "yarnLockMismatchReport"
override val lockFileAutoReplace: String = "yarnLockAutoReplace"
override val mismatchReportType: String = "yarn.YarnLockMismatchReport"
override val lockFileName: String = LockCopyTask.YARN_LOCK
override val setProperty: (String) -> String = { " = $it" }
}
@JsGradlePluginTests
abstract class PackageManagerGradlePluginIT : KGPBaseTest() {
abstract val yarn: Boolean
abstract val plugin: String
abstract val extension: String
abstract val upgradeTaskName: String
abstract val storeTaskName: String
abstract val restoreTaskName: String
abstract val reportNewLockFile: String
abstract val lockMismatchReport: String
abstract val lockFileAutoReplace: String
abstract val mismatchReportType: String
abstract val lockFileName: String
abstract val setProperty: (String) -> String
override val defaultBuildOptions: BuildOptions
get() = super.defaultBuildOptions.copy(
jsOptions = super.defaultBuildOptions.jsOptions?.copy(
yarn = yarn
)
)
@DisplayName("js composite build works with lock file persistence")
@GradleTest
@GradleTestVersions(minVersion = G_7_6)
fun testJsCompositeBuildWithUpgradeLockFile(gradleVersion: GradleVersion) {
project("js-composite-build", gradleVersion) {
testJsCompositeBuildWithUpgradeLockFile(
upgradeTaskName,
storeTaskName
)
}
}
private fun TestProject.testJsCompositeBuildWithUpgradeLockFile(
upgradeTask: String,
storeTask: String,
) {
buildGradleKts.modify(::transformBuildScriptWithPluginsDsl)
subProject("lib").apply {
buildGradleKts.modify(::transformBuildScriptWithPluginsDsl)
}
subProject("base").apply {
buildGradleKts.modify(::transformBuildScriptWithPluginsDsl)
}
build(upgradeTask) {
assertTasksExecuted(":base:publicPackageJson")
assertTasksExecuted(":lib:lib-2:publicPackageJson")
assertTasksExecuted(":kotlinNpmInstall")
assertTasksExecuted(":$upgradeTask")
}
build(":nodeTest") {
assertTasksUpToDate(":base:publicPackageJson")
assertTasksUpToDate(":lib:lib-2:publicPackageJson")
assertTasksUpToDate(":kotlinNpmInstall")
assertTasksExecuted(":$storeTask")
}
}
@DisplayName("Failing with lock file update")
@GradleTest
fun testFailingWithLockFileUpdate(gradleVersion: GradleVersion) {
project("kotlin-js-package-lock-project", gradleVersion) {
testFailingWithLockFileUpdate(
storeTaskName = storeTaskName,
restoreTaskName = restoreTaskName,
upgradeTaskName = upgradeTaskName,
extension = extension,
reportNewLockFile = reportNewLockFile,
lockMismatchReport = lockMismatchReport,
lockFileAutoReplace = lockFileAutoReplace,
mismatchReport = mismatchReportType,
set = setProperty
)
}
}
private fun TestProject.testFailingWithLockFileUpdate(
storeTaskName: String,
restoreTaskName: String,
upgradeTaskName: String,
extension: String,
reportNewLockFile: String,
lockMismatchReport: String,
lockFileAutoReplace: String,
mismatchReport: String,
set: (String) -> String,
) {
build(storeTaskName) {
assertTasksSkipped(":$restoreTaskName")
assertTasksExecuted(":$storeTaskName")
}
projectPath.resolve(LockCopyTask.KOTLIN_JS_STORE).deleteRecursively()
buildGradleKts.modify {
it + "\n" +
"""
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin::class.java) {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.$extension>().$reportNewLockFile${set("true")}
}
""".trimIndent()
}
buildAndFail(storeTaskName) {
assertTasksSkipped(":$restoreTaskName")
assertTasksFailed(":$storeTaskName")
}
buildGradleKts.modify {
it + "\n" +
"""
dependencies {
implementation(npm("decamelize", "6.0.0"))
}
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin::class.java) {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.$extension>().$lockMismatchReport${
set(
"org.jetbrains.kotlin.gradle.targets.js.$mismatchReport.FAIL"
)
}
}
""".trimIndent()
}
buildAndFail(storeTaskName) {
assertTasksExecuted(":$restoreTaskName")
assertTasksFailed(":$storeTaskName")
}
// yarn.lock was not updated
buildAndFail(storeTaskName) {
assertTasksExecuted(":$restoreTaskName")
assertTasksFailed(":$storeTaskName")
}
build(upgradeTaskName) {
assertTasksExecuted(":$restoreTaskName")
assertTasksExecuted(":$upgradeTaskName")
}
buildGradleKts.modify {
val replaced = it.replace("implementation(npm(\"decamelize\", \"6.0.0\"))", "")
replaced + "\n" +
"""
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin::class.java) {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.$extension>().$lockMismatchReport${
set(
"org.jetbrains.kotlin.gradle.targets.js.$mismatchReport.WARNING"
)
}
}
""".trimIndent()
}
build(storeTaskName) {
assertTasksExecuted(":$restoreTaskName")
assertTasksExecuted(":$storeTaskName")
assertOutputContains(PACKAGE_LOCK_MISMATCH_MESSAGE)
}
build(upgradeTaskName) {
assertTasksExecuted(":$restoreTaskName")
assertTasksExecuted(":$upgradeTaskName")
}
buildGradleKts.modify {
it + "\n" +
"""
dependencies {
implementation(npm("decamelize", "6.0.0"))
}
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin::class.java) {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.$extension>().$lockMismatchReport${
set(
"org.jetbrains.kotlin.gradle.targets.js.$mismatchReport.NONE"
)
}
}
""".trimIndent()
}
build(storeTaskName) {
assertTasksExecuted(":$restoreTaskName")
assertTasksExecuted(":$storeTaskName")
assertOutputDoesNotContain(PACKAGE_LOCK_MISMATCH_MESSAGE)
}
build(upgradeTaskName) {
assertTasksExecuted(":$restoreTaskName")
assertTasksExecuted(":$upgradeTaskName")
}
buildGradleKts.modify {
it.replace("implementation(npm(\"decamelize\", \"6.0.0\"))", "")
}
projectPath.resolve(LockCopyTask.KOTLIN_JS_STORE).deleteRecursively()
buildGradleKts.modify {
it + "\n" +
"""
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin::class.java) {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.$extension>().$lockMismatchReport${
set(
"org.jetbrains.kotlin.gradle.targets.js.$mismatchReport.NONE"
)
}
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.$extension>().$reportNewLockFile${
set(
"true"
)
}
}
""".trimIndent()
}
build(storeTaskName) {
assertTasksSkipped(":$restoreTaskName")
assertTasksExecuted(":$storeTaskName")
assertOutputDoesNotContain(PACKAGE_LOCK_MISMATCH_MESSAGE)
}
buildGradleKts.modify {
it + "\n" +
"""
dependencies {
implementation(npm("decamelize", "6.0.0"))
}
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin::class.java) {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.$extension>().$lockMismatchReport${
set(
"org.jetbrains.kotlin.gradle.targets.js.$mismatchReport.FAIL"
)
}
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.$extension>().$lockFileAutoReplace${
set(
"true"
)
}
}
""".trimIndent()
}
buildAndFail(storeTaskName) {
assertTasksUpToDate(":$restoreTaskName")
assertTasksFailed(":$storeTaskName")
}
//yarn.lock was updated
build(storeTaskName) {
assertTasksExecuted(":$restoreTaskName")
assertTasksExecuted(":$storeTaskName")
}
buildGradleKts.modify {
it.replace("implementation(npm(\"decamelize\", \"6.0.0\"))", "")
}
// check if everything ok without build/js/yarn.lock
build("clean") {
assertTasksExecuted(":clean")
}
build("clean") {
assertTasksUpToDate(":clean")
}
buildAndFail(storeTaskName) {
assertTasksExecuted(":$restoreTaskName")
assertTasksFailed(":$storeTaskName")
}
projectPath.resolve(LockCopyTask.KOTLIN_JS_STORE).deleteRecursively()
//check if independent tasks can be executed
build("help") {
assertTasksExecuted(":help")
}
}
@DisplayName("Lock file persistence")
@GradleTest
fun testLockStore(gradleVersion: GradleVersion) {
project("nodeJsDownload", gradleVersion) {
testLockStore(
storeTaskName,
lockFileName
)
}
}
private fun TestProject.testLockStore(
taskName: String,
lockFile: String,
) {
build("assemble", taskName) {
assertFileExists(projectPath.resolve(LockCopyTask.KOTLIN_JS_STORE).resolve(lockFile))
assert(
projectPath
.resolve(LockCopyTask.KOTLIN_JS_STORE)
.resolve(lockFile)
.readText() == projectPath.resolve("build/js/${lockFile}").readText()
)
}
}
@DisplayName("Package manager ignore scripts")
@GradleTest
fun testIgnoreScripts(gradleVersion: GradleVersion) {
project("nodeJsDownload", gradleVersion) {
testIgnoreScripts(
plugin,
extension,
setProperty
)
}
}
private fun TestProject.testIgnoreScripts(
plugin: String,
extension: String,
set: (String) -> String,
) {
buildGradleKts.modify {
it + "\n" +
"""
dependencies {
implementation(npm("puppeteer", "11.0.0"))
}
""".trimIndent()
}
build("assemble", "kotlinNpmInstall") {
assert(
projectPath
.resolve("build")
.resolve("js")
.resolve("node_modules")
.resolve("puppeteer")
.resolve(".local-chromium")
.notExists()
) {
"Chromium should not be installed with --ignore-scripts"
}
}
buildGradleKts.modify {
it + "\n" +
"""
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.$plugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.$extension>().ignoreScripts${set("false")}
}
""".trimIndent()
}
build("clean")
build("assemble", "kotlinNpmInstall") {
assertDirectoryExists(
projectPath
.resolve("build")
.resolve("js")
.resolve("node_modules")
.resolve("puppeteer")
.resolve(".local-chromium")
)
}
}
}
@@ -37,7 +37,7 @@ data class BuildOptions(
val buildCacheEnabled: Boolean = false,
val kaptOptions: KaptOptions? = null,
val androidVersion: String? = null,
val jsOptions: JsOptions? = null,
val jsOptions: JsOptions? = JsOptions(),
val buildReport: List<BuildReportType> = emptyList(),
val usePreciseJavaTracking: Boolean? = null,
val languageVersion: String? = null,
@@ -79,7 +79,8 @@ data class BuildOptions(
data class JsOptions(
val incrementalJs: Boolean? = null,
val incrementalJsKlib: Boolean? = null,
val incrementalJsIr: Boolean? = null
val incrementalJsIr: Boolean? = null,
val yarn: Boolean? = null,
)
data class NativeOptions(
@@ -163,6 +164,7 @@ data class BuildOptions(
jsOptions.incrementalJs?.let { arguments.add("-Pkotlin.incremental.js=$it") }
jsOptions.incrementalJsKlib?.let { arguments.add("-Pkotlin.incremental.js.klib=$it") }
jsOptions.incrementalJsIr?.let { arguments.add("-Pkotlin.incremental.js.ir=$it") }
jsOptions.yarn?.let { arguments.add("-Pkotlin.js.yarn=$it") }
}
if (androidVersion != null) {
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.targets.js.yarn.yarn
plugins {
kotlin("js")
}
@@ -10,6 +12,8 @@ kotlin {
}
}
yarn
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().downloadBaseUrl = null
}
@@ -10,13 +10,13 @@ repositories {
mavenCentral()
}
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin::class.java) {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().apply {
resolution("lodash") {
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin::class.java) {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.npm.NpmExtension>().apply {
override("lodash") {
include("^1.0.0")
exclude("~1.2.1", "1.3.0 - 1.4.0")
}
resolution("react", "16.0.0")
override("react", "16.0.0")
}
}
@@ -0,0 +1,4 @@
rootProject.name = "kotlin-js-npm-overrides"
include("base")
include("lib")
@@ -1,4 +0,0 @@
rootProject.name = "kotlin-js-yarn-resolutions"
include("base")
include("lib")
@@ -25,9 +25,9 @@ allprojects.forEach {
}
}
tasks.named<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask>("kotlinStoreYarnLock") {
tasks.named<org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask>("kotlinStorePackageLock") {
//A little hacky way to make yarn results
inputFile.fileValue(projectDir.resolve("yarnLockStub"))
inputFile.fileValue(projectDir.resolve("packageLockStub"))
}
kotlin {
@@ -5,9 +5,9 @@ plugins {
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().nodeVersion = "20.2.0"
tasks.named<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask>("kotlinStoreYarnLock") {
tasks.named<org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask>("kotlinStorePackageLock") {
//A little hacky way to make yarn results
inputFile.fileValue(projectDir.resolve("yarnLockStub"))
inputFile.fileValue(projectDir.resolve("packageLockStub"))
}
}
@@ -20,9 +20,9 @@ allprojects.forEach {
}
}
tasks.named<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask>("kotlinStoreYarnLock") {
tasks.named<org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask>("kotlinStorePackageLock") {
//A little hacky way to make yarn results
inputFile.fileValue(projectDir.resolve("yarnLockStub"))
inputFile.fileValue(projectDir.resolve("packageLockStub"))
}
repositories {
@@ -5,15 +5,11 @@
package org.jetbrains.kotlin.gradle.internal
import jetbrains.buildServer.messages.serviceMessages.ServiceMessageParserCallback
import org.gradle.api.Project
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.internal.logging.progress.ProgressLogger
import org.gradle.internal.service.ServiceRegistry
import org.gradle.process.ExecResult
import org.gradle.process.internal.ExecAction
import org.gradle.process.internal.ExecActionFactory
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessageOutputStreamHandler
import java.io.ByteArrayOutputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
@@ -26,6 +26,7 @@ import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLI
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_INTERNAL_DIAGNOSTICS_USE_PARSABLE_FORMATTING
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_JS_KARMA_BROWSERS
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_JS_STDLIB_DOM_API_INCLUDED
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_JS_YARN
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ALLOW_LEGACY_DEPENDENCIES
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ANDROID_GRADLE_PLUGIN_COMPATIBILITY_NO_WARN
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ANDROID_SOURCE_SET_LAYOUT_ANDROID_STYLE_NO_WARN
@@ -418,6 +419,9 @@ internal class PropertiesProvider private constructor(private val project: Proje
val stdlibDomApiIncluded: Boolean
get() = booleanProperty(KOTLIN_JS_STDLIB_DOM_API_INCLUDED) ?: true
val yarn: Boolean
get() = booleanProperty(KOTLIN_JS_YARN) ?: true
val kotlinTestInferJvmVariant: Boolean
get() = booleanProperty("kotlin.test.infer.jvm.variant") ?: true
@@ -613,6 +617,7 @@ internal class PropertiesProvider private constructor(private val project: Proje
val KOTLIN_STDLIB_DEFAULT_DEPENDENCY = property("kotlin.stdlib.default.dependency")
val KOTLIN_STDLIB_JDK_VARIANTS_VERSION_ALIGNMENT = property("kotlin.stdlib.jdk.variants.version.alignment")
val KOTLIN_JS_STDLIB_DOM_API_INCLUDED = property("kotlin.js.stdlib.dom.api.included")
val KOTLIN_JS_YARN = property("kotlin.js.yarn")
val KOTLIN_MPP_ENABLE_GRANULAR_SOURCE_SETS_METADATA = property("kotlin.mpp.enableGranularSourceSetsMetadata")
val KOTLIN_MPP_ENABLE_COMPATIBILITY_METADATA_VARIANT = property("kotlin.mpp.enableCompatibilityMetadataVariant")
val KOTLIN_MPP_ENABLE_CINTEROP_COMMONIZATION = property("kotlin.mpp.enableCInteropCommonization")
@@ -138,9 +138,10 @@ abstract class AbstractSetupTask<Env : AbstractEnv, Settings : AbstractSettings<
if (file.exists()) {
file.useLines { seq ->
val list = seq.first().split(" ")
list.size == 2 &&
list[0] == fileHasher.calculateDirHash(destination) &&
list[1] == fileHasher.hash(dist).toByteArray().toHex().also { distHash = it }
list.size == 3 &&
list[0] == CACHE_VERSION &&
list[1] == fileHasher.calculateDirHash(destination) &&
list[2] == fileHasher.hash(dist).toByteArray().toHex().also { distHash = it }
}
} else false
}
@@ -156,11 +157,17 @@ abstract class AbstractSetupTask<Env : AbstractEnv, Settings : AbstractSettings<
extract(dist, destination.parentFile)
destinationHashFile.writeText(
fileHasher.calculateDirHash(destination)!! +
CACHE_VERSION +
" " +
fileHasher.calculateDirHash(destination)!! +
" " +
(distHash ?: fileHasher.hash(dist).toByteArray().toHex())
)
}
abstract fun extract(archive: File, destination: File)
companion object {
const val CACHE_VERSION = "2"
}
}
@@ -52,9 +52,9 @@ abstract class KotlinBrowserJsIr @Inject constructor(target: KotlinJsIrTarget) :
override fun configureTestDependencies(test: KotlinJsTest) {
test.dependsOn(
nodeJs.npmInstallTaskProvider,
nodeJs.storeYarnLockTaskProvider,
nodeJs.nodeJsSetupTaskProvider
)
test.dependsOn(nodeJs.packageManagerExtension.map { it.postInstallTasks })
}
override fun configureDefaultTestFramework(test: KotlinJsTest) {
@@ -267,10 +267,11 @@ abstract class KotlinBrowserJsIr @Inject constructor(target: KotlinJsIrTarget) :
) {
dependsOn(
nodeJs.npmInstallTaskProvider,
nodeJs.storeYarnLockTaskProvider,
target.project.tasks.named(compilation.processResourcesTaskName)
)
dependsOn(nodeJs.packageManagerExtension.map { it.postInstallTasks })
configureOptimization(mode)
this.inputFilesDirectory.set(inputFilesDirectory)
@@ -62,8 +62,8 @@ abstract class KotlinNodeJsIr @Inject constructor(target: KotlinJsIrTarget) :
if (target.wasmTargetType != KotlinWasmTargetType.WASI) {
test.dependsOn(
nodeJsTaskProviders.npmInstallTaskProvider,
nodeJsTaskProviders.storeYarnLockTaskProvider,
)
test.dependsOn(nodeJs.packageManagerExtension.map { it.postInstallTasks })
}
}
@@ -1,7 +1,7 @@
package org.jetbrains.kotlin.gradle.targets.js.nodejs
import org.jetbrains.kotlin.gradle.targets.js.AbstractEnv
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApi
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApiExecution
import org.jetbrains.kotlin.gradle.tasks.internal.CleanableStore
import java.io.File
@@ -17,7 +17,7 @@ data class NodeJsEnv(
override val ivyDependency: String,
override val downloadBaseUrl: String?,
val packageManager: NpmApi,
val packageManager: NpmApiExecution<PackageManagerEnvironment>,
) : AbstractEnv {
val isWindows: Boolean
get() = platformName == "win"
@@ -113,8 +113,8 @@ constructor(
it.workingDir(npmProject.dir)
it.dependsOn(
nodeJsTaskProviders.npmInstallTaskProvider,
nodeJsTaskProviders.storeYarnLockTaskProvider,
)
it.dependsOn(nodeJs.packageManagerExtension.map { it.postInstallTasks })
}
it.dependsOn(nodeJsTaskProviders.nodeJsSetupTaskProvider)
it.dependsOn(compilation.compileKotlinTaskProvider)
@@ -14,14 +14,14 @@ import org.jetbrains.kotlin.gradle.logging.kotlinInfo
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider
import org.jetbrains.kotlin.gradle.targets.js.AbstractSettings
import org.jetbrains.kotlin.gradle.targets.js.NpmVersions
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApi
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinRootNpmResolver
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.PACKAGE_JSON_UMBRELLA_TASK_NAME
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.YarnLockStoreTask
import org.jetbrains.kotlin.gradle.targets.js.yarn.yarn
import org.jetbrains.kotlin.gradle.tasks.internal.CleanableStore
import org.jetbrains.kotlin.gradle.utils.getFile
import org.jetbrains.kotlin.gradle.utils.property
@@ -80,7 +80,7 @@ open class NodeJsRootExtension(
// Release schedule: https://github.com/nodejs/Release
// Actual LTS and Current versions: https://nodejs.org/en/download/
// Older versions and more information, e.g. V8 version inside: https://nodejs.org/en/download/releases/
override var version by Property("18.12.1")
override var version by Property("20.10.0")
override var command by Property("node")
@@ -91,7 +91,7 @@ open class NodeJsRootExtension(
command = value
}
var packageManager: NpmApi by Property(Yarn())
val packageManagerExtension: org.gradle.api.provider.Property<NpmApiExt> = project.objects.property()
val taskRequirements: TasksRequirements
get() = resolver.tasksRequirements
@@ -135,7 +135,7 @@ open class NodeJsRootExtension(
val name = platform.get().name
val architecture = platform.get().arch
val nodeDirName = "node-v$nodeVersion-$name-$architecture"
val nodeDirName = "node-v$version-$name-$architecture"
val cleanableStore = CleanableStore[installationDir.absolutePath]
val nodeDir = cleanableStore[nodeDirName].use()
val isWindows = platform.get().isWindows()
@@ -148,21 +148,23 @@ open class NodeJsRootExtension(
fun getIvyDependency(): String {
val type = if (isWindows) "zip" else "tar.gz"
return "org.nodejs:node:$nodeVersion:$name-$architecture@$type"
return "org.nodejs:node:$version:$name-$architecture@$type"
}
packageManagerExtension.disallowChanges()
return NodeJsEnv(
download = download,
cleanableStore = cleanableStore,
rootPackageDir = rootPackageDirectory.getFile(),
dir = nodeDir,
nodeBinDir = nodeBinDir,
nodeExecutable = getExecutable("node", nodeCommand, "exe"),
nodeExecutable = getExecutable("node", command, "exe"),
platformName = name,
architectureName = architecture,
ivyDependency = getIvyDependency(),
downloadBaseUrl = nodeDownloadBaseUrl,
packageManager = packageManager
downloadBaseUrl = downloadBaseUrl,
packageManager = packageManagerExtension.get().packageManager
)
}
@@ -181,8 +183,9 @@ open class NodeJsRootExtension(
val npmCachesSetupTaskProvider: TaskProvider<out KotlinNpmCachesSetup>
get() = project.tasks.withType(KotlinNpmCachesSetup::class.java).named(KotlinNpmCachesSetup.NAME)
val storeYarnLockTaskProvider: TaskProvider<out YarnLockCopyTask>
get() = project.tasks.withType(YarnLockCopyTask::class.java).named(YarnLockCopyTask.STORE_YARN_LOCK_NAME)
@Deprecated("This is deprecated and will be removed. Use corresponding property from YarnRootExtension")
val storeYarnLockTaskProvider: TaskProvider<YarnLockStoreTask>
get() = project.yarn.storeYarnLockTaskProvider
companion object {
const val EXTENSION_NAME: String = "kotlinNodeJs"
@@ -8,24 +8,29 @@ package org.jetbrains.kotlin.gradle.targets.js.nodejs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.RegularFile
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.internal.configurationTimePropertiesAccessor
import org.jetbrains.kotlin.gradle.plugin.internal.usedAtConfigurationTime
import org.jetbrains.kotlin.gradle.plugin.variantImplementationFactory
import org.jetbrains.kotlin.gradle.targets.js.MultiplePluginDeclarationDetector
import org.jetbrains.kotlin.gradle.targets.js.npm.GradleNodeModulesCache
import org.jetbrains.kotlin.gradle.targets.js.npm.KotlinNpmResolutionManager
import org.jetbrains.kotlin.gradle.targets.js.npm.UsesKotlinNpmResolutionManager
import org.jetbrains.kotlin.gradle.targets.js.npm.*
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinRootNpmResolver
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.PACKAGE_JSON_UMBRELLA_TASK_NAME
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.implementing
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.YarnPlugin
import org.jetbrains.kotlin.gradle.tasks.CleanDataTask
import org.jetbrains.kotlin.gradle.tasks.registerTask
import org.jetbrains.kotlin.gradle.tasks.withType
import org.jetbrains.kotlin.gradle.utils.*
import org.jetbrains.kotlin.gradle.utils.castIsolatedKotlinPluginClassLoaderAware
import org.jetbrains.kotlin.gradle.utils.getFile
import org.jetbrains.kotlin.gradle.utils.onlyIfCompat
import org.jetbrains.kotlin.gradle.utils.providerWithLazyConvention
open class NodeJsRootPlugin : Plugin<Project> {
override fun apply(project: Project) {
@@ -43,8 +48,24 @@ open class NodeJsRootPlugin : Plugin<Project> {
project
)
val npm = project.extensions.create(
NpmExtension.EXTENSION_NAME,
NpmExtension::class.java,
project
)
addPlatform(project, nodeJs)
npm.nodeJsEnvironment.value(
project.provider {
nodeJs.requireConfigured()
}
).disallowChanges()
nodeJs.packageManagerExtension.convention(
npm
)
val setupTask = project.registerTask<NodeJsSetupTask>(NodeJsSetupTask.NAME) {
it.group = TASKS_GROUP_NAME
it.description = "Download and install a local node/npm version"
@@ -97,21 +118,125 @@ open class NodeJsRootPlugin : Plugin<Project> {
val objectFactory = project.objects
// TODO: Could we use common approach with build services to KotlinNpmResolutionManager?
val npmResolutionManager = project.gradle.sharedServices.registerIfAbsent(
KotlinNpmResolutionManager::class.java.name,
KotlinNpmResolutionManager::class.java
) {
it.parameters.resolution.set(
objectFactory.providerWithLazyConvention {
nodeJs.resolver.close()
}
)
it.parameters.gradleNodeModulesProvider.set(gradleNodeModulesProvider)
it.parameters.packagesDir.set(nodeJs.projectPackagesDirectory)
val npmResolutionManager: Provider<KotlinNpmResolutionManager> = KotlinNpmResolutionManager.registerIfAbsent(
project,
objectFactory.providerWithLazyConvention {
nodeJs.resolver.close()
},
gradleNodeModulesProvider,
nodeJs.projectPackagesDirectory
)
val rootPackageJson = project.tasks.register(RootPackageJsonTask.NAME, RootPackageJsonTask::class.java) { task ->
task.dependsOn(nodeJs.npmCachesSetupTaskProvider)
task.group = TASKS_GROUP_NAME
task.description = "Create root package.json"
task.npmResolutionManager.value(npmResolutionManager)
.disallowChanges()
task.onlyIfCompat("Prepare NPM project only in configuring state") {
it as RootPackageJsonTask
it.npmResolutionManager.get().isConfiguringState()
}
}
YarnPlugin.apply(project)
configureRequiresNpmDependencies(project, rootPackageJson)
val packageJsonUmbrella = nodeJs
.packageJsonUmbrellaTaskProvider
nodeJs.rootPackageJsonTaskProvider.configure {
it.dependsOn(packageJsonUmbrella)
}
npmInstall.configure {
it.dependsOn(rootPackageJson)
it.inputs.property("npmIgnoreScripts", { npm.ignoreScripts })
}
project.tasks.register(LockCopyTask.STORE_PACKAGE_LOCK_NAME, LockStoreTask::class.java) { task ->
task.dependsOn(npmInstall)
task.inputFile.set(nodeJs.rootPackageDirectory.map { it.file(LockCopyTask.PACKAGE_LOCK) })
task.additionalInputFiles.from(
nodeJs.rootPackageDirectory.map { it.file(LockCopyTask.YARN_LOCK) }
)
task.additionalInputFiles.from(
task.outputDirectory.map { it.file(LockCopyTask.YARN_LOCK) }
)
task.outputDirectory.set(npm.lockFileDirectory)
task.fileName.set(npm.lockFileName)
task.lockFileMismatchReport.value(
project.provider { npm.requireConfigured().packageLockMismatchReport }
).disallowChanges()
task.reportNewLockFile.value(
project.provider { npm.requireConfigured().reportNewPackageLock }
).disallowChanges()
task.lockFileAutoReplace.value(
project.provider { npm.requireConfigured().packageLockAutoReplace }
).disallowChanges()
}
project.tasks.register(LockCopyTask.UPGRADE_PACKAGE_LOCK, LockStoreTask::class.java) { task ->
task.dependsOn(npmInstall)
task.inputFile.set(nodeJs.rootPackageDirectory.map { it.file(LockCopyTask.PACKAGE_LOCK) })
task.outputDirectory.set(npm.lockFileDirectory)
task.fileName.set(npm.lockFileName)
task.additionalInputFiles.from(
nodeJs.rootPackageDirectory.map { it.file(LockCopyTask.YARN_LOCK) }
)
task.additionalInputFiles.from(
task.outputDirectory.map { it.file(LockCopyTask.YARN_LOCK) }
)
task.lockFileMismatchReport.value(
LockFileMismatchReport.NONE
).disallowChanges()
task.reportNewLockFile.value(
false
).disallowChanges()
task.lockFileAutoReplace.value(
true
).disallowChanges()
}
project.tasks.register(LockCopyTask.RESTORE_PACKAGE_LOCK_NAME, LockCopyTask::class.java) { task ->
task.inputFile.set(
npm.lockFileDirectory.flatMap { dir ->
dir.file(npm.lockFileName)
}
)
task.additionalInputFiles.from(
npm.lockFileDirectory.map { it.file(LockCopyTask.YARN_LOCK) }
)
task.outputDirectory.set(nodeJs.rootPackageDirectory)
task.fileName.set(LockCopyTask.PACKAGE_LOCK)
task.onlyIf {
val inputFileExists = task.inputFile.getOrNull()?.asFile?.exists() == true
// Workaround for "skip if not exists"
// https://github.com/gradle/gradle/issues/2919
if (!inputFileExists) {
task.inputFile.set(null as RegularFile?)
}
inputFileExists || task.additionalInputFiles.files.any { it.exists() }
}
}
npm.preInstallTasks.value(
listOf(npm.restorePackageLockTaskProvider)
).disallowChanges()
npm.postInstallTasks.value(
listOf(npm.storePackageLockTaskProvider)
).disallowChanges()
npmInstall.configure {
it.dependsOn(nodeJs.packageManagerExtension.map { it.preInstallTasks })
}
npmInstall.configure {
it.npmResolutionManager.value(npmResolutionManager).disallowChanges()
@@ -122,6 +247,12 @@ open class NodeJsRootPlugin : Plugin<Project> {
it.group = TASKS_GROUP_NAME
it.description = "Clean unused local node version"
}
val propertiesProvider = PropertiesProvider(project)
if (propertiesProvider.yarn) {
YarnPlugin.apply(project)
}
}
// from https://github.com/node-gradle/gradle-node-plugin
@@ -142,6 +273,42 @@ open class NodeJsRootPlugin : Plugin<Project> {
).disallowChanges()
}
// Yes, we need to break Task Configuration Avoidance here
// In case when we need to create package.json's files and execute kotlinNpmInstall,
// We need to configure all RequiresNpmDependencies tasks to install them,
// Because we need to persist lock file
// We execute this block in configure phase of rootPackageJson to be sure,
// That Task Configuration Avoidance will not be broken for tasks not related with NPM installing
// https://youtrack.jetbrains.com/issue/KT-48241
private fun configureRequiresNpmDependencies(
project: Project,
rootPackageJson: TaskProvider<RootPackageJsonTask>,
) {
val fn: (Project) -> Unit = {
it.tasks.implementing(RequiresNpmDependencies::class)
.forEach {}
}
rootPackageJson.configure {
project.allprojects
.forEach { project ->
if (it.state.executed) {
fn(project)
}
}
}
project.allprojects
.forEach {
if (!it.state.executed) {
it.afterEvaluate { project ->
rootPackageJson.configure {
fn(project)
}
}
}
}
}
companion object {
const val TASKS_GROUP_NAME: String = "nodeJs"
@@ -154,30 +321,14 @@ open class NodeJsRootPlugin : Plugin<Project> {
val Project.kotlinNodeJsExtension: NodeJsRootExtension
get() = extensions.getByName(NodeJsRootExtension.EXTENSION_NAME).castIsolatedKotlinPluginClassLoaderAware()
private val Project.gradleNodeModules
get() = GradleNodeModulesCache.registerIfAbsent(
this,
null,
null
)
val Project.kotlinNpmResolutionManager: Provider<KotlinNpmResolutionManager>
get() {
val npmResolutionManager = project.gradle.sharedServices.registerIfAbsent(
return project.gradle.sharedServices.registerIfAbsent(
KotlinNpmResolutionManager::class.java.name,
KotlinNpmResolutionManager::class.java
) {
error("Must be already registered")
}
SingleActionPerProject.run(project, UsesKotlinNpmResolutionManager::class.java.name) {
project.tasks.withType<UsesKotlinNpmResolutionManager>().configureEach { task ->
task.usesService(npmResolutionManager)
task.usesService(gradleNodeModules)
}
}
return npmResolutionManager
}
}
}
@@ -43,7 +43,7 @@ abstract class NodeJsSetupTask : AbstractSetupTask<NodeJsEnv, NodeJsRootExtensio
it.into(destination)
}
fixBrokenSymlinks(destination, env.isWindows, fixBrokenSymLinks)
fixBrokenSymlinks(this.destination, env.isWindows, fixBrokenSymLinks)
if (!env.isWindows) {
File(env.nodeExecutable).setExecutable(true)
@@ -0,0 +1,25 @@
/*
* 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.nodejs
import org.gradle.api.file.FileCollection
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApiExecution
interface NpmApiExtension<out Env : PackageManagerEnvironment, out NpmApi : NpmApiExecution<Env>> {
val packageManager: NpmApi
val environment: Env
val additionalInstallOutput: FileCollection
val preInstallTasks: ListProperty<TaskProvider<*>>
val postInstallTasks: ListProperty<TaskProvider<*>>
}
typealias NpmApiExt = NpmApiExtension<PackageManagerEnvironment, NpmApiExecution<PackageManagerEnvironment>>
@@ -0,0 +1,10 @@
/*
* 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.nodejs
import java.io.Serializable
interface PackageManagerEnvironment : Serializable
@@ -77,7 +77,7 @@ internal class GradleNodeModuleBuilder(
packageJson.devDependencies.clear()
// yarn requires semver
// npm requires semver
packageJson.version = fixSemver(packageJson.version)
return makeNodeModule(cacheDir, packageJson) { nodeModule ->
@@ -5,20 +5,27 @@
package org.jetbrains.kotlin.gradle.targets.js.npm
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.logging.Logger
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.api.tasks.Internal
import org.gradle.internal.service.ServiceRegistry
import org.jetbrains.kotlin.gradle.targets.js.nodejs.PackageManagerEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.Installation
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.KotlinRootNpmResolution
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinCompilationNpmResolver
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinProjectNpmResolver
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinRootNpmResolver
import org.jetbrains.kotlin.gradle.tasks.withType
import org.jetbrains.kotlin.gradle.utils.SingleActionPerProject
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
internal interface UsesKotlinNpmResolutionManager : Task {
@get:Internal
@@ -78,16 +85,16 @@ abstract class KotlinNpmResolutionManager : BuildService<KotlinNpmResolutionMana
internal fun prepare(
logger: Logger,
npmEnvironment: NpmEnvironment,
yarnEnvironment: YarnEnvironment,
) = prepareIfNeeded(logger = logger, npmEnvironment, yarnEnvironment)
nodeJsEnvironment: NodeJsEnvironment,
environment: PackageManagerEnvironment,
) = prepareIfNeeded(logger = logger, nodeJsEnvironment, environment)
internal fun installIfNeeded(
args: List<String> = emptyList(),
services: ServiceRegistry,
logger: Logger,
npmEnvironment: NpmEnvironment,
yarnEnvironment: YarnEnvironment,
nodeJsEnvironment: NodeJsEnvironment,
packageManagerEnvironment: PackageManagerEnvironment,
): Unit? {
synchronized(this) {
if (state is ResolutionState.Installed) {
@@ -99,8 +106,8 @@ abstract class KotlinNpmResolutionManager : BuildService<KotlinNpmResolutionMana
}
return try {
val installation: Installation = prepareIfNeeded(logger = logger, npmEnvironment, yarnEnvironment)
installation.install(args, services, logger, npmEnvironment, yarnEnvironment)
val installation: Installation = prepareIfNeeded(logger = logger, nodeJsEnvironment, packageManagerEnvironment)
installation.install(args, services, logger, nodeJsEnvironment, packageManagerEnvironment)
state = ResolutionState.Installed()
} catch (e: Exception) {
state = ResolutionState.Error(e)
@@ -111,8 +118,8 @@ abstract class KotlinNpmResolutionManager : BuildService<KotlinNpmResolutionMana
private fun prepareIfNeeded(
logger: Logger,
npmEnvironment: NpmEnvironment,
yarnEnvironment: YarnEnvironment,
nodeJsEnvironment: NodeJsEnvironment,
packageManagerEnvironment: PackageManagerEnvironment,
): Installation {
val state0 = this.state
return when (state0) {
@@ -128,8 +135,8 @@ abstract class KotlinNpmResolutionManager : BuildService<KotlinNpmResolutionMana
is ResolutionState.Configuring -> {
state1.resolution.prepareInstallation(
logger,
npmEnvironment,
yarnEnvironment,
nodeJsEnvironment,
packageManagerEnvironment,
this
).also {
this.state = ResolutionState.Prepared(it)
@@ -146,4 +153,52 @@ abstract class KotlinNpmResolutionManager : BuildService<KotlinNpmResolutionMana
is ResolutionState.Error -> throw state0.wrappedException
}
}
companion object {
private val serviceClass = KotlinNpmResolutionManager::class.java
private val serviceName = serviceClass.name
private fun registerIfAbsentImpl(
project: Project,
resolution: Provider<KotlinRootNpmResolution>?,
gradleNodeModulesProvider: Provider<GradleNodeModulesCache>?,
packagesDir: Provider<Directory>
): Provider<KotlinNpmResolutionManager> {
project.gradle.sharedServices.registrations.findByName(serviceName)?.let {
@Suppress("UNCHECKED_CAST")
return it.service as Provider<KotlinNpmResolutionManager>
}
val message = {
"Build service KotlinNpmResolutionManager should be already registered"
}
requireNotNull(resolution, message)
requireNotNull(gradleNodeModulesProvider, message)
return project.gradle.sharedServices.registerIfAbsent(serviceName, serviceClass) {
it.parameters.resolution.set(
resolution
)
it.parameters.gradleNodeModulesProvider.set(gradleNodeModulesProvider)
it.parameters.packagesDir.set(packagesDir)
}
}
fun registerIfAbsent(
project: Project,
resolution: Provider<KotlinRootNpmResolution>?,
gradleNodeModulesProvider: Provider<GradleNodeModulesCache>?,
packagesDir: Provider<Directory>
) = registerIfAbsentImpl(project, resolution, gradleNodeModulesProvider, packagesDir).also { serviceProvider ->
SingleActionPerProject.run(project, UsesKotlinNpmResolutionManager::class.java.name) {
project.tasks.withType<UsesKotlinNpmResolutionManager>().configureEach { task ->
task.usesService(serviceProvider)
if (gradleNodeModulesProvider != null) {
task.usesService(gradleNodeModulesProvider)
}
}
}
}
}
}
@@ -0,0 +1,76 @@
/*
* 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.npm
import org.gradle.api.DefaultTask
import org.gradle.api.file.*
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.gradle.work.DisableCachingByDefault
import org.gradle.work.NormalizeLineEndings
import java.io.File
import javax.inject.Inject
@DisableCachingByDefault
abstract class LockCopyTask : DefaultTask() {
@get:NormalizeLineEndings
@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputFile: RegularFileProperty
@get:NormalizeLineEndings
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
val additionalInputFiles: ConfigurableFileCollection = project.objects.fileCollection()
@get:Internal
abstract val outputDirectory: DirectoryProperty
@get:Internal
abstract val fileName: Property<String>
@get:OutputFile
val outputFile: Provider<File>
get() = outputDirectory.map { dir ->
dir.asFile.resolve(fileName.get())
}
@get:Inject
abstract val fs: FileSystemOperations
@TaskAction
open fun copy() {
fs.copy { copy ->
inputFile.getOrNull()?.let { inputFile ->
copy.from(inputFile) {
it.rename { fileName.get() }
}
}
copy.from(additionalInputFiles)
copy.into(outputDirectory)
}
}
companion object {
const val STORE_PACKAGE_LOCK_NAME = "kotlinStorePackageLock"
const val RESTORE_PACKAGE_LOCK_NAME = "kotlinRestorePackageLock"
const val UPGRADE_PACKAGE_LOCK = "kotlinUpgradePackageLock"
const val PACKAGE_LOCK_MISMATCH_MESSAGE = "Lock file was changed. Run the `$UPGRADE_PACKAGE_LOCK` task to actualize lock file"
const val KOTLIN_JS_STORE = "kotlin-js-store"
const val PACKAGE_LOCK = "package-lock.json"
const val YARN_LOCK = "yarn.lock"
}
}
enum class LockFileMismatchReport {
NONE,
WARNING,
FAIL,
}
@@ -0,0 +1,59 @@
/*
* 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.npm
import org.gradle.api.GradleException
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.work.DisableCachingByDefault
import org.jetbrains.kotlin.gradle.utils.contentEquals
@DisableCachingByDefault
abstract class LockStoreTask : LockCopyTask() {
@get:Input
abstract val lockFileMismatchReport: Property<LockFileMismatchReport>
@get:Input
abstract val reportNewLockFile: Property<Boolean>
@get:Input
abstract val lockFileAutoReplace: Property<Boolean>
override fun copy() {
val outputFile = outputDirectory.get().asFile.resolve(fileName.get())
val value = inputFile.orNull
requireNotNull(value) {
"Input file $fileName should exist"
}
val shouldReportMismatch = if (!outputFile.exists()) {
reportNewLockFile.get()
} else {
lockFileMismatchReport.get() != LockFileMismatchReport.NONE && !contentEquals(value.asFile, outputFile)
}
// outputFile is updated only with auto replace or not existed, but we need delete all other files initially
fs.delete {
it.delete(additionalInputFiles)
}
if (!outputFile.exists() || lockFileAutoReplace.get()) {
super.copy()
}
if (shouldReportMismatch) {
when (lockFileMismatchReport.get()) {
LockFileMismatchReport.NONE -> {}
LockFileMismatchReport.WARNING -> {
logger.warn(PACKAGE_LOCK_MISMATCH_MESSAGE)
}
LockFileMismatchReport.FAIL -> throw GradleException(PACKAGE_LOCK_MISMATCH_MESSAGE)
else -> error("Unknown mismatch report kind")
}
}
}
}
@@ -0,0 +1,139 @@
/*
* 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.npm
import org.gradle.api.logging.Logger
import org.gradle.internal.service.ServiceRegistry
import org.jetbrains.kotlin.gradle.internal.execWithProgress
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.PreparedKotlinCompilationNpmResolution
import org.jetbrains.kotlin.gradle.utils.getFile
import java.io.File
class Npm : NpmApiExecution<NpmEnvironment> {
override fun preparedFiles(nodeJs: NodeJsEnvironment): Collection<File> {
return listOf(
nodeJs
.rootPackageDir
.resolve(NpmProject.PACKAGE_JSON)
)
}
override fun prepareRootProject(
nodeJs: NodeJsEnvironment,
packageManagerEnvironment: NpmEnvironment,
rootProjectName: String,
rootProjectVersion: String,
subProjects: Collection<PreparedKotlinCompilationNpmResolution>,
) {
return prepareRootPackageJson(
nodeJs,
rootProjectName,
rootProjectVersion,
subProjects,
packageManagerEnvironment.overrides
.associate { it.path to it.toVersionString() },
)
}
private fun prepareRootPackageJson(
nodeJs: NodeJsEnvironment,
rootProjectName: String,
rootProjectVersion: String,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
overrides: Map<String, String>,
) {
val rootPackageJsonFile = preparedFiles(nodeJs).single()
saveRootProjectWorkspacesPackageJson(
rootProjectName,
rootProjectVersion,
npmProjects,
overrides,
rootPackageJsonFile
)
}
override fun resolveRootProject(
services: ServiceRegistry,
logger: Logger,
nodeJs: NodeJsEnvironment,
packageManagerEnvironment: NpmEnvironment,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
cliArgs: List<String>,
) {
val nodeJsWorldDir = nodeJs.rootPackageDir
npmExec(
services,
logger,
nodeJs,
packageManagerEnvironment,
nodeJsWorldDir,
NpmApiExecution.resolveOperationDescription("npm"),
cliArgs
)
}
fun npmExec(
services: ServiceRegistry,
logger: Logger,
nodeJs: NodeJsEnvironment,
environment: NpmEnvironment,
dir: File,
description: String,
args: List<String>,
) {
services.execWithProgress(description) { exec ->
val arguments = listOf("install") + args
.plus(
if (logger.isDebugEnabled) "--verbose" else ""
)
.plus(
if (environment.ignoreScripts) "--ignore-scripts" else ""
).filter { it.isNotEmpty() }
if (!environment.standalone) {
val nodeExecutable = nodeJs.nodeExecutable
val nodePath = File(nodeExecutable).parent
exec.environment(
"PATH",
"$nodePath${File.pathSeparator}${System.getenv("PATH")}"
)
}
val command = environment.executable
exec.executable = command
exec.args = arguments
exec.workingDir = dir
}
}
private fun saveRootProjectWorkspacesPackageJson(
rootProjectName: String,
rootProjectVersion: String,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
overrides: Map<String, String>,
rootPackageJsonFile: File,
) {
val nodeJsWorldDir = rootPackageJsonFile.parentFile
val rootPackageJson = PackageJson(rootProjectName, rootProjectVersion)
rootPackageJson.private = true
val npmProjectWorkspaces = npmProjects.map { it.npmProjectDir.getFile().relativeTo(nodeJsWorldDir).path }
val importedProjectWorkspaces =
NpmImportedPackagesVersionResolver(npmProjects, nodeJsWorldDir).resolveAndUpdatePackages()
rootPackageJson.workspaces = npmProjectWorkspaces + importedProjectWorkspaces
rootPackageJson.overrides = overrides
rootPackageJson.saveTo(
rootPackageJsonFile
)
}
}
@@ -8,32 +8,30 @@ package org.jetbrains.kotlin.gradle.targets.js.npm
import org.gradle.api.logging.Logger
import org.gradle.internal.service.ServiceRegistry
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnv
import org.jetbrains.kotlin.gradle.targets.js.nodejs.PackageManagerEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.PreparedKotlinCompilationNpmResolution
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnEnv
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnResolution
import java.io.File
import java.io.Serializable
/**
* NodeJS package manager API
*/
interface NpmApi : Serializable {
fun preparedFiles(nodeJs: NpmEnvironment): Collection<File>
interface NpmApiExecution<out T : PackageManagerEnvironment> : Serializable {
fun preparedFiles(nodeJs: NodeJsEnvironment): Collection<File>
fun prepareRootProject(
nodeJs: NpmEnvironment,
nodeJs: NodeJsEnvironment,
packageManagerEnvironment: @UnsafeVariance T,
rootProjectName: String,
rootProjectVersion: String,
logger: Logger,
subProjects: Collection<PreparedKotlinCompilationNpmResolution>,
resolutions: Map<String, String>,
)
fun resolveRootProject(
services: ServiceRegistry,
logger: Logger,
nodeJs: NpmEnvironment,
yarn: YarnEnvironment,
nodeJs: NodeJsEnvironment,
packageManagerEnvironment: @UnsafeVariance T,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
cliArgs: List<String>
)
@@ -44,32 +42,15 @@ interface NpmApi : Serializable {
}
}
data class NpmEnvironment(
data class NodeJsEnvironment(
val rootPackageDir: File,
val nodeExecutable: String,
val isWindows: Boolean,
val packageManager: NpmApi
val packageManager: NpmApiExecution<PackageManagerEnvironment>
) : Serializable
internal val NodeJsEnv.asNpmEnvironment
get() = NpmEnvironment(
internal val NodeJsEnv.asNodeJsEnvironment
get() = NodeJsEnvironment(
rootPackageDir,
nodeExecutable,
isWindows,
packageManager
)
data class YarnEnvironment(
val executable: String,
val standalone: Boolean,
val ignoreScripts: Boolean,
val yarnResolutions: List<YarnResolution>
) : Serializable
internal val YarnEnv.asYarnEnvironment
get() = YarnEnvironment(
executable,
standalone,
ignoreScripts,
yarnResolutions
)
@@ -0,0 +1,16 @@
/*
* 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.npm
data class NpmEnv(
val executable: String,
val ignoreScripts: Boolean,
val standalone: Boolean,
val packageLockMismatchReport: LockFileMismatchReport,
val reportNewPackageLock: Boolean,
val packageLockAutoReplace: Boolean,
val overrides: List<NpmOverride>
)
@@ -0,0 +1,23 @@
/*
* 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.npm
import org.jetbrains.kotlin.gradle.targets.js.nodejs.PackageManagerEnvironment
data class NpmEnvironment(
val executable: String,
val ignoreScripts: Boolean,
val standalone: Boolean,
val overrides: List<NpmOverride>
) : PackageManagerEnvironment
internal val NpmEnv.asNpmEnvironment
get() = NpmEnvironment(
executable,
ignoreScripts,
standalone,
overrides
)
@@ -0,0 +1,126 @@
/*
* Copyright 2010-2020 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.npm
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.internal.ConfigurationPhaseAware
import org.jetbrains.kotlin.gradle.logging.kotlinInfo
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnv
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NpmApiExtension
open class NpmExtension(
val project: Project,
) : ConfigurationPhaseAware<NpmEnv>(), NpmApiExtension<NpmEnvironment, Npm> {
init {
check(project == project.rootProject)
}
private val gradleHome = project.gradle.gradleUserHomeDir.also {
project.logger.kotlinInfo("Storing cached files in $it")
}
override val packageManager: Npm by lazy {
Npm()
}
override val environment: NpmEnvironment by lazy {
requireConfigured().asNpmEnvironment
}
override val additionalInstallOutput: FileCollection = project.objects.fileCollection().from(
{
nodeJsEnvironment.get().rootPackageDir.resolve("package-lock.json")
}
)
override val preInstallTasks: ListProperty<TaskProvider<*>> = project.objects.listProperty(TaskProvider::class.java)
override val postInstallTasks: ListProperty<TaskProvider<*>> = project.objects.listProperty(TaskProvider::class.java)
val command: org.gradle.api.provider.Property<String> = project.objects.property(String::class.java)
.convention("npm")
val lockFileName: org.gradle.api.provider.Property<String> = project.objects.property(String::class.java)
.convention(LockCopyTask.PACKAGE_LOCK)
val lockFileDirectory: DirectoryProperty = project.objects.directoryProperty()
.convention(project.layout.projectDirectory.dir(LockCopyTask.KOTLIN_JS_STORE))
val ignoreScripts: org.gradle.api.provider.Property<Boolean> = project.objects.property(Boolean::class.java)
.convention(true)
val packageLockMismatchReport: org.gradle.api.provider.Property<LockFileMismatchReport> =
project.objects.property(LockFileMismatchReport::class.java)
.convention(LockFileMismatchReport.FAIL)
val reportNewPackageLock: org.gradle.api.provider.Property<Boolean> = project.objects.property(Boolean::class.java)
.convention(false)
val packageLockAutoReplace: org.gradle.api.provider.Property<Boolean> = project.objects.property(Boolean::class.java)
.convention(false)
val overrides: ListProperty<NpmOverride> = project.objects.listProperty(NpmOverride::class.java)
fun override(path: String, configure: Action<NpmOverride>) {
overrides.add(
NpmOverride(path)
.apply { configure.execute(this) }
)
}
fun override(path: String, version: String) {
override(path, Action {
it.include(version)
})
}
internal val nodeJsEnvironment: org.gradle.api.provider.Property<NodeJsEnv> = project.objects.property(NodeJsEnv::class.java)
override fun finalizeConfiguration(): NpmEnv {
val nodeJsEnvironmentValue = nodeJsEnvironment.get()
val isWindows = nodeJsEnvironmentValue.isWindows
fun getExecutable(command: String, customCommand: String, windowsExtension: String): String {
val finalCommand = if (isWindows && customCommand == command) "$command.$windowsExtension" else customCommand
return if (nodeJsEnvironmentValue.download)
nodeJsEnvironmentValue.nodeBinDir
.resolve(finalCommand).absolutePath
else
finalCommand
}
return NpmEnv(
executable = getExecutable("npm", command.get(), "cmd"),
ignoreScripts = ignoreScripts.get(),
standalone = !nodeJsEnvironmentValue.download,
packageLockMismatchReport = packageLockMismatchReport.get(),
reportNewPackageLock = reportNewPackageLock.get(),
packageLockAutoReplace = packageLockAutoReplace.get(),
overrides = overrides.get()
)
}
val restorePackageLockTaskProvider: TaskProvider<out Task>
get() = project.tasks.named(LockCopyTask.RESTORE_PACKAGE_LOCK_NAME)
val storePackageLockTaskProvider: TaskProvider<out Task>
get() = project.tasks.named(LockCopyTask.STORE_PACKAGE_LOCK_NAME)
companion object {
const val EXTENSION_NAME: String = "kotlinNpm"
operator fun get(project: Project): NpmExtension {
val rootProject = project.rootProject
rootProject.plugins.apply(NodeJsRootPlugin::class.java)
return rootProject.extensions.getByName(EXTENSION_NAME) as NpmExtension
}
}
}
@@ -1,17 +1,14 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.yarn
package org.jetbrains.kotlin.gradle.targets.js.npm
import org.gradle.api.logging.Logger
import org.jetbrains.kotlin.gradle.targets.js.npm.GradleNodeModule
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.PreparedKotlinCompilationNpmResolution
import java.io.File
class YarnImportedPackagesVersionResolver(
private val logger: Logger,
class NpmImportedPackagesVersionResolver(
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
private val nodeJsWorldDir: File
) {
@@ -0,0 +1,27 @@
/*
* 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.npm
import java.io.Serializable
class NpmOverride(
val path: String
) : Serializable {
var includedVersions = mutableListOf<String>()
var excludedVersions = mutableListOf<String>()
fun include(vararg include: String) {
includedVersions.addAll(include)
}
fun exclude(vararg exclude: String) {
excludedVersions.addAll(exclude)
}
}
fun NpmOverride.toVersionString(): String {
return buildNpmVersion(includedVersions, excludedVersions)
}
@@ -30,8 +30,9 @@ class NpmResolverPlugin : Plugin<Project> {
}
task.dependsOn(
kotlinNodeJsTaskProvidersExtension.npmInstallTaskProvider,
kotlinNodeJsTaskProvidersExtension.storeYarnLockTaskProvider,
)
task.dependsOn(nodeJs.packageManagerExtension.map { it.postInstallTasks })
}
}
}
@@ -35,7 +35,7 @@ class PackageJson(
var workspaces: Collection<String>? = null
var resolutions: Map<String, String>? = null
var overrides: Map<String, String>? = null
var types: String? = null
@@ -7,10 +7,9 @@ package org.jetbrains.kotlin.gradle.targets.js.npm.resolved
import org.gradle.api.logging.Logger
import org.gradle.internal.service.ServiceRegistry
import org.jetbrains.kotlin.gradle.targets.js.nodejs.PackageManagerEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.KotlinNpmResolutionManager
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.YarnEnvironment
import org.jetbrains.kotlin.gradle.targets.js.yarn.toVersionString
import org.jetbrains.kotlin.gradle.targets.js.npm.NodeJsEnvironment
import java.io.Serializable
class KotlinRootNpmResolution(
@@ -25,8 +24,8 @@ class KotlinRootNpmResolution(
*/
internal fun prepareInstallation(
logger: Logger,
npmEnvironment: NpmEnvironment,
yarnEnvironment: YarnEnvironment,
nodeJsEnvironment: NodeJsEnvironment,
packageManagerEnvironment: PackageManagerEnvironment,
npmResolutionManager: KotlinNpmResolutionManager,
): Installation {
synchronized(projects) {
@@ -41,14 +40,12 @@ class KotlinRootNpmResolution(
)
}
npmEnvironment.packageManager.prepareRootProject(
npmEnvironment,
nodeJsEnvironment.packageManager.prepareRootProject(
nodeJsEnvironment,
packageManagerEnvironment,
rootProjectName,
rootProjectVersion,
logger,
projectResolutions,
yarnEnvironment.yarnResolutions
.associate { it.path to it.toVersionString() },
)
return Installation(
@@ -63,15 +60,15 @@ class Installation(val compilationResolutions: Collection<PreparedKotlinCompilat
args: List<String>,
services: ServiceRegistry,
logger: Logger,
npmEnvironment: NpmEnvironment,
yarnEnvironment: YarnEnvironment,
nodeJsEnvironment: NodeJsEnvironment,
packageManagerEnvironment: PackageManagerEnvironment,
) {
synchronized(compilationResolutions) {
npmEnvironment.packageManager.resolveRootProject(
nodeJsEnvironment.packageManager.resolveRootProject(
services,
logger,
npmEnvironment,
yarnEnvironment,
nodeJsEnvironment,
packageManagerEnvironment,
compilationResolutions,
args
)
@@ -10,14 +10,7 @@ import org.gradle.api.file.Directory
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFile
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.IgnoreEmptyDirectories
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.*
import org.gradle.work.DisableCachingByDefault
import org.gradle.work.NormalizeLineEndings
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
@@ -25,10 +18,8 @@ import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin.Companion.
import org.jetbrains.kotlin.gradle.targets.js.npm.KotlinNpmResolutionManager
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject
import org.jetbrains.kotlin.gradle.targets.js.npm.UsesKotlinNpmResolutionManager
import org.jetbrains.kotlin.gradle.targets.js.npm.asNpmEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.asYarnEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.asNodeJsEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinRootNpmResolver
import org.jetbrains.kotlin.gradle.targets.js.yarn.yarn
import org.jetbrains.kotlin.gradle.utils.getFile
import java.io.File
@@ -46,9 +37,6 @@ abstract class KotlinNpmInstallTask :
private val nodeJs: NodeJsRootExtension
get() = project.rootProject.kotlinNodeJsExtension
private val yarn
get() = project.rootProject.yarn
private val rootResolver: KotlinRootNpmResolver
get() = nodeJs.resolver
@@ -57,12 +45,12 @@ abstract class KotlinNpmInstallTask :
// -----
private val npmEnvironment by lazy {
nodeJs.requireConfigured().asNpmEnvironment
private val nodsJsEnvironment by lazy {
nodeJs.requireConfigured().asNodeJsEnvironment
}
private val yarnEnv by lazy {
yarn.requireConfigured().asYarnEnvironment
private val packageManagerEnv by lazy {
nodeJs.packageManagerExtension.get().environment
}
@Input
@@ -73,7 +61,7 @@ abstract class KotlinNpmInstallTask :
@get:NormalizeLineEndings
@get:InputFiles
val preparedFiles: Collection<File> by lazy {
nodeJs.packageManager.preparedFiles(npmEnvironment)
nodeJs.packageManagerExtension.get().packageManager.preparedFiles(nodsJsEnvironment)
}
@get:PathSensitive(PathSensitivity.RELATIVE)
@@ -92,12 +80,21 @@ abstract class KotlinNpmInstallTask :
}
)
@get:OutputFile
@get:OutputFiles
val additionalFiles: FileCollection by lazy {
nodeJs.packageManagerExtension.get().additionalInstallOutput
}
@Deprecated(
"This property is deprecated and will be removed in future. Use additionalFiles instead",
replaceWith = ReplaceWith("additionalFiles")
)
@get:Internal
val yarnLockFile: Provider<RegularFile> = nodeJs.rootPackageDirectory.map { it.file("yarn.lock") }
@Deprecated(
"This property is deprecated and will be removed in future. Use yarnLockFile instead",
replaceWith = ReplaceWith("yarnLockFile")
"This property is deprecated and will be removed in future. Use additionalFiles instead",
replaceWith = ReplaceWith("additionalFiles")
)
@get:Internal
val yarnLock: File
@@ -115,8 +112,8 @@ abstract class KotlinNpmInstallTask :
args = args,
services = services,
logger = logger,
npmEnvironment,
yarnEnv
nodsJsEnvironment,
packageManagerEnv
) ?: throw (npmResolutionManager.get().state as KotlinNpmResolutionManager.ResolutionState.Error).wrappedException
}
@@ -16,10 +16,8 @@ import org.gradle.work.NormalizeLineEndings
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin.Companion.kotlinNodeJsExtension
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject
import org.jetbrains.kotlin.gradle.targets.js.npm.UsesKotlinNpmResolutionManager
import org.jetbrains.kotlin.gradle.targets.js.npm.asNpmEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.asYarnEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.asNodeJsEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinRootNpmResolver
import org.jetbrains.kotlin.gradle.targets.js.yarn.yarn
import org.jetbrains.kotlin.gradle.utils.getFile
import java.io.File
@@ -37,9 +35,6 @@ abstract class RootPackageJsonTask :
private val nodeJs
get() = project.rootProject.kotlinNodeJsExtension
private val yarn
get() = project.rootProject.yarn
private val rootResolver: KotlinRootNpmResolver
get() = nodeJs.resolver
@@ -48,12 +43,12 @@ abstract class RootPackageJsonTask :
// -----
private val npmEnvironment by lazy {
nodeJs.requireConfigured().asNpmEnvironment
private val nodeJsEnvironment by lazy {
nodeJs.requireConfigured().asNodeJsEnvironment
}
private val yarnEnv by lazy {
yarn.requireConfigured().asYarnEnvironment
private val packageManagerEnv by lazy {
nodeJs.packageManagerExtension.get().environment
}
@get:OutputFile
@@ -87,7 +82,7 @@ abstract class RootPackageJsonTask :
@TaskAction
fun resolve() {
npmResolutionManager.get().prepare(logger, npmEnvironment, yarnEnv)
npmResolutionManager.get().prepare(logger, nodeJsEnvironment, packageManagerEnv)
}
companion object {
@@ -46,7 +46,7 @@ fun FileHasher.calculateDirHash(
dir.walk()
.forEach { file ->
hasher.putString(file.toRelativeString(dir))
if (file.isFile && !Files.isSymbolicLink(file.toPath())) {
if (file.isFile) {
if (!Files.isSymbolicLink(file.toPath())) {
hasher.putHash(hash(file))
} else {
@@ -0,0 +1,48 @@
/*
* Copyright 2010-2021 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.provider.Provider
import org.gradle.api.tasks.Internal
import org.gradle.work.DisableCachingByDefault
import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask
import org.jetbrains.kotlin.gradle.targets.js.npm.LockFileMismatchReport
import org.jetbrains.kotlin.gradle.targets.js.npm.LockStoreTask
@DisableCachingByDefault
abstract class YarnLockCopyTask : LockCopyTask()
@DisableCachingByDefault
abstract class YarnLockStoreTask : LockStoreTask() {
@Internal
val yarnLockMismatchReport: Provider<YarnLockMismatchReport> = lockFileMismatchReport.map { it.fromLockFileMismatchReport() }
@Internal
val reportNewYarnLock: Provider<Boolean> = reportNewLockFile
@Internal
val yarnLockAutoReplace: Provider<Boolean> = lockFileAutoReplace
}
enum class YarnLockMismatchReport {
NONE,
WARNING,
FAIL;
fun toLockFileMismatchReport(): LockFileMismatchReport =
when(this) {
NONE -> LockFileMismatchReport.NONE
WARNING -> LockFileMismatchReport.WARNING
FAIL -> LockFileMismatchReport.FAIL
}
}
fun LockFileMismatchReport.fromLockFileMismatchReport(): YarnLockMismatchReport =
when(this) {
LockFileMismatchReport.NONE -> YarnLockMismatchReport.NONE
LockFileMismatchReport.WARNING -> YarnLockMismatchReport.WARNING
LockFileMismatchReport.FAIL -> YarnLockMismatchReport.FAIL
}
@@ -7,39 +7,36 @@ package org.jetbrains.kotlin.gradle.targets.js.yarn
import org.gradle.api.logging.Logger
import org.gradle.internal.service.ServiceRegistry
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApi
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.YarnEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApiExecution
import org.jetbrains.kotlin.gradle.targets.js.npm.NodeJsEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.PreparedKotlinCompilationNpmResolution
import java.io.File
class Yarn : NpmApi {
class Yarn : NpmApiExecution<YarnEnvironment> {
private val yarnWorkspaces = YarnWorkspaces()
override fun preparedFiles(nodeJs: NpmEnvironment): Collection<File> =
override fun preparedFiles(nodeJs: NodeJsEnvironment): Collection<File> =
yarnWorkspaces.preparedFiles(nodeJs)
override fun prepareRootProject(
nodeJs: NpmEnvironment,
nodeJs: NodeJsEnvironment,
yarn: YarnEnvironment,
rootProjectName: String,
rootProjectVersion: String,
logger: Logger,
subProjects: Collection<PreparedKotlinCompilationNpmResolution>,
resolutions: Map<String, String>,
) = yarnWorkspaces
.prepareRootProject(
nodeJs,
yarn,
rootProjectName,
rootProjectVersion,
logger,
subProjects,
resolutions,
)
override fun resolveRootProject(
services: ServiceRegistry,
logger: Logger,
nodeJs: NpmEnvironment,
nodeJs: NodeJsEnvironment,
yarn: YarnEnvironment,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
cliArgs: List<String>
@@ -8,17 +8,16 @@ package org.jetbrains.kotlin.gradle.targets.js.yarn
import org.gradle.api.logging.Logger
import org.gradle.internal.service.ServiceRegistry
import org.jetbrains.kotlin.gradle.internal.execWithProgress
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApi
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.YarnEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApiExecution
import org.jetbrains.kotlin.gradle.targets.js.npm.NodeJsEnvironment
import java.io.File
abstract class YarnBasics : NpmApi {
abstract class YarnBasics : NpmApiExecution<YarnEnvironment> {
fun yarnExec(
services: ServiceRegistry,
logger: Logger,
nodeJs: NpmEnvironment,
nodeJs: NodeJsEnvironment,
yarn: YarnEnvironment,
dir: File,
description: String,
@@ -35,11 +34,7 @@ abstract class YarnBasics : NpmApi {
val nodeExecutable = nodeJs.nodeExecutable
if (!yarn.ignoreScripts) {
val nodePath = if (nodeJs.isWindows) {
File(nodeExecutable).parent
} else {
nodeExecutable
}
val nodePath = File(nodeExecutable).parent
exec.environment(
"PATH",
"$nodePath${File.pathSeparator}${System.getenv("PATH")}"
@@ -1,17 +0,0 @@
/*
* Copyright 2010-2020 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.jetbrains.kotlin.gradle.targets.js.npm.FILE_VERSION_PREFIX
import org.jetbrains.kotlin.gradle.targets.js.npm.includedRange
import org.jetbrains.kotlin.gradle.targets.js.npm.intersect
import java.io.File
private const val SEPARATOR = "@"
private const val FILE_MARKER = "${SEPARATOR}$FILE_VERSION_PREFIX"
private const val GITHUB_MARKER = "${SEPARATOR}github:"
@@ -16,10 +16,12 @@ data class YarnEnv(
override val dir: File,
val executable: String,
override val ivyDependency: String,
val standalone: Boolean,
val ignoreScripts: Boolean,
val yarnLockMismatchReport: YarnLockMismatchReport,
val reportNewYarnLock: Boolean,
val yarnLockAutoReplace: Boolean,
val yarnResolutions: List<YarnResolution>
) : AbstractEnv
) : AbstractEnv {
val standalone: Boolean
get() = !download
}
@@ -0,0 +1,23 @@
/*
* 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.yarn
import org.jetbrains.kotlin.gradle.targets.js.nodejs.PackageManagerEnvironment
data class YarnEnvironment(
val executable: String,
val standalone: Boolean,
val ignoreScripts: Boolean,
val yarnResolutions: List<YarnResolution>
) : PackageManagerEnvironment
internal val YarnEnv.asYarnEnvironment
get() = YarnEnvironment(
executable,
standalone,
ignoreScripts,
yarnResolutions
)
@@ -1,103 +0,0 @@
/*
* Copyright 2010-2021 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.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.DirectoryProperty
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.gradle.work.DisableCachingByDefault
import org.gradle.work.NormalizeLineEndings
import org.jetbrains.kotlin.gradle.utils.contentEquals
import java.io.File
import javax.inject.Inject
@DisableCachingByDefault
abstract class YarnLockCopyTask : DefaultTask() {
@get:NormalizeLineEndings
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputFile: RegularFileProperty
@get:Internal
abstract val outputDirectory: DirectoryProperty
@get:Internal
abstract val fileName: Property<String>
@get:OutputFile
val outputFile: Provider<File>
get() = outputDirectory.map { regularFile ->
regularFile.asFile.resolve(fileName.get())
}
@get:Inject
abstract val fs: FileSystemOperations
@TaskAction
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"
val YARN_LOCK_MISMATCH_MESSAGE = "yarn.lock was changed. Run the `${YarnLockCopyTask.UPGRADE_YARN_LOCK}` task to actualize yarn.lock file"
}
}
@DisableCachingByDefault
abstract class YarnLockStoreTask : YarnLockCopyTask() {
@Input
lateinit var yarnLockMismatchReport: Provider<YarnLockMismatchReport>
@Input
lateinit var reportNewYarnLock: Provider<Boolean>
@Input
lateinit var yarnLockAutoReplace: Provider<Boolean>
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)
else -> error("Unknown yarn.lock mismatch report kind")
}
}
}
}
enum class YarnLockMismatchReport {
NONE,
WARNING,
FAIL,
}
@@ -7,22 +7,14 @@ package org.jetbrains.kotlin.gradle.targets.js.yarn
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.targets.js.MultiplePluginDeclarationDetector
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin.Companion.kotlinNodeJsExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin.Companion.kotlinNpmResolutionManager
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.LockCopyTask
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
import org.jetbrains.kotlin.gradle.utils.detachedResolvable
import org.jetbrains.kotlin.gradle.utils.onlyIfCompat
open class YarnPlugin : Plugin<Project> {
override fun apply(project: Project): Unit = project.run {
@@ -40,6 +32,10 @@ open class YarnPlugin : Plugin<Project> {
yarnRootExtension.platform.value(nodeJs.platform)
.disallowChanges()
nodeJs.packageManagerExtension.set(
yarnRootExtension
)
val setupTask = registerTask<YarnSetupTask>(YarnSetupTask.NAME) {
it.dependsOn(nodeJsTaskProviders.nodeJsSetupTaskProvider)
@@ -52,110 +48,64 @@ open class YarnPlugin : Plugin<Project> {
}
}
val kotlinNpmResolutionManager = project.kotlinNpmResolutionManager
val rootPackageJson = tasks.register(RootPackageJsonTask.NAME, RootPackageJsonTask::class.java) { task ->
task.dependsOn(nodeJsTaskProviders.npmCachesSetupTaskProvider)
task.group = NodeJsRootPlugin.TASKS_GROUP_NAME
task.description = "Create root package.json"
task.npmResolutionManager.value(kotlinNpmResolutionManager)
.disallowChanges()
task.onlyIfCompat("Prepare NPM project only in configuring state") {
it as RootPackageJsonTask
it.npmResolutionManager.get().isConfiguringState()
}
}
configureRequiresNpmDependencies(project, rootPackageJson)
val kotlinNpmInstall = tasks.named(KotlinNpmInstallTask.NAME)
kotlinNpmInstall.configure {
it.dependsOn(rootPackageJson)
it.dependsOn(setupTask)
it.inputs.property("ignoreScripts", { yarnRootExtension.ignoreScripts })
it.inputs.property("yarnIgnoreScripts", { yarnRootExtension.ignoreScripts })
}
yarnRootExtension.nodeJsEnvironment.value(
project.provider {
nodeJs.requireConfigured()
}
).disallowChanges()
tasks.register("yarn" + CleanDataTask.NAME_SUFFIX, CleanDataTask::class.java) {
it.cleanableStoreProvider = provider { yarnRootExtension.requireConfigured().cleanableStore }
it.description = "Clean unused local yarn version"
}
val packageJsonUmbrella = nodeJsTaskProviders
.packageJsonUmbrellaTaskProvider
yarnRootExtension.rootPackageJsonTaskProvider.configure {
it.dependsOn(packageJsonUmbrella)
}
tasks.register(STORE_YARN_LOCK_NAME, YarnLockStoreTask::class.java) { task ->
task.dependsOn(kotlinNpmInstall)
task.inputFile.set(nodeJs.rootPackageDirectory.map { it.file("yarn.lock") })
task.inputFile.set(nodeJs.rootPackageDirectory.map { it.file(LockCopyTask.YARN_LOCK) })
task.outputDirectory.set(yarnRootExtension.lockFileDirectory)
task.fileName.set(yarnRootExtension.lockFileName)
task.yarnLockMismatchReport = provider { yarnRootExtension.requireConfigured().yarnLockMismatchReport }
task.reportNewYarnLock = provider { yarnRootExtension.requireConfigured().reportNewYarnLock }
task.yarnLockAutoReplace = provider { yarnRootExtension.requireConfigured().yarnLockAutoReplace }
task.lockFileMismatchReport.value(
provider { yarnRootExtension.requireConfigured().yarnLockMismatchReport.toLockFileMismatchReport() }
).disallowChanges()
task.reportNewLockFile.value(
provider { yarnRootExtension.requireConfigured().reportNewYarnLock }
).disallowChanges()
task.lockFileAutoReplace.value(
provider { yarnRootExtension.requireConfigured().yarnLockAutoReplace }
).disallowChanges()
}
tasks.register(UPGRADE_YARN_LOCK, YarnLockCopyTask::class.java) { task ->
task.dependsOn(kotlinNpmInstall)
task.inputFile.set(nodeJs.rootPackageDirectory.map { it.file("yarn.lock") })
task.inputFile.set(nodeJs.rootPackageDirectory.map { it.file(LockCopyTask.YARN_LOCK) })
task.outputDirectory.set(yarnRootExtension.lockFileDirectory)
task.fileName.set(yarnRootExtension.lockFileName)
}
val restoreYarnLock = tasks.register(RESTORE_YARN_LOCK_NAME, YarnLockCopyTask::class.java) {
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.rootPackageDirectory)
it.fileName.set("yarn.lock")
it.fileName.set(LockCopyTask.YARN_LOCK)
it.onlyIf {
lockFile.exists()
}
}
kotlinNpmInstall.configure {
it.dependsOn(restoreYarnLock)
}
}
yarnRootExtension.preInstallTasks.value(
listOf(yarnRootExtension.restoreYarnLockTaskProvider)
).disallowChanges()
// Yes, we need to break Task Configuration Avoidance here
// In case when we need to create package.json's files and execute kotlinNpmInstall,
// We need to configure all RequiresNpmDependencies tasks to install them,
// Because we need to persist yarn.lock
// We execute this block in configure phase of rootPackageJson to be sure,
// That Task Configuration Avoidance will not be broken for tasks not related with NPM installing
// https://youtrack.jetbrains.com/issue/KT-48241
private fun configureRequiresNpmDependencies(
project: Project,
rootPackageJson: TaskProvider<RootPackageJsonTask>,
) {
val fn: (Project) -> Unit = {
it.tasks.implementing(RequiresNpmDependencies::class)
.forEach {}
}
rootPackageJson.configure {
project.allprojects
.forEach { project ->
if (it.state.executed) {
fn(project)
}
}
}
project.allprojects
.forEach {
if (!it.state.executed) {
it.afterEvaluate { project ->
rootPackageJson.configure {
fn(project)
}
}
}
}
yarnRootExtension.postInstallTasks.value(
listOf(yarnRootExtension.storeYarnLockTaskProvider)
).disallowChanges()
}
companion object {
@@ -164,5 +114,9 @@ open class YarnPlugin : Plugin<Project> {
rootProject.plugins.apply(YarnPlugin::class.java)
return rootProject.extensions.getByName(YarnRootExtension.YARN) as YarnRootExtension
}
const val STORE_YARN_LOCK_NAME = "kotlinStoreYarnLock"
const val RESTORE_YARN_LOCK_NAME = "kotlinRestoreYarnLock"
const val UPGRADE_YARN_LOCK = "kotlinUpgradeYarnLock"
}
}
@@ -7,17 +7,24 @@ package org.jetbrains.kotlin.gradle.targets.js.yarn
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.FileCollection
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.logging.kotlinInfo
import org.jetbrains.kotlin.gradle.targets.js.AbstractSettings
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnv
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NpmApiExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.Platform
import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.RootPackageJsonTask
import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin.Companion.RESTORE_YARN_LOCK_NAME
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin.Companion.STORE_YARN_LOCK_NAME
import org.jetbrains.kotlin.gradle.tasks.internal.CleanableStore
import java.io.File
open class YarnRootExtension(
val project: Project
) : AbstractSettings<YarnEnv>() {
) : AbstractSettings<YarnEnv>(), NpmApiExtension<YarnEnvironment, Yarn> {
init {
check(project == project.rootProject)
}
@@ -26,6 +33,24 @@ open class YarnRootExtension(
project.logger.kotlinInfo("Storing cached files in $it")
}
override val packageManager: Yarn by lazy {
Yarn()
}
override val environment: YarnEnvironment by lazy {
requireConfigured().asYarnEnvironment
}
override val additionalInstallOutput: FileCollection = project.objects.fileCollection().from(
{
nodeJsEnvironment.get().rootPackageDir.resolve(LockCopyTask.YARN_LOCK)
}
)
override val preInstallTasks: ListProperty<TaskProvider<*>> = project.objects.listProperty(TaskProvider::class.java)
override val postInstallTasks: ListProperty<TaskProvider<*>> = project.objects.listProperty(TaskProvider::class.java)
override var installationDir by Property(gradleHome.resolve("yarn"))
override var downloadBaseUrl: String? by Property("https://github.com/yarnpkg/yarn/releases/download")
@@ -34,8 +59,8 @@ open class YarnRootExtension(
override var command by Property("yarn")
override var download by Property(true)
var lockFileName by Property("yarn.lock")
var lockFileDirectory: File by Property(project.rootDir.resolve("kotlin-js-store"))
var lockFileName by Property(LockCopyTask.YARN_LOCK)
var lockFileDirectory: File by Property(project.rootDir.resolve(LockCopyTask.KOTLIN_JS_STORE))
var ignoreScripts by Property(true)
@@ -50,11 +75,6 @@ open class YarnRootExtension(
.withType(YarnSetupTask::class.java)
.named(YarnSetupTask.NAME)
val rootPackageJsonTaskProvider: TaskProvider<RootPackageJsonTask>
get() = project.tasks
.withType(RootPackageJsonTask::class.java)
.named(RootPackageJsonTask.NAME)
internal val platform: org.gradle.api.provider.Property<Platform> = project.objects.property(Platform::class.java)
var resolutions: MutableList<YarnResolution> by Property(mutableListOf())
@@ -72,6 +92,8 @@ open class YarnRootExtension(
})
}
internal val nodeJsEnvironment: org.gradle.api.provider.Property<NodeJsEnv> = project.objects.property(NodeJsEnv::class.java)
override fun finalizeConfiguration(): YarnEnv {
val cleanableStore = CleanableStore[installationDir.path]
@@ -93,7 +115,6 @@ open class YarnRootExtension(
cleanableStore = cleanableStore,
dir = home,
executable = getExecutable("yarn", command, "cmd"),
standalone = !download,
ivyDependency = "com.yarnpkg:yarn:$version@tar.gz",
ignoreScripts = ignoreScripts,
yarnLockMismatchReport = yarnLockMismatchReport,
@@ -103,6 +124,12 @@ open class YarnRootExtension(
)
}
val restoreYarnLockTaskProvider: TaskProvider<YarnLockCopyTask>
get() = project.tasks.withType(YarnLockCopyTask::class.java).named(RESTORE_YARN_LOCK_NAME)
val storeYarnLockTaskProvider: TaskProvider<YarnLockStoreTask>
get() = project.tasks.withType(YarnLockStoreTask::class.java).named(STORE_YARN_LOCK_NAME)
companion object {
const val YARN: String = "kotlinYarn"
@@ -13,7 +13,7 @@ import org.jetbrains.kotlin.gradle.utils.getFile
import java.io.File
class YarnWorkspaces : YarnBasics() {
override fun preparedFiles(nodeJs: NpmEnvironment): Collection<File> {
override fun preparedFiles(nodeJs: NodeJsEnvironment): Collection<File> {
return listOf(
nodeJs
.rootPackageDir
@@ -22,28 +22,26 @@ class YarnWorkspaces : YarnBasics() {
}
override fun prepareRootProject(
nodeJs: NpmEnvironment,
nodeJs: NodeJsEnvironment,
yarn: YarnEnvironment,
rootProjectName: String,
rootProjectVersion: String,
logger: Logger,
subProjects: Collection<PreparedKotlinCompilationNpmResolution>,
resolutions: Map<String, String>,
) {
return prepareRootPackageJson(
nodeJs,
rootProjectName,
rootProjectVersion,
logger,
subProjects,
resolutions
yarn.yarnResolutions
.associate { it.path to it.toVersionString() },
)
}
private fun prepareRootPackageJson(
nodeJs: NpmEnvironment,
nodeJs: NodeJsEnvironment,
rootProjectName: String,
rootProjectVersion: String,
logger: Logger,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
resolutions: Map<String, String>
) {
@@ -52,7 +50,6 @@ class YarnWorkspaces : YarnBasics() {
saveRootProjectWorkspacesPackageJson(
rootProjectName,
rootProjectVersion,
logger,
npmProjects,
resolutions,
rootPackageJsonFile
@@ -62,7 +59,7 @@ class YarnWorkspaces : YarnBasics() {
override fun resolveRootProject(
services: ServiceRegistry,
logger: Logger,
nodeJs: NpmEnvironment,
nodeJs: NodeJsEnvironment,
yarn: YarnEnvironment,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
cliArgs: List<String>
@@ -75,7 +72,7 @@ class YarnWorkspaces : YarnBasics() {
nodeJs,
yarn,
nodeJsWorldDir,
NpmApi.resolveOperationDescription("yarn"),
NpmApiExecution.resolveOperationDescription("yarn"),
cliArgs
)
}
@@ -83,7 +80,6 @@ class YarnWorkspaces : YarnBasics() {
private fun saveRootProjectWorkspacesPackageJson(
rootProjectName: String,
rootProjectVersion: String,
logger: Logger,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
resolutions: Map<String, String>,
rootPackageJsonFile: File
@@ -94,10 +90,10 @@ class YarnWorkspaces : YarnBasics() {
val npmProjectWorkspaces = npmProjects.map { it.npmProjectDir.getFile().relativeTo(nodeJsWorldDir).path }
val importedProjectWorkspaces =
YarnImportedPackagesVersionResolver(logger, npmProjects, nodeJsWorldDir).resolveAndUpdatePackages()
NpmImportedPackagesVersionResolver(npmProjects, nodeJsWorldDir).resolveAndUpdatePackages()
rootPackageJson.workspaces = npmProjectWorkspaces + importedProjectWorkspaces
rootPackageJson.resolutions = resolutions
rootPackageJson.customField("resolutions", resolutions)
rootPackageJson.saveTo(
rootPackageJsonFile
)