From a8799f5a8da25cf2c79479294f600d28d3746d99 Mon Sep 17 00:00:00 2001 From: Sergey Rostov Date: Tue, 9 Apr 2019 10:31:01 +0300 Subject: [PATCH] Gradle: npm integration using yarn #KT-31018 Fixed --- .../gradle/plugin/HasKotlinDependencies.kt | 3 + .../kotlin-gradle-plugin/build.gradle.kts | 1 + .../gradle/internal/ProcessedFilesCache.kt | 131 +++++++++++ .../mpp/DefaultKotlinDependencyHandler.kt | 7 + .../KotlinJsCompilationTestsConfigurator.kt | 72 +----- .../targets/js/KotlinJsTargetConfigurator.kt | 17 +- .../internal/RewriteSourceMapFilterReader.kt | 6 +- .../gradle/targets/js/nodejs/NodeJsEnv.kt | 16 +- .../targets/js/nodejs/NodeJsExtension.kt | 73 ++---- .../targets/js/nodejs/NodeJsPlatform.kt | 2 +- .../gradle/targets/js/nodejs/NodeJsPlugin.kt | 48 ++-- .../targets/js/nodejs/NodeJsRootExtension.kt | 71 ++++++ .../targets/js/nodejs/NodeJsSetupTask.kt | 27 ++- .../kotlin/gradle/targets/js/nodejs/utils.kt | 19 ++ .../gradle/targets/js/npm/GradleNodeModule.kt | 10 + .../targets/js/npm/GradleNodeModuleBuilder.kt | 120 ++++++++++ .../targets/js/npm/GradleNodeModules.kt | 118 ++++++++++ .../js/npm/NodeModulesGradleComponents.kt | 148 ++++++++++++ .../kotlin/gradle/targets/js/npm/Npm.kt | 19 ++ .../kotlin/gradle/targets/js/npm/NpmApi.kt | 41 ++++ .../gradle/targets/js/npm/NpmDependency.kt | 118 ++++++++++ .../gradle/targets/js/npm/NpmProject.kt | 93 ++++++++ .../gradle/targets/js/npm/NpmResolver.kt | 218 ++++++++++++++++++ .../gradle/targets/js/npm/PackageJson.kt | 43 ++++ .../js/tasks/KotlinJsNodeModulesTask.kt | 4 +- .../gradle/targets/js/testing/KotlinJsTest.kt | 79 +++++++ .../targets/js/webpack/KotlinWebpack.kt | 179 ++++++++++++++ .../kotlin/gradle/targets/js/yarn/Yarn.kt | 153 ++++++++++++ .../kotlin/gradle/targets/js/yarn/YarnEnv.kt | 13 ++ .../gradle/targets/js/yarn/YarnExtension.kt | 46 ++++ .../kotlin/gradle/targets/js/yarn/YarnLock.kt | 108 +++++++++ .../gradle/targets/js/yarn/YarnPlugin.kt | 33 +++ .../targets/js/yarn/YarnRootExtension.kt | 58 +++++ .../gradle/targets/js/yarn/YarnSetupTask.kt | 66 ++++++ .../js/npm/GradleNodeModuleBuilderKtTest.kt | 18 ++ 35 files changed, 2019 insertions(+), 159 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/ProcessedFilesCache.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/utils.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModule.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModuleBuilder.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModules.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NodeModulesGradleComponents.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/Npm.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmApi.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmDependency.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmProject.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmResolver.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/PackageJson.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/KotlinJsTest.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpack.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/Yarn.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnEnv.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnExtension.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnLock.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnPlugin.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnRootExtension.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnSetupTask.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModuleBuilderKtTest.kt diff --git a/libraries/tools/kotlin-gradle-plugin-api/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/HasKotlinDependencies.kt b/libraries/tools/kotlin-gradle-plugin-api/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/HasKotlinDependencies.kt index 1aa1f7451f9..ee556808403 100644 --- a/libraries/tools/kotlin-gradle-plugin-api/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/HasKotlinDependencies.kt +++ b/libraries/tools/kotlin-gradle-plugin-api/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/HasKotlinDependencies.kt @@ -43,6 +43,9 @@ interface KotlinDependencyHandler { project(listOf("path", "configuration").zip(listOfNotNull(path, configuration)).toMap()) fun project(notation: Map): ProjectDependency + + fun npm(packageName: String, version: String = "*"): Dependency? + fun npm(org: String, packageName: String, version: String = "*"): Dependency? } interface HasKotlinDependencies { diff --git a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts index 6fff2a763f8..ee7ea55ac2e 100644 --- a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts @@ -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") diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/ProcessedFilesCache.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/ProcessedFilesCache.kt new file mode 100644 index 00000000000..5192e820203 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/ProcessedFilesCache.kt @@ -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 = 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 + get() = new.visited.mapNotNullTo(mutableSetOf()) { it.value.target } + + private fun getDeletedTargets(): MutableSet { + val result = mutableSetOf() + + 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) + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/DefaultKotlinDependencyHandler.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/DefaultKotlinDependencyHandler.kt index bb4eedfca09..982e7d87438 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/DefaultKotlinDependencyHandler.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/DefaultKotlinDependencyHandler.kt @@ -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) } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsCompilationTestsConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsCompilationTestsConfigurator.kt index edd0500cbdc..67bf4c9d4e5 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsCompilationTestsConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsCompilationTestsConfigurator.kt @@ -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 - } - } - } } } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetConfigurator.kt index d6b59ab6746..32ac54c2894 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetConfigurator.kt @@ -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) { + 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 diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/internal/RewriteSourceMapFilterReader.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/internal/RewriteSourceMapFilterReader.kt index f1b2e00a4ab..728ef0f3c31 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/internal/RewriteSourceMapFilterReader.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/internal/RewriteSourceMapFilterReader.kt @@ -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() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsEnv.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsEnv.kt index 223f097d5ac..78f65df4ecc 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsEnv.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsEnv.kt @@ -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" diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsExtension.kt index c40c04ed88a..4479d8a7b1b 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsExtension.kt @@ -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 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] \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsPlatform.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsPlatform.kt index 8d25659b6b0..43a33bd2fc2 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsPlatform.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsPlatform.kt @@ -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() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsPlugin.kt index 02cbced2834..6315cdc6a77 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsPlugin.kt @@ -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 { 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 } } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt new file mode 100644 index 00000000000..eeb0d348ce0 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt @@ -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) + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsSetupTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsSetupTask.kt index 5c32e72670c..76315f038ad 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsSetupTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsSetupTask.kt @@ -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 - @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) } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/utils.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/utils.kt new file mode 100644 index 00000000000..4b1bf71b955 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/utils.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModule.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModule.kt new file mode 100644 index 00000000000..37f2d921624 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModule.kt @@ -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) \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModuleBuilder.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModuleBuilder.kt new file mode 100644 index 00000000000..a0e1da4c50a --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModuleBuilder.kt @@ -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, + val cache: GradleNodeModules +) { + var srcPackageJsonFile: File? = null + val files = mutableListOf() + + 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 +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModules.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModules.kt new file mode 100644 index 00000000000..0a5ac2d17ce --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModules.kt @@ -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() + private val doLast = mutableListOf<() -> Unit>() + + val modules + get() = cache.targets.map { + GradleNodeModule(it, dir.resolve(it)) + } + + fun visitCompilation(compilation: KotlinCompilation) { + 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, + dependency: ResolvedDependency + ) { + if (!visited.add(dependency)) return + + visitArtifacts(dependency, dependency.moduleArtifacts) + + dependency.children.forEach { + visitDependency(compilation, it) + } + } + + private fun visitArtifacts( + dependency: ResolvedDependency, + artifacts: MutableSet + ) { + 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() + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NodeModulesGradleComponents.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NodeModulesGradleComponents.kt new file mode 100644 index 00000000000..f35d0def675 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NodeModulesGradleComponents.kt @@ -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 = mutableMapOf() + } + + private class Element { + val fileNames: MutableList = mutableListOf() + + @Transient + val files = mutableListOf() + } + + 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() + + 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, + project: Project, + transitiveDependencies: MutableList + ): List { + 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 + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/Npm.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/Npm.kt new file mode 100644 index 00000000000..0c4dc97e023 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/Npm.kt @@ -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. + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmApi.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmApi.kt new file mode 100644 index 00000000000..0e30e1b7f10 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmApi.kt @@ -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 + ) + + @Suppress("EXPOSED_PARAMETER_TYPE") + fun hookRootPackage(rootProject: Project, rootPackageJson: PackageJson, allWorkspaces: Collection) { + + } + + 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" + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmDependency.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmDependency.kt new file mode 100644 index 00000000000..5bdc23f800b --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmDependency.kt @@ -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() + + override fun resolve(): MutableSet { + resolveProject() ?: return mutableSetOf() + + val all = mutableSetOf() + val visited = mutableSetOf() + + 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 = 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 +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmProject.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmProject.kt new file mode 100644 index 00000000000..b899d196000 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmProject.kt @@ -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] \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmResolver.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmResolver.kt new file mode 100644 index 00000000000..207997c5f12 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmResolver.kt @@ -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() + 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) { + val dependencies by lazy { + npmPackages.flatMapTo(mutableSetOf()) { it.npmDependencies } + } + } + + class NpmPackage( + val project: Project, + val packageJson: PackageJson, + val npmDependencies: Set + ) { + 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() + + 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, + 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, + 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) + } + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/PackageJson.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/PackageJson.kt new file mode 100644 index 00000000000..7e4f64781ea --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/PackageJson.kt @@ -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? = null + + val devDependencies = mutableMapOf() + val dependencies = mutableMapOf() + + 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" + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinJsNodeModulesTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinJsNodeModulesTask.kt index b0e1d72fcc0..cd2f70fb5ed 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinJsNodeModulesTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinJsNodeModulesTask.kt @@ -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") } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/KotlinJsTest.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/KotlinJsTest.kt new file mode 100644 index 00000000000..436a6bca7e1 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/KotlinJsTest.kt @@ -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 = 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 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() + + if (debug) { + nodeJsArgs.add("--inspect-brk") + } + + return testFramework!!.createTestExecutionSpec(this, forkOptions, nodeJsArgs) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpack.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpack.kt new file mode 100644 index 00000000000..1eaeb9fa0db --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpack.kt @@ -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("webpack") { + val compileKotlinTask = compilation.compileKotlinTask + compileKotlinTask as Kotlin2JsCompile + + it.dependsOn(compileKotlinTask) + + it.entry = npmProject.moduleOutput(compileKotlinTask) + } + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/Yarn.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/Yarn.kt new file mode 100644 index 00000000000..f1fc285e65f --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/Yarn.kt @@ -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 + ) { + val yarnLock = nodeWorkDir.resolve("yarn.lock") + if (yarnLock.isFile) { + val byKey = YarnLock.parse(yarnLock).entries.associateBy { it.key } + val visited = mutableSetOf() + + 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 + ): 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 + ) { + 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() + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnEnv.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnEnv.kt new file mode 100644 index 00000000000..991eddca7a4 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnEnv.kt @@ -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 +) diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnExtension.kt new file mode 100644 index 00000000000..b93fb4d4046 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnExtension.kt @@ -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") + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnLock.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnLock.kt new file mode 100644 index 00000000000..f52935277a4 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnLock.kt @@ -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) { + data class Entry( + val key: String, + val version: String?, + val resolved: String?, + val integrity: String?, + val dependencies: List + ) + + 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() + } + + 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 + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnPlugin.kt new file mode 100644 index 00000000000..a36976edea2 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnPlugin.kt @@ -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 { + 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 + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnRootExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnRootExtension.kt new file mode 100644 index 00000000000..f1b2490be7a --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnRootExtension.kt @@ -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] \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnSetupTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnSetupTask.kt new file mode 100644 index 00000000000..8d7b989ba98 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnSetupTask.kt @@ -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" + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModuleBuilderKtTest.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModuleBuilderKtTest.kt new file mode 100644 index 00000000000..58799fb48dc --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModuleBuilderKtTest.kt @@ -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")) + } +} \ No newline at end of file