[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") @DisplayName("KT-48241: configuration cache works with test dependencies")
@GradleTest @GradleTest
fun testTestDependencies(gradleVersion: GradleVersion) { 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) { project("kotlin-js-project-with-test-dependencies", gradleVersion) {
assertSimpleConfigurationCacheScenarioWorks( assertSimpleConfigurationCacheScenarioWorks(
"assemble", "kotlinStoreYarnLock", "assemble", "kotlinStoreYarnLock",
buildOptions = defaultBuildOptions, buildOptions = defaultBuildOptions.copy(
jsOptions = defaultBuildOptions.jsOptions?.copy(
yarn = true
)
),
executedTaskNames = listOf(":rootPackageJson") executedTaskNames = listOf(":rootPackageJson")
) )
} }
@@ -28,16 +28,47 @@ class JsSetupConfigurationCacheIT : KGPBaseTest() {
@DisplayName("Check Node.JS setup on different platforms") @DisplayName("Check Node.JS setup on different platforms")
@GradleTest @GradleTest
fun checkNodeJsSetup(gradleVersion: GradleVersion) { fun checkNodeJsSetup(gradleVersion: GradleVersion) {
project("kotlin-js-browser-project", gradleVersion) { project(
build("kotlinUpgradeYarnLock") { "kotlin-js-browser-project",
assertTasksExecuted(":kotlinUpgradeYarnLock") gradleVersion,
assertConfigurationCacheStored() buildOptions = defaultBuildOptions.copy(
} jsOptions = defaultBuildOptions.jsOptions?.copy(
yarn = false
)
)
) {
checkNodeJsSetup("kotlinUpgradePackageLock")
}
}
build("kotlinUpgradeYarnLock") { // hack to be run on Mac m*
assertTasksUpToDate(":kotlinUpgradeYarnLock") @DisplayName("Check Node.JS setup on different platforms with Yarn")
assertConfigurationCacheReused() @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.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.targets.js.dsl.Distribution 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.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.NpmProject
import org.jetbrains.kotlin.gradle.targets.js.npm.PackageJson 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.npm.fromSrcPackageJson
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask.Companion.STORE_YARN_LOCK_NAME import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin
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.testbase.* import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.testbase.TestVersions.Gradle.G_7_6 import org.jetbrains.kotlin.gradle.testbase.TestVersions.Gradle.G_7_6
import org.jetbrains.kotlin.gradle.util.replaceText import org.jetbrains.kotlin.gradle.util.replaceText
@@ -34,6 +37,7 @@ import kotlin.test.assertTrue
@JsGradlePluginTests @JsGradlePluginTests
class Kotlin2JsIrGradlePluginIT : KGPBaseTest() { class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
@DisplayName("TS type declarations are generated") @DisplayName("TS type declarations are generated")
@GradleTest @GradleTest
fun generateDts(gradleVersion: GradleVersion) { 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 @GradleTest
fun testJsIrIncrementalInParallel(gradleVersion: GradleVersion) { fun testJsIrIncrementalInParallel(gradleVersion: GradleVersion) {
project("kotlin-js-browser-project", gradleVersion) { project("kotlin-js-browser-project", gradleVersion) {
@@ -1029,7 +1002,15 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
@DisplayName("public NPM dependencies are included into package.json") @DisplayName("public NPM dependencies are included into package.json")
@GradleTest @GradleTest
fun testPackageJsonWithPublicNpmDependencies(gradleVersion: GradleVersion) { fun testPackageJsonWithPublicNpmDependencies(gradleVersion: GradleVersion) {
project("npm-dependencies", gradleVersion) { project(
"npm-dependencies",
gradleVersion,
buildOptions = defaultBuildOptions.copy(
jsOptions = defaultBuildOptions.jsOptions?.copy(
yarn = false
)
)
) {
build("jsJar") { build("jsJar") {
val archive = projectPath val archive = projectPath
.resolve("build/libs") .resolve("build/libs")
@@ -1097,8 +1078,8 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
} }
} }
build(UPGRADE_YARN_LOCK) { build(UPGRADE_PACKAGE_LOCK) {
assertTasksExecuted(":$UPGRADE_YARN_LOCK") assertTasksExecuted(":$UPGRADE_PACKAGE_LOCK")
} }
build("jsJar") { build("jsJar") {
@@ -1216,10 +1197,18 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
} }
} }
@DisplayName("yarn resolutions works") @DisplayName("npm overrides works")
@GradleTest @GradleTest
fun testYarnResolution(gradleVersion: GradleVersion) { fun testNpmOverrides(gradleVersion: GradleVersion) {
project("kotlin-js-yarn-resolutions", gradleVersion) { project(
"kotlin-js-npm-overrides",
gradleVersion,
buildOptions = defaultBuildOptions.copy(
jsOptions = defaultBuildOptions.jsOptions?.copy(
yarn = false
)
)
) {
build("packageJson", "rootPackageJson", "kotlinNpmInstall") { build("packageJson", "rootPackageJson", "kotlinNpmInstall") {
fun getPackageJson() = fun getPackageJson() =
projectPath.resolve("build/js") projectPath.resolve("build/js")
@@ -1229,16 +1218,16 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
} }
val name = "lodash" 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" 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 version == requiredVersion
} }
val react = "react" val react = "react"
val reactVersion = getPackageJson().resolutions?.get(react) val reactVersion = getPackageJson().overrides?.get(react)
val requiredReactVersion = "16.0.0" 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 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") @DisplayName("webpack configuration is valid")
@GradleTest @GradleTest
fun testWebpackConfig(gradleVersion: GradleVersion) { fun testWebpackConfig(gradleVersion: GradleVersion) {
@@ -1622,7 +1441,15 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() {
@DisplayName("Disable download should not download Node.JS and Yarn") @DisplayName("Disable download should not download Node.JS and Yarn")
@GradleTest @GradleTest
fun testNodeJsAndYarnNotDownloaded(gradleVersion: GradleVersion) { fun testNodeJsAndYarnNotDownloaded(gradleVersion: GradleVersion) {
project("nodeJsDownload", gradleVersion) { project(
"nodeJsDownload",
gradleVersion,
buildOptions = defaultBuildOptions.copy(
jsOptions = defaultBuildOptions.jsOptions?.copy(
yarn = true
)
)
) {
buildGradleKts.modify { buildGradleKts.modify {
it + "\n" + 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") @DisplayName("js files from dependency are installed")
@GradleTest @GradleTest
fun testKotlinJsDependencyWithJsFiles(gradleVersion: GradleVersion) { 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 @GradleTest
fun testPackageJsonSkippedWithUpToDatePackageJsons(gradleVersion: GradleVersion) { fun testPackageJsonSkippedWithUpToDatePackageJsons(gradleVersion: GradleVersion) {
project("kotlin-js-browser-library-project", 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 buildCacheEnabled: Boolean = false,
val kaptOptions: KaptOptions? = null, val kaptOptions: KaptOptions? = null,
val androidVersion: String? = null, val androidVersion: String? = null,
val jsOptions: JsOptions? = null, val jsOptions: JsOptions? = JsOptions(),
val buildReport: List<BuildReportType> = emptyList(), val buildReport: List<BuildReportType> = emptyList(),
val usePreciseJavaTracking: Boolean? = null, val usePreciseJavaTracking: Boolean? = null,
val languageVersion: String? = null, val languageVersion: String? = null,
@@ -79,7 +79,8 @@ data class BuildOptions(
data class JsOptions( data class JsOptions(
val incrementalJs: Boolean? = null, val incrementalJs: Boolean? = null,
val incrementalJsKlib: Boolean? = null, val incrementalJsKlib: Boolean? = null,
val incrementalJsIr: Boolean? = null val incrementalJsIr: Boolean? = null,
val yarn: Boolean? = null,
) )
data class NativeOptions( data class NativeOptions(
@@ -163,6 +164,7 @@ data class BuildOptions(
jsOptions.incrementalJs?.let { arguments.add("-Pkotlin.incremental.js=$it") } jsOptions.incrementalJs?.let { arguments.add("-Pkotlin.incremental.js=$it") }
jsOptions.incrementalJsKlib?.let { arguments.add("-Pkotlin.incremental.js.klib=$it") } jsOptions.incrementalJsKlib?.let { arguments.add("-Pkotlin.incremental.js.klib=$it") }
jsOptions.incrementalJsIr?.let { arguments.add("-Pkotlin.incremental.js.ir=$it") } jsOptions.incrementalJsIr?.let { arguments.add("-Pkotlin.incremental.js.ir=$it") }
jsOptions.yarn?.let { arguments.add("-Pkotlin.js.yarn=$it") }
} }
if (androidVersion != null) { if (androidVersion != null) {
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.targets.js.yarn.yarn
plugins { plugins {
kotlin("js") kotlin("js")
} }
@@ -10,6 +12,8 @@ kotlin {
} }
} }
yarn
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> { rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().downloadBaseUrl = null rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().downloadBaseUrl = null
} }
@@ -10,13 +10,13 @@ repositories {
mavenCentral() mavenCentral()
} }
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin::class.java) { rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin::class.java) {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().apply { rootProject.the<org.jetbrains.kotlin.gradle.targets.js.npm.NpmExtension>().apply {
resolution("lodash") { override("lodash") {
include("^1.0.0") include("^1.0.0")
exclude("~1.2.1", "1.3.0 - 1.4.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 //A little hacky way to make yarn results
inputFile.fileValue(projectDir.resolve("yarnLockStub")) inputFile.fileValue(projectDir.resolve("packageLockStub"))
} }
kotlin { kotlin {
@@ -5,9 +5,9 @@ plugins {
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> { 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" 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 //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 //A little hacky way to make yarn results
inputFile.fileValue(projectDir.resolve("yarnLockStub")) inputFile.fileValue(projectDir.resolve("packageLockStub"))
} }
repositories { repositories {
@@ -5,15 +5,11 @@
package org.jetbrains.kotlin.gradle.internal 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.logging.progress.ProgressLogger
import org.gradle.internal.service.ServiceRegistry import org.gradle.internal.service.ServiceRegistry
import org.gradle.process.ExecResult import org.gradle.process.ExecResult
import org.gradle.process.internal.ExecAction import org.gradle.process.internal.ExecAction
import org.gradle.process.internal.ExecActionFactory import org.gradle.process.internal.ExecActionFactory
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessageOutputStreamHandler
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.PipedInputStream import java.io.PipedInputStream
import java.io.PipedOutputStream 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_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_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_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_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_GRADLE_PLUGIN_COMPATIBILITY_NO_WARN
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ANDROID_SOURCE_SET_LAYOUT_ANDROID_STYLE_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 val stdlibDomApiIncluded: Boolean
get() = booleanProperty(KOTLIN_JS_STDLIB_DOM_API_INCLUDED) ?: true get() = booleanProperty(KOTLIN_JS_STDLIB_DOM_API_INCLUDED) ?: true
val yarn: Boolean
get() = booleanProperty(KOTLIN_JS_YARN) ?: true
val kotlinTestInferJvmVariant: Boolean val kotlinTestInferJvmVariant: Boolean
get() = booleanProperty("kotlin.test.infer.jvm.variant") ?: true 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_DEFAULT_DEPENDENCY = property("kotlin.stdlib.default.dependency")
val KOTLIN_STDLIB_JDK_VARIANTS_VERSION_ALIGNMENT = property("kotlin.stdlib.jdk.variants.version.alignment") 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_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_GRANULAR_SOURCE_SETS_METADATA = property("kotlin.mpp.enableGranularSourceSetsMetadata")
val KOTLIN_MPP_ENABLE_COMPATIBILITY_METADATA_VARIANT = property("kotlin.mpp.enableCompatibilityMetadataVariant") val KOTLIN_MPP_ENABLE_COMPATIBILITY_METADATA_VARIANT = property("kotlin.mpp.enableCompatibilityMetadataVariant")
val KOTLIN_MPP_ENABLE_CINTEROP_COMMONIZATION = property("kotlin.mpp.enableCInteropCommonization") val KOTLIN_MPP_ENABLE_CINTEROP_COMMONIZATION = property("kotlin.mpp.enableCInteropCommonization")
@@ -138,9 +138,10 @@ abstract class AbstractSetupTask<Env : AbstractEnv, Settings : AbstractSettings<
if (file.exists()) { if (file.exists()) {
file.useLines { seq -> file.useLines { seq ->
val list = seq.first().split(" ") val list = seq.first().split(" ")
list.size == 2 && list.size == 3 &&
list[0] == fileHasher.calculateDirHash(destination) && list[0] == CACHE_VERSION &&
list[1] == fileHasher.hash(dist).toByteArray().toHex().also { distHash = it } list[1] == fileHasher.calculateDirHash(destination) &&
list[2] == fileHasher.hash(dist).toByteArray().toHex().also { distHash = it }
} }
} else false } else false
} }
@@ -156,11 +157,17 @@ abstract class AbstractSetupTask<Env : AbstractEnv, Settings : AbstractSettings<
extract(dist, destination.parentFile) extract(dist, destination.parentFile)
destinationHashFile.writeText( destinationHashFile.writeText(
fileHasher.calculateDirHash(destination)!! + CACHE_VERSION +
" " +
fileHasher.calculateDirHash(destination)!! +
" " + " " +
(distHash ?: fileHasher.hash(dist).toByteArray().toHex()) (distHash ?: fileHasher.hash(dist).toByteArray().toHex())
) )
} }
abstract fun extract(archive: File, destination: File) 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) { override fun configureTestDependencies(test: KotlinJsTest) {
test.dependsOn( test.dependsOn(
nodeJs.npmInstallTaskProvider, nodeJs.npmInstallTaskProvider,
nodeJs.storeYarnLockTaskProvider,
nodeJs.nodeJsSetupTaskProvider nodeJs.nodeJsSetupTaskProvider
) )
test.dependsOn(nodeJs.packageManagerExtension.map { it.postInstallTasks })
} }
override fun configureDefaultTestFramework(test: KotlinJsTest) { override fun configureDefaultTestFramework(test: KotlinJsTest) {
@@ -267,10 +267,11 @@ abstract class KotlinBrowserJsIr @Inject constructor(target: KotlinJsIrTarget) :
) { ) {
dependsOn( dependsOn(
nodeJs.npmInstallTaskProvider, nodeJs.npmInstallTaskProvider,
nodeJs.storeYarnLockTaskProvider,
target.project.tasks.named(compilation.processResourcesTaskName) target.project.tasks.named(compilation.processResourcesTaskName)
) )
dependsOn(nodeJs.packageManagerExtension.map { it.postInstallTasks })
configureOptimization(mode) configureOptimization(mode)
this.inputFilesDirectory.set(inputFilesDirectory) this.inputFilesDirectory.set(inputFilesDirectory)
@@ -62,8 +62,8 @@ abstract class KotlinNodeJsIr @Inject constructor(target: KotlinJsIrTarget) :
if (target.wasmTargetType != KotlinWasmTargetType.WASI) { if (target.wasmTargetType != KotlinWasmTargetType.WASI) {
test.dependsOn( test.dependsOn(
nodeJsTaskProviders.npmInstallTaskProvider, nodeJsTaskProviders.npmInstallTaskProvider,
nodeJsTaskProviders.storeYarnLockTaskProvider,
) )
test.dependsOn(nodeJs.packageManagerExtension.map { it.postInstallTasks })
} }
} }
@@ -1,7 +1,7 @@
package org.jetbrains.kotlin.gradle.targets.js.nodejs package org.jetbrains.kotlin.gradle.targets.js.nodejs
import org.jetbrains.kotlin.gradle.targets.js.AbstractEnv 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 org.jetbrains.kotlin.gradle.tasks.internal.CleanableStore
import java.io.File import java.io.File
@@ -17,7 +17,7 @@ data class NodeJsEnv(
override val ivyDependency: String, override val ivyDependency: String,
override val downloadBaseUrl: String?, override val downloadBaseUrl: String?,
val packageManager: NpmApi, val packageManager: NpmApiExecution<PackageManagerEnvironment>,
) : AbstractEnv { ) : AbstractEnv {
val isWindows: Boolean val isWindows: Boolean
get() = platformName == "win" get() = platformName == "win"
@@ -113,8 +113,8 @@ constructor(
it.workingDir(npmProject.dir) it.workingDir(npmProject.dir)
it.dependsOn( it.dependsOn(
nodeJsTaskProviders.npmInstallTaskProvider, nodeJsTaskProviders.npmInstallTaskProvider,
nodeJsTaskProviders.storeYarnLockTaskProvider,
) )
it.dependsOn(nodeJs.packageManagerExtension.map { it.postInstallTasks })
} }
it.dependsOn(nodeJsTaskProviders.nodeJsSetupTaskProvider) it.dependsOn(nodeJsTaskProviders.nodeJsSetupTaskProvider)
it.dependsOn(compilation.compileKotlinTaskProvider) 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.plugin.PropertiesProvider
import org.jetbrains.kotlin.gradle.targets.js.AbstractSettings import org.jetbrains.kotlin.gradle.targets.js.AbstractSettings
import org.jetbrains.kotlin.gradle.targets.js.NpmVersions 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.KotlinRootNpmResolver
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.PACKAGE_JSON_UMBRELLA_TASK_NAME 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.KotlinNpmCachesSetup
import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask 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.npm.tasks.RootPackageJsonTask
import org.jetbrains.kotlin.gradle.targets.js.yarn.Yarn
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask
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.tasks.internal.CleanableStore
import org.jetbrains.kotlin.gradle.utils.getFile import org.jetbrains.kotlin.gradle.utils.getFile
import org.jetbrains.kotlin.gradle.utils.property import org.jetbrains.kotlin.gradle.utils.property
@@ -80,7 +80,7 @@ open class NodeJsRootExtension(
// Release schedule: https://github.com/nodejs/Release // Release schedule: https://github.com/nodejs/Release
// Actual LTS and Current versions: https://nodejs.org/en/download/ // 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/ // 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") override var command by Property("node")
@@ -91,7 +91,7 @@ open class NodeJsRootExtension(
command = value command = value
} }
var packageManager: NpmApi by Property(Yarn()) val packageManagerExtension: org.gradle.api.provider.Property<NpmApiExt> = project.objects.property()
val taskRequirements: TasksRequirements val taskRequirements: TasksRequirements
get() = resolver.tasksRequirements get() = resolver.tasksRequirements
@@ -135,7 +135,7 @@ open class NodeJsRootExtension(
val name = platform.get().name val name = platform.get().name
val architecture = platform.get().arch 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 cleanableStore = CleanableStore[installationDir.absolutePath]
val nodeDir = cleanableStore[nodeDirName].use() val nodeDir = cleanableStore[nodeDirName].use()
val isWindows = platform.get().isWindows() val isWindows = platform.get().isWindows()
@@ -148,21 +148,23 @@ open class NodeJsRootExtension(
fun getIvyDependency(): String { fun getIvyDependency(): String {
val type = if (isWindows) "zip" else "tar.gz" 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( return NodeJsEnv(
download = download, download = download,
cleanableStore = cleanableStore, cleanableStore = cleanableStore,
rootPackageDir = rootPackageDirectory.getFile(), rootPackageDir = rootPackageDirectory.getFile(),
dir = nodeDir, dir = nodeDir,
nodeBinDir = nodeBinDir, nodeBinDir = nodeBinDir,
nodeExecutable = getExecutable("node", nodeCommand, "exe"), nodeExecutable = getExecutable("node", command, "exe"),
platformName = name, platformName = name,
architectureName = architecture, architectureName = architecture,
ivyDependency = getIvyDependency(), ivyDependency = getIvyDependency(),
downloadBaseUrl = nodeDownloadBaseUrl, downloadBaseUrl = downloadBaseUrl,
packageManager = packageManager packageManager = packageManagerExtension.get().packageManager
) )
} }
@@ -181,8 +183,9 @@ open class NodeJsRootExtension(
val npmCachesSetupTaskProvider: TaskProvider<out KotlinNpmCachesSetup> val npmCachesSetupTaskProvider: TaskProvider<out KotlinNpmCachesSetup>
get() = project.tasks.withType(KotlinNpmCachesSetup::class.java).named(KotlinNpmCachesSetup.NAME) get() = project.tasks.withType(KotlinNpmCachesSetup::class.java).named(KotlinNpmCachesSetup.NAME)
val storeYarnLockTaskProvider: TaskProvider<out YarnLockCopyTask> @Deprecated("This is deprecated and will be removed. Use corresponding property from YarnRootExtension")
get() = project.tasks.withType(YarnLockCopyTask::class.java).named(YarnLockCopyTask.STORE_YARN_LOCK_NAME) val storeYarnLockTaskProvider: TaskProvider<YarnLockStoreTask>
get() = project.yarn.storeYarnLockTaskProvider
companion object { companion object {
const val EXTENSION_NAME: String = "kotlinNodeJs" 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.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.Task import org.gradle.api.Task
import org.gradle.api.file.RegularFile
import org.gradle.api.plugins.BasePlugin import org.gradle.api.plugins.BasePlugin
import org.gradle.api.provider.Provider 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.configurationTimePropertiesAccessor
import org.jetbrains.kotlin.gradle.plugin.internal.usedAtConfigurationTime import org.jetbrains.kotlin.gradle.plugin.internal.usedAtConfigurationTime
import org.jetbrains.kotlin.gradle.plugin.variantImplementationFactory import org.jetbrains.kotlin.gradle.plugin.variantImplementationFactory
import org.jetbrains.kotlin.gradle.targets.js.MultiplePluginDeclarationDetector 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.*
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.resolver.KotlinRootNpmResolver 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.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.KotlinNpmCachesSetup
import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask 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.targets.js.yarn.YarnPlugin
import org.jetbrains.kotlin.gradle.tasks.CleanDataTask import org.jetbrains.kotlin.gradle.tasks.CleanDataTask
import org.jetbrains.kotlin.gradle.tasks.registerTask import org.jetbrains.kotlin.gradle.tasks.registerTask
import org.jetbrains.kotlin.gradle.tasks.withType import org.jetbrains.kotlin.gradle.utils.castIsolatedKotlinPluginClassLoaderAware
import org.jetbrains.kotlin.gradle.utils.* 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> { open class NodeJsRootPlugin : Plugin<Project> {
override fun apply(project: Project) { override fun apply(project: Project) {
@@ -43,8 +48,24 @@ open class NodeJsRootPlugin : Plugin<Project> {
project project
) )
val npm = project.extensions.create(
NpmExtension.EXTENSION_NAME,
NpmExtension::class.java,
project
)
addPlatform(project, nodeJs) addPlatform(project, nodeJs)
npm.nodeJsEnvironment.value(
project.provider {
nodeJs.requireConfigured()
}
).disallowChanges()
nodeJs.packageManagerExtension.convention(
npm
)
val setupTask = project.registerTask<NodeJsSetupTask>(NodeJsSetupTask.NAME) { val setupTask = project.registerTask<NodeJsSetupTask>(NodeJsSetupTask.NAME) {
it.group = TASKS_GROUP_NAME it.group = TASKS_GROUP_NAME
it.description = "Download and install a local node/npm version" it.description = "Download and install a local node/npm version"
@@ -97,21 +118,125 @@ open class NodeJsRootPlugin : Plugin<Project> {
val objectFactory = project.objects val objectFactory = project.objects
// TODO: Could we use common approach with build services to KotlinNpmResolutionManager? val npmResolutionManager: Provider<KotlinNpmResolutionManager> = KotlinNpmResolutionManager.registerIfAbsent(
val npmResolutionManager = project.gradle.sharedServices.registerIfAbsent( project,
KotlinNpmResolutionManager::class.java.name, objectFactory.providerWithLazyConvention {
KotlinNpmResolutionManager::class.java nodeJs.resolver.close()
) { },
it.parameters.resolution.set( gradleNodeModulesProvider,
objectFactory.providerWithLazyConvention { nodeJs.projectPackagesDirectory
nodeJs.resolver.close() )
}
) val rootPackageJson = project.tasks.register(RootPackageJsonTask.NAME, RootPackageJsonTask::class.java) { task ->
it.parameters.gradleNodeModulesProvider.set(gradleNodeModulesProvider) task.dependsOn(nodeJs.npmCachesSetupTaskProvider)
it.parameters.packagesDir.set(nodeJs.projectPackagesDirectory) 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 { npmInstall.configure {
it.npmResolutionManager.value(npmResolutionManager).disallowChanges() it.npmResolutionManager.value(npmResolutionManager).disallowChanges()
@@ -122,6 +247,12 @@ open class NodeJsRootPlugin : Plugin<Project> {
it.group = TASKS_GROUP_NAME it.group = TASKS_GROUP_NAME
it.description = "Clean unused local node version" 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 // from https://github.com/node-gradle/gradle-node-plugin
@@ -142,6 +273,42 @@ open class NodeJsRootPlugin : Plugin<Project> {
).disallowChanges() ).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 { companion object {
const val TASKS_GROUP_NAME: String = "nodeJs" const val TASKS_GROUP_NAME: String = "nodeJs"
@@ -154,30 +321,14 @@ open class NodeJsRootPlugin : Plugin<Project> {
val Project.kotlinNodeJsExtension: NodeJsRootExtension val Project.kotlinNodeJsExtension: NodeJsRootExtension
get() = extensions.getByName(NodeJsRootExtension.EXTENSION_NAME).castIsolatedKotlinPluginClassLoaderAware() get() = extensions.getByName(NodeJsRootExtension.EXTENSION_NAME).castIsolatedKotlinPluginClassLoaderAware()
private val Project.gradleNodeModules
get() = GradleNodeModulesCache.registerIfAbsent(
this,
null,
null
)
val Project.kotlinNpmResolutionManager: Provider<KotlinNpmResolutionManager> val Project.kotlinNpmResolutionManager: Provider<KotlinNpmResolutionManager>
get() { get() {
val npmResolutionManager = project.gradle.sharedServices.registerIfAbsent( return project.gradle.sharedServices.registerIfAbsent(
KotlinNpmResolutionManager::class.java.name, KotlinNpmResolutionManager::class.java.name,
KotlinNpmResolutionManager::class.java KotlinNpmResolutionManager::class.java
) { ) {
error("Must be already registered") 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) it.into(destination)
} }
fixBrokenSymlinks(destination, env.isWindows, fixBrokenSymLinks) fixBrokenSymlinks(this.destination, env.isWindows, fixBrokenSymLinks)
if (!env.isWindows) { if (!env.isWindows) {
File(env.nodeExecutable).setExecutable(true) 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() packageJson.devDependencies.clear()
// yarn requires semver // npm requires semver
packageJson.version = fixSemver(packageJson.version) packageJson.version = fixSemver(packageJson.version)
return makeNodeModule(cacheDir, packageJson) { nodeModule -> return makeNodeModule(cacheDir, packageJson) { nodeModule ->
@@ -5,20 +5,27 @@
package org.jetbrains.kotlin.gradle.targets.js.npm package org.jetbrains.kotlin.gradle.targets.js.npm
import org.gradle.api.Project
import org.gradle.api.Task import org.gradle.api.Task
import org.gradle.api.file.Directory import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.DirectoryProperty
import org.gradle.api.logging.Logger import org.gradle.api.logging.Logger
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters import org.gradle.api.services.BuildServiceParameters
import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Internal
import org.gradle.internal.service.ServiceRegistry 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.Installation
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.KotlinRootNpmResolution 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.KotlinCompilationNpmResolver
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinProjectNpmResolver 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.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 { internal interface UsesKotlinNpmResolutionManager : Task {
@get:Internal @get:Internal
@@ -78,16 +85,16 @@ abstract class KotlinNpmResolutionManager : BuildService<KotlinNpmResolutionMana
internal fun prepare( internal fun prepare(
logger: Logger, logger: Logger,
npmEnvironment: NpmEnvironment, nodeJsEnvironment: NodeJsEnvironment,
yarnEnvironment: YarnEnvironment, environment: PackageManagerEnvironment,
) = prepareIfNeeded(logger = logger, npmEnvironment, yarnEnvironment) ) = prepareIfNeeded(logger = logger, nodeJsEnvironment, environment)
internal fun installIfNeeded( internal fun installIfNeeded(
args: List<String> = emptyList(), args: List<String> = emptyList(),
services: ServiceRegistry, services: ServiceRegistry,
logger: Logger, logger: Logger,
npmEnvironment: NpmEnvironment, nodeJsEnvironment: NodeJsEnvironment,
yarnEnvironment: YarnEnvironment, packageManagerEnvironment: PackageManagerEnvironment,
): Unit? { ): Unit? {
synchronized(this) { synchronized(this) {
if (state is ResolutionState.Installed) { if (state is ResolutionState.Installed) {
@@ -99,8 +106,8 @@ abstract class KotlinNpmResolutionManager : BuildService<KotlinNpmResolutionMana
} }
return try { return try {
val installation: Installation = prepareIfNeeded(logger = logger, npmEnvironment, yarnEnvironment) val installation: Installation = prepareIfNeeded(logger = logger, nodeJsEnvironment, packageManagerEnvironment)
installation.install(args, services, logger, npmEnvironment, yarnEnvironment) installation.install(args, services, logger, nodeJsEnvironment, packageManagerEnvironment)
state = ResolutionState.Installed() state = ResolutionState.Installed()
} catch (e: Exception) { } catch (e: Exception) {
state = ResolutionState.Error(e) state = ResolutionState.Error(e)
@@ -111,8 +118,8 @@ abstract class KotlinNpmResolutionManager : BuildService<KotlinNpmResolutionMana
private fun prepareIfNeeded( private fun prepareIfNeeded(
logger: Logger, logger: Logger,
npmEnvironment: NpmEnvironment, nodeJsEnvironment: NodeJsEnvironment,
yarnEnvironment: YarnEnvironment, packageManagerEnvironment: PackageManagerEnvironment,
): Installation { ): Installation {
val state0 = this.state val state0 = this.state
return when (state0) { return when (state0) {
@@ -128,8 +135,8 @@ abstract class KotlinNpmResolutionManager : BuildService<KotlinNpmResolutionMana
is ResolutionState.Configuring -> { is ResolutionState.Configuring -> {
state1.resolution.prepareInstallation( state1.resolution.prepareInstallation(
logger, logger,
npmEnvironment, nodeJsEnvironment,
yarnEnvironment, packageManagerEnvironment,
this this
).also { ).also {
this.state = ResolutionState.Prepared(it) this.state = ResolutionState.Prepared(it)
@@ -146,4 +153,52 @@ abstract class KotlinNpmResolutionManager : BuildService<KotlinNpmResolutionMana
is ResolutionState.Error -> throw state0.wrappedException 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.api.logging.Logger
import org.gradle.internal.service.ServiceRegistry import org.gradle.internal.service.ServiceRegistry
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnv 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.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.File
import java.io.Serializable import java.io.Serializable
/** /**
* NodeJS package manager API * NodeJS package manager API
*/ */
interface NpmApi : Serializable { interface NpmApiExecution<out T : PackageManagerEnvironment> : Serializable {
fun preparedFiles(nodeJs: NpmEnvironment): Collection<File> fun preparedFiles(nodeJs: NodeJsEnvironment): Collection<File>
fun prepareRootProject( fun prepareRootProject(
nodeJs: NpmEnvironment, nodeJs: NodeJsEnvironment,
packageManagerEnvironment: @UnsafeVariance T,
rootProjectName: String, rootProjectName: String,
rootProjectVersion: String, rootProjectVersion: String,
logger: Logger,
subProjects: Collection<PreparedKotlinCompilationNpmResolution>, subProjects: Collection<PreparedKotlinCompilationNpmResolution>,
resolutions: Map<String, String>,
) )
fun resolveRootProject( fun resolveRootProject(
services: ServiceRegistry, services: ServiceRegistry,
logger: Logger, logger: Logger,
nodeJs: NpmEnvironment, nodeJs: NodeJsEnvironment,
yarn: YarnEnvironment, packageManagerEnvironment: @UnsafeVariance T,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>, npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
cliArgs: List<String> cliArgs: List<String>
) )
@@ -44,32 +42,15 @@ interface NpmApi : Serializable {
} }
} }
data class NpmEnvironment( data class NodeJsEnvironment(
val rootPackageDir: File, val rootPackageDir: File,
val nodeExecutable: String, val nodeExecutable: String,
val isWindows: Boolean, val packageManager: NpmApiExecution<PackageManagerEnvironment>
val packageManager: NpmApi
) : Serializable ) : Serializable
internal val NodeJsEnv.asNpmEnvironment internal val NodeJsEnv.asNodeJsEnvironment
get() = NpmEnvironment( get() = NodeJsEnvironment(
rootPackageDir, rootPackageDir,
nodeExecutable, nodeExecutable,
isWindows,
packageManager 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. * 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 org.jetbrains.kotlin.gradle.targets.js.npm.resolved.PreparedKotlinCompilationNpmResolution
import java.io.File import java.io.File
class YarnImportedPackagesVersionResolver( class NpmImportedPackagesVersionResolver(
private val logger: Logger,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>, npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
private val nodeJsWorldDir: File 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( task.dependsOn(
kotlinNodeJsTaskProvidersExtension.npmInstallTaskProvider, kotlinNodeJsTaskProvidersExtension.npmInstallTaskProvider,
kotlinNodeJsTaskProvidersExtension.storeYarnLockTaskProvider,
) )
task.dependsOn(nodeJs.packageManagerExtension.map { it.postInstallTasks })
} }
} }
} }
@@ -35,7 +35,7 @@ class PackageJson(
var workspaces: Collection<String>? = null var workspaces: Collection<String>? = null
var resolutions: Map<String, String>? = null var overrides: Map<String, String>? = null
var types: 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.api.logging.Logger
import org.gradle.internal.service.ServiceRegistry 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.KotlinNpmResolutionManager
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmEnvironment import org.jetbrains.kotlin.gradle.targets.js.npm.NodeJsEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.YarnEnvironment
import org.jetbrains.kotlin.gradle.targets.js.yarn.toVersionString
import java.io.Serializable import java.io.Serializable
class KotlinRootNpmResolution( class KotlinRootNpmResolution(
@@ -25,8 +24,8 @@ class KotlinRootNpmResolution(
*/ */
internal fun prepareInstallation( internal fun prepareInstallation(
logger: Logger, logger: Logger,
npmEnvironment: NpmEnvironment, nodeJsEnvironment: NodeJsEnvironment,
yarnEnvironment: YarnEnvironment, packageManagerEnvironment: PackageManagerEnvironment,
npmResolutionManager: KotlinNpmResolutionManager, npmResolutionManager: KotlinNpmResolutionManager,
): Installation { ): Installation {
synchronized(projects) { synchronized(projects) {
@@ -41,14 +40,12 @@ class KotlinRootNpmResolution(
) )
} }
npmEnvironment.packageManager.prepareRootProject( nodeJsEnvironment.packageManager.prepareRootProject(
npmEnvironment, nodeJsEnvironment,
packageManagerEnvironment,
rootProjectName, rootProjectName,
rootProjectVersion, rootProjectVersion,
logger,
projectResolutions, projectResolutions,
yarnEnvironment.yarnResolutions
.associate { it.path to it.toVersionString() },
) )
return Installation( return Installation(
@@ -63,15 +60,15 @@ class Installation(val compilationResolutions: Collection<PreparedKotlinCompilat
args: List<String>, args: List<String>,
services: ServiceRegistry, services: ServiceRegistry,
logger: Logger, logger: Logger,
npmEnvironment: NpmEnvironment, nodeJsEnvironment: NodeJsEnvironment,
yarnEnvironment: YarnEnvironment, packageManagerEnvironment: PackageManagerEnvironment,
) { ) {
synchronized(compilationResolutions) { synchronized(compilationResolutions) {
npmEnvironment.packageManager.resolveRootProject( nodeJsEnvironment.packageManager.resolveRootProject(
services, services,
logger, logger,
npmEnvironment, nodeJsEnvironment,
yarnEnvironment, packageManagerEnvironment,
compilationResolutions, compilationResolutions,
args args
) )
@@ -10,14 +10,7 @@ import org.gradle.api.file.Directory
import org.gradle.api.file.FileCollection import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFile import org.gradle.api.file.RegularFile
import org.gradle.api.provider.Provider import org.gradle.api.provider.Provider
import org.gradle.api.tasks.IgnoreEmptyDirectories import org.gradle.api.tasks.*
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.work.DisableCachingByDefault import org.gradle.work.DisableCachingByDefault
import org.gradle.work.NormalizeLineEndings import org.gradle.work.NormalizeLineEndings
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension 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.KotlinNpmResolutionManager
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject 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.UsesKotlinNpmResolutionManager
import org.jetbrains.kotlin.gradle.targets.js.npm.asNpmEnvironment import org.jetbrains.kotlin.gradle.targets.js.npm.asNodeJsEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.asYarnEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinRootNpmResolver 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 org.jetbrains.kotlin.gradle.utils.getFile
import java.io.File import java.io.File
@@ -46,9 +37,6 @@ abstract class KotlinNpmInstallTask :
private val nodeJs: NodeJsRootExtension private val nodeJs: NodeJsRootExtension
get() = project.rootProject.kotlinNodeJsExtension get() = project.rootProject.kotlinNodeJsExtension
private val yarn
get() = project.rootProject.yarn
private val rootResolver: KotlinRootNpmResolver private val rootResolver: KotlinRootNpmResolver
get() = nodeJs.resolver get() = nodeJs.resolver
@@ -57,12 +45,12 @@ abstract class KotlinNpmInstallTask :
// ----- // -----
private val npmEnvironment by lazy { private val nodsJsEnvironment by lazy {
nodeJs.requireConfigured().asNpmEnvironment nodeJs.requireConfigured().asNodeJsEnvironment
} }
private val yarnEnv by lazy { private val packageManagerEnv by lazy {
yarn.requireConfigured().asYarnEnvironment nodeJs.packageManagerExtension.get().environment
} }
@Input @Input
@@ -73,7 +61,7 @@ abstract class KotlinNpmInstallTask :
@get:NormalizeLineEndings @get:NormalizeLineEndings
@get:InputFiles @get:InputFiles
val preparedFiles: Collection<File> by lazy { val preparedFiles: Collection<File> by lazy {
nodeJs.packageManager.preparedFiles(npmEnvironment) nodeJs.packageManagerExtension.get().packageManager.preparedFiles(nodsJsEnvironment)
} }
@get:PathSensitive(PathSensitivity.RELATIVE) @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") } val yarnLockFile: Provider<RegularFile> = nodeJs.rootPackageDirectory.map { it.file("yarn.lock") }
@Deprecated( @Deprecated(
"This property is deprecated and will be removed in future. Use yarnLockFile instead", "This property is deprecated and will be removed in future. Use additionalFiles instead",
replaceWith = ReplaceWith("yarnLockFile") replaceWith = ReplaceWith("additionalFiles")
) )
@get:Internal @get:Internal
val yarnLock: File val yarnLock: File
@@ -115,8 +112,8 @@ abstract class KotlinNpmInstallTask :
args = args, args = args,
services = services, services = services,
logger = logger, logger = logger,
npmEnvironment, nodsJsEnvironment,
yarnEnv packageManagerEnv
) ?: throw (npmResolutionManager.get().state as KotlinNpmResolutionManager.ResolutionState.Error).wrappedException ) ?: 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.nodejs.NodeJsRootPlugin.Companion.kotlinNodeJsExtension
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject 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.UsesKotlinNpmResolutionManager
import org.jetbrains.kotlin.gradle.targets.js.npm.asNpmEnvironment import org.jetbrains.kotlin.gradle.targets.js.npm.asNodeJsEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.asYarnEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.KotlinRootNpmResolver 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 org.jetbrains.kotlin.gradle.utils.getFile
import java.io.File import java.io.File
@@ -37,9 +35,6 @@ abstract class RootPackageJsonTask :
private val nodeJs private val nodeJs
get() = project.rootProject.kotlinNodeJsExtension get() = project.rootProject.kotlinNodeJsExtension
private val yarn
get() = project.rootProject.yarn
private val rootResolver: KotlinRootNpmResolver private val rootResolver: KotlinRootNpmResolver
get() = nodeJs.resolver get() = nodeJs.resolver
@@ -48,12 +43,12 @@ abstract class RootPackageJsonTask :
// ----- // -----
private val npmEnvironment by lazy { private val nodeJsEnvironment by lazy {
nodeJs.requireConfigured().asNpmEnvironment nodeJs.requireConfigured().asNodeJsEnvironment
} }
private val yarnEnv by lazy { private val packageManagerEnv by lazy {
yarn.requireConfigured().asYarnEnvironment nodeJs.packageManagerExtension.get().environment
} }
@get:OutputFile @get:OutputFile
@@ -87,7 +82,7 @@ abstract class RootPackageJsonTask :
@TaskAction @TaskAction
fun resolve() { fun resolve() {
npmResolutionManager.get().prepare(logger, npmEnvironment, yarnEnv) npmResolutionManager.get().prepare(logger, nodeJsEnvironment, packageManagerEnv)
} }
companion object { companion object {
@@ -46,7 +46,7 @@ fun FileHasher.calculateDirHash(
dir.walk() dir.walk()
.forEach { file -> .forEach { file ->
hasher.putString(file.toRelativeString(dir)) hasher.putString(file.toRelativeString(dir))
if (file.isFile && !Files.isSymbolicLink(file.toPath())) { if (file.isFile) {
if (!Files.isSymbolicLink(file.toPath())) { if (!Files.isSymbolicLink(file.toPath())) {
hasher.putHash(hash(file)) hasher.putHash(hash(file))
} else { } 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.api.logging.Logger
import org.gradle.internal.service.ServiceRegistry import org.gradle.internal.service.ServiceRegistry
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApi import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApiExecution
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmEnvironment import org.jetbrains.kotlin.gradle.targets.js.npm.NodeJsEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.YarnEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.PreparedKotlinCompilationNpmResolution import org.jetbrains.kotlin.gradle.targets.js.npm.resolved.PreparedKotlinCompilationNpmResolution
import java.io.File import java.io.File
class Yarn : NpmApi { class Yarn : NpmApiExecution<YarnEnvironment> {
private val yarnWorkspaces = YarnWorkspaces() private val yarnWorkspaces = YarnWorkspaces()
override fun preparedFiles(nodeJs: NpmEnvironment): Collection<File> = override fun preparedFiles(nodeJs: NodeJsEnvironment): Collection<File> =
yarnWorkspaces.preparedFiles(nodeJs) yarnWorkspaces.preparedFiles(nodeJs)
override fun prepareRootProject( override fun prepareRootProject(
nodeJs: NpmEnvironment, nodeJs: NodeJsEnvironment,
yarn: YarnEnvironment,
rootProjectName: String, rootProjectName: String,
rootProjectVersion: String, rootProjectVersion: String,
logger: Logger,
subProjects: Collection<PreparedKotlinCompilationNpmResolution>, subProjects: Collection<PreparedKotlinCompilationNpmResolution>,
resolutions: Map<String, String>,
) = yarnWorkspaces ) = yarnWorkspaces
.prepareRootProject( .prepareRootProject(
nodeJs, nodeJs,
yarn,
rootProjectName, rootProjectName,
rootProjectVersion, rootProjectVersion,
logger,
subProjects, subProjects,
resolutions,
) )
override fun resolveRootProject( override fun resolveRootProject(
services: ServiceRegistry, services: ServiceRegistry,
logger: Logger, logger: Logger,
nodeJs: NpmEnvironment, nodeJs: NodeJsEnvironment,
yarn: YarnEnvironment, yarn: YarnEnvironment,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>, npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
cliArgs: List<String> cliArgs: List<String>
@@ -8,17 +8,16 @@ package org.jetbrains.kotlin.gradle.targets.js.yarn
import org.gradle.api.logging.Logger import org.gradle.api.logging.Logger
import org.gradle.internal.service.ServiceRegistry import org.gradle.internal.service.ServiceRegistry
import org.jetbrains.kotlin.gradle.internal.execWithProgress import org.jetbrains.kotlin.gradle.internal.execWithProgress
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApi import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApiExecution
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmEnvironment import org.jetbrains.kotlin.gradle.targets.js.npm.NodeJsEnvironment
import org.jetbrains.kotlin.gradle.targets.js.npm.YarnEnvironment
import java.io.File import java.io.File
abstract class YarnBasics : NpmApi { abstract class YarnBasics : NpmApiExecution<YarnEnvironment> {
fun yarnExec( fun yarnExec(
services: ServiceRegistry, services: ServiceRegistry,
logger: Logger, logger: Logger,
nodeJs: NpmEnvironment, nodeJs: NodeJsEnvironment,
yarn: YarnEnvironment, yarn: YarnEnvironment,
dir: File, dir: File,
description: String, description: String,
@@ -35,11 +34,7 @@ abstract class YarnBasics : NpmApi {
val nodeExecutable = nodeJs.nodeExecutable val nodeExecutable = nodeJs.nodeExecutable
if (!yarn.ignoreScripts) { if (!yarn.ignoreScripts) {
val nodePath = if (nodeJs.isWindows) { val nodePath = File(nodeExecutable).parent
File(nodeExecutable).parent
} else {
nodeExecutable
}
exec.environment( exec.environment(
"PATH", "PATH",
"$nodePath${File.pathSeparator}${System.getenv("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, override val dir: File,
val executable: String, val executable: String,
override val ivyDependency: String, override val ivyDependency: String,
val standalone: Boolean,
val ignoreScripts: Boolean, val ignoreScripts: Boolean,
val yarnLockMismatchReport: YarnLockMismatchReport, val yarnLockMismatchReport: YarnLockMismatchReport,
val reportNewYarnLock: Boolean, val reportNewYarnLock: Boolean,
val yarnLockAutoReplace: Boolean, val yarnLockAutoReplace: Boolean,
val yarnResolutions: List<YarnResolution> 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.Plugin
import org.gradle.api.Project 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.MultiplePluginDeclarationDetector
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin 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.kotlinNodeJsExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin.Companion.kotlinNpmResolutionManager import org.jetbrains.kotlin.gradle.targets.js.npm.LockCopyTask
import org.jetbrains.kotlin.gradle.targets.js.npm.RequiresNpmDependencies
import org.jetbrains.kotlin.gradle.targets.js.npm.resolver.implementing
import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.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.CleanDataTask
import org.jetbrains.kotlin.gradle.tasks.registerTask import org.jetbrains.kotlin.gradle.tasks.registerTask
import org.jetbrains.kotlin.gradle.utils.detachedResolvable import org.jetbrains.kotlin.gradle.utils.detachedResolvable
import org.jetbrains.kotlin.gradle.utils.onlyIfCompat
open class YarnPlugin : Plugin<Project> { open class YarnPlugin : Plugin<Project> {
override fun apply(project: Project): Unit = project.run { override fun apply(project: Project): Unit = project.run {
@@ -40,6 +32,10 @@ open class YarnPlugin : Plugin<Project> {
yarnRootExtension.platform.value(nodeJs.platform) yarnRootExtension.platform.value(nodeJs.platform)
.disallowChanges() .disallowChanges()
nodeJs.packageManagerExtension.set(
yarnRootExtension
)
val setupTask = registerTask<YarnSetupTask>(YarnSetupTask.NAME) { val setupTask = registerTask<YarnSetupTask>(YarnSetupTask.NAME) {
it.dependsOn(nodeJsTaskProviders.nodeJsSetupTaskProvider) 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) val kotlinNpmInstall = tasks.named(KotlinNpmInstallTask.NAME)
kotlinNpmInstall.configure { kotlinNpmInstall.configure {
it.dependsOn(rootPackageJson)
it.dependsOn(setupTask) 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) { tasks.register("yarn" + CleanDataTask.NAME_SUFFIX, CleanDataTask::class.java) {
it.cleanableStoreProvider = provider { yarnRootExtension.requireConfigured().cleanableStore } it.cleanableStoreProvider = provider { yarnRootExtension.requireConfigured().cleanableStore }
it.description = "Clean unused local yarn version" 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 -> tasks.register(STORE_YARN_LOCK_NAME, YarnLockStoreTask::class.java) { task ->
task.dependsOn(kotlinNpmInstall) 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.outputDirectory.set(yarnRootExtension.lockFileDirectory)
task.fileName.set(yarnRootExtension.lockFileName) task.fileName.set(yarnRootExtension.lockFileName)
task.yarnLockMismatchReport = provider { yarnRootExtension.requireConfigured().yarnLockMismatchReport } task.lockFileMismatchReport.value(
task.reportNewYarnLock = provider { yarnRootExtension.requireConfigured().reportNewYarnLock } provider { yarnRootExtension.requireConfigured().yarnLockMismatchReport.toLockFileMismatchReport() }
task.yarnLockAutoReplace = provider { yarnRootExtension.requireConfigured().yarnLockAutoReplace } ).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 -> tasks.register(UPGRADE_YARN_LOCK, YarnLockCopyTask::class.java) { task ->
task.dependsOn(kotlinNpmInstall) 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.outputDirectory.set(yarnRootExtension.lockFileDirectory)
task.fileName.set(yarnRootExtension.lockFileName) 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) val lockFile = yarnRootExtension.lockFileDirectory.resolve(yarnRootExtension.lockFileName)
it.inputFile.set(yarnRootExtension.lockFileDirectory.resolve(yarnRootExtension.lockFileName)) it.inputFile.set(yarnRootExtension.lockFileDirectory.resolve(yarnRootExtension.lockFileName))
it.outputDirectory.set(nodeJs.rootPackageDirectory) it.outputDirectory.set(nodeJs.rootPackageDirectory)
it.fileName.set("yarn.lock") it.fileName.set(LockCopyTask.YARN_LOCK)
it.onlyIf { it.onlyIf {
lockFile.exists() lockFile.exists()
} }
} }
kotlinNpmInstall.configure { yarnRootExtension.preInstallTasks.value(
it.dependsOn(restoreYarnLock) listOf(yarnRootExtension.restoreYarnLockTaskProvider)
} ).disallowChanges()
}
// Yes, we need to break Task Configuration Avoidance here yarnRootExtension.postInstallTasks.value(
// In case when we need to create package.json's files and execute kotlinNpmInstall, listOf(yarnRootExtension.storeYarnLockTaskProvider)
// We need to configure all RequiresNpmDependencies tasks to install them, ).disallowChanges()
// 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)
}
}
}
}
} }
companion object { companion object {
@@ -164,5 +114,9 @@ open class YarnPlugin : Plugin<Project> {
rootProject.plugins.apply(YarnPlugin::class.java) rootProject.plugins.apply(YarnPlugin::class.java)
return rootProject.extensions.getByName(YarnRootExtension.YARN) as YarnRootExtension 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.Action
import org.gradle.api.Project 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.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.logging.kotlinInfo import org.jetbrains.kotlin.gradle.logging.kotlinInfo
import org.jetbrains.kotlin.gradle.targets.js.AbstractSettings 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.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 org.jetbrains.kotlin.gradle.tasks.internal.CleanableStore
import java.io.File import java.io.File
open class YarnRootExtension( open class YarnRootExtension(
val project: Project val project: Project
) : AbstractSettings<YarnEnv>() { ) : AbstractSettings<YarnEnv>(), NpmApiExtension<YarnEnvironment, Yarn> {
init { init {
check(project == project.rootProject) check(project == project.rootProject)
} }
@@ -26,6 +33,24 @@ open class YarnRootExtension(
project.logger.kotlinInfo("Storing cached files in $it") 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 installationDir by Property(gradleHome.resolve("yarn"))
override var downloadBaseUrl: String? by Property("https://github.com/yarnpkg/yarn/releases/download") 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 command by Property("yarn")
override var download by Property(true) override var download by Property(true)
var lockFileName by Property("yarn.lock") var lockFileName by Property(LockCopyTask.YARN_LOCK)
var lockFileDirectory: File by Property(project.rootDir.resolve("kotlin-js-store")) var lockFileDirectory: File by Property(project.rootDir.resolve(LockCopyTask.KOTLIN_JS_STORE))
var ignoreScripts by Property(true) var ignoreScripts by Property(true)
@@ -50,11 +75,6 @@ open class YarnRootExtension(
.withType(YarnSetupTask::class.java) .withType(YarnSetupTask::class.java)
.named(YarnSetupTask.NAME) .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) internal val platform: org.gradle.api.provider.Property<Platform> = project.objects.property(Platform::class.java)
var resolutions: MutableList<YarnResolution> by Property(mutableListOf()) 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 { override fun finalizeConfiguration(): YarnEnv {
val cleanableStore = CleanableStore[installationDir.path] val cleanableStore = CleanableStore[installationDir.path]
@@ -93,7 +115,6 @@ open class YarnRootExtension(
cleanableStore = cleanableStore, cleanableStore = cleanableStore,
dir = home, dir = home,
executable = getExecutable("yarn", command, "cmd"), executable = getExecutable("yarn", command, "cmd"),
standalone = !download,
ivyDependency = "com.yarnpkg:yarn:$version@tar.gz", ivyDependency = "com.yarnpkg:yarn:$version@tar.gz",
ignoreScripts = ignoreScripts, ignoreScripts = ignoreScripts,
yarnLockMismatchReport = yarnLockMismatchReport, 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 { companion object {
const val YARN: String = "kotlinYarn" const val YARN: String = "kotlinYarn"
@@ -13,7 +13,7 @@ import org.jetbrains.kotlin.gradle.utils.getFile
import java.io.File import java.io.File
class YarnWorkspaces : YarnBasics() { class YarnWorkspaces : YarnBasics() {
override fun preparedFiles(nodeJs: NpmEnvironment): Collection<File> { override fun preparedFiles(nodeJs: NodeJsEnvironment): Collection<File> {
return listOf( return listOf(
nodeJs nodeJs
.rootPackageDir .rootPackageDir
@@ -22,28 +22,26 @@ class YarnWorkspaces : YarnBasics() {
} }
override fun prepareRootProject( override fun prepareRootProject(
nodeJs: NpmEnvironment, nodeJs: NodeJsEnvironment,
yarn: YarnEnvironment,
rootProjectName: String, rootProjectName: String,
rootProjectVersion: String, rootProjectVersion: String,
logger: Logger,
subProjects: Collection<PreparedKotlinCompilationNpmResolution>, subProjects: Collection<PreparedKotlinCompilationNpmResolution>,
resolutions: Map<String, String>,
) { ) {
return prepareRootPackageJson( return prepareRootPackageJson(
nodeJs, nodeJs,
rootProjectName, rootProjectName,
rootProjectVersion, rootProjectVersion,
logger,
subProjects, subProjects,
resolutions yarn.yarnResolutions
.associate { it.path to it.toVersionString() },
) )
} }
private fun prepareRootPackageJson( private fun prepareRootPackageJson(
nodeJs: NpmEnvironment, nodeJs: NodeJsEnvironment,
rootProjectName: String, rootProjectName: String,
rootProjectVersion: String, rootProjectVersion: String,
logger: Logger,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>, npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
resolutions: Map<String, String> resolutions: Map<String, String>
) { ) {
@@ -52,7 +50,6 @@ class YarnWorkspaces : YarnBasics() {
saveRootProjectWorkspacesPackageJson( saveRootProjectWorkspacesPackageJson(
rootProjectName, rootProjectName,
rootProjectVersion, rootProjectVersion,
logger,
npmProjects, npmProjects,
resolutions, resolutions,
rootPackageJsonFile rootPackageJsonFile
@@ -62,7 +59,7 @@ class YarnWorkspaces : YarnBasics() {
override fun resolveRootProject( override fun resolveRootProject(
services: ServiceRegistry, services: ServiceRegistry,
logger: Logger, logger: Logger,
nodeJs: NpmEnvironment, nodeJs: NodeJsEnvironment,
yarn: YarnEnvironment, yarn: YarnEnvironment,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>, npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
cliArgs: List<String> cliArgs: List<String>
@@ -75,7 +72,7 @@ class YarnWorkspaces : YarnBasics() {
nodeJs, nodeJs,
yarn, yarn,
nodeJsWorldDir, nodeJsWorldDir,
NpmApi.resolveOperationDescription("yarn"), NpmApiExecution.resolveOperationDescription("yarn"),
cliArgs cliArgs
) )
} }
@@ -83,7 +80,6 @@ class YarnWorkspaces : YarnBasics() {
private fun saveRootProjectWorkspacesPackageJson( private fun saveRootProjectWorkspacesPackageJson(
rootProjectName: String, rootProjectName: String,
rootProjectVersion: String, rootProjectVersion: String,
logger: Logger,
npmProjects: Collection<PreparedKotlinCompilationNpmResolution>, npmProjects: Collection<PreparedKotlinCompilationNpmResolution>,
resolutions: Map<String, String>, resolutions: Map<String, String>,
rootPackageJsonFile: File rootPackageJsonFile: File
@@ -94,10 +90,10 @@ class YarnWorkspaces : YarnBasics() {
val npmProjectWorkspaces = npmProjects.map { it.npmProjectDir.getFile().relativeTo(nodeJsWorldDir).path } val npmProjectWorkspaces = npmProjects.map { it.npmProjectDir.getFile().relativeTo(nodeJsWorldDir).path }
val importedProjectWorkspaces = val importedProjectWorkspaces =
YarnImportedPackagesVersionResolver(logger, npmProjects, nodeJsWorldDir).resolveAndUpdatePackages() NpmImportedPackagesVersionResolver(npmProjects, nodeJsWorldDir).resolveAndUpdatePackages()
rootPackageJson.workspaces = npmProjectWorkspaces + importedProjectWorkspaces rootPackageJson.workspaces = npmProjectWorkspaces + importedProjectWorkspaces
rootPackageJson.resolutions = resolutions rootPackageJson.customField("resolutions", resolutions)
rootPackageJson.saveTo( rootPackageJson.saveTo(
rootPackageJsonFile rootPackageJsonFile
) )