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())
|
||||
|
||||
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 {
|
||||
|
||||
@@ -46,6 +46,7 @@ dependencies {
|
||||
compileOnly(project(":kotlin-scripting-compiler-impl"))
|
||||
|
||||
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-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.jetbrains.kotlin.gradle.plugin.HasKotlinDependencies
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
|
||||
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmDependency
|
||||
|
||||
class DefaultKotlinDependencyHandler(
|
||||
val parent: HasKotlinDependencies,
|
||||
@@ -77,4 +78,10 @@ class DefaultKotlinDependencyHandler(
|
||||
configure(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
|
||||
|
||||
import org.gradle.api.file.FileCollection
|
||||
import org.gradle.language.base.plugins.LifecycleBasePlugin
|
||||
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.NodeJsSetupTask
|
||||
import org.jetbrains.kotlin.gradle.targets.js.tasks.*
|
||||
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
|
||||
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
|
||||
import org.jetbrains.kotlin.gradle.tasks.registerTask
|
||||
import org.jetbrains.kotlin.gradle.testing.internal.configureConventions
|
||||
@@ -36,81 +33,28 @@ internal class KotlinJsCompilationTestsConfigurator(
|
||||
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
|
||||
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() {
|
||||
compilation.dependencies {
|
||||
implementation(kotlin("test-nodejs-runner"))
|
||||
}
|
||||
// apply plugin (cannot do it lazy)
|
||||
val nodeJs = NodeJsPlugin.apply(target.project)
|
||||
|
||||
val nodeModulesTask = registerTask(
|
||||
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 ->
|
||||
registerTask(project, testTaskName, KotlinJsTest::class.java) { testJs ->
|
||||
testJs.group = LifecycleBasePlugin.VERIFICATION_GROUP
|
||||
|
||||
testJs.dependsOn(nodeModulesTask.getTaskOrProvider())
|
||||
testJs.dependsOn(compileTestKotlin2Js, nodeJs.nodeJsSetupTask)
|
||||
|
||||
testJs.onlyIf {
|
||||
compileTestKotlin2Js.outputFile.exists()
|
||||
}
|
||||
|
||||
if (disambiguationClassifier != null) {
|
||||
testJs.targetName = disambiguationClassifier
|
||||
testJs.showTestTargetName = true
|
||||
}
|
||||
|
||||
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.runtimeDependencyHandler = compilation
|
||||
testJs.targetName = disambiguationClassifier
|
||||
testJs.nodeModulesToLoad.add(compileTestKotlin2Js.outputFile.name)
|
||||
|
||||
testJs.configureConventions()
|
||||
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.mpp.KotlinJsCompilation
|
||||
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
|
||||
|
||||
class KotlinJsTargetConfigurator(kotlinPluginVersion: String) :
|
||||
@@ -23,13 +25,26 @@ class KotlinJsTargetConfigurator(kotlinPluginVersion: String) :
|
||||
|
||||
platformTarget.compilations.all {
|
||||
it.compileKotlinTask.kotlinOptions {
|
||||
moduleKind = "umd"
|
||||
moduleKind = "commonjs"
|
||||
sourceMap = true
|
||||
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 {
|
||||
internal fun isTestCompilation(it: KotlinJsCompilation) =
|
||||
it.name == KotlinCompilation.TEST_COMPILATION_NAME
|
||||
|
||||
+5
-1
@@ -142,7 +142,11 @@ open class RewriteSourceMapFilterReader(
|
||||
}
|
||||
|
||||
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 {
|
||||
maybeReadFirst()
|
||||
|
||||
+8
-8
@@ -1,16 +1,16 @@
|
||||
package org.jetbrains.kotlin.gradle.targets.js.nodejs
|
||||
|
||||
import java.io.*
|
||||
import java.io.File
|
||||
|
||||
internal data class NodeJsEnv(
|
||||
val nodeDir: File,
|
||||
val nodeBinDir: File,
|
||||
val nodeExec: String,
|
||||
val npmExec: String,
|
||||
val nodeDir: File,
|
||||
val nodeBinDir: File,
|
||||
val nodeExecutable: String,
|
||||
|
||||
val platformName: String,
|
||||
val architectureName: String,
|
||||
val ivyDependency: String
|
||||
|
||||
val platformName: String,
|
||||
val architectureName: String,
|
||||
val ivyDependency: String
|
||||
) {
|
||||
val isWindows: Boolean
|
||||
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
|
||||
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.Project
|
||||
import org.jetbrains.kotlin.gradle.logging.kotlinInfo
|
||||
import java.io.File
|
||||
import org.jetbrains.kotlin.gradle.targets.js.npm.PackageJson
|
||||
|
||||
open class NodeJsExtension(project: Project) {
|
||||
private val gradleHome = project.gradle.gradleUserHomeDir.also {
|
||||
project.logger.kotlinInfo("Storing cached files in $it")
|
||||
}
|
||||
open class NodeJsExtension(val project: Project) {
|
||||
val root: NodeJsRootExtension
|
||||
get() = this as? NodeJsRootExtension ?: NodeJsRootExtension[project]
|
||||
|
||||
var installationDir = gradleHome.resolve("nodejs")
|
||||
internal val packageJsonHandlers = mutableListOf<PackageJson.() -> Unit>()
|
||||
|
||||
var distBaseUrl = "https://nodejs.org/dist"
|
||||
var version = "10.15.3"
|
||||
var npmVersion = ""
|
||||
|
||||
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()
|
||||
)
|
||||
@Suppress("unused")
|
||||
fun packageJson(handler: PackageJson.() -> Unit) {
|
||||
packageJsonHandlers.add(handler)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NODE_JS: String = "nodeJs"
|
||||
|
||||
operator fun get(project: Project): NodeJsExtension {
|
||||
val extension = project.extensions.findByType(NodeJsExtension::class.java)
|
||||
if (extension != null)
|
||||
return extension
|
||||
|
||||
val parentProject = project.parent
|
||||
if (parentProject != null)
|
||||
return get(parentProject)
|
||||
|
||||
throw GradleException("NodeJsExtension is not installed")
|
||||
NodeJsPlugin.apply(project.rootProject)
|
||||
return project.extensions.getByName(NodeJsRootExtension.NODE_JS) as NodeJsExtension
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
* See [NodeJsEnv.ivyDependency] that is filled in [NodeJsExtension.buildEnv].
|
||||
* See [NodeJsEnv.ivyDependency] that is filled in [NodeJsRootExtension.environment].
|
||||
*/
|
||||
internal object NodeJsPlatform {
|
||||
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.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> {
|
||||
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)
|
||||
|
||||
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 {
|
||||
fun ensureAppliedInHierarchy(myProject: Project): Project =
|
||||
findInHeirarchy(myProject) ?: myProject.also {
|
||||
it.pluginManager.apply(NodeJsPlugin::class.java)
|
||||
}
|
||||
|
||||
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
|
||||
fun apply(project: Project): NodeJsRootExtension {
|
||||
val rootProject = project.rootProject
|
||||
rootProject.plugins.apply(NodeJsPlugin::class.java)
|
||||
return rootProject.extensions.getByName(NODE_JS) as NodeJsRootExtension
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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
|
||||
|
||||
open class NodeJsSetupTask : DefaultTask() {
|
||||
private val settings = NodeJsExtension[project]
|
||||
private val env by lazy { settings.buildEnv() }
|
||||
private val settings = project.nodeJs.root
|
||||
private val env by lazy { settings.environment }
|
||||
|
||||
init {
|
||||
group = NodeJsExtension.NODE_JS
|
||||
group = NodeJsRootExtension.NODE_JS
|
||||
description = "Download and install a local node/npm version."
|
||||
}
|
||||
|
||||
val input: Set<String>
|
||||
@Input get() = setOf(settings.download.toString(), env.ivyDependency)
|
||||
val ivyDependency: String
|
||||
@Input get() = env.ivyDependency
|
||||
|
||||
val destination: File
|
||||
@OutputDirectory get() = env.nodeDir
|
||||
|
||||
init {
|
||||
onlyIf {
|
||||
settings.download && !env.nodeBinDir.isDirectory
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@TaskAction
|
||||
fun exec() {
|
||||
if (!settings.download)
|
||||
return
|
||||
|
||||
@Suppress("UnstableApiUsage", "DEPRECATION")
|
||||
val repo = project.repositories.ivy { repo ->
|
||||
repo.name = "Node Distributions at ${settings.distBaseUrl}"
|
||||
repo.url = URI(settings.distBaseUrl)
|
||||
repo.name = "Node Distributions at ${settings.nodeDownloadBaseUrl}"
|
||||
repo.url = URI(settings.nodeDownloadBaseUrl)
|
||||
|
||||
if (isGradleVersionAtLeast(5, 0)) {
|
||||
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)
|
||||
conf.isTransitive = false
|
||||
val result = conf.resolve().single()
|
||||
@@ -63,7 +66,7 @@ open class NodeJsSetupTask : DefaultTask() {
|
||||
unpackNodeArchive(result, destination.parentFile) // parent because archive contains name already
|
||||
|
||||
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"))
|
||||
|
||||
internal fun isKotlinJsRuntimeFile(file: File): Boolean {
|
||||
if (!file.isFile) return false
|
||||
val name = file.name
|
||||
return (name.endsWith(".js") && !name.endsWith(".meta.js"))
|
||||
|| name.endsWith(".js.map")
|
||||
|| 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