Gradle, js: create npm packages per compilation

#KT-30530
This commit is contained in:
Sergey Rostov
2019-05-15 22:07:04 +03:00
parent b195d18d2d
commit 5d8f2acf5f
32 changed files with 580 additions and 526 deletions
@@ -3,13 +3,16 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
package org.jetbrains.kotlin.gradle.targets.js.npm package org.jetbrains.kotlin.gradle.targets.js
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.ExternalModuleDependency import org.jetbrains.kotlin.gradle.targets.js.npm.NpmDependency
class NpmPackages { /**
* Package versions used by tasks
*/
class NpmVersions {
val webpack = NpmPackageVersion("webpack", "4.29.6") val webpack = NpmPackageVersion("webpack", "4.29.6")
val webpackCli = NpmPackageVersion("webpack-cli", "3.3.0") val webpackCli = NpmPackageVersion("webpack-cli", "3.3.0")
val webpackBundleAnalyzer = NpmPackageVersion("webpack-bundle-analyzer", "3.3.2") val webpackBundleAnalyzer = NpmPackageVersion("webpack-bundle-analyzer", "3.3.2")
@@ -47,10 +50,11 @@ interface RequiredKotlinJsDependency {
} }
data class NpmPackageVersion(val name: String, var version: String) : RequiredKotlinJsDependency { data class NpmPackageVersion(val name: String, var version: String) : RequiredKotlinJsDependency {
override fun createDependency(project: Project) = NpmDependency(project, null, name, version) override fun createDependency(project: Project): Dependency =
NpmDependency(project, null, name, version)
} }
data class KotlinGradleNpmPackage(val simpleModuleName: String): RequiredKotlinJsDependency { data class KotlinGradleNpmPackage(val simpleModuleName: String) : RequiredKotlinJsDependency {
override fun createDependency(project: Project): Dependency = override fun createDependency(project: Project): Dependency =
project.dependencies.create("org.jetbrains.kotlin:kotlin-$simpleModuleName") project.dependencies.create("org.jetbrains.kotlin:kotlin-$simpleModuleName")
} }
@@ -6,7 +6,7 @@
package org.jetbrains.kotlin.gradle.targets.js.nodejs package org.jetbrains.kotlin.gradle.targets.js.nodejs
import org.gradle.api.Project import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmPackages import org.jetbrains.kotlin.gradle.targets.js.NpmVersions
import org.jetbrains.kotlin.gradle.targets.js.npm.PackageJson import org.jetbrains.kotlin.gradle.targets.js.npm.PackageJson
open class NodeJsExtension(val project: Project) { open class NodeJsExtension(val project: Project) {
@@ -15,7 +15,7 @@ open class NodeJsExtension(val project: Project) {
internal val packageJsonHandlers = mutableListOf<PackageJson.() -> Unit>() internal val packageJsonHandlers = mutableListOf<PackageJson.() -> Unit>()
val versions = NpmPackages() val versions = NpmVersions()
@Suppress("unused") @Suppress("unused")
fun packageJson(handler: PackageJson.() -> Unit) { fun packageJson(handler: PackageJson.() -> Unit) {
@@ -6,7 +6,6 @@ import org.gradle.api.plugins.BasePlugin
import org.gradle.api.tasks.Delete import org.gradle.api.tasks.Delete
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension.Companion.NODE_JS import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension.Companion.NODE_JS
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolveTask import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolveTask
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
open class NodeJsPlugin : Plugin<Project> { open class NodeJsPlugin : Plugin<Project> {
override fun apply(project: Project): Unit = project.run { override fun apply(project: Project): Unit = project.run {
@@ -33,25 +32,15 @@ open class NodeJsPlugin : Plugin<Project> {
private fun setupCleanNodeModulesTask(project: Project) { private fun setupCleanNodeModulesTask(project: Project) {
project.tasks.create("cleanNodeModules", Delete::class.java) { project.tasks.create("cleanNodeModules", Delete::class.java) {
it.description = "Deletes node_modules and package.json file" it.description = "Deletes nodeJs projects created during build"
it.group = BasePlugin.BUILD_GROUP it.group = BasePlugin.BUILD_GROUP
it.delete.add(project.nodeJs.root.rootPackageDir)
it.doLast {
project.nodeJs.root.packageManager.cleanProject(project)
}
project.tasks.maybeCreate(BasePlugin.CLEAN_TASK_NAME).dependsOn(it)
} }
project.tasks.create("cleanGradleNodeModules", Delete::class.java) { project.tasks.create("cleanGradleNodeModules", Delete::class.java) {
it.description = "Deletes node_modules_gradle" it.description = "Deletes node modules imported from gradle external modules"
it.group = BasePlugin.BUILD_GROUP it.group = BasePlugin.BUILD_GROUP
it.delete.add(project.nodeJs.root.nodeModulesGradleCacheDir)
it.doLast {
project.npmProject.gradleNodeModulesDir.deleteRecursively()
}
project.tasks.maybeCreate(BasePlugin.CLEAN_TASK_NAME).dependsOn(it)
} }
} }
@@ -2,12 +2,9 @@ package org.jetbrains.kotlin.gradle.targets.js.nodejs
import org.gradle.api.Project import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.logging.kotlinInfo import org.jetbrains.kotlin.gradle.logging.kotlinInfo
import org.jetbrains.kotlin.gradle.plugin.TaskHolder
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProjectLayout
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApi import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApi
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolveTask import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolveTask
import org.jetbrains.kotlin.gradle.targets.js.yarn.Yarn import org.jetbrains.kotlin.gradle.targets.js.yarn.Yarn
import org.jetbrains.kotlin.gradle.tasks.locateOrRegisterTask
import java.io.File import java.io.File
open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) { open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) {
@@ -23,7 +20,6 @@ open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) {
var nodeCommand = "node" var nodeCommand = "node"
var layout: NpmProjectLayout = NpmProjectLayout.ROOT_PROJECT_BUILD_DIR
var packageManager: NpmApi = Yarn var packageManager: NpmApi = Yarn
val nodeJsSetupTask: NodeJsSetupTask val nodeJsSetupTask: NodeJsSetupTask
@@ -32,6 +28,15 @@ open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) {
val npmResolveTask: NpmResolveTask val npmResolveTask: NpmResolveTask
get() = project.tasks.getByName(NpmResolveTask.NAME) as NpmResolveTask get() = project.tasks.getByName(NpmResolveTask.NAME) as NpmResolveTask
val nodeModulesGradleCacheDir: File
get() = project.buildDir.resolve("node_modules_gradle")
val rootPackageDir: File
get() = project.buildDir.resolve("js")
val projectPackagesDir: File
get() = rootPackageDir.resolve("packages")
internal val environment: NodeJsEnv internal val environment: NodeJsEnv
get() { get() {
val platform = NodeJsPlatform.name val platform = NodeJsPlatform.name
@@ -7,4 +7,7 @@ package org.jetbrains.kotlin.gradle.targets.js.npm
import java.io.File import java.io.File
/**
* Fake NodeJS module directory created from Gradle external module
*/
class GradleNodeModule(val name: String, val path: File) class GradleNodeModule(val name: String, val path: File)
@@ -12,6 +12,9 @@ import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.artifacts.ResolvedDependency import org.gradle.api.artifacts.ResolvedDependency
import java.io.File import java.io.File
/**
* Creates fake NodeJS module directory from given gradle [dependency].
*/
internal class GradleNodeModuleBuilder( internal class GradleNodeModuleBuilder(
val project: Project, val project: Project,
val dependency: ResolvedDependency, val dependency: ResolvedDependency,
@@ -11,13 +11,17 @@ import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.artifacts.ResolvedDependency import org.gradle.api.artifacts.ResolvedDependency
import org.gradle.api.artifacts.component.ProjectComponentIdentifier import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.jetbrains.kotlin.gradle.internal.ProcessedFilesCache import org.jetbrains.kotlin.gradle.internal.ProcessedFilesCache
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
/**
* Cache for storing already created [GradleNodeModule]s
*/
internal class GradleNodeModulesCache(val project: Project) : AutoCloseable { internal class GradleNodeModulesCache(val project: Project) : AutoCloseable {
companion object { companion object {
const val STATE_FILE_NAME = ".visited" const val STATE_FILE_NAME = ".visited"
} }
internal val dir = project.buildDir.resolve("node_modules_gradle") internal val dir = project.nodeJs.root.nodeModulesGradleCacheDir
private val cache = ProcessedFilesCache(project, dir, STATE_FILE_NAME, "5") private val cache = ProcessedFilesCache(project, dir, STATE_FILE_NAME, "5")
private val visited = mutableSetOf<ResolvedDependency>() private val visited = mutableSetOf<ResolvedDependency>()
@@ -7,36 +7,19 @@ package org.jetbrains.kotlin.gradle.targets.js.npm
import org.gradle.api.Project import org.gradle.api.Project
/**
* NodeJS package manager API
*/
interface NpmApi { interface NpmApi {
fun setup(project: Project) fun setup(project: Project)
@Suppress("EXPOSED_PARAMETER_TYPE") fun resolveProject(resolvedNpmProject: NpmProjectPackage)
fun resolveProject(npmPackage: NpmResolver.NpmPackage)
@Suppress("EXPOSED_PARAMETER_TYPE")
fun resolveRootProject( fun resolveRootProject(
rootProject: Project, rootProject: Project,
subprojects: MutableList<NpmResolver.NpmPackage> subProjects: MutableList<NpmProjectPackage>
) )
/**
* Change contents of root's project `package.json`.
* @return true if `package.json` is required even it is empty
*/
@Suppress("EXPOSED_PARAMETER_TYPE")
fun hookRootPackage(
rootProject: Project,
rootPackageJson: PackageJson,
allWorkspaces: Collection<NpmResolver.NpmPackage>
): Boolean = false
fun cleanProject(project: Project) {
val npmProject = project.npmProject
npmProject.nodeModulesDir.deleteRecursively()
npmProject.packageJsonFile.delete()
}
companion object { companion object {
fun resolveOperationDescription(packageManagerTitle: String): String = fun resolveOperationDescription(packageManagerTitle: String): String =
"Resolving NPM dependencies using $packageManagerTitle" "Resolving NPM dependencies using $packageManagerTitle"
@@ -28,12 +28,14 @@ data class NpmDependency(
SelfResolvingDependencyInternal, SelfResolvingDependencyInternal,
ResolvableDependency, ResolvableDependency,
FileCollectionDependency { FileCollectionDependency {
override fun getGroup(): String? = org override fun getGroup(): String? = org
internal val dependencies = mutableSetOf<NpmDependency>() internal val dependencies = mutableSetOf<NpmDependency>()
override fun resolve(): MutableSet<File> { override fun resolve(transitive: Boolean): MutableSet<File> {
resolveProject() ?: return mutableSetOf() val npmPackage = resolveProject() ?: return mutableSetOf()
val npmProject = npmPackage.npmProject
val all = mutableSetOf<File>() val all = mutableSetOf<File>()
val visited = mutableSetOf<NpmDependency>() val visited = mutableSetOf<NpmDependency>()
@@ -41,9 +43,16 @@ data class NpmDependency(
fun visit(item: NpmDependency) { fun visit(item: NpmDependency) {
if (item in visited) return if (item in visited) return
visited.add(item) visited.add(item)
item.project.npmProject.resolve(item.key)?.let {
npmProject.resolve(item.key)?.let {
all.add(it) all.add(it)
} }
if (transitive) {
item.dependencies.forEach {
visit(it)
}
}
} }
visit(this) visit(this)
@@ -51,45 +60,42 @@ data class NpmDependency(
return all return all
} }
private fun resolveProject(): NpmResolver.ResolvedProject? { override fun resolve(): MutableSet<File> {
val npmPackage = resolveProject() ?: return mutableSetOf()
return mutableSetOf(npmPackage.npmProject.resolve(key)!!)
}
override fun resolve(context: DependencyResolveContext) {
val npmPackage = resolveProject()
if (npmPackage != null) {
npmPackage.project.files(npmPackage.npmProject.resolve(key))
dependencies.forEach {
context.add(it)
}
}
}
private fun resolveProject(): NpmProjectPackage? {
val result = val result =
if (isInIdeaSync) NpmResolver.resolve(project) if (isInIdeaSync) NpmResolver.resolveIfNeeded(project)
else NpmResolver.getAlreadyResolvedOrNull(project) else NpmResolver.getAlreadyResolvedOrNull(project)
return when (result) { return when (result) {
null -> null null -> null
is AlreadyInProgress -> null is AlreadyInProgress -> null
is AlreadyResolved -> { is AlreadyResolved -> findIn(result.resolution) ?: error("Project hierarchy is already resolved in NPM without $this")
check(this in result.resolution.npmPackage!!.npmDependencies) { is ResolvedNow -> findIn(result.resolution) ?: error("Cannot find $this after NPM project resolve")
"Project hierarchy is already resolved in NPM without $this"
}
result.resolution
}
is ResolvedNow -> {
check(this in result.resolution.npmPackage!!.npmDependencies) {
"$this was not visited during resolution"
}
result.resolution
}
} }
} }
private fun findIn(npmProjects: NpmProjects) =
npmProjects.npmProjectsByNpmDependency[this]
val key: String = if (org == null) name else "@$org/$name" val key: String = if (org == null) name else "@$org/$name"
override fun toString() = "$key: $version" override fun toString() = "$key: $version"
override fun resolve(context: DependencyResolveContext) { override fun getFiles(): FileCollection = project.files(resolve(true))
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 getName() = name
@@ -7,41 +7,52 @@ package org.jetbrains.kotlin.gradle.targets.js.npm
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.process.ExecSpec import org.gradle.process.ExecSpec
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
import java.io.File import java.io.File
open class NpmProject( val KotlinJsCompilation.npmProject: NpmProject
val project: Project, get() = NpmProject(this)
val nodeWorkDir: File,
val searchInParents: Boolean /**
) { * Basic info for [NpmProject] created from [compilation].
* This class contains only basic info.
*
* More info can be obtained from [NpmProjectPackage], which is available after project resolution (after [NpmResolveTask] execution).
*/
open class NpmProject(val compilation: KotlinJsCompilation) {
val name: String
get() = buildNpmProjectName()
val dir: File
get() = project.nodeJs.root.projectPackagesDir.resolve(name)
val target: KotlinTarget
get() = compilation.target
val project: Project
get() = target.project
val nodeModulesDir val nodeModulesDir
get() = nodeWorkDir.resolve(NODE_MODULES) get() = dir.resolve(NODE_MODULES)
val packageJsonFile: File val packageJsonFile: File
get() = nodeWorkDir.resolve(PACKAGE_JSON) get() = dir.resolve(PACKAGE_JSON)
open val compileOutputCopyDest: File? val main: String
get() = nodeModulesDir get() = "kotlin/$name.js"
private val modules = object : NpmProjectModules(nodeWorkDir, nodeModulesDir) { private val modules = object : NpmProjectModules(dir, nodeModulesDir) {
override val parent get() = if (searchInParents) parentModules else null override val parent get() = rootNodeModules
} }
private val parentModules: NpmProjectModules? private val rootNodeModules: NpmProjectModules?
get() = project.parent?.npmProject?.modules get() = NpmProjectModules(project.nodeJs.root.rootPackageDir)
val gradleNodeModulesDir: File
get() = project.rootProject.npmProject.nodeModulesDir
open fun compileOutput(compilationTask: Kotlin2JsCompile): File {
return compileOutputCopyDest?.resolve(compilationTask.outputFile.name) ?: compilationTask.outputFile
}
fun useTool(exec: ExecSpec, tool: String, vararg args: String) { fun useTool(exec: ExecSpec, tool: String, vararg args: String) {
exec.workingDir = nodeWorkDir exec.workingDir = dir
exec.executable = project.nodeJs.root.environment.nodeExecutable exec.executable = project.nodeJs.root.environment.nodeExecutable
exec.args = listOf(require(tool)) + args exec.args = listOf(require(tool)) + args
} }
@@ -60,19 +71,32 @@ open class NpmProject(
*/ */
internal fun resolve(name: String): File? = modules.resolve(name) internal fun resolve(name: String): File? = modules.resolve(name)
override fun toString() = "NpmProject($nodeWorkDir)" private fun buildNpmProjectName(): String {
val project = target.project
val name = StringBuilder()
name.append(project.rootProject.name)
if (project != project.rootProject) {
name.append("-")
name.append(project.name)
}
if (target.name.isNotEmpty() && target.name.toLowerCase() != "js") {
name.append("-").append(target.name)
}
if (compilation.name != KotlinCompilation.MAIN_COMPILATION_NAME) {
name.append("-").append(compilation.name)
}
return name.toString()
}
override fun toString() = "NpmProject($name)"
companion object { companion object {
const val PACKAGE_JSON = "package.json" const val PACKAGE_JSON = "package.json"
const val NODE_MODULES = "node_modules" const val NODE_MODULES = "node_modules"
operator fun get(project: Project): NpmProject {
val nodeJsRootExtension = NodeJsPlugin.apply(project)
return nodeJsRootExtension.root.layout.newNpmProject(project)
}
} }
} }
val Project.npmProject
get() = NpmProject[this]
@@ -7,6 +7,9 @@ package org.jetbrains.kotlin.gradle.targets.js.npm
import org.gradle.api.Project import org.gradle.api.Project
/**
* Gradle dependencies for [NpmProject].
*/
class NpmProjectGradleDeps { class NpmProjectGradleDeps {
val internalModules = mutableSetOf<Project>() val internalModules = mutableSetOf<Project>()
val externalModules = mutableSetOf<GradleNodeModule>() val externalModules = mutableSetOf<GradleNodeModule>()
@@ -1,47 +0,0 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.targets.js.npm
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
import java.io.File
enum class NpmProjectLayout {
PROJECT_DIR {
override val allowYarnWorkspaces: Boolean
get() = true
override fun newNpmProject(project: Project) =
NpmProject(project, project.projectDir, searchInParents = true)
},
ROOT_PROJECT_BUILD_DIR {
override val allowYarnWorkspaces: Boolean
get() = true
override fun newNpmProject(project: Project): NpmProject {
val nodeJsWorldDir = project.rootProject.buildDir.resolve("nodejs-project")
val workDir =
if (project == project.rootProject) nodeJsWorldDir
else {
val projectPackage = project.path.removePrefix(":").replace(":", File.separator)
nodeJsWorldDir.resolve("packages").resolve(projectPackage)
}
return NpmProject(project, workDir, searchInParents = true)
}
},
BUILD_DIR {
override fun newNpmProject(project: Project) =
NpmProject(project, project.buildDir, searchInParents = false)
};
open val allowYarnWorkspaces: Boolean
get() = false
abstract fun newNpmProject(project: Project): NpmProject
}
@@ -11,9 +11,12 @@ import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject.Companion.NODE_MODU
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject.Companion.PACKAGE_JSON import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject.Companion.PACKAGE_JSON
import java.io.File import java.io.File
/**
* Search modules in node_modules according to https://nodejs.org/api/modules.html.
*/
internal open class NpmProjectModules( internal open class NpmProjectModules(
val dir: File, val dir: File,
val nodeModulesDir: File = dir.resolve(NODE_MODULES) private val nodeModulesDir: File = dir.resolve(NODE_MODULES)
) { ) {
/** /**
* Require [request] nodejs module and return canonical path to it's main js file. * Require [request] nodejs module and return canonical path to it's main js file.
@@ -0,0 +1,18 @@
/*
* 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.npm
import org.gradle.api.Project
/**
* [NpmProject] full package info
*/
class NpmProjectPackage(
val project: Project,
val npmProject: NpmProject,
val npmDependencies: Collection<NpmDependency>,
val packageJson: PackageJson
)
@@ -0,0 +1,164 @@
/*
* 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.npm
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.Dependency
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.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
/**
* Visits given gradle [project] for all of its [NpmProject],
* creates `package.json` for them ([NpmProjectPackage]) and runs
* selected [NodeJsRootExtension.packageManager] to download and install all of it's dependencies.
*/
internal class NpmProjectVisitor(val resolver: NpmResolver, val project: Project) {
private val npmProjects = mutableListOf<NpmProjectPackage>()
private val byCompilation = mutableMapOf<KotlinJsCompilation, NpmProjectPackage>()
private val byNpmDependency = mutableMapOf<NpmDependency, NpmProjectPackage>()
private val taskRequirements = mutableMapOf<RequiresNpmDependencies, Collection<RequiredKotlinJsDependency>>()
private val requiredFromTasksByCompilation = mutableMapOf<KotlinJsCompilation, MutableList<Dependency>>()
private fun addTaskRequirements(task: RequiresNpmDependencies) {
val requirements = task.requiredNpmDependencies.toList()
taskRequirements[task] = requirements
val requiredDependenciesList = requiredFromTasksByCompilation.getOrPut(task.compilation) { mutableListOf() }
requirements.forEach { requiredDependency ->
requiredDependenciesList.add(requiredDependency.createDependency(project))
}
}
private fun addNpmProject(resolved: NpmProjectPackage) {
val compilation = resolved.npmProject.compilation
byCompilation[compilation] = resolved
resolved.npmDependencies.forEach {
byNpmDependency[it] = resolved
}
npmProjects.add(resolved)
}
fun visitProject(): NpmProjects {
visitTasksRequiredDependencies()
val kotlin = project.kotlinExtensionOrNull
if (kotlin != null) {
when (kotlin) {
is KotlinSingleTargetExtension -> visitTarget(kotlin.target)
is KotlinMultiplatformExtension -> kotlin.targets.forEach {
visitTarget(it)
}
}
}
return NpmProjects(
project,
npmProjects,
byCompilation,
byNpmDependency,
taskRequirements
)
}
private fun visitTasksRequiredDependencies() {
project.tasks.toList().forEach { task ->
if (task.enabled && task is RequiresNpmDependencies) {
addTaskRequirements(task)
}
}
}
private fun visitTarget(target: KotlinTarget) {
if (target.platformType == KotlinPlatformType.js) {
target.compilations.toList().forEach { compilation ->
compilation as KotlinJsCompilation
visitCompilation(compilation)
}
}
}
private fun visitCompilation(compilation: KotlinJsCompilation) {
val project = compilation.target.project
val npmProject = compilation.npmProject
val name = npmProject.name
val packageJson = PackageJson(
name,
project.version.toString()
)
val npmDependencies = mutableSetOf<NpmDependency>()
val gradleDeps = NpmProjectGradleDeps()
compilation.allKotlinSourceSets.forEach { sourceSet ->
sourceSet.relatedConfigurationNames.forEach { configurationName ->
val configuration = project.configurations.getByName(configurationName)
visitConfiguration(configuration, npmDependencies, gradleDeps)
}
}
packageJson.main = npmProject.main
val requiredByTasks = requiredFromTasksByCompilation[compilation]
if (requiredByTasks != null && requiredByTasks.isNotEmpty()) {
val configuration = project.configurations.create("$name-jsTools")
requiredByTasks.forEach {
configuration.dependencies.add(it)
}
configuration.resolve()
visitConfiguration(configuration, npmDependencies, gradleDeps)
}
npmDependencies.forEach {
packageJson.dependencies[it.key] = resolver.chooseVersion(packageJson.dependencies[it.key], it.version)
}
gradleDeps.externalModules.forEach {
val relativePath = it.path.relativeTo(npmProject.dir)
packageJson.dependencies[it.name] = "file:$relativePath"
}
gradleDeps.internalModules.forEach { target ->
val resolvedTarget = resolver.findDependentResolvedNpmProject(project, target)
if (resolvedTarget != null) {
packageJson.dependencies[resolvedTarget.packageJson.name] = resolvedTarget.packageJson.version
}
}
project.nodeJs.packageJsonHandlers.forEach {
it(packageJson)
}
val npmPackage = NpmProjectPackage(project, npmProject, npmDependencies, packageJson)
npmPackage.packageJson.saveTo(npmProject.packageJsonFile)
resolver.packageManager.resolveProject(npmPackage)
addNpmProject(npmPackage)
}
private fun visitConfiguration(
configuration: Configuration,
npmDependencies: MutableSet<NpmDependency>,
gradleDeps: NpmProjectGradleDeps
) {
resolver.gradleNodeModules.collectDependenciesFromConfiguration(configuration, gradleDeps)
configuration.allDependencies.forEach { dependency ->
when (dependency) {
is NpmDependency -> npmDependencies.add(dependency)
}
}
}
}
@@ -0,0 +1,23 @@
/*
* 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.npm
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency
/**
* Info about NPM projects inside particular gradle [project].
*/
class NpmProjects(
val project: Project,
val npmProjects: List<NpmProjectPackage>,
val npmProjectsByCompilation: Map<KotlinJsCompilation, NpmProjectPackage>,
val npmProjectsByNpmDependency: Map<NpmDependency, NpmProjectPackage>,
val taskRequirements: Map<RequiresNpmDependencies, Collection<RequiredKotlinJsDependency>>
)
@@ -11,7 +11,7 @@ import org.gradle.api.tasks.TaskAction
open class NpmResolveTask : AbstractTask() { open class NpmResolveTask : AbstractTask() {
@TaskAction @TaskAction
fun resolve() { fun resolve() {
NpmResolver.resolve(project) NpmResolver.resolveIfNeeded(project)
} }
companion object { companion object {
@@ -5,29 +5,14 @@
package org.jetbrains.kotlin.gradle.targets.js.npm 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.Project
import org.gradle.api.artifacts.Configuration import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.gradle.api.artifacts.Dependency
import org.gradle.api.file.CopySpec
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.internal.ensureParentDirsCreated
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.plugin.whenEvaluated
import org.jetbrains.kotlin.gradle.targets.js.internal.RewriteSourceMapFilterReader
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver.ResolutionCallResult.* import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver.ResolutionCallResult.*
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
/** /**
* Generates `package.json` file for projects with npm or js dependencies and * Generates `package.json` file for [NpmProject] with npm or js dependencies and
* runs selected [NodeJsRootExtension.packageManager] to download and install all of it's dependencies. * runs selected [NodeJsRootExtension.packageManager] to download and install all of it's dependencies.
* *
* All [NpmDependency] for configurations related to kotlin/js will be added to `package.json`. * All [NpmDependency] for configurations related to kotlin/js will be added to `package.json`.
@@ -36,44 +21,45 @@ import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
*/ */
internal class NpmResolver private constructor(val rootProject: Project) : AutoCloseable { internal class NpmResolver private constructor(val rootProject: Project) : AutoCloseable {
companion object { companion object {
fun resolve(project: Project): ResolutionCallResult { fun resolveIfNeeded(project: Project): ResolutionCallResult {
val rootProject = project.rootProject val rootProject = project.rootProject
val process = ProjectData[rootProject] val process = ProjectData[rootProject]
if (process != null && process.resolved == null) return AlreadyInProgress if (process != null && process.visit == null) return AlreadyInProgress
val resolved = process?.resolved val resolved = process?.visit
return if (resolved != null) AlreadyResolved(ProjectData[project]!!.resolved!!) return if (resolved != null) AlreadyResolved(resolved)
else { else {
val resolver = NpmResolver(rootProject) run(rootProject)
resolver.resolve(rootProject)!! ResolvedNow(ProjectData[project]!!.visit!!)
resolver.close()
ResolvedNow(ProjectData[project]!!.resolved!!)
} }
} }
private fun run(rootProject: Project) {
NpmResolver(rootProject).resolve()
}
fun getAlreadyResolvedOrNull(project: Project): ResolutionCallResult? { fun getAlreadyResolvedOrNull(project: Project): ResolutionCallResult? {
val rootProject = project.rootProject val rootProject = project.rootProject
val process = ProjectData[rootProject] val process = ProjectData[rootProject]
if (process != null && process.resolved == null) return AlreadyInProgress if (process != null && process.visit == null) return AlreadyInProgress
val resolved = process?.resolved val resolved = process?.visit
if (resolved != null) return AlreadyResolved(ProjectData[project]!!.resolved!!) if (resolved != null) return AlreadyResolved(ProjectData[project]!!.visit!!)
return null return null
} }
fun requireResolved(project: Project, reason: String = ""): ResolvedProject = fun requireResolved(project: Project, reason: String = ""): NpmProjects =
ProjectData[project.rootProject]?.resolved ProjectData[project]?.visit ?: error("NPM dependencies should be resolved $reason")
?: error("NPM dependencies should be resolved$reason")
fun checkRequiredDependencies(project: Project, target: RequiresNpmDependencies) { fun checkRequiredDependencies(project: Project, target: RequiresNpmDependencies) {
val required = requireResolved(project, "before $target execution").taskDependencies val requestedTaskDependencies = requireResolved(project, "before $target execution").taskRequirements
val targetRequired = required[target]?.toSet() ?: setOf() val targetRequired = requestedTaskDependencies[target]?.toSet() ?: setOf()
target.requiredNpmDependencies.forEach { target.requiredNpmDependencies.forEach {
check(it in targetRequired) { check(it in targetRequired) {
"$it required by $target after npm dependencies was resolved. " + "$it required by $target was not found resolved at the time of nodejs package manager call. " +
"This may be caused by changing $target configuration after npm dependencies resolution." "This may be caused by changing $target configuration after npm dependencies resolution."
} }
} }
@@ -82,21 +68,21 @@ internal class NpmResolver private constructor(val rootProject: Project) : AutoC
sealed class ResolutionCallResult { sealed class ResolutionCallResult {
object AlreadyInProgress : ResolutionCallResult() object AlreadyInProgress : ResolutionCallResult()
class AlreadyResolved(val resolution: ResolvedProject) : ResolutionCallResult() class AlreadyResolved(val resolution: NpmProjects) : ResolutionCallResult()
class ResolvedNow(val resolution: ResolvedProject) : ResolutionCallResult() class ResolvedNow(val resolution: NpmProjects) : ResolutionCallResult()
} }
private val nodeJs = NodeJsPlugin.apply(rootProject).root val gradleNodeModules = GradleNodeModulesCache(rootProject)
private val npmProject = rootProject.npmProject val nodeJs = NodeJsPlugin.apply(rootProject).root
private val gradleNodeModules = GradleNodeModulesCache(rootProject) val packageManager by lazy {
private val packageManager = nodeJs.packageManager val packageManager = nodeJs.packageManager
private val npmPackages = mutableListOf<NpmPackage>() packageManager.setup(rootProject)
private val requiredByTasks = mutableMapOf<RequiresNpmDependencies, Collection<RequiredKotlinJsDependency>>() packageManager
private val gson = GsonBuilder() }
.setPrettyPrinting()
.create()
class ProjectData(var resolved: ResolvedProject? = null) { private val allNpmPackages = mutableListOf<NpmProjectPackage>()
class ProjectData(var visit: NpmProjects? = null) {
companion object { companion object {
private const val KEY = "npmResolverData" private const val KEY = "npmResolverData"
operator fun get(project: Project) = project.extensions.findByName(KEY) as ProjectData? operator fun get(project: Project) = project.extensions.findByName(KEY) as ProjectData?
@@ -104,39 +90,31 @@ internal class NpmResolver private constructor(val rootProject: Project) : AutoC
} }
} }
class ResolvedProject( fun resolve() {
val npmPackage: NpmPackage?, resolve(rootProject)
val taskDependencies: Map<RequiresNpmDependencies, Collection<RequiredKotlinJsDependency>> if (allNpmPackages.isNotEmpty()) {
) removeOutdatedPackages()
packageManager.resolveRootProject(rootProject, allNpmPackages)
}
class NpmPackage( close()
val project: Project, }
val packageJson: PackageJson,
val npmDependencies: Set<NpmDependency> private fun removeOutdatedPackages() {
) { val packages = allNpmPackages.mapTo(mutableSetOf()) { it.npmProject.name }
fun savePackageJson(gson: Gson) { nodeJs.projectPackagesDir.listFiles()?.forEach {
val packageJsonFile = NpmProject[project].packageJsonFile if (it.name !in packages) {
packageJsonFile.ensureParentDirsCreated() it.deleteRecursively()
packageJsonFile.writer().use {
gson.toJson(packageJson, it)
} }
} }
} }
private var packageManagerInstalled = false private fun getOrResolve(project: Project): NpmProjects {
return ProjectData[project]?.visit ?: resolve(project)
private fun requirePackageManagerSetup() {
if (packageManagerInstalled) return
packageManager.setup(rootProject)
packageManagerInstalled = true
} }
private fun getOrResolve(project: Project): ResolvedProject { private fun resolve(project: Project): NpmProjects {
return (ProjectData[project] ?: resolve(project)!!).resolved!! val data = ProjectData().also {
}
private fun resolve(project: Project): ProjectData? {
val result = ProjectData().also {
ProjectData[project] = it ProjectData[project] = it
} }
@@ -144,195 +122,33 @@ internal class NpmResolver private constructor(val rootProject: Project) : AutoC
getOrResolve(it) getOrResolve(it)
} }
val npmPackage = extractNpmPackage(project) val visited = NpmProjectVisitor(this, project).visitProject()
if (npmPackage != null) { data.visit = visited
npmPackage.savePackageJson(gson) allNpmPackages.addAll(visited.npmProjects)
requirePackageManagerSetup() return visited
packageManager.resolveProject(npmPackage)
npmPackages.add(npmPackage)
}
result.resolved = ResolvedProject(npmPackage, requiredByTasks)
if (project == rootProject) {
if (npmPackages.isNotEmpty()) {
requirePackageManagerSetup()
packageManager.resolveRootProject(rootProject, npmPackages)
}
}
return result
} }
private fun extractNpmPackage(project: Project): NpmPackage? { fun findDependentResolvedNpmProject(src: Project, target: Project): NpmProjectPackage? {
val packageJson = PackageJson(project.name, project.version.toString()) // todo: proper finding using KotlinTargetComponent.findUsageContext
val npmDependencies = mutableSetOf<NpmDependency>() val resolvedTarget = getOrResolve(target)
val gradleDeps = NpmProjectGradleDeps() val mainCompilations = resolvedTarget.npmProjectsByCompilation.entries.filter { it.key.name == KotlinCompilation.MAIN_COMPILATION_NAME }
visitTasksRequiredDependencies(project, npmDependencies, gradleDeps) return if (mainCompilations.isNotEmpty()) {
if (mainCompilations.size > 1) {
collectDependencies(project, npmDependencies, gradleDeps, packageJson) error(
"Cannot resolve project dependency $src -> $target." +
gradleDeps.externalModules.forEach { "Dependency to project with multiple js compilation not supported yet."
val relativePath = it.path.relativeTo(NpmProject[project].nodeWorkDir)
packageJson.dependencies[it.name] = "file:$relativePath"
}
gradleDeps.internalModules.forEach {
val npmPackage = getOrResolve(it).npmPackage
if (npmPackage != null) {
packageJson.dependencies[npmPackage.packageJson.name] = npmPackage.packageJson.version
}
}
project.nodeJs.packageJsonHandlers.forEach {
it(packageJson)
}
val packageJsonRequired =
if (project == rootProject) packageManager.hookRootPackage(rootProject, packageJson, npmPackages)
else false
return if (!packageJsonRequired && packageJson.empty) null
else NpmPackage(project, packageJson, npmDependencies)
}
private fun visitTasksRequiredDependencies(
project: Project,
npmDependencies: MutableSet<NpmDependency>,
gradleDeps: NpmProjectGradleDeps
) {
val requiredDependencies = mutableListOf<Dependency>()
project.tasks.toList().forEach { task ->
if (task.enabled && task is RequiresNpmDependencies) {
val list = task.requiredNpmDependencies.toList()
requiredByTasks[task] = list
list.forEach { requiredDependency ->
requiredDependencies.add(requiredDependency.createDependency(project))
}
}
}
if (requiredDependencies.isNotEmpty()) {
val configuration = project.configurations.create("jsTools")
requiredDependencies.forEach {
configuration.dependencies.add(it)
}
configuration.resolve()
visitConfiguration(configuration, npmDependencies, gradleDeps)
}
}
private fun collectDependencies(
project: Project,
npmDependencies: MutableSet<NpmDependency>,
gradleDeps: NpmProjectGradleDeps,
packageJson: PackageJson
) {
val kotlin = project.kotlinExtensionOrNull
if (kotlin != null) {
when (kotlin) {
is KotlinSingleTargetExtension -> visitTarget(kotlin.target, project, npmDependencies, gradleDeps, packageJson)
is KotlinMultiplatformExtension -> kotlin.targets.forEach {
visitTarget(it, project, npmDependencies, gradleDeps, packageJson)
}
}
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>,
gradleDeps: NpmProjectGradleDeps,
packageJson: PackageJson
) {
if (target.platformType == KotlinPlatformType.js) {
target.compilations.toList().forEach { compilation ->
compilation.relatedConfigurationNames.forEach {
val configuration = project.configurations.getByName(it)
visitConfiguration(configuration, npmDependencies, gradleDeps)
}
if (compilation is KotlinJsCompilation) {
visitKotlinJsCompilation(project, compilation)
if (compilation.name == "main") {
packageJson.main = compilation.compileKotlinTask.outputFile.name
}
}
}
}
}
private fun visitConfiguration(
configuration: Configuration,
npmDependencies: MutableSet<NpmDependency>,
gradleDeps: NpmProjectGradleDeps
) {
gradleNodeModules.collectDependenciesFromConfiguration(configuration, gradleDeps)
configuration.allDependencies.forEach { dependency ->
when (dependency) {
is NpmDependency -> npmDependencies.add(dependency)
}
}
}
private fun visitKotlinJsCompilation(
project: Project,
compilation: KotlinJsCompilation
) {
val npmProject = project.npmProject
val kotlin2JsCompile = compilation.compileKotlinTask
if (npmProject.compileOutputCopyDest != null) {
project.whenEvaluated {
if (kotlin2JsCompile.outputFile.exists()) {
visitCompile(project.npmProject, kotlin2JsCompile)
}
}
kotlin2JsCompile.doLast {
visitCompile(project.npmProject, kotlin2JsCompile)
}
}
}
private fun visitCompile(npmProject: NpmProject, kotlin2JsCompile: Kotlin2JsCompile) {
npmProject.project.copy { copy ->
copy.from(kotlin2JsCompile.outputFile)
copy.from(kotlin2JsCompile.outputFile.path + ".map")
copy.into(npmProject.compileOutputCopyDest!!)
copy.withSourceMapRewriter(npmProject)
}
}
private fun CopySpec.withSourceMapRewriter(npmProject: NpmProject) {
eachFile {
if (it.name.endsWith(".js.map")) {
it.filter(
mapOf(
"srcSourceRoot" to it.file.parentFile,
"targetSourceRoot" to npmProject.compileOutputCopyDest!!
),
RewriteSourceMapFilterReader::class.java
) )
} }
}
mainCompilations.single().value
} else null
} }
fun chooseVersion(oldVersion: String?, newVersion: String): String =
oldVersion ?: newVersion // todo: real versions conflict resolution
override fun close() { override fun close() {
gradleNodeModules.close() gradleNodeModules.close()
} }
@@ -5,6 +5,10 @@
package org.jetbrains.kotlin.gradle.targets.js.npm package org.jetbrains.kotlin.gradle.targets.js.npm
import com.google.gson.GsonBuilder
import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated
import java.io.File
class PackageJson( class PackageJson(
var name: String, var name: String,
var version: String var version: String
@@ -16,10 +20,40 @@ class PackageJson(
dependencies.isEmpty() && dependencies.isEmpty() &&
devDependencies.isEmpty() devDependencies.isEmpty()
val scopedName: ScopedName
get() = scopedName(name)
var private: Boolean? = null var private: Boolean? = null
var main: String? = null
var workspaces: Collection<String>? = null var workspaces: Collection<String>? = null
val devDependencies = mutableMapOf<String, String>() val devDependencies = mutableMapOf<String, String>()
val dependencies = mutableMapOf<String, String>() val dependencies = mutableMapOf<String, String>()
companion object {
fun scopedName(name: String): ScopedName = if (name.contains("/")) ScopedName(
scope = name.substringBeforeLast("/").removePrefix("@"),
name = name.substringAfterLast("/")
) else ScopedName(scope = null, name = name)
operator fun invoke(scope: String, name: String, version: String) =
PackageJson(ScopedName(scope, name).toString(), version)
}
data class ScopedName(val scope: String?, val name: String) {
override fun toString() = if (scope == null) name else "@$scope/$name"
}
fun saveTo(packageJsonFile: File) {
val gson = GsonBuilder()
.setPrettyPrinting()
.create()
packageJsonFile.ensureParentDirsCreated()
packageJsonFile.writer().use {
gson.toJson(this, it)
}
}
} }
@@ -5,6 +5,10 @@
package org.jetbrains.kotlin.gradle.targets.js.npm package org.jetbrains.kotlin.gradle.targets.js.npm
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency
interface RequiresNpmDependencies { interface RequiresNpmDependencies {
val compilation: KotlinJsCompilation
val requiredNpmDependencies: Collection<RequiredKotlinJsDependency> val requiredNpmDependencies: Collection<RequiredKotlinJsDependency>
} }
@@ -10,7 +10,6 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget
import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBrowserDsl import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBrowserDsl
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfigWriter import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfigWriter
@@ -20,7 +19,6 @@ class KotlinBrowserJs(target: KotlinJsTarget) :
KotlinJsSubTarget(target, "browser"), KotlinJsSubTarget(target, "browser"),
KotlinJsBrowserDsl { KotlinJsBrowserDsl {
private val versions = project.nodeJs.versions
private val webpackTaskName = disambiguateCamelCased("webpack") private val webpackTaskName = disambiguateCamelCased("webpack")
override fun configureDefaultTestFramework(it: KotlinJsTest) { override fun configureDefaultTestFramework(it: KotlinJsTest) {
@@ -39,13 +37,12 @@ class KotlinBrowserJs(target: KotlinJsTarget) :
override fun configureRun(compilation: KotlinJsCompilation) { override fun configureRun(compilation: KotlinJsCompilation) {
val project = compilation.target.project val project = compilation.target.project
val npmProject = project.npmProject
project.createOrRegisterTask<KotlinWebpack>(disambiguateCamelCased("webpack")) { project.createOrRegisterTask<KotlinWebpack>(disambiguateCamelCased("webpack")) {
val compileKotlinTask = compilation.compileKotlinTask val compileKotlinTask = compilation.compileKotlinTask
it.dependsOn(target.project.nodeJs.root.npmResolveTask, compileKotlinTask) it.dependsOn(target.project.nodeJs.root.npmResolveTask, compileKotlinTask)
it.entry = npmProject.compileOutput(compileKotlinTask) it.compilation = compilation
project.tasks.getByName(LifecycleBasePlugin.ASSEMBLE_TASK_NAME).dependsOn(it) project.tasks.getByName(LifecycleBasePlugin.ASSEMBLE_TASK_NAME).dependsOn(it)
} }
@@ -59,7 +56,7 @@ class KotlinBrowserJs(target: KotlinJsTarget) :
) )
it.bin = "webpack-dev-server" it.bin = "webpack-dev-server"
it.entry = npmProject.compileOutput(compileKotlinTask) it.compilation = compilation
it.devServer = KotlinWebpackConfigWriter.DevServer( it.devServer = KotlinWebpackConfigWriter.DevServer(
open = true, open = true,
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget
import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsSubTargetDsl import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsSubTargetDsl
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
import org.jetbrains.kotlin.gradle.tasks.createOrRegisterTask import org.jetbrains.kotlin.gradle.tasks.createOrRegisterTask
import org.jetbrains.kotlin.gradle.testing.internal.configureConventions import org.jetbrains.kotlin.gradle.testing.internal.configureConventions
@@ -32,6 +33,11 @@ abstract class KotlinJsSubTarget(
fun configure() { fun configure() {
configureTests() configureTests()
configureRun() configureRun()
target.compilations.all {
val npmProject = it.npmProject
it.compileKotlinTask.kotlinOptions.outputFile = npmProject.dir.resolve(npmProject.main).canonicalPath
}
} }
private fun disambiguate(name: String): MutableList<String> { private fun disambiguate(name: String): MutableList<String> {
@@ -72,8 +78,8 @@ abstract class KotlinJsSubTarget(
compileTask.outputFile.exists() compileTask.outputFile.exists()
} }
testJs.compilation = compilation
testJs.targetName = target.disambiguationClassifier + "," + disambiguationClassifier testJs.targetName = target.disambiguationClassifier + "," + disambiguationClassifier
testJs.nodeModulesToLoad.add(compileTask.outputFile.nameWithoutExtension)
testJs.configureConventions() testJs.configureConventions()
} }
@@ -5,7 +5,6 @@
package org.jetbrains.kotlin.gradle.targets.js.subtargets package org.jetbrains.kotlin.gradle.targets.js.subtargets
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.TaskHolder import org.jetbrains.kotlin.gradle.plugin.TaskHolder
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget
@@ -35,19 +34,18 @@ class KotlinNodeJs(target: KotlinJsTarget) :
val compileKotlinTask = compilation.compileKotlinTask val compileKotlinTask = compilation.compileKotlinTask
runTask.dependsOn(target.project.nodeJs.root.npmResolveTask, compileKotlinTask) runTask.dependsOn(target.project.nodeJs.root.npmResolveTask, compileKotlinTask)
val npmProject = project.npmProject val npmProject = compilation.npmProject
runTask.args(npmProject.compileOutput(compileKotlinTask)) runTask.args(compileKotlinTask.outputFile)
} }
addSourceMapSupport(compilation, runTaskHolder, project) addSourceMapSupport(compilation, runTaskHolder)
target.runTask.dependsOn(runTaskHolder.getTaskOrProvider()) target.runTask.dependsOn(runTaskHolder.getTaskOrProvider())
} }
private fun addSourceMapSupport( private fun addSourceMapSupport(
compilation: KotlinJsCompilation, compilation: KotlinJsCompilation,
runTaskHolder: TaskHolder<NodeJsExec>, runTaskHolder: TaskHolder<NodeJsExec>
project: Project
) { ) {
compilation.dependencies { compilation.dependencies {
runtimeOnly(kotlin("test-nodejs-runner")) runtimeOnly(kotlin("test-nodejs-runner"))
@@ -57,7 +55,7 @@ class KotlinNodeJs(target: KotlinJsTarget) :
runTaskHolder.configure { runTask -> runTaskHolder.configure { runTask ->
runTask.args( runTask.args(
"--require", "--require",
project.npmProject.require("kotlin-test-nodejs-runner/kotlin-nodejs-source-map-support") compilation.npmProject.require("kotlin-test-nodejs-runner/kotlin-nodejs-source-map-support")
) )
} }
} }
@@ -12,9 +12,12 @@ import org.gradle.api.tasks.SkipWhenEmpty
import org.gradle.process.internal.DefaultProcessForkOptions import org.gradle.process.internal.DefaultProcessForkOptions
import org.gradle.util.ConfigureUtil import org.gradle.util.ConfigureUtil
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.*
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver
import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.npm.RequiresNpmDependencies
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import org.jetbrains.kotlin.gradle.targets.js.testing.karma.KotlinKarma 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.mocha.KotlinMocha
import org.jetbrains.kotlin.gradle.targets.js.testing.nodejs.KotlinNodeJsTestRunner import org.jetbrains.kotlin.gradle.targets.js.testing.nodejs.KotlinNodeJsTestRunner
@@ -31,27 +34,36 @@ open class KotlinJsTest : KotlinTest(), RequiresNpmDependencies {
@Input @Input
var debug: Boolean = false var debug: Boolean = false
@Input @Internal
var nodeModulesToLoad: MutableList<String> = mutableListOf() override lateinit var compilation: KotlinJsCompilation
val compilationId: String
@Input get() = compilation.let {
val target = it.target
target.project.path + "@" + target.name + ":" + it.compilationName
}
val nodeModulesToLoad: List<String>
@Internal get() = listOf("./" + compilation.npmProject.main)
override val requiredNpmDependencies: Collection<RequiredKotlinJsDependency> override val requiredNpmDependencies: Collection<RequiredKotlinJsDependency>
get() = testFramework!!.requiredNpmDependencies get() = testFramework!!.requiredNpmDependencies
fun useNodeJs(body: KotlinNodeJsTestRunner.() -> Unit) = use(KotlinNodeJsTestRunner(project), body) fun useNodeJs(body: KotlinNodeJsTestRunner.() -> Unit) = use(KotlinNodeJsTestRunner(compilation), body)
fun useNodeJs(fn: Closure<*>) { fun useNodeJs(fn: Closure<*>) {
useNodeJs { useNodeJs {
ConfigureUtil.configure(fn, this) ConfigureUtil.configure(fn, this)
} }
} }
fun useMocha(body: KotlinMocha.() -> Unit) = use(KotlinMocha(project), body) fun useMocha(body: KotlinMocha.() -> Unit) = use(KotlinMocha(compilation), body)
fun useMocha(fn: Closure<*>) { fun useMocha(fn: Closure<*>) {
useMocha { useMocha {
ConfigureUtil.configure(fn, this) ConfigureUtil.configure(fn, this)
} }
} }
fun useKarma(body: KotlinKarma.() -> Unit) = use(KotlinKarma(project), body) fun useKarma(body: KotlinKarma.() -> Unit) = use(KotlinKarma(compilation), body)
fun useKarma(fn: Closure<*>) { fun useKarma(fn: Closure<*>) {
useKarma { useKarma {
ConfigureUtil.configure(fn, this) ConfigureUtil.configure(fn, this)
@@ -70,17 +82,13 @@ open class KotlinJsTest : KotlinTest(), RequiresNpmDependencies {
} }
override fun executeTests() { override fun executeTests() {
NpmResolver.resolve(project)
NpmResolver.checkRequiredDependencies(project, this) NpmResolver.checkRequiredDependencies(project, this)
super.executeTests() super.executeTests()
} }
override fun createTestExecutionSpec(): TCServiceMessagesTestExecutionSpec { override fun createTestExecutionSpec(): TCServiceMessagesTestExecutionSpec {
val forkOptions = DefaultProcessForkOptions(fileResolver) val forkOptions = DefaultProcessForkOptions(fileResolver)
forkOptions.workingDir = compilation.npmProject.dir
NpmResolver.resolve(project)
forkOptions.workingDir = NpmProject[project].nodeWorkDir
forkOptions.executable = NodeJsPlugin.apply(project).root.environment.nodeExecutable forkOptions.executable = NodeJsPlugin.apply(project).root.environment.nodeExecutable
val nodeJsArgs = mutableListOf<String>() val nodeJsArgs = mutableListOf<String>()
@@ -15,11 +15,12 @@ import org.jetbrains.kotlin.gradle.internal.operation
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClient import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClient
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.appendConfigsFromDir import org.jetbrains.kotlin.gradle.targets.js.appendConfigsFromDir
import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTraceAsJvm import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTraceAsJvm
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmPackageVersion import org.jetbrains.kotlin.gradle.targets.js.NpmPackageVersion
import org.jetbrains.kotlin.gradle.targets.js.npm.RequiredKotlinJsDependency import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTestFramework import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTestFramework
@@ -29,7 +30,10 @@ import org.jetbrains.kotlin.gradle.testing.internal.reportsDir
import org.slf4j.Logger import org.slf4j.Logger
import java.io.File import java.io.File
class KotlinKarma(val project: Project) : KotlinJsTestFramework { class KotlinKarma(override val compilation: KotlinJsCompilation) : KotlinJsTestFramework {
val project: Project
get() = compilation.target.project
private val config: KarmaConfig = KarmaConfig() private val config: KarmaConfig = KarmaConfig()
private val requiredDependencies = mutableSetOf<NpmPackageVersion>() private val requiredDependencies = mutableSetOf<NpmPackageVersion>()
@@ -183,7 +187,7 @@ class KotlinKarma(val project: Project) : KotlinJsTestFramework {
ignoreOutOfRootNodes = true ignoreOutOfRootNodes = true
) )
val npmProject = task.project.npmProject val npmProject = compilation.npmProject
val files = task.nodeModulesToLoad.map { npmProject.require(it) } val files = task.nodeModulesToLoad.map { npmProject.require(it) }
config.files.addAll(files) config.files.addAll(files)
@@ -194,7 +198,7 @@ class KotlinKarma(val project: Project) : KotlinJsTestFramework {
it(task) it(task)
} }
val karmaConfJs = npmProject.nodeWorkDir.resolve("karma.conf.js") val karmaConfJs = npmProject.dir.resolve("karma.conf.js")
karmaConfJs.printWriter().use { confWriter -> karmaConfJs.printWriter().use { confWriter ->
confWriter.println("module.exports = function(config) {") confWriter.println("module.exports = function(config) {")
confWriter.println() confWriter.println()
@@ -9,15 +9,19 @@ import org.gradle.api.Project
import org.gradle.process.ProcessForkOptions import org.gradle.process.ProcessForkOptions
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTraceAsJvm import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTraceAsJvm
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.targets.js.npm.KotlinGradleNpmPackage import org.jetbrains.kotlin.gradle.targets.js.KotlinGradleNpmPackage
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.npm.RequiredKotlinJsDependency import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTestFramework import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTestFramework
class KotlinMocha(val project: Project) : KotlinJsTestFramework { class KotlinMocha(override val compilation: KotlinJsCompilation) : KotlinJsTestFramework {
val project: Project
get() = compilation.target.project
private val versions = project.nodeJs.versions private val versions = project.nodeJs.versions
override val settingsState: String override val settingsState: String
@@ -48,11 +52,11 @@ class KotlinMocha(val project: Project) : KotlinJsTestFramework {
task.nodeModulesToLoad.single() task.nodeModulesToLoad.single()
) )
val npmProjectLayout = NpmProject[task.project] val npmProject = compilation.npmProject
val args = nodeJsArgs + val args = nodeJsArgs +
nodeModules.map { nodeModules.map {
npmProjectLayout.nodeModulesDir.resolve(it).also { file -> npmProject.nodeModulesDir.resolve(it).also { file ->
check(file.isFile) { "Cannot find ${file.canonicalPath}" } check(file.isFile) { "Cannot find ${file.canonicalPath}" }
}.canonicalPath }.canonicalPath
} + } +
@@ -6,21 +6,22 @@
package org.jetbrains.kotlin.gradle.targets.js.testing.nodejs package org.jetbrains.kotlin.gradle.targets.js.testing.nodejs
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.tasks.Input
import org.gradle.process.ProcessForkOptions import org.gradle.process.ProcessForkOptions
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTraceAsJvm import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTraceAsJvm
import org.jetbrains.kotlin.gradle.targets.js.npm.KotlinGradleNpmPackage import org.jetbrains.kotlin.gradle.targets.js.KotlinGradleNpmPackage
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.npm.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTestFramework import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTestFramework
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
import org.jetbrains.kotlin.gradle.testing.IgnoredTestSuites import org.jetbrains.kotlin.gradle.testing.IgnoredTestSuites
class KotlinNodeJsTestRunner(val project: Project) : KotlinJsTestFramework { class KotlinNodeJsTestRunner(override val compilation: KotlinJsCompilation) : KotlinJsTestFramework {
@Input val project: Project
get() = compilation.target.project
var ignoredTestSuites: IgnoredTestSuites = IgnoredTestSuites.showWithContents var ignoredTestSuites: IgnoredTestSuites = IgnoredTestSuites.showWithContents
override val settingsState: String override val settingsState: String
@@ -34,7 +35,7 @@ class KotlinNodeJsTestRunner(val project: Project) : KotlinJsTestFramework {
forkOptions: ProcessForkOptions, forkOptions: ProcessForkOptions,
nodeJsArgs: MutableList<String> nodeJsArgs: MutableList<String>
): TCServiceMessagesTestExecutionSpec { ): TCServiceMessagesTestExecutionSpec {
val npmProject = project.npmProject val npmProject = compilation.npmProject
val cliArgs = KotlinNodeJsTestRunnerCliArgs( val cliArgs = KotlinNodeJsTestRunnerCliArgs(
task.nodeModulesToLoad.map { npmProject.require(it) }, task.nodeModulesToLoad.map { npmProject.require(it) },
@@ -55,11 +56,9 @@ class KotlinNodeJsTestRunner(val project: Project) : KotlinJsTestFramework {
"kotlin-test-nodejs-runner/kotlin-nodejs-source-map-support.js" "kotlin-test-nodejs-runner/kotlin-nodejs-source-map-support.js"
) )
val npmProjectLayout = NpmProject[task.project]
val args = nodeJsArgs + val args = nodeJsArgs +
testRuntimeNodeModules.map { testRuntimeNodeModules.map {
npmProjectLayout.require(it) npmProject.require(it)
} + } +
cliArgs.toList() cliArgs.toList()
@@ -13,8 +13,9 @@ import org.gradle.deployment.internal.DeploymentHandle
import org.gradle.deployment.internal.DeploymentRegistry import org.gradle.deployment.internal.DeploymentRegistry
import org.gradle.process.internal.ExecHandle import org.gradle.process.internal.ExecHandle
import org.gradle.process.internal.ExecHandleFactory import org.gradle.process.internal.ExecHandleFactory
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmPackageVersion import org.jetbrains.kotlin.gradle.targets.js.NpmPackageVersion
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver
import org.jetbrains.kotlin.gradle.targets.js.npm.RequiresNpmDependencies import org.jetbrains.kotlin.gradle.targets.js.npm.RequiresNpmDependencies
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
@@ -34,12 +35,20 @@ open class KotlinWebpack : DefaultTask(), RequiresNpmDependencies {
open val execHandleFactory: ExecHandleFactory open val execHandleFactory: ExecHandleFactory
get() = injected get() = injected
@Input @Internal
@SkipWhenEmpty override lateinit var compilation: KotlinJsCompilation
open lateinit var entry: File
val compilationId: String
@Input get() = compilation.let {
val target = it.target
target.project.path + "@" + target.name + ":" + it.compilationName
}
val entry: File
get() = compilation.compileKotlinTask.outputFile
open val configFile: File open val configFile: File
@OutputFile get() = project.npmProject.nodeWorkDir.resolve("webpack.config.js") @OutputFile get() = compilation.npmProject.dir.resolve("webpack.config.js")
@Input @Input
var saveEvaluatedConfigFile: Boolean = true var saveEvaluatedConfigFile: Boolean = true
@@ -89,7 +98,7 @@ open class KotlinWebpack : DefaultTask(), RequiresNpmDependencies {
} }
private fun createRunner() = KotlinWebpackRunner( private fun createRunner() = KotlinWebpackRunner(
project, compilation!!.npmProject,
configFile, configFile,
execHandleFactory, execHandleFactory,
bin, bin,
@@ -107,7 +116,6 @@ open class KotlinWebpack : DefaultTask(), RequiresNpmDependencies {
@TaskAction @TaskAction
fun execute() { fun execute() {
NpmResolver.resolve(project)
NpmResolver.checkRequiredDependencies(project, this) NpmResolver.checkRequiredDependencies(project, this)
val runner = createRunner() val runner = createRunner()
@@ -12,7 +12,6 @@
package org.jetbrains.kotlin.gradle.targets.js.webpack package org.jetbrains.kotlin.gradle.targets.js.webpack
import org.gradle.api.Project
import org.gradle.process.ExecResult import org.gradle.process.ExecResult
import org.gradle.process.internal.ExecHandle import org.gradle.process.internal.ExecHandle
import org.gradle.process.internal.ExecHandleFactory import org.gradle.process.internal.ExecHandleFactory
@@ -21,7 +20,7 @@ import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver
import java.io.File import java.io.File
internal class KotlinWebpackRunner( internal class KotlinWebpackRunner(
val project: Project, val npmProject: NpmProject,
val configFile: File, val configFile: File,
val execHandleFactory: ExecHandleFactory, val execHandleFactory: ExecHandleFactory,
val bin: String, val bin: String,
@@ -41,10 +40,6 @@ internal class KotlinWebpackRunner(
"${this}: Entry file not existed \"${configWriter.entry}\"" "${this}: Entry file not existed \"${configWriter.entry}\""
} }
NpmResolver.resolve(project)
val npmProjectLayout = NpmProject[project]
configWriter.save(configFile) configWriter.save(configFile)
val execFactory = execHandleFactory.newExec() val execFactory = execHandleFactory.newExec()
@@ -54,7 +49,7 @@ internal class KotlinWebpackRunner(
args.add("--progress") args.add("--progress")
} }
npmProjectLayout.useTool(execFactory, ".bin/$bin", *args.toTypedArray()) npmProject.useTool(execFactory, ".bin/$bin", *args.toTypedArray())
val exec = execFactory.build() val exec = execFactory.build()
exec.start() exec.start()
return exec return exec
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.gradle.targets.js.yarn
import org.gradle.api.Project import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.internal.execWithProgress import org.jetbrains.kotlin.gradle.internal.execWithProgress
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.targets.js.npm.* import org.jetbrains.kotlin.gradle.targets.js.npm.*
import java.io.File import java.io.File
@@ -16,24 +17,22 @@ object Yarn : NpmApi {
YarnPlugin.apply(project).executeSetup() YarnPlugin.apply(project).executeSetup()
} }
val Project.packageJsonHashFile: File val NpmProject.packageJsonHashFile: File
get() = buildDir.resolve("package.json.hash") get() = dir.resolve("package.json.hash")
fun yarnExec( fun yarnExec(
project: Project, project: Project,
dir: File,
description: String, description: String,
vararg args: String, vararg args: String
npmProject: NpmProject = NpmProject[project]
) { ) {
val nodeJsEnv = NodeJsPlugin.apply(project).root.environment val nodeJsEnv = NodeJsPlugin.apply(project).root.environment
val yarnEnv = YarnPlugin.apply(project).environment val yarnEnv = YarnPlugin.apply(project).environment
val nodeWorkDir = npmProject.nodeWorkDir
project.execWithProgress(description) { exec -> project.execWithProgress(description) { exec ->
exec.executable = nodeJsEnv.nodeExecutable exec.executable = nodeJsEnv.nodeExecutable
exec.args = listOf(yarnEnv.home.resolve("bin/yarn.js").absolutePath) + args exec.args = listOf(yarnEnv.home.resolve("bin/yarn.js").absolutePath) + args
exec.workingDir = nodeWorkDir exec.workingDir = dir
} }
} }
@@ -79,70 +78,62 @@ object Yarn : NpmApi {
} }
} }
@Suppress("EXPOSED_PARAMETER_TYPE") override fun resolveProject(resolvedNpmProject: NpmProjectPackage) {
override fun resolveProject(npmPackage: NpmResolver.NpmPackage) { val project = resolvedNpmProject.project
val project = npmPackage.project
if (!project.yarn.useWorkspaces) { if (!project.yarn.useWorkspaces) {
YarnAvoidance(project).updateIfNeeded { YarnAvoidance(resolvedNpmProject.npmProject).updateIfNeeded {
yarnExec(project, NpmApi.resolveOperationDescription("yarn for ${project.path}")) yarnExec(project, resolvedNpmProject.npmProject.dir, NpmApi.resolveOperationDescription("yarn for ${project.path}"))
yarnLockReadTransitiveDependencies(NpmProject[project].nodeWorkDir, npmPackage.npmDependencies) yarnLockReadTransitiveDependencies(resolvedNpmProject.npmProject.dir, resolvedNpmProject.npmDependencies)
} }
} }
} }
@Suppress("EXPOSED_PARAMETER_TYPE") override fun resolveRootProject(rootProject: Project, subProjects: MutableList<NpmProjectPackage>) {
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.npmProject.nodeWorkDir.relativeTo(rootProject.npmProject.nodeWorkDir).path }
return true
}
return false
}
// override fun shouldHoistGradleNodeModules(project: Project): Boolean =
// project.rootProject.yarn.useWorkspaces
@Suppress("EXPOSED_PARAMETER_TYPE")
override fun resolveRootProject(
rootProject: Project,
subprojects: MutableList<NpmResolver.NpmPackage>
) {
check(rootProject == rootProject.rootProject) check(rootProject == rootProject.rootProject)
if (rootProject.yarn.useWorkspaces) { if (rootProject.yarn.useWorkspaces) {
resolveWorkspaces(rootProject, subprojects) resolveWorkspaces(rootProject, subProjects)
} }
} }
private fun resolveWorkspaces( private fun resolveWorkspaces(
rootProject: Project, rootProject: Project,
subprojects: MutableList<NpmResolver.NpmPackage> npmProjects: MutableList<NpmProjectPackage>
) { ) {
val upToDateChecks = subprojects.map { val upToDateChecks = npmProjects.map {
YarnAvoidance(it.project) YarnAvoidance(it.npmProject)
} }
if (upToDateChecks.all { it.upToDate }) return if (upToDateChecks.all { it.upToDate }) return
yarnExec(rootProject, NpmApi.resolveOperationDescription("yarn")) val nodeJsWorldDir = rootProject.nodeJs.root.rootPackageDir
yarnLockReadTransitiveDependencies(NpmProject[rootProject].nodeWorkDir, subprojects.flatMap { it.npmDependencies })
saveRootProjectWorkspacesPackageJson(rootProject, npmProjects, nodeJsWorldDir)
yarnExec(rootProject, nodeJsWorldDir, NpmApi.resolveOperationDescription("yarn"))
yarnLockReadTransitiveDependencies(nodeJsWorldDir, npmProjects.flatMap { it.npmDependencies })
upToDateChecks.forEach { upToDateChecks.forEach {
it.commit() it.commit()
} }
} }
override fun cleanProject(project: Project) { private fun saveRootProjectWorkspacesPackageJson(
super.cleanProject(project) rootProject: Project,
project.packageJsonHashFile.delete() npmProjects: MutableList<NpmProjectPackage>,
nodeJsWorldDir: File
) {
val rootPackageJson = PackageJson(rootProject.name, rootProject.version.toString())
rootPackageJson.private = true
rootPackageJson.workspaces = npmProjects
.map { it.npmProject.dir.relativeTo(nodeJsWorldDir).path }
rootProject.nodeJs.packageJsonHandlers.forEach {
it(rootPackageJson)
}
rootPackageJson.saveTo(
nodeJsWorldDir.resolve(NpmProject.PACKAGE_JSON)
)
} }
} }
@@ -10,16 +10,21 @@ import org.gradle.api.internal.project.ProjectInternal
import org.gradle.internal.hash.FileHasher import org.gradle.internal.hash.FileHasher
import org.jetbrains.kotlin.daemon.common.toHexString import org.jetbrains.kotlin.daemon.common.toHexString
import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import org.jetbrains.kotlin.gradle.targets.js.yarn.Yarn.packageJsonHashFile import org.jetbrains.kotlin.gradle.targets.js.yarn.Yarn.packageJsonHashFile
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
class YarnAvoidance(val npmProject: NpmProject) {
val project: Project
get() = npmProject.project
class YarnAvoidance(val project: Project) {
private val prevHash by lazy { private val prevHash by lazy {
val packageJsonHashFile = project.packageJsonHashFile val packageJsonHashFile = npmProject.packageJsonHashFile
if (packageJsonHashFile.exists()) packageJsonHashFile.readText() else null if (packageJsonHashFile.exists()) packageJsonHashFile.readText() else null
} }
private val npmProject = project.npmProject
private val packageJsonFile = npmProject.packageJsonFile private val packageJsonFile = npmProject.packageJsonFile
private val hash by lazy { private val hash by lazy {
@@ -34,7 +39,7 @@ class YarnAvoidance(val project: Project) {
fun commit() { fun commit() {
if (!upToDate) { if (!upToDate) {
val packageJsonHashFile = project.packageJsonHashFile val packageJsonHashFile = npmProject.packageJsonHashFile
packageJsonHashFile.ensureParentDirsCreated() packageJsonHashFile.ensureParentDirsCreated()
packageJsonHashFile.writeText(hash) packageJsonHashFile.writeText(hash)
} }
@@ -26,7 +26,7 @@ open class YarnRootExtension(val project: Project) {
var disableWorkspaces: Boolean = false var disableWorkspaces: Boolean = false
val useWorkspaces: Boolean val useWorkspaces: Boolean
get() = !disableWorkspaces && project.nodeJs.root.layout.allowYarnWorkspaces get() = !disableWorkspaces
internal fun executeSetup() { internal fun executeSetup() {
NodeJsPlugin.apply(project).root.executeSetup() NodeJsPlugin.apply(project).root.executeSetup()