Gradle: npm integration using yarn
#KT-31018 Fixed
This commit is contained in:
+3
@@ -43,6 +43,9 @@ interface KotlinDependencyHandler {
|
|||||||
project(listOf("path", "configuration").zip(listOfNotNull(path, configuration)).toMap())
|
project(listOf("path", "configuration").zip(listOfNotNull(path, configuration)).toMap())
|
||||||
|
|
||||||
fun project(notation: Map<String, Any?>): ProjectDependency
|
fun project(notation: Map<String, Any?>): ProjectDependency
|
||||||
|
|
||||||
|
fun npm(packageName: String, version: String = "*"): Dependency?
|
||||||
|
fun npm(org: String, packageName: String, version: String = "*"): Dependency?
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HasKotlinDependencies {
|
interface HasKotlinDependencies {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ dependencies {
|
|||||||
compileOnly(project(":kotlin-scripting-compiler-impl"))
|
compileOnly(project(":kotlin-scripting-compiler-impl"))
|
||||||
|
|
||||||
compile("com.google.code.gson:gson:${rootProject.extra["versions.jar.gson"]}")
|
compile("com.google.code.gson:gson:${rootProject.extra["versions.jar.gson"]}")
|
||||||
|
compile("de.undercouch:gradle-download-task:3.4.3")
|
||||||
|
|
||||||
compileOnly("com.android.tools.build:gradle:2.0.0")
|
compileOnly("com.android.tools.build:gradle:2.0.0")
|
||||||
compileOnly("com.android.tools.build:gradle-core:2.0.0")
|
compileOnly("com.android.tools.build:gradle-core:2.0.0")
|
||||||
|
|||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* 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 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 java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache for preventing processing some files twice.
|
||||||
|
* Files are precessed some subdirectories of [targetDir] called `target`.
|
||||||
|
* For each target [getOrCompute] should be called.
|
||||||
|
* Absent target directories will be deleted on [close].
|
||||||
|
* `compute` will be called only for source files that was changed since last processing.
|
||||||
|
*
|
||||||
|
* See [org.jetbrains.kotlin.gradle.targets.js.npm.GradleNodeModuleBuilder] for example.
|
||||||
|
*
|
||||||
|
* @param version When updating logic in `compute`, `version` should be increased to invalidate cache
|
||||||
|
*/
|
||||||
|
internal class ProcessedFilesCache(
|
||||||
|
val project: Project,
|
||||||
|
val targetDir: File,
|
||||||
|
stateFileName: String,
|
||||||
|
val version: String
|
||||||
|
) : AutoCloseable {
|
||||||
|
private val hasher = (project as ProjectInternal).services.get(FileHasher::class.java)
|
||||||
|
private val gson = GsonBuilder().setPrettyPrinting().create()
|
||||||
|
|
||||||
|
private class State {
|
||||||
|
var version: String? = null
|
||||||
|
val visited: MutableMap<String, Element> = mutableMapOf()
|
||||||
|
|
||||||
|
operator fun get(elementHash: ByteArray) =
|
||||||
|
visited[elementHash.toHexString()]
|
||||||
|
|
||||||
|
operator fun set(elementHash: ByteArray, element: Element) {
|
||||||
|
visited[elementHash.toHexString()] = element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Element(
|
||||||
|
val src: String,
|
||||||
|
val target: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
private val stateFile = targetDir.resolve(stateFileName)
|
||||||
|
private val old: State
|
||||||
|
|
||||||
|
init {
|
||||||
|
targetDir.mkdirs()
|
||||||
|
|
||||||
|
val state: State? = if (stateFile.exists()) try {
|
||||||
|
stateFile.reader().use {
|
||||||
|
gson.fromJson(it, State::class.java)
|
||||||
|
}.let {
|
||||||
|
if (it.version != version) null else it
|
||||||
|
}
|
||||||
|
} catch (e: JsonParseException) {
|
||||||
|
project.logger.warn("Cannot read $stateFile", e)
|
||||||
|
if (targetDir.exists()) {
|
||||||
|
targetDir.deleteRecursively()
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} else null
|
||||||
|
|
||||||
|
old = state ?: State()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val new = State()
|
||||||
|
|
||||||
|
init {
|
||||||
|
new.version = version
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun getOrCompute(
|
||||||
|
file: File,
|
||||||
|
compute: () -> String?
|
||||||
|
) {
|
||||||
|
val hash = hasher.hash(file).toByteArray()
|
||||||
|
val old = old[hash]
|
||||||
|
if (old != null) {
|
||||||
|
new[hash] = old
|
||||||
|
} else {
|
||||||
|
val key = compute()
|
||||||
|
new[hash] = Element(file.canonicalPath, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteTarget(key: String) {
|
||||||
|
targetDir.resolve(key).deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
val targets: Collection<String>
|
||||||
|
get() = new.visited.mapNotNullTo(mutableSetOf()) { it.value.target }
|
||||||
|
|
||||||
|
private fun getDeletedTargets(): MutableSet<String> {
|
||||||
|
val result = mutableSetOf<String>()
|
||||||
|
|
||||||
|
old.visited.forEach {
|
||||||
|
val target = it.value.target
|
||||||
|
if (target != null) {
|
||||||
|
result.add(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new.visited.forEach {
|
||||||
|
val target = it.value.target
|
||||||
|
if (target != null) {
|
||||||
|
result.remove(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
getDeletedTargets().forEach {
|
||||||
|
deleteTarget(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateFile.writer().use {
|
||||||
|
gson.toJson(new, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -6,6 +6,7 @@ import org.gradle.api.artifacts.ExternalModuleDependency
|
|||||||
import org.gradle.api.artifacts.ProjectDependency
|
import org.gradle.api.artifacts.ProjectDependency
|
||||||
import org.jetbrains.kotlin.gradle.plugin.HasKotlinDependencies
|
import org.jetbrains.kotlin.gradle.plugin.HasKotlinDependencies
|
||||||
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
|
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmDependency
|
||||||
|
|
||||||
class DefaultKotlinDependencyHandler(
|
class DefaultKotlinDependencyHandler(
|
||||||
val parent: HasKotlinDependencies,
|
val parent: HasKotlinDependencies,
|
||||||
@@ -77,4 +78,10 @@ class DefaultKotlinDependencyHandler(
|
|||||||
configure(it)
|
configure(it)
|
||||||
project.dependencies.add(configurationName, it)
|
project.dependencies.add(configurationName, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun npm(packageName: String, version: String) =
|
||||||
|
NpmDependency(project, null, packageName, version)
|
||||||
|
|
||||||
|
override fun npm(org: String, packageName: String, version: String) =
|
||||||
|
NpmDependency(project, org, packageName, version)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-64
@@ -1,12 +1,9 @@
|
|||||||
package org.jetbrains.kotlin.gradle.targets.js
|
package org.jetbrains.kotlin.gradle.targets.js
|
||||||
|
|
||||||
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.testing.KotlinJsTest
|
||||||
import org.jetbrains.kotlin.gradle.targets.js.tasks.*
|
|
||||||
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
|
||||||
@@ -36,81 +33,28 @@ internal class KotlinJsCompilationTestsConfigurator(
|
|||||||
return components.first() + components.drop(1).joinToString("") { it.capitalize() }
|
return components.first() + components.drop(1).joinToString("") { it.capitalize() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("SameParameterValue")
|
|
||||||
private fun disambiguateUnderscored(name: String, includeCompilation: Boolean) =
|
|
||||||
disambiguate(name, includeCompilation).joinToString("_")
|
|
||||||
|
|
||||||
private val testTaskName: String
|
private val testTaskName: String
|
||||||
get() = disambiguateCamelCased("test", false)
|
get() = disambiguateCamelCased("test", false)
|
||||||
|
|
||||||
private val nodeModulesDir
|
|
||||||
get() = project.buildDir.resolve(disambiguateUnderscored("node_modules", true))
|
|
||||||
|
|
||||||
private val compileTask: Kotlin2JsCompile
|
|
||||||
get() = project.tasks.findByName(compileTestKotlin2Js.name) as Kotlin2JsCompile
|
|
||||||
|
|
||||||
private val Kotlin2JsCompile.jsRuntimeClasspath: FileCollection
|
|
||||||
get() = classpath.plus(project.files(destinationDir))
|
|
||||||
|
|
||||||
fun configure() {
|
fun configure() {
|
||||||
compilation.dependencies {
|
// apply plugin (cannot do it lazy)
|
||||||
implementation(kotlin("test-nodejs-runner"))
|
val nodeJs = NodeJsPlugin.apply(target.project)
|
||||||
}
|
|
||||||
|
|
||||||
val nodeModulesTask = registerTask(
|
registerTask(project, testTaskName, KotlinJsTest::class.java) { testJs ->
|
||||||
project,
|
|
||||||
disambiguateCamelCased("nodeModules", true),
|
|
||||||
KotlinJsNodeModulesTask::class.java
|
|
||||||
) {
|
|
||||||
it.dependsOn(compileTestKotlin2Js)
|
|
||||||
|
|
||||||
it.onlyIf {
|
|
||||||
compileTestKotlin2Js.outputFile.exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
it.nodeModulesDir = nodeModulesDir
|
|
||||||
it.classpath = compileTask.jsRuntimeClasspath
|
|
||||||
}
|
|
||||||
|
|
||||||
val projectWithNodeJsPlugin = NodeJsPlugin.ensureAppliedInHierarchy(target.project)
|
|
||||||
|
|
||||||
val testTask = registerTask(project, testTaskName, KotlinNodeJsTestTask::class.java) { testJs ->
|
|
||||||
testJs.group = LifecycleBasePlugin.VERIFICATION_GROUP
|
testJs.group = LifecycleBasePlugin.VERIFICATION_GROUP
|
||||||
|
|
||||||
testJs.dependsOn(nodeModulesTask.getTaskOrProvider())
|
testJs.dependsOn(compileTestKotlin2Js, nodeJs.nodeJsSetupTask)
|
||||||
|
|
||||||
testJs.onlyIf {
|
testJs.onlyIf {
|
||||||
compileTestKotlin2Js.outputFile.exists()
|
compileTestKotlin2Js.outputFile.exists()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disambiguationClassifier != null) {
|
testJs.runtimeDependencyHandler = compilation
|
||||||
testJs.targetName = disambiguationClassifier
|
testJs.targetName = disambiguationClassifier
|
||||||
testJs.showTestTargetName = true
|
testJs.nodeModulesToLoad.add(compileTestKotlin2Js.outputFile.name)
|
||||||
}
|
|
||||||
|
|
||||||
testJs.nodeJsProcessOptions.workingDir = project.projectDir
|
|
||||||
|
|
||||||
testJs.nodeModulesDir = nodeModulesDir
|
|
||||||
testJs.testRuntimeNodeModules = listOf(
|
|
||||||
"kotlin-test-nodejs-runner.js",
|
|
||||||
"kotlin-nodejs-source-map-support.js"
|
|
||||||
)
|
|
||||||
testJs.nodeModulesToLoad = setOf(compileTestKotlin2Js.outputFile.name)
|
|
||||||
|
|
||||||
testJs.configureConventions()
|
testJs.configureConventions()
|
||||||
registerTestTask(testJs)
|
registerTestTask(testJs)
|
||||||
}
|
}
|
||||||
|
|
||||||
project.afterEvaluate {
|
|
||||||
// defer nodeJs executable setup, as nodejs project settings may change during configuration
|
|
||||||
testTask.configure {
|
|
||||||
val nodeJsSetupTask = projectWithNodeJsPlugin.tasks.findByName(NodeJsSetupTask.NAME)
|
|
||||||
it.dependsOn(nodeJsSetupTask)
|
|
||||||
|
|
||||||
if (it.nodeJsProcessOptions.executable == null) {
|
|
||||||
it.nodeJsProcessOptions.executable = NodeJsExtension[projectWithNodeJsPlugin].buildEnv().nodeExec
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+16
-1
@@ -8,6 +8,8 @@ package org.jetbrains.kotlin.gradle.targets.js
|
|||||||
import org.jetbrains.kotlin.gradle.plugin.*
|
import org.jetbrains.kotlin.gradle.plugin.*
|
||||||
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
|
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
|
||||||
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTarget
|
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTarget
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinTasksProvider
|
import org.jetbrains.kotlin.gradle.tasks.KotlinTasksProvider
|
||||||
|
|
||||||
class KotlinJsTargetConfigurator(kotlinPluginVersion: String) :
|
class KotlinJsTargetConfigurator(kotlinPluginVersion: String) :
|
||||||
@@ -23,13 +25,26 @@ class KotlinJsTargetConfigurator(kotlinPluginVersion: String) :
|
|||||||
|
|
||||||
platformTarget.compilations.all {
|
platformTarget.compilations.all {
|
||||||
it.compileKotlinTask.kotlinOptions {
|
it.compileKotlinTask.kotlinOptions {
|
||||||
moduleKind = "umd"
|
moduleKind = "commonjs"
|
||||||
sourceMap = true
|
sourceMap = true
|
||||||
sourceMapEmbedSources = null
|
sourceMapEmbedSources = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun configureTest(target: KotlinOnlyTarget<KotlinJsCompilation>) {
|
||||||
|
val project = target.project
|
||||||
|
val npmProject = NpmProject[project]
|
||||||
|
|
||||||
|
target.compilations.all { compilation ->
|
||||||
|
if (isTestCompilation(compilation)) {
|
||||||
|
KotlinJsCompilationTestsConfigurator(compilation).configure()
|
||||||
|
} else {
|
||||||
|
KotlinWebpack.configure(compilation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal fun isTestCompilation(it: KotlinJsCompilation) =
|
internal fun isTestCompilation(it: KotlinJsCompilation) =
|
||||||
it.name == KotlinCompilation.TEST_COMPILATION_NAME
|
it.name == KotlinCompilation.TEST_COMPILATION_NAME
|
||||||
|
|||||||
+5
-1
@@ -142,7 +142,11 @@ open class RewriteSourceMapFilterReader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected open fun transformString(value: String): String =
|
protected open fun transformString(value: String): String =
|
||||||
File(srcSourceRoot).resolve(value).canonicalFile.relativeToOrSelf(File(targetSourceRoot)).path
|
File(srcSourceRoot)
|
||||||
|
.resolve(value)
|
||||||
|
.canonicalFile
|
||||||
|
.relativeToOrSelf(File(targetSourceRoot))
|
||||||
|
.path
|
||||||
|
|
||||||
override fun read(): Int {
|
override fun read(): Int {
|
||||||
maybeReadFirst()
|
maybeReadFirst()
|
||||||
|
|||||||
+8
-8
@@ -1,16 +1,16 @@
|
|||||||
package org.jetbrains.kotlin.gradle.targets.js.nodejs
|
package org.jetbrains.kotlin.gradle.targets.js.nodejs
|
||||||
|
|
||||||
import java.io.*
|
import java.io.File
|
||||||
|
|
||||||
internal data class NodeJsEnv(
|
internal data class NodeJsEnv(
|
||||||
val nodeDir: File,
|
val nodeDir: File,
|
||||||
val nodeBinDir: File,
|
val nodeBinDir: File,
|
||||||
val nodeExec: String,
|
val nodeExecutable: String,
|
||||||
val npmExec: String,
|
|
||||||
|
|
||||||
val platformName: String,
|
|
||||||
val architectureName: String,
|
val platformName: String,
|
||||||
val ivyDependency: String
|
val architectureName: String,
|
||||||
|
val ivyDependency: String
|
||||||
) {
|
) {
|
||||||
val isWindows: Boolean
|
val isWindows: Boolean
|
||||||
get() = platformName == "win"
|
get() = platformName == "win"
|
||||||
|
|||||||
+18
-55
@@ -1,68 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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.nodejs
|
package org.jetbrains.kotlin.gradle.targets.js.nodejs
|
||||||
|
|
||||||
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.targets.js.npm.PackageJson
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
open class NodeJsExtension(project: Project) {
|
open class NodeJsExtension(val project: Project) {
|
||||||
private val gradleHome = project.gradle.gradleUserHomeDir.also {
|
val root: NodeJsRootExtension
|
||||||
project.logger.kotlinInfo("Storing cached files in $it")
|
get() = this as? NodeJsRootExtension ?: NodeJsRootExtension[project]
|
||||||
}
|
|
||||||
|
|
||||||
var installationDir = gradleHome.resolve("nodejs")
|
internal val packageJsonHandlers = mutableListOf<PackageJson.() -> Unit>()
|
||||||
|
|
||||||
var distBaseUrl = "https://nodejs.org/dist"
|
@Suppress("unused")
|
||||||
var version = "10.15.3"
|
fun packageJson(handler: PackageJson.() -> Unit) {
|
||||||
var npmVersion = ""
|
packageJsonHandlers.add(handler)
|
||||||
|
|
||||||
var nodeCommand = "node"
|
|
||||||
var npmCommand = "npm"
|
|
||||||
|
|
||||||
var download = true
|
|
||||||
|
|
||||||
internal fun buildEnv(): NodeJsEnv {
|
|
||||||
val platform = NodeJsPlatform.name
|
|
||||||
val architecture = NodeJsPlatform.architecture
|
|
||||||
|
|
||||||
val nodeDir = installationDir.resolve("node-v$version-$platform-$architecture")
|
|
||||||
val isWindows = NodeJsPlatform.name == NodeJsPlatform.WIN
|
|
||||||
val nodeBinDir = if (isWindows) nodeDir else nodeDir.resolve("bin")
|
|
||||||
|
|
||||||
fun getExecutable(command: String, customCommand: String, windowsExtension: String): String {
|
|
||||||
val finalCommand = if (isWindows && customCommand == command) "$command.$windowsExtension" else customCommand
|
|
||||||
return if (download) File(nodeBinDir, finalCommand).absolutePath else finalCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIvyDependency(): String {
|
|
||||||
val type = if (isWindows) "zip" else "tar.gz"
|
|
||||||
return "org.nodejs:node:$version:$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()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val NODE_JS: String = "nodeJs"
|
|
||||||
|
|
||||||
operator fun get(project: Project): NodeJsExtension {
|
operator fun get(project: Project): NodeJsExtension {
|
||||||
val extension = project.extensions.findByType(NodeJsExtension::class.java)
|
NodeJsPlugin.apply(project.rootProject)
|
||||||
if (extension != null)
|
return project.extensions.getByName(NodeJsRootExtension.NODE_JS) as NodeJsExtension
|
||||||
return extension
|
|
||||||
|
|
||||||
val parentProject = project.parent
|
|
||||||
if (parentProject != null)
|
|
||||||
return get(parentProject)
|
|
||||||
|
|
||||||
throw GradleException("NodeJsExtension is not installed")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val Project.nodeJs: NodeJsExtension
|
||||||
|
get() = NodeJsExtension[this]
|
||||||
+1
-1
@@ -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 [NodeJsExtension.buildEnv].
|
* See [NodeJsEnv.ivyDependency] that is filled in [NodeJsRootExtension.environment].
|
||||||
*/
|
*/
|
||||||
internal object NodeJsPlatform {
|
internal object NodeJsPlatform {
|
||||||
private val props = System.getProperties()
|
private val props = System.getProperties()
|
||||||
|
|||||||
+33
-15
@@ -2,28 +2,46 @@ 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.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsExtension.Companion.NODE_JS
|
import org.gradle.api.plugins.BasePlugin
|
||||||
|
import org.gradle.api.tasks.Delete
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension.Companion.NODE_JS
|
||||||
|
|
||||||
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 {
|
||||||
this.extensions.create(NODE_JS, NodeJsExtension::class.java, this)
|
check(project == project.rootProject) {
|
||||||
|
"NodeJsPlugin can be applied only to root project"
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
if (it != project) {
|
||||||
|
it.extensions.create(NODE_JS, NodeJsExtension::class.java, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupCleanNodeModulesTask(project: Project) {
|
||||||
|
project.tasks.create("cleanNodeModules", Delete::class.java) {
|
||||||
|
it.description = "Deletes node_modules and package.json file"
|
||||||
|
it.group = BasePlugin.BUILD_GROUP
|
||||||
|
|
||||||
|
it.doLast {
|
||||||
|
project.nodeJs.root.packageManager.cleanProject(project)
|
||||||
|
}
|
||||||
|
|
||||||
|
project.tasks.maybeCreate(BasePlugin.CLEAN_TASK_NAME).dependsOn(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun ensureAppliedInHierarchy(myProject: Project): Project =
|
fun apply(project: Project): NodeJsRootExtension {
|
||||||
findInHeirarchy(myProject) ?: myProject.also {
|
val rootProject = project.rootProject
|
||||||
it.pluginManager.apply(NodeJsPlugin::class.java)
|
rootProject.plugins.apply(NodeJsPlugin::class.java)
|
||||||
}
|
return rootProject.extensions.getByName(NODE_JS) as NodeJsRootExtension
|
||||||
|
|
||||||
fun findInHeirarchy(myProject: Project): Project? {
|
|
||||||
var project: Project? = myProject
|
|
||||||
while (project != null) {
|
|
||||||
if (myProject.plugins.hasPlugin(NodeJsPlugin::class.java)) return project
|
|
||||||
project = project.parent
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
package org.jetbrains.kotlin.gradle.targets.js.nodejs
|
||||||
|
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.jetbrains.kotlin.gradle.logging.kotlinInfo
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApi
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.yarn.Yarn
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) {
|
||||||
|
private val gradleHome = project.gradle.gradleUserHomeDir.also {
|
||||||
|
project.logger.kotlinInfo("Storing cached files in $it")
|
||||||
|
}
|
||||||
|
|
||||||
|
var installationDir = gradleHome.resolve("nodejs")
|
||||||
|
|
||||||
|
var download = true
|
||||||
|
var nodeDownloadBaseUrl = "https://nodejs.org/dist"
|
||||||
|
var nodeVersion = "10.15.3"
|
||||||
|
|
||||||
|
var nodeCommand = "node"
|
||||||
|
|
||||||
|
var manageNodeModules: Boolean = false
|
||||||
|
var packageManager: NpmApi = Yarn
|
||||||
|
|
||||||
|
val nodeJsSetupTask: NodeJsSetupTask
|
||||||
|
get() = project.tasks.getByName(NodeJsSetupTask.NAME) as NodeJsSetupTask
|
||||||
|
|
||||||
|
internal val environment: NodeJsEnv
|
||||||
|
get() {
|
||||||
|
val platform = NodeJsPlatform.name
|
||||||
|
val architecture = NodeJsPlatform.architecture
|
||||||
|
|
||||||
|
val nodeDir = installationDir.resolve("node-v$nodeVersion-$platform-$architecture")
|
||||||
|
val isWindows = NodeJsPlatform.name == NodeJsPlatform.WIN
|
||||||
|
val nodeBinDir = if (isWindows) nodeDir else nodeDir.resolve("bin")
|
||||||
|
|
||||||
|
fun getExecutable(command: String, customCommand: String, windowsExtension: String): String {
|
||||||
|
val finalCommand = if (isWindows && customCommand == command) "$command.$windowsExtension" else customCommand
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun executeSetup() {
|
||||||
|
val nodeJsEnv = environment
|
||||||
|
if (download) {
|
||||||
|
if (!nodeJsEnv.nodeBinDir.isDirectory) {
|
||||||
|
nodeJsSetupTask.exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val NODE_JS: String = "nodeJs"
|
||||||
|
|
||||||
|
operator fun get(project: Project) = NodeJsPlugin.apply(project)
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-12
@@ -11,30 +11,33 @@ import java.io.File
|
|||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
open class NodeJsSetupTask : DefaultTask() {
|
open class NodeJsSetupTask : DefaultTask() {
|
||||||
private val settings = NodeJsExtension[project]
|
private val settings = project.nodeJs.root
|
||||||
private val env by lazy { settings.buildEnv() }
|
private val env by lazy { settings.environment }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
group = NodeJsExtension.NODE_JS
|
group = NodeJsRootExtension.NODE_JS
|
||||||
description = "Download and install a local node/npm version."
|
description = "Download and install a local node/npm version."
|
||||||
}
|
}
|
||||||
|
|
||||||
val input: Set<String>
|
val ivyDependency: String
|
||||||
@Input get() = setOf(settings.download.toString(), env.ivyDependency)
|
@Input get() = env.ivyDependency
|
||||||
|
|
||||||
val destination: File
|
val destination: File
|
||||||
@OutputDirectory get() = env.nodeDir
|
@OutputDirectory get() = env.nodeDir
|
||||||
|
|
||||||
|
init {
|
||||||
|
onlyIf {
|
||||||
|
settings.download && !env.nodeBinDir.isDirectory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@TaskAction
|
@TaskAction
|
||||||
fun exec() {
|
fun exec() {
|
||||||
if (!settings.download)
|
|
||||||
return
|
|
||||||
|
|
||||||
@Suppress("UnstableApiUsage", "DEPRECATION")
|
@Suppress("UnstableApiUsage", "DEPRECATION")
|
||||||
val repo = project.repositories.ivy { repo ->
|
val repo = project.repositories.ivy { repo ->
|
||||||
repo.name = "Node Distributions at ${settings.distBaseUrl}"
|
repo.name = "Node Distributions at ${settings.nodeDownloadBaseUrl}"
|
||||||
repo.url = URI(settings.distBaseUrl)
|
repo.url = URI(settings.nodeDownloadBaseUrl)
|
||||||
|
|
||||||
if (isGradleVersionAtLeast(5, 0)) {
|
if (isGradleVersionAtLeast(5, 0)) {
|
||||||
repo.patternLayout { layout ->
|
repo.patternLayout { layout ->
|
||||||
@@ -52,7 +55,7 @@ open class NodeJsSetupTask : DefaultTask() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val dep = this.project.dependencies.create(env.ivyDependency)
|
val dep = this.project.dependencies.create(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()
|
||||||
@@ -63,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.nodeExec).setExecutable(true)
|
File(env.nodeExecutable).setExecutable(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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.nodejs
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.io.File
|
||||||
|
|
||||||
|
class GradleNodeModule(val name: String, val path: File)
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* 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.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.api.artifacts.ResolvedArtifact
|
||||||
|
import org.gradle.api.artifacts.ResolvedDependency
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal class GradleNodeModuleBuilder(
|
||||||
|
val project: Project,
|
||||||
|
val dependency: ResolvedDependency,
|
||||||
|
val artifacts: Set<ResolvedArtifact>,
|
||||||
|
val cache: GradleNodeModules
|
||||||
|
) {
|
||||||
|
var srcPackageJsonFile: File? = null
|
||||||
|
val files = mutableListOf<File>()
|
||||||
|
|
||||||
|
fun visitArtifacts() {
|
||||||
|
artifacts.forEach { artifact ->
|
||||||
|
val srcFile = artifact.file
|
||||||
|
when {
|
||||||
|
isKotlinJsRuntimeFile(srcFile) -> files.add(srcFile)
|
||||||
|
srcFile.isZip -> project.zipTree(srcFile).forEach { innerFile ->
|
||||||
|
when {
|
||||||
|
innerFile.name == NpmProject.PACKAGE_JSON -> srcPackageJsonFile = innerFile
|
||||||
|
isKotlinJsRuntimeFile(innerFile) -> files.add(innerFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rebuild(): File? {
|
||||||
|
if (files.isEmpty()) return null
|
||||||
|
|
||||||
|
val packageJson = srcPackageJsonFile?.reader()?.use {
|
||||||
|
Gson().fromJson(it, PackageJson::class.java)
|
||||||
|
} ?: PackageJson(dependency.moduleName, dependency.moduleVersion)
|
||||||
|
|
||||||
|
val jsFiles = files.filter { it.name.endsWith(".js") }
|
||||||
|
if (jsFiles.size == 1) {
|
||||||
|
val jsFile = jsFiles.single()
|
||||||
|
packageJson.name = jsFile.nameWithoutExtension
|
||||||
|
packageJson.main = jsFile.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// yarn requires semver
|
||||||
|
packageJson.version = fixSemver(packageJson.version)
|
||||||
|
|
||||||
|
return makeNodeModule(cache.dir, packageJson) { nodeModule ->
|
||||||
|
project.copy { copy ->
|
||||||
|
copy.from(files)
|
||||||
|
copy.into(nodeModule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun fixSemver(version: String): String {
|
||||||
|
val c = version.split("-", limit = 2)
|
||||||
|
val main = c[0]
|
||||||
|
val preRelease = if (c.size > 1) c[1] else null
|
||||||
|
val numbers = main.split(".")
|
||||||
|
val major = numbers[0]
|
||||||
|
val minor = if (numbers.size > 1) numbers[1] else null
|
||||||
|
val patch = if (numbers.size > 2) numbers[2] else null
|
||||||
|
return buildString {
|
||||||
|
append(major.filter { it.isDigit() })
|
||||||
|
append(".")
|
||||||
|
append(minor?.filter { it.isDigit() } ?: "0")
|
||||||
|
append(".")
|
||||||
|
append(patch?.filter { it.isDigit() } ?: "0")
|
||||||
|
if (preRelease != null) {
|
||||||
|
append("-")
|
||||||
|
append(preRelease)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeNodeModule(
|
||||||
|
container: File,
|
||||||
|
packageJson: PackageJson,
|
||||||
|
files: (File) -> Unit
|
||||||
|
): File {
|
||||||
|
val dir = container.resolve(packageJson.name)
|
||||||
|
|
||||||
|
if (dir.exists()) dir.deleteRecursively()
|
||||||
|
|
||||||
|
check(dir.mkdirs()) {
|
||||||
|
"Cannot create directory: $dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.setPrettyPrinting()
|
||||||
|
.create()
|
||||||
|
|
||||||
|
files(dir)
|
||||||
|
|
||||||
|
dir.resolve("package.json").writer().use {
|
||||||
|
gson.toJson(packageJson, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* 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.gradle.api.artifacts.ResolvedArtifact
|
||||||
|
import org.gradle.api.artifacts.ResolvedDependency
|
||||||
|
import org.gradle.api.file.CopySpec
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions
|
||||||
|
import org.jetbrains.kotlin.gradle.internal.ProcessedFilesCache
|
||||||
|
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.internal.RewriteSourceMapFilterReader
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
|
||||||
|
|
||||||
|
internal class GradleNodeModules(val project: Project) : AutoCloseable {
|
||||||
|
companion object {
|
||||||
|
const val STATE_FILE_NAME = ".visited"
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val dir = project.buildDir.resolve("node_modules_gradle")
|
||||||
|
private val nodeModulesDir = project.npmProject.nodeModulesDir
|
||||||
|
private val cache = ProcessedFilesCache(project, dir, STATE_FILE_NAME, "5")
|
||||||
|
private val visited = mutableSetOf<ResolvedDependency>()
|
||||||
|
private val doLast = mutableListOf<() -> Unit>()
|
||||||
|
|
||||||
|
val modules
|
||||||
|
get() = cache.targets.map {
|
||||||
|
GradleNodeModule(it, dir.resolve(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun visitCompilation(compilation: KotlinCompilation<KotlinCommonOptions>) {
|
||||||
|
val project = compilation.target.project
|
||||||
|
val kotlin2JsCompile = compilation.compileKotlinTask as Kotlin2JsCompile
|
||||||
|
|
||||||
|
// classpath
|
||||||
|
compilation.relatedConfigurationNames.forEach {
|
||||||
|
val configuration = project.configurations.getByName(it)
|
||||||
|
if (configuration.isCanBeResolved) {
|
||||||
|
configuration.resolvedConfiguration.firstLevelModuleDependencies.forEach {
|
||||||
|
visitDependency(compilation, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// output
|
||||||
|
doLast.add {
|
||||||
|
// we should do it only after node package manger work (as it can clean files)
|
||||||
|
if (kotlin2JsCompile.state.executed) {
|
||||||
|
visitCompile(project, kotlin2JsCompile)
|
||||||
|
} else {
|
||||||
|
kotlin2JsCompile.doLast {
|
||||||
|
visitCompile(project, kotlin2JsCompile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun visitDependency(
|
||||||
|
compilation: KotlinCompilation<KotlinCommonOptions>,
|
||||||
|
dependency: ResolvedDependency
|
||||||
|
) {
|
||||||
|
if (!visited.add(dependency)) return
|
||||||
|
|
||||||
|
visitArtifacts(dependency, dependency.moduleArtifacts)
|
||||||
|
|
||||||
|
dependency.children.forEach {
|
||||||
|
visitDependency(compilation, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun visitArtifacts(
|
||||||
|
dependency: ResolvedDependency,
|
||||||
|
artifacts: MutableSet<ResolvedArtifact>
|
||||||
|
) {
|
||||||
|
val lazyDirName: String? by lazy {
|
||||||
|
val module = GradleNodeModuleBuilder(project, dependency, artifacts, this)
|
||||||
|
module.visitArtifacts()
|
||||||
|
module.rebuild()?.canonicalFile?.relativeTo(dir)?.path
|
||||||
|
}
|
||||||
|
|
||||||
|
artifacts.forEach { artifact ->
|
||||||
|
cache.getOrCompute(artifact.file) {
|
||||||
|
lazyDirName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun visitCompile(project: Project, kotlin2JsCompile: Kotlin2JsCompile) {
|
||||||
|
project.copy { copy ->
|
||||||
|
copy.from(kotlin2JsCompile.outputFile)
|
||||||
|
copy.from(kotlin2JsCompile.outputFile.path + ".map")
|
||||||
|
copy.into(nodeModulesDir)
|
||||||
|
copy.withSourceMapRewriter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CopySpec.withSourceMapRewriter() {
|
||||||
|
eachFile {
|
||||||
|
if (it.name.endsWith(".js.map")) {
|
||||||
|
it.filter(
|
||||||
|
mapOf(
|
||||||
|
"srcSourceRoot" to it.file.parentFile,
|
||||||
|
"targetSourceRoot" to nodeModulesDir
|
||||||
|
),
|
||||||
|
RewriteSourceMapFilterReader::class.java
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
doLast.forEach { it() }
|
||||||
|
cache.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
interface NpmApi {
|
||||||
|
fun setup(project: Project)
|
||||||
|
|
||||||
|
@Suppress("EXPOSED_PARAMETER_TYPE")
|
||||||
|
fun resolveProject(npmPackage: NpmResolver.NpmPackage)
|
||||||
|
|
||||||
|
@Suppress("EXPOSED_PARAMETER_TYPE")
|
||||||
|
fun resolveRootProject(
|
||||||
|
rootProject: Project,
|
||||||
|
subprojects: MutableList<NpmResolver.NpmPackage>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("EXPOSED_PARAMETER_TYPE")
|
||||||
|
fun hookRootPackage(rootProject: Project, rootPackageJson: PackageJson, allWorkspaces: Collection<NpmResolver.NpmPackage>) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
open val hoistGradleNodeModules
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
fun cleanProject(project: Project) {
|
||||||
|
val npmProject = project.npmProject
|
||||||
|
|
||||||
|
npmProject.nodeModulesDir.deleteRecursively()
|
||||||
|
npmProject.packageJsonFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun resolveOperationDescription(packageManagerTitle: String): String =
|
||||||
|
"Resolving NPM dependencies using $packageManagerTitle"
|
||||||
|
}
|
||||||
|
}
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* 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.gradle.api.artifacts.Dependency
|
||||||
|
import org.gradle.api.artifacts.FileCollectionDependency
|
||||||
|
import org.gradle.api.artifacts.SelfResolvingDependency
|
||||||
|
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.tasks.TaskDependency
|
||||||
|
import org.gradle.internal.component.local.model.DefaultLibraryBinaryIdentifier
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver.ResolutionCallResult.*
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
data class NpmDependency(
|
||||||
|
internal val project: Project,
|
||||||
|
private val org: String?,
|
||||||
|
private val name: String,
|
||||||
|
private val version: String
|
||||||
|
) : SelfResolvingDependency,
|
||||||
|
SelfResolvingDependencyInternal,
|
||||||
|
ResolvableDependency,
|
||||||
|
FileCollectionDependency {
|
||||||
|
override fun getGroup(): String? = org
|
||||||
|
|
||||||
|
internal val dependencies = mutableSetOf<NpmDependency>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
return when (result) {
|
||||||
|
is AlreadyInProgress -> null
|
||||||
|
is AlreadyResolved -> {
|
||||||
|
check(this in result.resolution.dependencies) {
|
||||||
|
"Project hierarchy is already resolved in NPM without $this"
|
||||||
|
}
|
||||||
|
|
||||||
|
result.resolution
|
||||||
|
}
|
||||||
|
is ResolvedNow -> {
|
||||||
|
check(this in result.resolution.dependencies) {
|
||||||
|
"$this was not visited during resolution"
|
||||||
|
}
|
||||||
|
|
||||||
|
result.resolution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryFindNodeModule(): File? {
|
||||||
|
var p = project
|
||||||
|
do {
|
||||||
|
val result = NpmProject[p].nodeModulesDir.resolve(key)
|
||||||
|
if (result.exists()) return result
|
||||||
|
p = project.parent ?: return null
|
||||||
|
} while (true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val key: String = if (org == null) name else "@$org/$name"
|
||||||
|
|
||||||
|
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 getFiles(): FileCollection = project.files(resolve())
|
||||||
|
|
||||||
|
override fun getName() = name
|
||||||
|
|
||||||
|
override fun getVersion() = version
|
||||||
|
|
||||||
|
override fun getBuildDependencies(): TaskDependency = TaskDependency { mutableSetOf() }
|
||||||
|
|
||||||
|
override fun contentEquals(dependency: Dependency) = this == dependency
|
||||||
|
|
||||||
|
override fun getTargetComponentId() = DefaultLibraryBinaryIdentifier(project.path, key, "npm")
|
||||||
|
|
||||||
|
override fun copy(): Dependency = this.copy(org = org)
|
||||||
|
|
||||||
|
private var reason: String? = null
|
||||||
|
|
||||||
|
override fun because(reason: String?) {
|
||||||
|
this.reason = reason
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReason(): String? = reason
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* 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.Gson
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import groovy.transform.TailRecursive
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.process.ExecSpec
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class NpmProject(
|
||||||
|
val project: Project,
|
||||||
|
val nodeWorkDir: File,
|
||||||
|
val managed: Boolean
|
||||||
|
) {
|
||||||
|
val nodeModulesDir
|
||||||
|
get() = nodeWorkDir.resolve(NODE_MODULES)
|
||||||
|
|
||||||
|
val packageJsonFile: File
|
||||||
|
get() {
|
||||||
|
return nodeWorkDir.resolve(PACKAGE_JSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun useTool(exec: ExecSpec, tool: String, vararg args: String) {
|
||||||
|
NpmResolver.resolve(project)
|
||||||
|
|
||||||
|
exec.workingDir = nodeWorkDir
|
||||||
|
exec.executable = project.nodeJs.root.environment.nodeExecutable
|
||||||
|
exec.args = listOf(findModule(tool)) + args
|
||||||
|
}
|
||||||
|
|
||||||
|
val hoistGradleNodeModules: Boolean
|
||||||
|
get() = project.nodeJs.root.packageManager.getHoistGradleNodeModules(project)
|
||||||
|
|
||||||
|
val gradleNodeModulesDir: File
|
||||||
|
get() =
|
||||||
|
if (hoistGradleNodeModules) project.rootProject.npmProject.nodeModulesDir
|
||||||
|
else nodeModulesDir
|
||||||
|
|
||||||
|
fun moduleOutput(compilationTask: Kotlin2JsCompile): File {
|
||||||
|
return gradleNodeModulesDir.resolve(compilationTask.outputFile.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TailRecursive
|
||||||
|
fun findModule(name: String, src: NpmProject = this): String {
|
||||||
|
val file = nodeModulesDir.resolve(name)
|
||||||
|
if (file.isFile) return file.canonicalPath
|
||||||
|
if (file.isDirectory) {
|
||||||
|
val packageJsonFile = file.resolve("package.json")
|
||||||
|
val main: String = (if (packageJsonFile.isFile) {
|
||||||
|
val packageJson = packageJsonFile.reader().use {
|
||||||
|
Gson().fromJson(it, JsonObject::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
packageJson["main"] as? String? ?: packageJson["module"] as? String? ?: packageJson["browser"] as? String?
|
||||||
|
} else null) ?: "index.js"
|
||||||
|
|
||||||
|
val mainFile = file.resolve(main)
|
||||||
|
if (mainFile.isFile) return mainFile.canonicalPath
|
||||||
|
}
|
||||||
|
|
||||||
|
val parent = project.parent
|
||||||
|
return if (!managed || parent == null) error("Cannot find node module $name in $src")
|
||||||
|
else NpmProject[parent].findModule(name, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PACKAGE_JSON = "package.json"
|
||||||
|
const val NODE_MODULES = "node_modules"
|
||||||
|
|
||||||
|
operator fun get(project: Project): NpmProject {
|
||||||
|
val nodeJsRootExtension = NodeJsPlugin.apply(project)
|
||||||
|
|
||||||
|
val manageNodeModules = nodeJsRootExtension.root.manageNodeModules
|
||||||
|
|
||||||
|
val nodeWorkDir =
|
||||||
|
if (manageNodeModules) project.projectDir
|
||||||
|
else project.buildDir
|
||||||
|
|
||||||
|
return NpmProject(project, nodeWorkDir, manageNodeModules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val Project.npmProject
|
||||||
|
get() = NpmProject[this]
|
||||||
+218
@@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
* 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.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.kotlinExtensionOrNull
|
||||||
|
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
|
||||||
|
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
|
||||||
|
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.nodeJs
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver.ResolutionCallResult.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates `package.json` file for projects with npm or js dependencies and
|
||||||
|
* runs selected [NodeJsRootExtension.packageManager] to download and install it.
|
||||||
|
*
|
||||||
|
* All [NpmDependency] for configurations related to kotlin/js will be added to `package.json`.
|
||||||
|
* For external gradle modules, fake npm packages will be created and added to `package.json`
|
||||||
|
* as path to directory.
|
||||||
|
*/
|
||||||
|
internal class NpmResolver private constructor(val rootProject: Project) {
|
||||||
|
companion object {
|
||||||
|
fun resolve(project: Project): ResolutionCallResult {
|
||||||
|
val rootProject = project.rootProject
|
||||||
|
val process = ProjectData[rootProject]
|
||||||
|
|
||||||
|
if (process != null && process.resolved == null)
|
||||||
|
return AlreadyInProgress
|
||||||
|
|
||||||
|
val resolved = process?.resolved
|
||||||
|
|
||||||
|
return if (resolved != null) AlreadyResolved(resolved)
|
||||||
|
else {
|
||||||
|
val resolver = NpmResolver(rootProject)
|
||||||
|
check(resolver.resolve(rootProject, null))
|
||||||
|
ResolvedNow(ResolvedProject(resolver.npmPackages))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ResolutionCallResult {
|
||||||
|
object AlreadyInProgress : ResolutionCallResult()
|
||||||
|
class AlreadyResolved(val resolution: ResolvedProject) : ResolutionCallResult()
|
||||||
|
class ResolvedNow(val resolution: ResolvedProject) : ResolutionCallResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val nodeJs = NodeJsPlugin.apply(rootProject).root
|
||||||
|
private val npmProject = rootProject.npmProject
|
||||||
|
private val packageManager = nodeJs.packageManager
|
||||||
|
private val hoistGradleNodeModules = npmProject.hoistGradleNodeModules
|
||||||
|
private val npmPackages = mutableListOf<NpmPackage>()
|
||||||
|
private val gson = GsonBuilder()
|
||||||
|
.setPrettyPrinting()
|
||||||
|
.create()
|
||||||
|
|
||||||
|
class ProjectData(var resolved: ResolvedProject? = null) {
|
||||||
|
companion object {
|
||||||
|
private const val KEY = "npmResolverData"
|
||||||
|
operator fun get(project: Project) = project.extensions.findByName(KEY) as ProjectData?
|
||||||
|
operator fun set(project: Project, value: ProjectData) = project.extensions.add(KEY, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResolvedProject(val npmPackages: Collection<NpmPackage>) {
|
||||||
|
val dependencies by lazy {
|
||||||
|
npmPackages.flatMapTo(mutableSetOf()) { it.npmDependencies }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NpmPackage(
|
||||||
|
val project: Project,
|
||||||
|
val packageJson: PackageJson,
|
||||||
|
val npmDependencies: Set<NpmDependency>
|
||||||
|
) {
|
||||||
|
fun savePackageJson(gson: Gson) {
|
||||||
|
NpmProject[project].packageJsonFile.writer().use {
|
||||||
|
gson.toJson(packageJson, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var packageManagerInstalled = false
|
||||||
|
|
||||||
|
private fun requirePackageManagerSetup() {
|
||||||
|
if (packageManagerInstalled) return
|
||||||
|
packageManager.setup(rootProject)
|
||||||
|
packageManagerInstalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolve(
|
||||||
|
project: Project,
|
||||||
|
parentGradleModules: GradleNodeModules?
|
||||||
|
): Boolean {
|
||||||
|
val existedProcess = ProjectData[project]
|
||||||
|
if (existedProcess != null) {
|
||||||
|
if (existedProcess.resolved != null) error("yarn dependencies for $project already resolved")
|
||||||
|
else {
|
||||||
|
// called inside resolution of classpath (from visitTarget)
|
||||||
|
// we should return as we are already in progress
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else ProjectData().also {
|
||||||
|
ProjectData[project] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
val gradleModules =
|
||||||
|
if (hoistGradleNodeModules && parentGradleModules != null) parentGradleModules
|
||||||
|
else GradleNodeModules(project)
|
||||||
|
|
||||||
|
project.subprojects.forEach {
|
||||||
|
resolve(it, gradleModules)
|
||||||
|
}
|
||||||
|
|
||||||
|
val npmPackage = extractNpmPackage(project, gradleModules)
|
||||||
|
if (npmPackage != null) {
|
||||||
|
npmPackage.savePackageJson(gson)
|
||||||
|
|
||||||
|
requirePackageManagerSetup()
|
||||||
|
packageManager.resolveProject(npmPackage)
|
||||||
|
|
||||||
|
npmPackages.add(npmPackage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project == rootProject) {
|
||||||
|
if (npmPackages.isNotEmpty()) {
|
||||||
|
requirePackageManagerSetup()
|
||||||
|
packageManager.resolveRootProject(rootProject, npmPackages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gradleModules != parentGradleModules) {
|
||||||
|
gradleModules.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractNpmPackage(
|
||||||
|
project: Project,
|
||||||
|
gradleComponents: GradleNodeModules
|
||||||
|
): NpmPackage? {
|
||||||
|
val packageJson = PackageJson(project.name, project.version.toString())
|
||||||
|
val npmDependencies = mutableSetOf<NpmDependency>()
|
||||||
|
|
||||||
|
visitNpmDependencies(project, npmDependencies, gradleComponents, packageJson)
|
||||||
|
|
||||||
|
if (!hoistGradleNodeModules || project == project.rootProject) {
|
||||||
|
gradleComponents.modules.forEach {
|
||||||
|
val relativePath = it.path.relativeTo(NpmProject[project].nodeWorkDir)
|
||||||
|
packageJson.dependencies[it.name] = "file:$relativePath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
project.nodeJs.packageJsonHandlers.forEach {
|
||||||
|
it(packageJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
val packageJsonRequired = if (project == rootProject) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chooseVersion(oldVersion: String?, newVersion: String): String =
|
||||||
|
oldVersion ?: newVersion // todo: real versions conflict resolution
|
||||||
|
|
||||||
|
private fun visitTarget(
|
||||||
|
target: KotlinTarget,
|
||||||
|
project: Project,
|
||||||
|
npmDependencies: MutableSet<NpmDependency>,
|
||||||
|
gradleComponents: GradleNodeModules
|
||||||
|
) {
|
||||||
|
if (target.platformType == KotlinPlatformType.js) {
|
||||||
|
target.compilations.toList().forEach { compilation ->
|
||||||
|
compilation.relatedConfigurationNames.forEach {
|
||||||
|
project.configurations.getByName(it).allDependencies.forEach { dependency ->
|
||||||
|
when (dependency) {
|
||||||
|
is NpmDependency -> npmDependencies.add(dependency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gradleComponents.visitCompilation(compilation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -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.targets.js.npm
|
||||||
|
|
||||||
|
class PackageJson(
|
||||||
|
var name: String,
|
||||||
|
var version: String
|
||||||
|
) {
|
||||||
|
val empty: Boolean
|
||||||
|
get() = private == null &&
|
||||||
|
workspaces == null &&
|
||||||
|
dependencies.isEmpty() &&
|
||||||
|
devDependencies.isEmpty()
|
||||||
|
|
||||||
|
val scopedName: ScopedName
|
||||||
|
get() = scopedName(name)
|
||||||
|
|
||||||
|
var private: Boolean? = null
|
||||||
|
|
||||||
|
var main: String? = null
|
||||||
|
|
||||||
|
var workspaces: Collection<String>? = null
|
||||||
|
|
||||||
|
val devDependencies = 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -52,12 +52,12 @@ open class KotlinJsNodeModulesTask : DefaultTask() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val File.isZip
|
internal val File.isZip
|
||||||
get() = isFile && (name.endsWith(".jar") || name.endsWith(".zip"))
|
get() = isFile && (name.endsWith(".jar") || name.endsWith(".zip"))
|
||||||
|
|
||||||
internal fun isKotlinJsRuntimeFile(file: File): Boolean {
|
internal fun isKotlinJsRuntimeFile(file: File): Boolean {
|
||||||
if (!file.isFile) return false
|
if (!file.isFile) return false
|
||||||
val name = file.name
|
val name = file.name
|
||||||
return (name.endsWith(".js") && !name.endsWith(".meta.js"))
|
return (name.endsWith(".js") && !name.endsWith(".meta.js"))
|
||||||
|| name.endsWith(".js.map")
|
|| name.endsWith(".js.map")
|
||||||
}
|
}
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* 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.testing
|
||||||
|
|
||||||
|
import org.gradle.api.tasks.Input
|
||||||
|
import org.gradle.api.tasks.Internal
|
||||||
|
import org.gradle.api.tasks.SkipWhenEmpty
|
||||||
|
import org.gradle.process.internal.DefaultProcessForkOptions
|
||||||
|
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec
|
||||||
|
import org.jetbrains.kotlin.gradle.plugin.HasKotlinDependencies
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.testing.karma.KotlinKarma
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.testing.mocha.KotlinMocha
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.testing.nodejs.KotlinNodeJsTestRunner
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinTest
|
||||||
|
|
||||||
|
open class KotlinJsTest : KotlinTest() {
|
||||||
|
@Internal
|
||||||
|
@SkipWhenEmpty
|
||||||
|
internal var testFramework: KotlinJsTestFramework? = null
|
||||||
|
|
||||||
|
@Input
|
||||||
|
var debug: Boolean = false
|
||||||
|
|
||||||
|
@Internal
|
||||||
|
var runtimeDependencyHandler: HasKotlinDependencies? = null
|
||||||
|
|
||||||
|
@Input
|
||||||
|
var nodeModulesToLoad: MutableList<String> = mutableListOf()
|
||||||
|
|
||||||
|
fun useNodeJs(body: KotlinNodeJsTestRunner.() -> Unit) = use(KotlinNodeJsTestRunner(), body)
|
||||||
|
|
||||||
|
fun useMocha(body: KotlinMocha.() -> Unit) = use(KotlinMocha(), body)
|
||||||
|
|
||||||
|
fun useKarma(body: KotlinKarma.() -> Unit) = use(KotlinKarma(), body)
|
||||||
|
|
||||||
|
private inline fun <T : KotlinJsTestFramework> use(runner: T, body: T.() -> Unit): T {
|
||||||
|
check(testFramework == null) {
|
||||||
|
"testFramework already configured for task ${this.path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
val testFramework = runner.also(body)
|
||||||
|
this.testFramework = testFramework
|
||||||
|
|
||||||
|
val dependenciesHolder = runtimeDependencyHandler
|
||||||
|
if (dependenciesHolder != null) {
|
||||||
|
testFramework.configure(dependenciesHolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
return testFramework
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun executeTests() {
|
||||||
|
NpmResolver.resolve(project)
|
||||||
|
super.executeTests()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createTestExecutionSpec(): TCServiceMessagesTestExecutionSpec {
|
||||||
|
val forkOptions = DefaultProcessForkOptions(fileResolver)
|
||||||
|
|
||||||
|
NpmResolver.resolve(project)
|
||||||
|
|
||||||
|
forkOptions.workingDir = NpmProject[project].nodeWorkDir
|
||||||
|
forkOptions.executable = NodeJsPlugin.apply(project).root.environment.nodeExecutable
|
||||||
|
|
||||||
|
val nodeJsArgs = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
nodeJsArgs.add("--inspect-brk")
|
||||||
|
}
|
||||||
|
|
||||||
|
return testFramework!!.createTestExecutionSpec(this, forkOptions, nodeJsArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
/*
|
||||||
|
* 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.webpack
|
||||||
|
|
||||||
|
import com.google.gson.stream.JsonWriter
|
||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.internal.file.FileResolver
|
||||||
|
import org.gradle.api.tasks.*
|
||||||
|
import org.gradle.process.internal.ExecHandleFactory
|
||||||
|
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationToRunnableFiles
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.createOrRegisterTask
|
||||||
|
import org.jetbrains.kotlin.gradle.testing.internal.reportsDir
|
||||||
|
import org.jetbrains.kotlin.gradle.utils.injected
|
||||||
|
import java.io.File
|
||||||
|
import java.io.StringWriter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
open class KotlinWebpack : DefaultTask() {
|
||||||
|
@get:Inject
|
||||||
|
open val fileResolver: FileResolver
|
||||||
|
get() = injected
|
||||||
|
|
||||||
|
@get:Inject
|
||||||
|
open val execHandleFactory: ExecHandleFactory
|
||||||
|
get() = injected
|
||||||
|
|
||||||
|
@Input
|
||||||
|
@SkipWhenEmpty
|
||||||
|
open lateinit var entry: File
|
||||||
|
|
||||||
|
open val configFile: File
|
||||||
|
@OutputFile get() = project.buildDir.resolve("webpack.config.js")
|
||||||
|
|
||||||
|
@Input
|
||||||
|
var saveConfigFileEffective: Boolean = true
|
||||||
|
|
||||||
|
open val configFileEffective: File
|
||||||
|
@OutputFile get() = project.buildDir.resolve("webpack.config.evaluated.js")
|
||||||
|
|
||||||
|
open val outputPath: File
|
||||||
|
@OutputDirectory get() = project.buildDir.resolve("lib")
|
||||||
|
|
||||||
|
open val configDirectory: File
|
||||||
|
@InputDirectory get() = project.projectDir.resolve("webpack.config.d")
|
||||||
|
|
||||||
|
@Input
|
||||||
|
var report: Boolean = false
|
||||||
|
|
||||||
|
open val reportDir: File
|
||||||
|
@OutputDirectory get() = project.reportsDir.resolve("webpack").resolve(entry.nameWithoutExtension)
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun execute() {
|
||||||
|
check(entry.isFile) {
|
||||||
|
"${this}: Entry file not existed \"$entry\""
|
||||||
|
}
|
||||||
|
|
||||||
|
NpmResolver.resolve(project)
|
||||||
|
|
||||||
|
val npmProjectLayout = NpmProject[project]
|
||||||
|
|
||||||
|
configFile.writeText(buildConfig())
|
||||||
|
|
||||||
|
val execFactory = execHandleFactory.newExec()
|
||||||
|
npmProjectLayout.useTool(
|
||||||
|
execFactory,
|
||||||
|
".bin/webpack",
|
||||||
|
"--config", configFile.absolutePath
|
||||||
|
)
|
||||||
|
val exec = execFactory.build()
|
||||||
|
exec.start()
|
||||||
|
exec.waitForFinish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildConfig(): String {
|
||||||
|
return buildString {
|
||||||
|
//language=JavaScript 1.8
|
||||||
|
append(
|
||||||
|
"""
|
||||||
|
const config = {
|
||||||
|
mode: 'development',
|
||||||
|
entry: '${entry.canonicalPath}',
|
||||||
|
output: {
|
||||||
|
path: '${outputPath.canonicalPath}',
|
||||||
|
filename: '${entry.name}'
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
modules: [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
module: {
|
||||||
|
rules: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
if (saveConfigFileEffective) {
|
||||||
|
//language=JavaScript 1.8
|
||||||
|
append(
|
||||||
|
"""
|
||||||
|
const util = require('util');
|
||||||
|
const fs = require("fs");
|
||||||
|
const evaluatedConfig = util.inspect(config, {showHidden: false, depth: null, compact: false});
|
||||||
|
fs.writeFile(${jsQuotedString(configFileEffective.canonicalPath)}, evaluatedConfig, (err) => {});
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report) {
|
||||||
|
//language=JavaScript 1.8
|
||||||
|
append(
|
||||||
|
"""
|
||||||
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
config.plugins.push(new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: "static",
|
||||||
|
reportFilename: "${entry.name}.report.html",
|
||||||
|
openAnalyzer: false,
|
||||||
|
generateStatsFile: true,
|
||||||
|
statsFilename: "${entry.name}.stats.json"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadConfigs(configDirectory)
|
||||||
|
|
||||||
|
append("module.exports = config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun jsQuotedString(str: String) = StringWriter().also {
|
||||||
|
JsonWriter(it).value(str)
|
||||||
|
}.toString()
|
||||||
|
|
||||||
|
private fun StringBuilder.loadConfigs(confDir: File) {
|
||||||
|
if (confDir.isDirectory) confDir
|
||||||
|
.listFiles()
|
||||||
|
?.toList()
|
||||||
|
?.filter { it.name.endsWith(".js") }
|
||||||
|
?.forEach {
|
||||||
|
append(it.readText())
|
||||||
|
appendln()
|
||||||
|
appendln()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun configure(compilation: KotlinCompilationToRunnableFiles<*>) {
|
||||||
|
val project = compilation.target.project
|
||||||
|
val npmProject = project.npmProject
|
||||||
|
|
||||||
|
compilation.dependencies {
|
||||||
|
runtimeOnly(npm("webpack", "4.29.6"))
|
||||||
|
runtimeOnly(npm("webpack-cli", "3.3.0"))
|
||||||
|
runtimeOnly(npm("webpack-bundle-analyzer", "3.3.2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
project.createOrRegisterTask<KotlinWebpack>("webpack") {
|
||||||
|
val compileKotlinTask = compilation.compileKotlinTask
|
||||||
|
compileKotlinTask as Kotlin2JsCompile
|
||||||
|
|
||||||
|
it.dependsOn(compileKotlinTask)
|
||||||
|
|
||||||
|
it.entry = npmProject.moduleOutput(compileKotlinTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* 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.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.targets.js.nodejs.NodeJsPlugin
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.npm.*
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object Yarn : NpmApi {
|
||||||
|
override fun setup(project: Project) {
|
||||||
|
YarnPlugin.apply(project).executeSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
val Project.packageJsonHashFile: File
|
||||||
|
get() = buildDir.resolve("package.json.hash")
|
||||||
|
|
||||||
|
fun yarnExec(
|
||||||
|
project: Project,
|
||||||
|
description: String,
|
||||||
|
vararg args: String,
|
||||||
|
npmProject: NpmProject = NpmProject[project]
|
||||||
|
) {
|
||||||
|
val nodeJsEnv = NodeJsPlugin.apply(project).root.environment
|
||||||
|
val yarnEnv = YarnPlugin.apply(project).environment
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
exec.executable = nodeJsEnv.nodeExecutable
|
||||||
|
exec.args = listOf(yarnEnv.home.resolve("bin/yarn.js").absolutePath) + args
|
||||||
|
exec.workingDir = 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")
|
||||||
|
override fun resolveProject(npmPackage: NpmResolver.NpmPackage) {
|
||||||
|
val project = npmPackage.project
|
||||||
|
if (!project.yarn.useWorkspaces) {
|
||||||
|
yarnExec(project, NpmApi.resolveOperationDescription("yarn for ${project.path}"))
|
||||||
|
yarnLockReadTransitiveDependencies(NpmProject[project].nodeWorkDir, npmPackage.npmDependencies)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("EXPOSED_PARAMETER_TYPE")
|
||||||
|
override fun hookRootPackage(
|
||||||
|
rootProject: Project,
|
||||||
|
rootPackageJson: PackageJson,
|
||||||
|
allWorkspaces: Collection<NpmResolver.NpmPackage>
|
||||||
|
): Boolean {
|
||||||
|
if (rootProject.yarn.useWorkspaces) {
|
||||||
|
rootPackageJson.private = true
|
||||||
|
rootPackageJson.workspaces = allWorkspaces
|
||||||
|
.filter { it.project != rootProject }
|
||||||
|
.map { it.project.projectDir.relativeTo(rootProject.rootDir).path }
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHoistGradleNodeModules(project: Project): Boolean =
|
||||||
|
project.rootProject.yarn.useWorkspaces
|
||||||
|
|
||||||
|
@Suppress("EXPOSED_PARAMETER_TYPE")
|
||||||
|
override fun resolveRootProject(
|
||||||
|
rootProject: Project,
|
||||||
|
subprojects: MutableList<NpmResolver.NpmPackage>
|
||||||
|
) {
|
||||||
|
check(rootProject == rootProject.rootProject)
|
||||||
|
|
||||||
|
if (rootProject.yarn.useWorkspaces) {
|
||||||
|
yarnExec(rootProject, NpmApi.resolveOperationDescription("yarn"))
|
||||||
|
yarnLockReadTransitiveDependencies(NpmProject[rootProject].nodeWorkDir, subprojects.flatMap { it.npmDependencies })
|
||||||
|
} else {
|
||||||
|
if (subprojects.any { it.project != rootProject }) {
|
||||||
|
// todo: proofread message
|
||||||
|
rootProject.logger.warn(
|
||||||
|
"Build contains sub projects with NPM dependencies. " +
|
||||||
|
"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" +
|
||||||
|
"nodeJs { manageNodeModules = true } \n" +
|
||||||
|
"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."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cleanProject(project: Project) {
|
||||||
|
super.cleanProject(project)
|
||||||
|
project.packageJsonHashFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.io.File
|
||||||
|
|
||||||
|
internal data class YarnEnv(
|
||||||
|
val downloadUrl: String,
|
||||||
|
val home: File
|
||||||
|
)
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.google.gson.stream.JsonReader
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.npm.PackageJson
|
||||||
|
import java.io.File
|
||||||
|
import java.io.StringReader
|
||||||
|
|
||||||
|
data class YarnLock(val entries: List<Entry>) {
|
||||||
|
data class Entry(
|
||||||
|
val key: String,
|
||||||
|
val version: String?,
|
||||||
|
val resolved: String?,
|
||||||
|
val integrity: String?,
|
||||||
|
val dependencies: List<Dependency>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Dependency(val key: String, val version: String?) {
|
||||||
|
val scopedName: PackageJson.ScopedName
|
||||||
|
get() = PackageJson.scopedName(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun key(packageKey: String, version: String) = "$packageKey@$version"
|
||||||
|
|
||||||
|
private class Node(val parent: Node?, val value: String? = null) {
|
||||||
|
var indent: String? = null
|
||||||
|
val children = mutableMapOf<String, Node>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parse(yarnLock: File): YarnLock {
|
||||||
|
val root = parseTree(yarnLock)
|
||||||
|
|
||||||
|
return YarnLock(root.children.map {
|
||||||
|
val values = it.value.children
|
||||||
|
Entry(
|
||||||
|
it.key,
|
||||||
|
values["version"]?.value,
|
||||||
|
values["resolved"]?.value,
|
||||||
|
values["integrity"]?.value,
|
||||||
|
values["dependencies"]?.children?.map { dep ->
|
||||||
|
Dependency(dep.key, dep.value.value)
|
||||||
|
} ?: listOf()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseTree(yarnLock: File): Node {
|
||||||
|
val root = Node(null)
|
||||||
|
var parent = root
|
||||||
|
var onNewLevel = true
|
||||||
|
yarnLock.useLines {
|
||||||
|
it.forEach { line ->
|
||||||
|
if (!line.startsWith("#")) {
|
||||||
|
var i = 0
|
||||||
|
while (i < line.length && line[i].isWhitespace()) i++
|
||||||
|
val indentPos = i
|
||||||
|
val indent = line.substring(0, indentPos)
|
||||||
|
|
||||||
|
var key = false
|
||||||
|
val line1: String = if (line.endsWith(":")) {
|
||||||
|
key = true
|
||||||
|
line.removeSuffix(":")
|
||||||
|
} else line
|
||||||
|
|
||||||
|
if (line1.isNotEmpty()) {
|
||||||
|
val values = line1.substring(indentPos).split(" ").map { value ->
|
||||||
|
val value1 = value.removeSuffix(",")
|
||||||
|
if (value1.startsWith("\""))
|
||||||
|
try {
|
||||||
|
JsonReader(StringReader(value1)).nextString()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
value1.removePrefix("\"").removeSuffix("\"")
|
||||||
|
}
|
||||||
|
else value1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onNewLevel) {
|
||||||
|
parent.indent = indent
|
||||||
|
onNewLevel = false
|
||||||
|
} else {
|
||||||
|
while (indent != parent.indent!!) {
|
||||||
|
parent = parent.parent!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val child = Node(parent, if (values.size > 1) values[1] else null)
|
||||||
|
if (key) {
|
||||||
|
values.forEach {
|
||||||
|
parent.children[it] = child
|
||||||
|
}
|
||||||
|
parent = child
|
||||||
|
onNewLevel = true
|
||||||
|
} else {
|
||||||
|
parent.children[values[0]] = child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
|
||||||
|
|
||||||
|
open class YarnPlugin : Plugin<Project> {
|
||||||
|
override fun apply(project: Project): Unit = project.run {
|
||||||
|
check(project == project.rootProject) {
|
||||||
|
"YarnPlugin can be applied only to root project"
|
||||||
|
}
|
||||||
|
|
||||||
|
val nodeJs = NodeJsPlugin.apply(this)
|
||||||
|
|
||||||
|
this.extensions.create(YarnRootExtension.YARN, YarnRootExtension::class.java, this)
|
||||||
|
tasks.create(YarnSetupTask.NAME, YarnSetupTask::class.java) {
|
||||||
|
it.dependsOn(nodeJs.nodeJsSetupTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun apply(project: Project): YarnRootExtension {
|
||||||
|
val rootProject = project.rootProject
|
||||||
|
rootProject.plugins.apply(YarnPlugin::class.java)
|
||||||
|
return rootProject.extensions.getByName(YarnRootExtension.YARN) as YarnRootExtension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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.Project
|
||||||
|
import org.jetbrains.kotlin.gradle.logging.kotlinInfo
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
|
||||||
|
|
||||||
|
open class YarnRootExtension(val project: Project) {
|
||||||
|
private val gradleHome = project.gradle.gradleUserHomeDir.also {
|
||||||
|
project.logger.kotlinInfo("Storing cached files in $it")
|
||||||
|
}
|
||||||
|
|
||||||
|
var installationDir = gradleHome.resolve("yarn")
|
||||||
|
|
||||||
|
var downloadBaseUrl = "https://github.com/yarnpkg/yarn/releases/download"
|
||||||
|
var version = "1.15.2"
|
||||||
|
|
||||||
|
val yarnSetupTask: YarnSetupTask
|
||||||
|
get() = project.tasks.getByName(YarnSetupTask.NAME) as YarnSetupTask
|
||||||
|
|
||||||
|
var disableWorkspaces: Boolean = false
|
||||||
|
|
||||||
|
val useWorkspaces: Boolean
|
||||||
|
get() = project.nodeJs.root.manageNodeModules && !disableWorkspaces
|
||||||
|
|
||||||
|
internal fun executeSetup() {
|
||||||
|
NodeJsPlugin.apply(project).executeSetup()
|
||||||
|
|
||||||
|
val env = environment
|
||||||
|
if (!env.home.isDirectory) {
|
||||||
|
yarnSetupTask.setup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val environment
|
||||||
|
get() = YarnEnv(
|
||||||
|
downloadUrl = "$downloadBaseUrl/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): YarnRootExtension {
|
||||||
|
val rootProject = project.rootProject
|
||||||
|
rootProject.plugins.apply(YarnPlugin::class.java)
|
||||||
|
return rootProject.extensions.getByName(YARN) as YarnRootExtension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val Project.yarn: YarnRootExtension
|
||||||
|
get() = YarnRootExtension[this]
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* 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 de.undercouch.gradle.tasks.download.DownloadAction
|
||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.tasks.Input
|
||||||
|
import org.gradle.api.tasks.OutputDirectory
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
|
||||||
|
import java.io.File
|
||||||
|
import javax.xml.ws.Action
|
||||||
|
|
||||||
|
open class YarnSetupTask : DefaultTask() {
|
||||||
|
private val settings = project.yarn
|
||||||
|
private val env by lazy { settings.environment }
|
||||||
|
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
val downloadUrl
|
||||||
|
@Input get() = env.downloadUrl
|
||||||
|
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
val destination: File
|
||||||
|
@OutputDirectory get() = env.home
|
||||||
|
|
||||||
|
init {
|
||||||
|
group = NodeJsRootExtension.NODE_JS
|
||||||
|
description = "Download and install a local yarn version."
|
||||||
|
|
||||||
|
onlyIf {
|
||||||
|
!settings.installationDir.exists()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Action
|
||||||
|
fun setup() {
|
||||||
|
val dir = temporaryDir
|
||||||
|
val tar = download(dir)
|
||||||
|
extract(tar, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun download(tar: File): File {
|
||||||
|
val action = DownloadAction(project)
|
||||||
|
action.src(downloadUrl)
|
||||||
|
action.dest(tar)
|
||||||
|
action.execute()
|
||||||
|
return action.outputFiles.singleOrNull() ?: error("Cannot get downloaded file $downloadUrl")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extract(archive: File, destination: File) {
|
||||||
|
val dirInTar = archive.name.removeSuffix(".tar.gz") + File.pathSeparator
|
||||||
|
project.copy {
|
||||||
|
it.from(project.tarTree(archive))
|
||||||
|
it.into(destination)
|
||||||
|
it.eachFile { fileCopy ->
|
||||||
|
fileCopy.path = fileCopy.path.removePrefix(dirInTar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val NAME: String = "yarnSetup"
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* 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.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
class GradleNodeModuleBuilderKtTest {
|
||||||
|
@Test
|
||||||
|
fun fixSemver() {
|
||||||
|
assertEquals("1.3.0-SNAPSHOT", fixSemver("1.3-SNAPSHOT"))
|
||||||
|
assertEquals("1.2.3-RC1-1234", fixSemver("1.2.3-RC1-1234"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user