Gradle, npm: disable manageNodeModules by default

#KT-30530
This commit is contained in:
Sergey Rostov
2019-04-10 10:03:58 +03:00
parent d82725718a
commit 4c2c8a1788
24 changed files with 541 additions and 608 deletions
@@ -0,0 +1,43 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. 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.internal
import org.gradle.api.Project
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.process.internal.ExecAction
import org.gradle.process.internal.ExecActionFactory
import java.io.ByteArrayOutputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
import kotlin.concurrent.thread
internal fun Project.execWithProgress(description: String, body: (ExecAction) -> Unit) {
this as ProjectInternal
val stderr = ByteArrayOutputStream()
val stdout = StringBuilder()
val stdInPipe = PipedInputStream()
val exec = services.get(ExecActionFactory::class.java).newExecAction()
body(exec)
project.operation(description) {
exec.errorOutput = stderr
exec.standardOutput = PipedOutputStream(stdInPipe)
val outputReaderThread = thread(name = "output reader for [$description]") {
stdInPipe.reader().useLines { lines ->
lines.forEach {
stdout.appendln(it)
progress(it)
}
}
}
exec.isIgnoreExitValue = true
val result = exec.execute()
outputReaderThread.join()
if (result.exitValue != 0) {
error(stderr.toString() + "\n" + stdout)
}
}
}
@@ -0,0 +1,27 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. 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.internal
import org.gradle.api.Project
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.internal.logging.events.ProgressStartEvent
import org.gradle.internal.logging.progress.ProgressLogger
fun Project.operation(
description: String,
initialStatus: String? = null,
body: ProgressLogger.() -> Unit
) {
val services = (project as ProjectInternal).services
val progressFactory = services.get(org.gradle.internal.logging.progress.ProgressLoggerFactory::class.java)
val operation = progressFactory.newOperation(ProgressStartEvent.BUILD_OP_CATEGORY)
operation.start(description, initialStatus)
try {
operation.body()
} finally {
operation.completed()
}
}
@@ -3,10 +3,9 @@ package org.jetbrains.kotlin.gradle.targets.js
import org.gradle.api.file.FileCollection import org.gradle.api.file.FileCollection
import org.gradle.language.base.plugins.LifecycleBasePlugin import org.gradle.language.base.plugins.LifecycleBasePlugin
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationToRunnableFiles import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationToRunnableFiles
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsSetupTask import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProjectLayout
import org.jetbrains.kotlin.gradle.targets.js.tasks.* import org.jetbrains.kotlin.gradle.targets.js.tasks.KotlinNodeJsTestTask
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
import org.jetbrains.kotlin.gradle.tasks.registerTask import org.jetbrains.kotlin.gradle.tasks.registerTask
import org.jetbrains.kotlin.gradle.testing.internal.configureConventions import org.jetbrains.kotlin.gradle.testing.internal.configureConventions
@@ -51,7 +50,7 @@ internal class KotlinJsCompilationTestsConfigurator(
implementation(kotlin("test-nodejs-runner")) implementation(kotlin("test-nodejs-runner"))
} }
val projectWithNodeJsPlugin = NodeJsPlugin.ensureAppliedInHierarchy(target.project) val nodeJs = NodeJsPlugin[target.project]
val testTask = registerTask(project, testTaskName, KotlinNodeJsTestTask::class.java) { testJs -> val testTask = registerTask(project, testTaskName, KotlinNodeJsTestTask::class.java) { testJs ->
testJs.group = LifecycleBasePlugin.VERIFICATION_GROUP testJs.group = LifecycleBasePlugin.VERIFICATION_GROUP
@@ -68,8 +67,6 @@ internal class KotlinJsCompilationTestsConfigurator(
} }
testJs.nodeJsProcessOptions.workingDir = project.projectDir testJs.nodeJsProcessOptions.workingDir = project.projectDir
testJs.nodeModulesDir = project.rootDir.resolve("node_modules")
testJs.testRuntimeNodeModules = listOf( testJs.testRuntimeNodeModules = listOf(
"kotlin-test-nodejs-runner.js", "kotlin-test-nodejs-runner.js",
"kotlin-nodejs-source-map-support.js" "kotlin-nodejs-source-map-support.js"
@@ -83,11 +80,11 @@ internal class KotlinJsCompilationTestsConfigurator(
project.afterEvaluate { project.afterEvaluate {
// defer nodeJs executable setup, as nodejs project settings may change during configuration // defer nodeJs executable setup, as nodejs project settings may change during configuration
testTask.configure { testTask.configure {
val nodeJsSetupTask = projectWithNodeJsPlugin.tasks.findByName(NodeJsSetupTask.NAME) val nodeJsSetupTask = nodeJs.nodeJsSetupTask
it.dependsOn(nodeJsSetupTask) it.dependsOn(nodeJsSetupTask)
if (it.nodeJsProcessOptions.executable == null) { if (it.nodeJsProcessOptions.executable == null) {
it.nodeJsProcessOptions.executable = NodeJsExtension[projectWithNodeJsPlugin].buildEnv().nodeExec it.nodeJsProcessOptions.executable = nodeJs.buildEnv().nodeExec
} }
} }
} }
@@ -9,23 +9,16 @@ import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.targets.js.npm.PackageJson import org.jetbrains.kotlin.gradle.targets.js.npm.PackageJson
open class NodeJsExtension(val project: Project) { open class NodeJsExtension(val project: Project) {
val root: NodeJsRootExtension
get() = this as? NodeJsRootExtension ?: NodeJsRootExtension[project]
internal val packageJsonHandlers = mutableListOf<PackageJson.() -> Unit>() internal val packageJsonHandlers = mutableListOf<PackageJson.() -> Unit>()
@Suppress("unused")
fun packageJson(handler: PackageJson.() -> Unit) { fun packageJson(handler: PackageJson.() -> Unit) {
packageJsonHandlers.add(handler) packageJsonHandlers.add(handler)
} }
companion object { companion object {
operator fun get(project: Project): NodeJsExtension { operator fun get(project: Project): NodeJsExtension {
NodeJsPlugin.apply(project.rootProject) NodeJsPlugin[project.rootProject]
return project.extensions.getByName(NodeJsRootExtension.NODE_JS) as NodeJsExtension return project.extensions.getByName(NodeJsRootExtension.NODE_JS) as NodeJsExtension
} }
} }
} }
val Project.nodeJs: NodeJsExtension
get() = NodeJsExtension[this]
@@ -2,7 +2,7 @@ package org.jetbrains.kotlin.gradle.targets.js.nodejs
/** /**
* Provides platform and architecture names that is used to download NodeJs. * Provides platform and architecture names that is used to download NodeJs.
* See [NodeJsEnv.ivyDependency] that is filled in [NodeJsRootExtension.environment]. * See [NodeJsEnv.ivyDependency] that is filled in [NodeJsRootExtension.buildEnv].
*/ */
internal object NodeJsPlatform { internal object NodeJsPlatform {
private val props = System.getProperties() private val props = System.getProperties()
@@ -4,7 +4,10 @@ import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.plugins.BasePlugin import org.gradle.api.plugins.BasePlugin
import org.gradle.api.tasks.Delete import org.gradle.api.tasks.Delete
import org.jetbrains.kotlin.backend.common.onlyIf
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension.Companion.NODE_JS import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension.Companion.NODE_JS
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProjectLayout
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver
open class NodeJsPlugin : Plugin<Project> { open class NodeJsPlugin : Plugin<Project> {
override fun apply(project: Project): Unit = project.run { override fun apply(project: Project): Unit = project.run {
@@ -12,10 +15,10 @@ open class NodeJsPlugin : Plugin<Project> {
"NodeJsPlugin can be applied only to root project" "NodeJsPlugin can be applied only to root project"
} }
this.extensions.create(NODE_JS, NodeJsRootExtension::class.java, this) val nodeJs = this.extensions.create(NODE_JS, NodeJsRootExtension::class.java, this)
tasks.create(NodeJsSetupTask.NAME, NodeJsSetupTask::class.java) tasks.create(NodeJsSetupTask.NAME, NodeJsSetupTask::class.java)
setupCleanNodeModulesTask(project) // setupCleanNodeModulesTask(project, nodeJs)
allprojects { allprojects {
if (it != project) { if (it != project) {
@@ -24,13 +27,21 @@ open class NodeJsPlugin : Plugin<Project> {
} }
} }
private fun setupCleanNodeModulesTask(project: Project) { private fun setupCleanNodeModulesTask(
project: Project,
nodeJs: NodeJsRootExtension
) {
project.tasks.create("cleanNodeModules", Delete::class.java) { project.tasks.create("cleanNodeModules", Delete::class.java) {
it.description = "Deletes node_modules and package.json file" it.description = "Deletes node_modules and package.json file"
it.group = BasePlugin.BUILD_GROUP it.group = BasePlugin.BUILD_GROUP
it.doLast { val npmProjectLayout = NpmProjectLayout[it.project]
project.nodeJs.root.packageManager.cleanProject(project)
project.delete(npmProjectLayout.nodeModulesDir)
project.afterEvaluate {
if (nodeJs.manageNodeModules) {
project.delete(npmProjectLayout.packageJsonFile)
}
} }
project.tasks.maybeCreate(BasePlugin.CLEAN_TASK_NAME).dependsOn(it) project.tasks.maybeCreate(BasePlugin.CLEAN_TASK_NAME).dependsOn(it)
@@ -38,7 +49,7 @@ open class NodeJsPlugin : Plugin<Project> {
} }
companion object { companion object {
fun apply(project: Project): NodeJsRootExtension { operator fun get(project: Project): NodeJsRootExtension {
val rootProject = project.rootProject val rootProject = project.rootProject
rootProject.plugins.apply(NodeJsPlugin::class.java) rootProject.plugins.apply(NodeJsPlugin::class.java)
return rootProject.extensions.getByName(NODE_JS) as NodeJsRootExtension return rootProject.extensions.getByName(NODE_JS) as NodeJsRootExtension
@@ -19,43 +19,46 @@ open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) {
var nodeCommand = "node" var nodeCommand = "node"
var npmCustomVersion: String? = null
var npmCommand = "npm"
var manageNodeModules: Boolean = false var manageNodeModules: Boolean = false
var packageManager: NpmApi = Yarn var packageManager: NpmApi = Yarn
val nodeJsSetupTask: NodeJsSetupTask val nodeJsSetupTask: NodeJsSetupTask
get() = project.tasks.getByName(NodeJsSetupTask.NAME) as NodeJsSetupTask get() = project.tasks.getByName(NodeJsSetupTask.NAME) as NodeJsSetupTask
internal val environment: NodeJsEnv internal fun buildEnv(): NodeJsEnv {
get() { val platform = NodeJsPlatform.name
val platform = NodeJsPlatform.name val architecture = NodeJsPlatform.architecture
val architecture = NodeJsPlatform.architecture
val nodeDir = installationDir.resolve("node-v$nodeVersion-$platform-$architecture") val nodeDir = installationDir.resolve("node-v$nodeVersion-$platform-$architecture")
val isWindows = NodeJsPlatform.name == NodeJsPlatform.WIN val isWindows = NodeJsPlatform.name == NodeJsPlatform.WIN
val nodeBinDir = if (isWindows) nodeDir else nodeDir.resolve("bin") val nodeBinDir = if (isWindows) nodeDir else nodeDir.resolve("bin")
fun getExecutable(command: String, customCommand: String, windowsExtension: String): String { fun getExecutable(command: String, customCommand: String, windowsExtension: String): String {
val finalCommand = if (isWindows && customCommand == command) "$command.$windowsExtension" else customCommand val finalCommand = if (isWindows && customCommand == command) "$command.$windowsExtension" else customCommand
return if (download) File(nodeBinDir, finalCommand).absolutePath else finalCommand return if (download) File(nodeBinDir, finalCommand).absolutePath else finalCommand
}
fun getIvyDependency(): String {
val type = if (isWindows) "zip" else "tar.gz"
return "org.nodejs:node:$nodeVersion:$platform-$architecture@$type"
}
return NodeJsEnv(
nodeDir = nodeDir,
nodeBinDir = nodeBinDir,
nodeExecutable = getExecutable("node", nodeCommand, "exe"),
platformName = platform,
architectureName = architecture,
ivyDependency = getIvyDependency()
)
} }
fun getIvyDependency(): String {
val type = if (isWindows) "zip" else "tar.gz"
return "org.nodejs:node:$nodeVersion:$platform-$architecture@$type"
}
return NodeJsEnv(
nodeDir = nodeDir,
nodeBinDir = nodeBinDir,
nodeExec = getExecutable("node", nodeCommand, "exe"),
npmExec = getExecutable("npm", npmCommand, "cmd"),
platformName = platform,
architectureName = architecture,
ivyDependency = getIvyDependency()
)
}
internal fun executeSetup() { internal fun executeSetup() {
val nodeJsEnv = environment val nodeJsEnv = buildEnv()
if (download) { if (download) {
if (!nodeJsEnv.nodeBinDir.isDirectory) { if (!nodeJsEnv.nodeBinDir.isDirectory) {
nodeJsSetupTask.exec() nodeJsSetupTask.exec()
@@ -66,6 +69,6 @@ open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) {
companion object { companion object {
const val NODE_JS: String = "nodeJs" const val NODE_JS: String = "nodeJs"
operator fun get(project: Project) = NodeJsPlugin.apply(project) operator fun get(project: Project): NodeJsRootExtension = NodeJsPlugin[project]
} }
} }
@@ -11,8 +11,8 @@ import java.io.File
import java.net.URI import java.net.URI
open class NodeJsSetupTask : DefaultTask() { open class NodeJsSetupTask : DefaultTask() {
private val settings = project.nodeJs.root private val settings = NodeJsRootExtension[project]
private val env by lazy { settings.environment } private val env by lazy { settings.buildEnv() }
init { init {
group = NodeJsRootExtension.NODE_JS group = NodeJsRootExtension.NODE_JS
@@ -55,7 +55,7 @@ open class NodeJsSetupTask : DefaultTask() {
} }
} }
val dep = this.project.dependencies.create(ivyDependency) val dep = this.project.dependencies.create(env.ivyDependency)
val conf = this.project.configurations.detachedConfiguration(dep) val conf = this.project.configurations.detachedConfiguration(dep)
conf.isTransitive = false conf.isTransitive = false
val result = conf.resolve().single() val result = conf.resolve().single()
@@ -66,7 +66,7 @@ open class NodeJsSetupTask : DefaultTask() {
unpackNodeArchive(result, destination.parentFile) // parent because archive contains name already unpackNodeArchive(result, destination.parentFile) // parent because archive contains name already
if (!env.isWindows) { if (!env.isWindows) {
File(env.nodeExecutable).setExecutable(true) File(env.nodeExec).setExecutable(true)
} }
} }
@@ -5,15 +5,4 @@
package org.jetbrains.kotlin.gradle.targets.js.nodejs package org.jetbrains.kotlin.gradle.targets.js.nodejs
import org.gradle.api.Project import org.gradle.api.Project
internal fun nodeJsSetupWithoutTasks(project: Project) {
val nodeJsProject = NodeJsPlugin.ensureAppliedInHierarchy(project)
val nodeJs = NodeJsExtension[nodeJsProject]
val nodeJsEnv = nodeJs.buildEnv()
if (nodeJs.download) {
if (!nodeJsEnv.nodeBinDir.isDirectory) {
(nodeJsProject.tasks.findByName(NodeJsSetupTask.NAME) as NodeJsSetupTask).exec()
}
}
}
@@ -0,0 +1,241 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. 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 com.google.gson.GsonBuilder
import com.google.gson.JsonParseException
import org.gradle.api.Project
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.internal.hash.FileHasher
import org.jetbrains.kotlin.daemon.common.toHexString
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.targets.js.npm.GradleNodeModulesSync.*
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProjectLayout.Companion.PACKAGE_JSON
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
import java.io.File
/**
* Incrementally sync gradle JS dependencies into node_modules directory.
*
* Working by traversing giving classpath.
* For each classpath element (called [Element]) `.js` and `.js.map` files are stored into `node_modules`.
*
* To do it incrementally, state of previously traversed classpath elements are stored in
* `node_modules/.from-gradle` file. Unchanged elements will not been visited.
*
* See [Element] and [getOrCompute] for more details.
*/
internal class GradleNodeModulesSync(val project: Project) {
companion object {
const val STATE_FILE_NAME = ".from-gradle"
}
private val hasher = (project as ProjectInternal).services.get(FileHasher::class.java)
private val npmProjectLayout = NpmProjectLayout[project]
private val nodeModulesDir = npmProjectLayout.nodeModulesDir
private val gson = GsonBuilder().setPrettyPrinting().create()
private class State {
val contents: MutableMap<String, Element> = mutableMapOf()
operator fun get(elementHash: ByteArray) =
contents[elementHash.toHexString()]
operator fun set(elementHash: ByteArray, element: Element) {
contents[elementHash.toHexString()] = element
}
}
/**
* Traversed element from classpath.
* Stored in state file.
*
* When classpath traversed all elements are in one of this states:
* - unchanged: was loaded from previous state and contents not changed.
* only [fileNames] are known and contents of element not visited,
* but visitor are saved in [_fillFiles] to load in next case
* - unchanged but missed in node_modules: some of [fileNames] missed in node_modules
* in this case [_fillFiles] called before [files] was getted for copy spec
* - unchanged and existed in node_modules: nothing will be done in this case
* - new: not existed in previous state. In this case element is visited,
* both [fileNames] and [_files] are filled already
* - deleted: was loaded from previous state, but not existed in current state
* only [fileNames] are known for this case, which is used to remove outdated files
* - changed: this is just combination of deleted and new state
*/
private class Element {
lateinit var source: String
val fileNames: MutableList<String> = mutableListOf()
@Transient
@Suppress("PropertyName")
var _files: List<File>? = null
@Transient
@Suppress("PropertyName")
var _fillFiles: (() -> List<File>)? = null
val files: List<File>
get() {
if (_files == null) {
_files = _fillFiles!!()
}
return _files!!
}
}
/**
* Get info of classpath element from previous state (with storing visitor for case when files are deleted in node_modules)
* or compute it now.
*
* @param file classpath element
* @param buildElement js files list provider
*/
private fun getOrCompute(file: File, buildElement: () -> List<File>) {
val hash = hasher.hash(file).toByteArray()
val old = old[hash]
if (old != null) old._fillFiles = buildElement
new[hash] = old ?: Element().also {
it.source = file.absolutePath
it._files = buildElement()
it._files!!.mapTo(it.fileNames) { it.name }
}
}
private lateinit var old: State
private val new = State()
private val stateFile = nodeModulesDir.resolve(STATE_FILE_NAME)
fun loadOldState() {
val state: State? = if (stateFile.isFile) try {
stateFile.reader().use {
gson.fromJson(it, State::class.java)
}
} catch (e: JsonParseException) {
project.logger.warn("Cannot load previous state of Gradle depencies stored in node_modules", e)
null
} else {
if (stateFile.isDirectory) stateFile.deleteRecursively()
null
}
old = state ?: State()
}
fun sync() {
val children = nodeModulesDir.list()?.toSet() ?: setOf()
val deleted = old.contents.keys.toMutableSet()
val absented = mutableSetOf<String>()
new.contents.forEach { (key, value) ->
deleted.remove(key)
if (key !in old.contents.keys) absented.add(key)
else if (value.fileNames.any { it !in children }) absented.add(key)
}
deleted.forEach {
old.contents[it]!!.fileNames.forEach { fileName ->
nodeModulesDir.resolve(fileName).delete()
}
}
if (absented.isNotEmpty()) {
project.copy { copy ->
absented.forEach { key ->
copy.from(new.contents[key]!!.files)
}
copy.into(nodeModulesDir)
}
}
if (deleted.isNotEmpty() || absented.isNotEmpty()) {
saveNewState()
}
}
private fun saveNewState() {
stateFile.writer().use {
gson.toJson(new, it)
}
}
class TransitiveNpmDependency(
val key: String,
val version: String
)
fun visitCompilation(
compilation: KotlinCompilation<KotlinCommonOptions>,
project: Project,
transitiveDependencies: MutableList<TransitiveNpmDependency>
): List<TransitiveNpmDependency> {
val kotlin2JsCompile = compilation.compileKotlinTask as Kotlin2JsCompile
// classpath
kotlin2JsCompile.classpath.forEach { srcFile ->
when {
srcFile.name == PACKAGE_JSON -> visitPackageJson(srcFile, transitiveDependencies)
isKotlinJsRuntimeFile(srcFile) -> getOrCompute(srcFile) {
listOf(srcFile)
}
srcFile.isZip -> getOrCompute(srcFile) {
mutableListOf<File>().also { files ->
this.project.zipTree(srcFile).forEach { innerFile ->
when {
innerFile.name == PACKAGE_JSON -> visitPackageJson(innerFile, transitiveDependencies)
isKotlinJsRuntimeFile(innerFile) -> files.add(innerFile)
}
}
}
}
}
}
// output
if (kotlin2JsCompile.state.executed) {
copyCompileOutput(project, kotlin2JsCompile)
} else {
kotlin2JsCompile.doLast {
copyCompileOutput(project, kotlin2JsCompile)
}
}
return transitiveDependencies
}
private fun copyCompileOutput(project: Project, kotlin2JsCompile: Kotlin2JsCompile) {
project.copy { copy ->
copy.from(kotlin2JsCompile.outputFile)
copy.from(kotlin2JsCompile.outputFile.path + ".map")
copy.into(nodeModulesDir)
}
}
private fun visitPackageJson(
innerFile: File,
transitiveDependencies: MutableList<TransitiveNpmDependency>
) {
val packageJson = innerFile.reader().use {
gson.fromJson(it, PackageJson::class.java)
}
packageJson.dependencies.forEach { (key, version) ->
transitiveDependencies.add(TransitiveNpmDependency(key, version))
}
}
private val File.isZip
get() = isFile && (name.endsWith(".jar") || name.endsWith(".zip"))
private fun isKotlinJsRuntimeFile(file: File): Boolean {
if (!file.isFile) return false
val name = file.name
return (name.endsWith(".js") && !name.endsWith(".meta.js"))
|| name.endsWith(".js.map")
}
}
@@ -1,148 +0,0 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. 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 com.google.gson.GsonBuilder
import com.google.gson.JsonIOException
import com.google.gson.JsonSyntaxException
import org.gradle.api.Project
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.internal.hash.FileHasher
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.targets.js.tasks.isKotlinJsRuntimeFile
import org.jetbrains.kotlin.gradle.targets.js.tasks.isZip
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
import java.io.File
class NodeModulesGradleComponents(val rootProject: Project) {
val hasher = (rootProject as ProjectInternal).services.get(FileHasher::class.java)
val nodeModulesDir = rootProject.rootDir.resolve("node_modules")
val gson = GsonBuilder().setPrettyPrinting().create()
private class State {
val contents: MutableMap<ByteArray, Element> = mutableMapOf()
}
private class Element {
val fileNames: MutableList<String> = mutableListOf()
@Transient
val files = mutableListOf<File>()
}
class TransitiveNpmDependency(
val project: String,
val from: String,
val key: String,
val version: String
)
private lateinit var old: State
private val new = State()
val stateFile = rootProject.buildDir.resolve("nodeModulesGradleComponents.json")
fun loadOldState() {
val state: State? = if (stateFile.exists()) try {
stateFile.reader().use {
gson.fromJson(it, State::class.java)
}
} catch (e: JsonSyntaxException) {
// todo: warn
null
} catch (e: JsonIOException) {
// todo: warn
null
} else null
old = state ?: State()
}
fun sync() {
val children = nodeModulesDir.list()?.toSet() ?: setOf()
val deleted = old.contents.keys.toMutableSet()
val absense = mutableSetOf<ByteArray>()
new.contents.forEach { (key, value) ->
deleted.remove(key)
if (key !in old.contents.keys) absense.add(key)
else if (value.fileNames.any { it !in children }) absense.add(key)
}
deleted.forEach {
old.contents[it]!!.fileNames.forEach { fileName ->
nodeModulesDir.resolve(fileName).delete()
}
}
rootProject.copy { copy ->
absense.forEach { key ->
copy.from(new.contents[key]!!.files)
copy.into(nodeModulesDir)
}
}
saveNewState()
}
private fun saveNewState() {
stateFile.writer().use {
gson.toJson(new, it)
}
}
private fun getOrCompute(file: File, buildElement: (Element) -> Unit) {
val hash = hasher.hash(file).toByteArray()
new.contents[hash] = old.contents[hash] ?: Element().also {
buildElement(it)
it.files.mapTo(it.fileNames) { it.name }
}
}
fun visitCompilation(
compilation: KotlinCompilation<KotlinCommonOptions>,
project: Project,
transitiveDependencies: MutableList<TransitiveNpmDependency>
): List<TransitiveNpmDependency> {
val kotlin2JsCompile = compilation.compileKotlinTask as Kotlin2JsCompile
// classpath
kotlin2JsCompile.classpath.forEach { srcFile ->
when {
isKotlinJsRuntimeFile(srcFile) -> getOrCompute(srcFile) { element ->
element.files.add(srcFile)
}
srcFile.isZip -> getOrCompute(srcFile) { element ->
rootProject.zipTree(srcFile).forEach { innerFile ->
if (innerFile.name == "package.json") {
val packageJson = innerFile.reader().use {
gson.fromJson(it, PackageJson::class.java)
}
packageJson.dependencies.forEach { (key, version) ->
transitiveDependencies.add(TransitiveNpmDependency(project.path, srcFile.path, key, version))
}
} else if (isKotlinJsRuntimeFile(innerFile)) {
element.files.add(innerFile)
}
}
}
}
}
// output
kotlin2JsCompile.doLast {
project.copy { copy ->
copy.from(kotlin2JsCompile.outputFile)
copy.from(kotlin2JsCompile.outputFile.path + ".map")
copy.into(nodeModulesDir)
}
}
return transitiveDependencies
}
}
@@ -1,19 +0,0 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. 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.Project
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJsSetupWithoutTasks
object Npm: NpmApi {
override fun setup(project: Project) {
nodeJsSetupWithoutTasks(project)
}
override fun resolveRootProject(project: Project) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
@@ -27,13 +27,6 @@ interface NpmApi {
open val hoistGradleNodeModules open val hoistGradleNodeModules
get() = false get() = false
fun cleanProject(project: Project) {
val npmProject = project.npmProject
npmProject.nodeModulesDir.deleteRecursively()
npmProject.packageJsonFile.delete()
}
companion object { companion object {
fun resolveOperationDescription(packageManagerTitle: String): String = fun resolveOperationDescription(packageManagerTitle: String): String =
"Resolving NPM dependencies using $packageManagerTitle" "Resolving NPM dependencies using $packageManagerTitle"
@@ -10,74 +10,50 @@ import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.FileCollectionDependency import org.gradle.api.artifacts.FileCollectionDependency
import org.gradle.api.artifacts.SelfResolvingDependency import org.gradle.api.artifacts.SelfResolvingDependency
import org.gradle.api.file.FileCollection import org.gradle.api.file.FileCollection
import org.gradle.api.internal.artifacts.DependencyResolveContext
import org.gradle.api.internal.artifacts.ResolvableDependency
import org.gradle.api.internal.artifacts.dependencies.SelfResolvingDependencyInternal import org.gradle.api.internal.artifacts.dependencies.SelfResolvingDependencyInternal
import org.gradle.api.tasks.TaskDependency import org.gradle.api.tasks.TaskDependency
import java.io.File
import org.gradle.internal.component.local.model.DefaultLibraryBinaryIdentifier import org.gradle.internal.component.local.model.DefaultLibraryBinaryIdentifier
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver.ResolutionCallResult.* import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver.ResolutionCallResult.*
import java.io.File
data class NpmDependency( data class NpmDependency(
internal val project: Project, private val project: Project,
private val org: String?, private val org: String?,
private val name: String, private val name: String,
private val version: String private val version: String
) : SelfResolvingDependency, ) : SelfResolvingDependency,
SelfResolvingDependencyInternal, SelfResolvingDependencyInternal,
ResolvableDependency,
FileCollectionDependency { FileCollectionDependency {
override fun getGroup(): String? = org override fun getGroup(): String? = org
internal val dependencies = mutableSetOf<NpmDependency>()
override fun resolve(): MutableSet<File> { override fun resolve(): MutableSet<File> {
resolveProject() ?: return mutableSetOf()
val all = mutableSetOf<File>()
val visited = mutableSetOf<NpmDependency>()
fun visit(item: NpmDependency) {
if (item in visited) return
visited.add(item)
item.tryFindNodeModule()?.let {
all.add(it)
}
}
visit(this)
return all
}
internal fun resolveProject(): NpmResolver.ResolvedProject? {
val result = NpmResolver.resolve(project) val result = NpmResolver.resolve(project)
return when (result) { return when (result) {
is AlreadyInProgress -> null is AlreadyInProgress -> mutableSetOf()
is AlreadyResolved -> { is AlreadyResolved -> {
check(this in result.resolution.dependencies) { check(this in result.resolution.dependencies) {
"Project hierarchy is already resolved in NPM without $this" "Project hierarchy is already resolved in NPM without $this"
} }
result.resolution tryFindNodeModule()
} }
is ResolvedNow -> { is ResolvedNow -> {
check(this in result.resolution.dependencies) { check(this in result.resolution.dependencies) {
"$this was not visited during resolution" "$this was not visited during resolution"
} }
result.resolution tryFindNodeModule()
} }
} }
} }
private fun tryFindNodeModule(): File? { private fun tryFindNodeModule(): MutableSet<File> {
var p = project var p = project
do { do {
val result = NpmProject[p].nodeModulesDir.resolve(key) val result = p.rootDir.resolve("node_modules/$key")
if (result.exists()) return result if (result.exists()) return mutableSetOf(result)
p = project.parent ?: return null p = project.parent ?: return mutableSetOf()
} while (true) } while (true)
} }
@@ -85,13 +61,6 @@ data class NpmDependency(
override fun toString() = "$key: $version" override fun toString() = "$key: $version"
override fun resolve(context: DependencyResolveContext) {
resolve()
dependencies.forEach {
context.add(it)
}
}
override fun resolve(transitive: Boolean): MutableSet<File> = resolve() override fun resolve(transitive: Boolean): MutableSet<File> = resolve()
override fun getFiles(): FileCollection = project.files(resolve()) override fun getFiles(): FileCollection = project.files(resolve())
@@ -0,0 +1,35 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. 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.Project
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
import java.io.File
class NpmProjectLayout(val nodeWorkDir: File) {
val nodeModulesDir
get() = nodeWorkDir.resolve(NODE_MODULES)
val packageJsonFile: File
get() {
return nodeWorkDir.resolve(PACKAGE_JSON)
}
companion object {
const val PACKAGE_JSON = "package.json"
const val NODE_MODULES = "node_modules"
operator fun get(project: Project): NpmProjectLayout {
val manageNodeModules = NodeJsPlugin[project].manageNodeModules
val nodeWorkDir =
if (manageNodeModules) project.rootDir
else project.buildDir
return NpmProjectLayout(nodeWorkDir)
}
}
}
@@ -10,21 +10,23 @@ import com.google.gson.GsonBuilder
import org.gradle.api.Project import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension
import org.jetbrains.kotlin.gradle.dsl.kotlinExtensionOrNull import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver.ResolutionCallResult.* import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver.ResolutionCallResult.*
/** /**
* Generates `package.json` file for projects with npm or js dependencies and * [NpmResolver] runs selected [NodeJsRootExtension.packageManager] on root project
* runs selected [NodeJsRootExtension.packageManager] to download and install it. * with configured `package.json` files in all projects.
* *
* All [NpmDependency] for configurations related to kotlin/js will be added to `package.json`. * `package.json`
* For external gradle modules, fake npm packages will be created and added to `package.json` *
* as path to directory. * - compile(npm(...)) dependencies
* - compile(npm(...)) dependencies from external modules
* - compile(non npm(...)) dependencies transitively
*/ */
internal class NpmResolver private constructor(val rootProject: Project) { internal class NpmResolver private constructor(val rootProject: Project) {
companion object { companion object {
@@ -38,11 +40,7 @@ internal class NpmResolver private constructor(val rootProject: Project) {
val resolved = process?.resolved val resolved = process?.resolved
return if (resolved != null) AlreadyResolved(resolved) return if (resolved != null) AlreadyResolved(resolved)
else { else ResolvedNow(NpmResolver(rootProject).resolve(rootProject, null)!!)
val resolver = NpmResolver(rootProject)
check(resolver.resolve(rootProject, null))
ResolvedNow(ResolvedProject(resolver.npmPackages))
}
} }
} }
@@ -52,10 +50,9 @@ internal class NpmResolver private constructor(val rootProject: Project) {
class ResolvedNow(val resolution: ResolvedProject) : ResolutionCallResult() class ResolvedNow(val resolution: ResolvedProject) : ResolutionCallResult()
} }
private val nodeJs = NodeJsPlugin.apply(rootProject).root private val nodeJs = NodeJsPlugin[rootProject]
private val npmProject = rootProject.npmProject
private val packageManager = nodeJs.packageManager private val packageManager = nodeJs.packageManager
private val hoistGradleNodeModules = npmProject.hoistGradleNodeModules private val hoistGradleNodeModules = packageManager.hoistGradleNodeModules
private val npmPackages = mutableListOf<NpmPackage>() private val npmPackages = mutableListOf<NpmPackage>()
private val gson = GsonBuilder() private val gson = GsonBuilder()
.setPrettyPrinting() .setPrettyPrinting()
@@ -69,11 +66,7 @@ internal class NpmResolver private constructor(val rootProject: Project) {
} }
} }
class ResolvedProject(val npmPackages: Collection<NpmPackage>) { class ResolvedProject(val dependencies: Set<NpmDependency>)
val dependencies by lazy {
npmPackages.flatMapTo(mutableSetOf()) { it.npmDependencies }
}
}
class NpmPackage( class NpmPackage(
val project: Project, val project: Project,
@@ -81,7 +74,7 @@ internal class NpmResolver private constructor(val rootProject: Project) {
val npmDependencies: Set<NpmDependency> val npmDependencies: Set<NpmDependency>
) { ) {
fun savePackageJson(gson: Gson) { fun savePackageJson(gson: Gson) {
NpmProject[project].packageJsonFile.writer().use { NpmProjectLayout[project].packageJsonFile.writer().use {
gson.toJson(packageJson, it) gson.toJson(packageJson, it)
} }
} }
@@ -97,29 +90,29 @@ internal class NpmResolver private constructor(val rootProject: Project) {
private fun resolve( private fun resolve(
project: Project, project: Project,
parentGradleModules: GradleNodeModules? parentGradleNodeModules: GradleNodeModulesSync?
): Boolean { ): ResolvedProject? {
val existedProcess = ProjectData[project] val existedProcess = ProjectData[project]
if (existedProcess != null) { val process = if (existedProcess != null) {
if (existedProcess.resolved != null) error("yarn dependencies for $project already resolved") if (existedProcess.resolved != null) error("yarn dependencies for $project already resolved")
else { else {
// called inside resolution of classpath (from visitTarget) // called inside resolution of classpath (from visitTarget)
// we should return as we are already in progress // we should return as we are already in progress
return false return null
} }
} else ProjectData().also { } else ProjectData().also {
ProjectData[project] = it ProjectData[project] = it
} }
val gradleModules = val gradleNodeModules =
if (hoistGradleNodeModules && parentGradleModules != null) parentGradleModules if (hoistGradleNodeModules && parentGradleNodeModules != null) parentGradleNodeModules
else GradleNodeModules(project) else newGradleNodeModulesSync(project)
project.subprojects.forEach { project.subprojects.forEach {
resolve(it, gradleModules) resolve(it, gradleNodeModules)
} }
val npmPackage = extractNpmPackage(project, gradleModules) val npmPackage = extractNpmPackage(project, gradleNodeModules)
if (npmPackage != null) { if (npmPackage != null) {
npmPackage.savePackageJson(gson) npmPackage.savePackageJson(gson)
@@ -136,60 +129,61 @@ internal class NpmResolver private constructor(val rootProject: Project) {
} }
} }
if (gradleModules != parentGradleModules) { if (gradleNodeModules != parentGradleNodeModules) {
gradleModules.close() gradleNodeModules.sync()
} }
return true val npmDependencies = npmPackage?.npmDependencies ?: setOf()
return ResolvedProject(npmDependencies).also {
process.resolved = it
}
}
private fun newGradleNodeModulesSync(project: Project): GradleNodeModulesSync {
return GradleNodeModulesSync(project).also {
it.loadOldState()
}
} }
private fun extractNpmPackage( private fun extractNpmPackage(
project: Project, project: Project,
gradleComponents: GradleNodeModules gradleComponentsSync: GradleNodeModulesSync
): NpmPackage? { ): NpmPackage? {
val packageJson = PackageJson(project.name, project.version.toString()) val packageJson = PackageJson(project.name, project.version.toString())
val transitiveDependencies = mutableListOf<GradleNodeModulesSync.TransitiveNpmDependency>()
val kotlin = project.kotlinExtension
val npmDependencies = mutableSetOf<NpmDependency>() val npmDependencies = mutableSetOf<NpmDependency>()
when (kotlin) {
visitNpmDependencies(project, npmDependencies, gradleComponents, packageJson) is KotlinSingleTargetExtension -> visitTarget(
kotlin.target,
if (!hoistGradleNodeModules || project == project.rootProject) { project,
gradleComponents.modules.forEach { npmDependencies,
val relativePath = it.path.relativeTo(NpmProject[project].nodeWorkDir) gradleComponentsSync,
packageJson.dependencies[it.name] = "file:$relativePath" transitiveDependencies
)
is KotlinMultiplatformExtension -> kotlin.targets.forEach {
visitTarget(it, project, npmDependencies, gradleComponentsSync, transitiveDependencies)
} }
} }
project.nodeJs.packageJsonHandlers.forEach { npmDependencies.forEach {
packageJson.dependencies[it.key] = chooseVersion(packageJson.dependencies[it.key], it.version)
}
transitiveDependencies.forEach {
packageJson.dependencies[it.key] = chooseVersion(packageJson.dependencies[it.key], it.version)
}
NodeJsExtension[project].packageJsonHandlers.forEach {
it(packageJson) it(packageJson)
} }
val packageJsonRequired = if (project == rootProject) { if (project == rootProject) {
packageManager.hookRootPackage(rootProject, packageJson, npmPackages) packageManager.hookRootPackage(rootProject, packageJson, npmPackages)
} else false
return if (packageJsonRequired || !packageJson.empty) NpmPackage(project, packageJson, npmDependencies) else null
}
private fun visitNpmDependencies(
project: Project,
npmDependencies: MutableSet<NpmDependency>,
gradleComponents: GradleNodeModules,
packageJson: PackageJson
) {
val kotlin = project.kotlinExtensionOrNull
if (kotlin != null) {
when (kotlin) {
is KotlinSingleTargetExtension -> visitTarget(kotlin.target, project, npmDependencies, gradleComponents)
is KotlinMultiplatformExtension -> kotlin.targets.forEach {
visitTarget(it, project, npmDependencies, gradleComponents)
}
}
npmDependencies.forEach {
packageJson.dependencies[it.key] = chooseVersion(packageJson.dependencies[it.key], it.version)
}
} }
return if (!packageJson.empty) NpmPackage(project, packageJson, npmDependencies) else null
} }
private fun chooseVersion(oldVersion: String?, newVersion: String): String = private fun chooseVersion(oldVersion: String?, newVersion: String): String =
@@ -199,7 +193,8 @@ internal class NpmResolver private constructor(val rootProject: Project) {
target: KotlinTarget, target: KotlinTarget,
project: Project, project: Project,
npmDependencies: MutableSet<NpmDependency>, npmDependencies: MutableSet<NpmDependency>,
gradleComponents: GradleNodeModules gradleComponentsSync: GradleNodeModulesSync,
transitiveDependencies: MutableList<GradleNodeModulesSync.TransitiveNpmDependency>
) { ) {
if (target.platformType == KotlinPlatformType.js) { if (target.platformType == KotlinPlatformType.js) {
target.compilations.toList().forEach { compilation -> target.compilations.toList().forEach { compilation ->
@@ -211,7 +206,7 @@ internal class NpmResolver private constructor(val rootProject: Project) {
} }
} }
gradleComponents.visitCompilation(compilation) gradleComponentsSync.visitCompilation(compilation, project, transitiveDependencies)
} }
} }
} }
@@ -10,34 +10,11 @@ class PackageJson(
var version: String var version: String
) { ) {
val empty: Boolean val empty: Boolean
get() = private == null && get() = private == null && workspaces == null && dependencies.isEmpty()
workspaces == null &&
dependencies.isEmpty() &&
devDependencies.isEmpty()
val scopedName: ScopedName
get() = scopedName(name)
var private: Boolean? = null var private: Boolean? = null
var main: String? = null
var workspaces: Collection<String>? = null var workspaces: Collection<String>? = null
val devDependencies = mutableMapOf<String, String>()
val dependencies = mutableMapOf<String, String>() val dependencies = mutableMapOf<String, String>()
companion object {
fun scopedName(name: String): ScopedName = if (name.contains("/")) ScopedName(
scope = name.substringBeforeLast("/").removePrefix("@"),
name = name.substringAfterLast("/")
) else ScopedName(scope = null, name = name)
operator fun invoke(scope: String, name: String, version: String) =
PackageJson(ScopedName(scope, name).toString(), version)
}
data class ScopedName(val scope: String?, val name: String) {
override fun toString() = if (scope == null) name else "@$scope/$name"
}
} }
@@ -1,63 +0,0 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.targets.js.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.*
import org.jetbrains.kotlin.gradle.targets.js.internal.RewriteSourceMapFilterReader
import java.io.File
open class KotlinJsNodeModulesTask : DefaultTask() {
@OutputDirectory
@SkipWhenEmpty
lateinit var nodeModulesDir: File
@Classpath
@InputFiles
@SkipWhenEmpty
lateinit var classpath: FileCollection
@TaskAction
fun copyFromRuntimeClasspath() {
project.sync { sync ->
sync.includeEmptyDirs = false
classpath.forEach {
if (it.isZip) sync.from(project.zipTree(it))
else sync.from(it)
}
sync.include { fileTreeElement ->
isKotlinJsRuntimeFile(fileTreeElement.file)
}
sync.eachFile {
if (it.name.endsWith(".js.map")) {
it.filter(
mapOf(
"srcSourceRoot" to it.file.parentFile,
"targetSourceRoot" to nodeModulesDir
),
RewriteSourceMapFilterReader::class.java
)
}
}
sync.into(nodeModulesDir)
}
}
}
internal val File.isZip
get() = isFile && (name.endsWith(".jar") || name.endsWith(".zip"))
internal fun isKotlinJsRuntimeFile(file: File): Boolean {
if (!file.isFile) return false
val name = file.name
return (name.endsWith(".js") && !name.endsWith(".meta.js"))
|| name.endsWith(".js.map")
}
@@ -15,6 +15,8 @@ import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSetti
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec
import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTrace import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTrace
import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTraceAsJvm import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTraceAsJvm
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProjectLayout
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver
import org.jetbrains.kotlin.gradle.tasks.KotlinTestTask import org.jetbrains.kotlin.gradle.tasks.KotlinTestTask
import org.jetbrains.kotlin.gradle.testing.IgnoredTestSuites import org.jetbrains.kotlin.gradle.testing.IgnoredTestSuites
import java.io.File import java.io.File
@@ -24,9 +26,6 @@ open class KotlinNodeJsTestTask : KotlinTestTask() {
var ignoredTestSuites: IgnoredTestSuites = var ignoredTestSuites: IgnoredTestSuites =
IgnoredTestSuites.showWithContents IgnoredTestSuites.showWithContents
@InputDirectory
lateinit var nodeModulesDir: File
@Input @Input
@SkipWhenEmpty @SkipWhenEmpty
var nodeModulesToLoad: Set<String> = setOf() var nodeModulesToLoad: Set<String> = setOf()
@@ -53,11 +52,19 @@ open class KotlinNodeJsTestTask : KotlinTestTask() {
options(nodeJsProcessOptions) options(nodeJsProcessOptions)
} }
override fun executeTests() {
NpmResolver.resolve(project)
super.executeTests()
}
override fun createTestExecutionSpec(): TCServiceMessagesTestExecutionSpec { override fun createTestExecutionSpec(): TCServiceMessagesTestExecutionSpec {
val extendedForkOptions = DefaultProcessForkOptions(fileResolver) val extendedForkOptions = DefaultProcessForkOptions(fileResolver)
nodeJsProcessOptions.copyTo(extendedForkOptions) nodeJsProcessOptions.copyTo(extendedForkOptions)
extendedForkOptions.environment.addPath("NODE_PATH", nodeModulesDir.canonicalPath) NpmResolver.resolve(project)
val npmProjectLayout = NpmProjectLayout[project]
extendedForkOptions.workingDir = npmProjectLayout.nodeWorkDir
val nodeJsArgs = mutableListOf<String>() val nodeJsArgs = mutableListOf<String>()
@@ -83,7 +90,7 @@ open class KotlinNodeJsTestTask : KotlinTestTask() {
extendedForkOptions, extendedForkOptions,
nodeJsArgs + nodeJsArgs +
testRuntimeNodeModules testRuntimeNodeModules
.map { nodeModulesDir.resolve(it) } .map { npmProjectLayout.nodeModulesDir.resolve(it) }
.filter { it.exists() } .filter { it.exists() }
.map { it.absolutePath } + .map { it.absolutePath } +
cliArgs.toList(), cliArgs.toList(),
@@ -122,10 +129,4 @@ data class KotlinNodeJsTestRunnerCliArgs(
enum class IgnoredTestSuitesReporting { enum class IgnoredTestSuitesReporting {
skip, reportAsIgnoredTest, reportAllInnerTestsAsIgnored skip, reportAsIgnoredTest, reportAllInnerTestsAsIgnored
} }
}
private fun MutableMap<String, Any>.addPath(key: String, path: String) {
val prev = get(key)
if (prev == null) set(key, path)
else set(key, prev as String + File.pathSeparator + path)
} }
@@ -6,121 +6,56 @@
package org.jetbrains.kotlin.gradle.targets.js.yarn package org.jetbrains.kotlin.gradle.targets.js.yarn
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.internal.hash.FileHasher
import org.jetbrains.kotlin.daemon.common.toHexString
import org.jetbrains.kotlin.gradle.internal.execWithProgress import org.jetbrains.kotlin.gradle.internal.execWithProgress
import org.jetbrains.kotlin.gradle.internal.operation
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.* import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApi
import java.io.File import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProjectLayout
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver
import org.jetbrains.kotlin.gradle.targets.js.npm.PackageJson
import org.slf4j.LoggerFactory
object Yarn : NpmApi { object Yarn : NpmApi {
override fun setup(project: Project) { private val log = LoggerFactory.getLogger("org.jetbrains.kotlin.gradle.targets.js.yarn.Yarn")
YarnPlugin.apply(project).executeSetup()
}
val Project.packageJsonHashFile: File override fun setup(project: Project) {
get() = buildDir.resolve("package.json.hash") YarnPlugin[project].executeSetup()
}
fun yarnExec( fun yarnExec(
project: Project, project: Project,
description: String, description: String,
vararg args: String, vararg args: String,
npmProject: NpmProject = NpmProject[project] npmProjectLayout: NpmProjectLayout = NpmProjectLayout[project]
) { ) {
val nodeJsEnv = NodeJsPlugin.apply(project).root.environment val nodeJsEnv = NodeJsPlugin[project].buildEnv()
val yarnEnv = YarnPlugin.apply(project).environment val yarnEnv = YarnPlugin[project].buildEnv()
val packageJsonHashFile = project.packageJsonHashFile
val packageJsonHash = if (packageJsonHashFile.exists()) packageJsonHashFile.readText() else null
val hasher = (project as ProjectInternal).services.get(FileHasher::class.java)
val hash = hasher.hash(npmProject.packageJsonFile).toByteArray().toHexString()
if (packageJsonHash == hash) return
packageJsonHashFile.writeText(hash)
val nodeWorkDir = npmProject.nodeWorkDir
project.execWithProgress(description) { exec -> project.execWithProgress(description) { exec ->
exec.executable = nodeJsEnv.nodeExecutable exec.executable = nodeJsEnv.nodeExec
exec.args = listOf(yarnEnv.home.resolve("bin/yarn.js").absolutePath) + args exec.args = listOf(yarnEnv.home.resolve("bin/yarn.js").absolutePath) + args
exec.workingDir = nodeWorkDir exec.workingDir = npmProjectLayout.nodeWorkDir
}
}
private fun yarnLockReadTransitiveDependencies(
nodeWorkDir: File,
srcDependenciesList: Collection<NpmDependency>
) {
val yarnLock = nodeWorkDir.resolve("yarn.lock")
if (yarnLock.isFile) {
val byKey = YarnLock.parse(yarnLock).entries.associateBy { it.key }
val visited = mutableSetOf<NpmDependency>()
fun resolveRecursively(src: NpmDependency): NpmDependency {
if (src in visited) return src
visited.add(src)
val key = YarnLock.key(src.key, src.version)
val deps = byKey[key]
if (deps != null) {
src.dependencies.addAll(deps.dependencies.map { dep ->
val scopedName = dep.scopedName
resolveRecursively(
NpmDependency(
src.project,
scopedName.scope,
scopedName.name,
dep.version ?: "*"
)
)
})
} else {
// todo: [WARN] cannot find $key in yarn.lock
}
return src
}
srcDependenciesList.forEach { src ->
resolveRecursively(src)
}
} }
} }
@Suppress("EXPOSED_PARAMETER_TYPE") @Suppress("EXPOSED_PARAMETER_TYPE")
override fun resolveProject(npmPackage: NpmResolver.NpmPackage) { override fun resolveProject(npmPackage: NpmResolver.NpmPackage) {
val project = npmPackage.project val project = npmPackage.project
if (!project.yarn.useWorkspaces) { if (!YarnPlugin[project].checkUseWorkspace()) {
yarnExec(project, NpmApi.resolveOperationDescription("yarn for ${project.path}")) yarnExec(project, NpmApi.resolveOperationDescription("yarn for ${project.path}"))
yarnLockReadTransitiveDependencies(NpmProject[project].nodeWorkDir, npmPackage.npmDependencies)
} }
} }
@Suppress("EXPOSED_PARAMETER_TYPE") @Suppress("EXPOSED_PARAMETER_TYPE")
override fun hookRootPackage( override fun hookRootPackage(rootProject: Project, rootPackageJson: PackageJson, allWorkspaces: Collection<NpmResolver.NpmPackage>) {
rootProject: Project, if (YarnPlugin[rootProject].checkUseWorkspace()) {
rootPackageJson: PackageJson,
allWorkspaces: Collection<NpmResolver.NpmPackage>
): Boolean {
if (rootProject.yarn.useWorkspaces) {
rootPackageJson.private = true rootPackageJson.private = true
rootPackageJson.workspaces = allWorkspaces rootPackageJson.workspaces = allWorkspaces
.filter { it.project != rootProject } .filter { it.project != rootProject }
.map { it.project.projectDir.relativeTo(rootProject.rootDir).path } .map { it.project.rootDir.relativeTo(rootProject.rootDir).path }
return true
} }
return false
} }
override fun getHoistGradleNodeModules(project: Project): Boolean =
project.rootProject.yarn.useWorkspaces
@Suppress("EXPOSED_PARAMETER_TYPE") @Suppress("EXPOSED_PARAMETER_TYPE")
override fun resolveRootProject( override fun resolveRootProject(
rootProject: Project, rootProject: Project,
@@ -128,9 +63,8 @@ object Yarn : NpmApi {
) { ) {
check(rootProject == rootProject.rootProject) check(rootProject == rootProject.rootProject)
if (rootProject.yarn.useWorkspaces) { if (YarnPlugin[rootProject].checkUseWorkspace()) {
yarnExec(rootProject, NpmApi.resolveOperationDescription("yarn")) yarnExec(rootProject, NpmApi.resolveOperationDescription("yarn"))
yarnLockReadTransitiveDependencies(NpmProject[rootProject].nodeWorkDir, subprojects.flatMap { it.npmDependencies })
} else { } else {
if (subprojects.any { it.project != rootProject }) { if (subprojects.any { it.project != rootProject }) {
// todo: proofread message // todo: proofread message
@@ -139,15 +73,11 @@ object Yarn : NpmApi {
"It is recommended to enable yarn workspaces to store common NPM dependencies in root project. " + "It is recommended to enable yarn workspaces to store common NPM dependencies in root project. " +
"To enable it add this to your root project: \n" + "To enable it add this to your root project: \n" +
"nodeJs { manageNodeModules = true } \n" + "nodeJs { manageNodeModules = true } \n" +
"yarn { useWorkspaces = true } \n" +
"Note: with `manageNodeModules` enabled, your `node_modules` and `package.json` files will be managed by " + "Note: with `manageNodeModules` enabled, your `node_modules` and `package.json` files will be managed by " +
"Gradle, will be overridden during build and should be ignored in VCS." "Gradle, will be overridden during build and should be ignored in VCS."
) )
} }
} }
} }
override fun cleanProject(project: Project) {
super.cleanProject(project)
project.packageJsonHashFile.delete()
}
} }
@@ -1,46 +0,0 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.targets.js.yarn
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.logging.kotlinInfo
open class YarnExtension(project: Project) {
private val gradleHome = project.gradle.gradleUserHomeDir.also {
project.logger.kotlinInfo("Storing cached files in $it")
}
var installationDir = gradleHome.resolve("yarn")
var distBaseUrl = "https://github.com/yarnpkg/yarn/releases/download"
var version = "1.15.2"
var command = "yarn"
var download = true
internal fun buildEnv() = YarnEnv(
downloadUrl = "$distBaseUrl/v$version/yarn-v$version.tar.gz",
home = installationDir.resolve("yarn-v$version")
)
companion object {
const val YARN: String = "yarn"
operator fun get(project: Project): YarnExtension {
val extension = project.extensions.findByType(YarnExtension::class.java)
if (extension != null)
return extension
val parentProject = project.parent
if (parentProject != null)
return get(parentProject)
throw GradleException("YarnExtension is not installed")
}
}
}
@@ -15,16 +15,14 @@ open class YarnPlugin : Plugin<Project> {
"YarnPlugin can be applied only to root project" "YarnPlugin can be applied only to root project"
} }
val nodeJs = NodeJsPlugin.apply(this)
this.extensions.create(YarnRootExtension.YARN, YarnRootExtension::class.java, this) this.extensions.create(YarnRootExtension.YARN, YarnRootExtension::class.java, this)
tasks.create(YarnSetupTask.NAME, YarnSetupTask::class.java) { tasks.create(YarnSetupTask.NAME, YarnSetupTask::class.java) {
it.dependsOn(nodeJs.nodeJsSetupTask) it.dependsOn(NodeJsPlugin[this].nodeJsSetupTask)
} }
} }
companion object { companion object {
fun apply(project: Project): YarnRootExtension { operator fun get(project: Project): YarnRootExtension {
val rootProject = project.rootProject val rootProject = project.rootProject
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
@@ -5,10 +5,10 @@
package org.jetbrains.kotlin.gradle.targets.js.yarn package org.jetbrains.kotlin.gradle.targets.js.yarn
import org.gradle.api.GradleException
import org.gradle.api.Project import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.logging.kotlinInfo import org.jetbrains.kotlin.gradle.logging.kotlinInfo
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
open class YarnRootExtension(val project: Project) { open class YarnRootExtension(val project: Project) {
private val gradleHome = project.gradle.gradleUserHomeDir.also { private val gradleHome = project.gradle.gradleUserHomeDir.also {
@@ -23,36 +23,43 @@ open class YarnRootExtension(val project: Project) {
val yarnSetupTask: YarnSetupTask val yarnSetupTask: YarnSetupTask
get() = project.tasks.getByName(YarnSetupTask.NAME) as YarnSetupTask get() = project.tasks.getByName(YarnSetupTask.NAME) as YarnSetupTask
var disableWorkspaces: Boolean = false var useWorkspaces: Boolean = false
val useWorkspaces: Boolean internal fun checkUseWorkspace(): Boolean = if (useWorkspaces) {
get() = project.nodeJs.root.manageNodeModules && !disableWorkspaces check(NodeJsPlugin[project].manageNodeModules) {
"Yarn workspace can be used only with `manageNodeModules`: (add `nodeJs { manageNodeModules = true }` to your root project)"
}
true
} else false
internal fun executeSetup() { internal fun executeSetup() {
NodeJsPlugin.apply(project).executeSetup() NodeJsPlugin[project].executeSetup()
val env = environment val env = buildEnv()
if (!env.home.isDirectory) { if (!env.home.isDirectory) {
yarnSetupTask.setup() yarnSetupTask.setup()
} }
} }
internal val environment internal fun buildEnv() = YarnEnv(
get() = YarnEnv( downloadUrl = "$downloadBaseUrl/v$version/yarn-v$version.tar.gz",
downloadUrl = "$downloadBaseUrl/v$version/yarn-v$version.tar.gz", home = installationDir.resolve("yarn-v$version")
home = installationDir.resolve("yarn-v$version") )
)
companion object { companion object {
const val YARN: String = "yarn" const val YARN: String = "yarn"
operator fun get(project: Project): YarnRootExtension { operator fun get(project: Project): YarnRootExtension {
val rootProject = project.rootProject val extension = project.extensions.findByType(YarnRootExtension::class.java)
rootProject.plugins.apply(YarnPlugin::class.java) if (extension != null)
return rootProject.extensions.getByName(YARN) as YarnRootExtension return extension
val parentProject = project.parent
if (parentProject != null)
return get(parentProject)
throw GradleException("YarnRootExtension is not installed")
} }
} }
} }
val Project.yarn: YarnRootExtension
get() = YarnRootExtension[this]
@@ -14,8 +14,8 @@ import java.io.File
import javax.xml.ws.Action import javax.xml.ws.Action
open class YarnSetupTask : DefaultTask() { open class YarnSetupTask : DefaultTask() {
private val settings = project.yarn private val settings = YarnRootExtension[project]
private val env by lazy { settings.environment } private val env by lazy { settings.buildEnv() }
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
val downloadUrl val downloadUrl