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.
*/
package org.jetbrains.kotlin.gradle.targets.js.npm
package org.jetbrains.kotlin.gradle.targets.js
import org.gradle.api.Project
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 webpackCli = NpmPackageVersion("webpack-cli", "3.3.0")
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 {
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 =
project.dependencies.create("org.jetbrains.kotlin:kotlin-$simpleModuleName")
}
@@ -6,7 +6,7 @@
package org.jetbrains.kotlin.gradle.targets.js.nodejs
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
open class NodeJsExtension(val project: Project) {
@@ -15,7 +15,7 @@ open class NodeJsExtension(val project: Project) {
internal val packageJsonHandlers = mutableListOf<PackageJson.() -> Unit>()
val versions = NpmPackages()
val versions = NpmVersions()
@Suppress("unused")
fun packageJson(handler: PackageJson.() -> Unit) {
@@ -6,7 +6,6 @@ import org.gradle.api.plugins.BasePlugin
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.npm.NpmResolveTask
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
open class NodeJsPlugin : Plugin<Project> {
override fun apply(project: Project): Unit = project.run {
@@ -33,25 +32,15 @@ open class NodeJsPlugin : Plugin<Project> {
private fun setupCleanNodeModulesTask(project: Project) {
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.doLast {
project.nodeJs.root.packageManager.cleanProject(project)
}
project.tasks.maybeCreate(BasePlugin.CLEAN_TASK_NAME).dependsOn(it)
it.delete.add(project.nodeJs.root.rootPackageDir)
}
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.doLast {
project.npmProject.gradleNodeModulesDir.deleteRecursively()
}
project.tasks.maybeCreate(BasePlugin.CLEAN_TASK_NAME).dependsOn(it)
it.delete.add(project.nodeJs.root.nodeModulesGradleCacheDir)
}
}
@@ -2,12 +2,9 @@ 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.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.NpmResolveTask
import org.jetbrains.kotlin.gradle.targets.js.yarn.Yarn
import org.jetbrains.kotlin.gradle.tasks.locateOrRegisterTask
import java.io.File
open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) {
@@ -23,7 +20,6 @@ open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) {
var nodeCommand = "node"
var layout: NpmProjectLayout = NpmProjectLayout.ROOT_PROJECT_BUILD_DIR
var packageManager: NpmApi = Yarn
val nodeJsSetupTask: NodeJsSetupTask
@@ -32,6 +28,15 @@ open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) {
val npmResolveTask: 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
get() {
val platform = NodeJsPlatform.name
@@ -7,4 +7,7 @@ package org.jetbrains.kotlin.gradle.targets.js.npm
import java.io.File
/**
* Fake NodeJS module directory created from Gradle external module
*/
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 java.io.File
/**
* Creates fake NodeJS module directory from given gradle [dependency].
*/
internal class GradleNodeModuleBuilder(
val project: Project,
val dependency: ResolvedDependency,
@@ -11,13 +11,17 @@ import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.artifacts.ResolvedDependency
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
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 {
companion object {
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 visited = mutableSetOf<ResolvedDependency>()
@@ -7,36 +7,19 @@ package org.jetbrains.kotlin.gradle.targets.js.npm
import org.gradle.api.Project
/**
* NodeJS package manager API
*/
interface NpmApi {
fun setup(project: Project)
@Suppress("EXPOSED_PARAMETER_TYPE")
fun resolveProject(npmPackage: NpmResolver.NpmPackage)
fun resolveProject(resolvedNpmProject: NpmProjectPackage)
@Suppress("EXPOSED_PARAMETER_TYPE")
fun resolveRootProject(
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 {
fun resolveOperationDescription(packageManagerTitle: String): String =
"Resolving NPM dependencies using $packageManagerTitle"
@@ -28,12 +28,14 @@ data class NpmDependency(
SelfResolvingDependencyInternal,
ResolvableDependency,
FileCollectionDependency {
override fun getGroup(): String? = org
internal val dependencies = mutableSetOf<NpmDependency>()
override fun resolve(): MutableSet<File> {
resolveProject() ?: return mutableSetOf()
override fun resolve(transitive: Boolean): MutableSet<File> {
val npmPackage = resolveProject() ?: return mutableSetOf()
val npmProject = npmPackage.npmProject
val all = mutableSetOf<File>()
val visited = mutableSetOf<NpmDependency>()
@@ -41,9 +43,16 @@ data class NpmDependency(
fun visit(item: NpmDependency) {
if (item in visited) return
visited.add(item)
item.project.npmProject.resolve(item.key)?.let {
npmProject.resolve(item.key)?.let {
all.add(it)
}
if (transitive) {
item.dependencies.forEach {
visit(it)
}
}
}
visit(this)
@@ -51,45 +60,42 @@ data class NpmDependency(
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 =
if (isInIdeaSync) NpmResolver.resolve(project)
if (isInIdeaSync) NpmResolver.resolveIfNeeded(project)
else NpmResolver.getAlreadyResolvedOrNull(project)
return when (result) {
null -> null
is AlreadyInProgress -> null
is AlreadyResolved -> {
check(this in result.resolution.npmPackage!!.npmDependencies) {
"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
}
is AlreadyResolved -> findIn(result.resolution) ?: error("Project hierarchy is already resolved in NPM without $this")
is ResolvedNow -> findIn(result.resolution) ?: error("Cannot find $this after NPM project resolve")
}
}
private fun findIn(npmProjects: NpmProjects) =
npmProjects.npmProjectsByNpmDependency[this]
val key: String = if (org == null) name else "@$org/$name"
override fun toString() = "$key: $version"
override fun resolve(context: DependencyResolveContext) {
resolve()
dependencies.forEach {
context.add(it)
}
}
override fun resolve(transitive: Boolean): MutableSet<File> = resolve()
override fun getFiles(): FileCollection = project.files(resolve())
override fun getFiles(): FileCollection = project.files(resolve(true))
override fun getName() = name
@@ -7,41 +7,52 @@ package org.jetbrains.kotlin.gradle.targets.js.npm
import org.gradle.api.Project
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.tasks.Kotlin2JsCompile
import java.io.File
open class NpmProject(
val project: Project,
val nodeWorkDir: File,
val searchInParents: Boolean
) {
val KotlinJsCompilation.npmProject: NpmProject
get() = NpmProject(this)
/**
* 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
get() = nodeWorkDir.resolve(NODE_MODULES)
get() = dir.resolve(NODE_MODULES)
val packageJsonFile: File
get() = nodeWorkDir.resolve(PACKAGE_JSON)
get() = dir.resolve(PACKAGE_JSON)
open val compileOutputCopyDest: File?
get() = nodeModulesDir
val main: String
get() = "kotlin/$name.js"
private val modules = object : NpmProjectModules(nodeWorkDir, nodeModulesDir) {
override val parent get() = if (searchInParents) parentModules else null
private val modules = object : NpmProjectModules(dir, nodeModulesDir) {
override val parent get() = rootNodeModules
}
private val parentModules: NpmProjectModules?
get() = project.parent?.npmProject?.modules
val gradleNodeModulesDir: File
get() = project.rootProject.npmProject.nodeModulesDir
open fun compileOutput(compilationTask: Kotlin2JsCompile): File {
return compileOutputCopyDest?.resolve(compilationTask.outputFile.name) ?: compilationTask.outputFile
}
private val rootNodeModules: NpmProjectModules?
get() = NpmProjectModules(project.nodeJs.root.rootPackageDir)
fun useTool(exec: ExecSpec, tool: String, vararg args: String) {
exec.workingDir = nodeWorkDir
exec.workingDir = dir
exec.executable = project.nodeJs.root.environment.nodeExecutable
exec.args = listOf(require(tool)) + args
}
@@ -60,19 +71,32 @@ open class NpmProject(
*/
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 {
const val PACKAGE_JSON = "package.json"
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
/**
* Gradle dependencies for [NpmProject].
*/
class NpmProjectGradleDeps {
val internalModules = mutableSetOf<Project>()
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 java.io.File
/**
* Search modules in node_modules according to https://nodejs.org/api/modules.html.
*/
internal open class NpmProjectModules(
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.
@@ -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() {
@TaskAction
fun resolve() {
NpmResolver.resolve(project)
NpmResolver.resolveIfNeeded(project)
}
companion object {
@@ -5,29 +5,14 @@
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.Configuration
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.plugin.KotlinCompilation
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.*
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.
*
* 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 {
companion object {
fun resolve(project: Project): ResolutionCallResult {
fun resolveIfNeeded(project: Project): ResolutionCallResult {
val rootProject = project.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 {
val resolver = NpmResolver(rootProject)
resolver.resolve(rootProject)!!
resolver.close()
ResolvedNow(ProjectData[project]!!.resolved!!)
run(rootProject)
ResolvedNow(ProjectData[project]!!.visit!!)
}
}
private fun run(rootProject: Project) {
NpmResolver(rootProject).resolve()
}
fun getAlreadyResolvedOrNull(project: Project): ResolutionCallResult? {
val rootProject = project.rootProject
val process = ProjectData[rootProject]
if (process != null && process.resolved == null) return AlreadyInProgress
val resolved = process?.resolved
if (resolved != null) return AlreadyResolved(ProjectData[project]!!.resolved!!)
if (process != null && process.visit == null) return AlreadyInProgress
val resolved = process?.visit
if (resolved != null) return AlreadyResolved(ProjectData[project]!!.visit!!)
return null
}
fun requireResolved(project: Project, reason: String = ""): ResolvedProject =
ProjectData[project.rootProject]?.resolved
?: error("NPM dependencies should be resolved$reason")
fun requireResolved(project: Project, reason: String = ""): NpmProjects =
ProjectData[project]?.visit ?: error("NPM dependencies should be resolved $reason")
fun checkRequiredDependencies(project: Project, target: RequiresNpmDependencies) {
val required = requireResolved(project, "before $target execution").taskDependencies
val targetRequired = required[target]?.toSet() ?: setOf()
val requestedTaskDependencies = requireResolved(project, "before $target execution").taskRequirements
val targetRequired = requestedTaskDependencies[target]?.toSet() ?: setOf()
target.requiredNpmDependencies.forEach {
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."
}
}
@@ -82,21 +68,21 @@ internal class NpmResolver private constructor(val rootProject: Project) : AutoC
sealed class ResolutionCallResult {
object AlreadyInProgress : ResolutionCallResult()
class AlreadyResolved(val resolution: ResolvedProject) : ResolutionCallResult()
class ResolvedNow(val resolution: ResolvedProject) : ResolutionCallResult()
class AlreadyResolved(val resolution: NpmProjects) : ResolutionCallResult()
class ResolvedNow(val resolution: NpmProjects) : ResolutionCallResult()
}
private val nodeJs = NodeJsPlugin.apply(rootProject).root
private val npmProject = rootProject.npmProject
private val gradleNodeModules = GradleNodeModulesCache(rootProject)
private val packageManager = nodeJs.packageManager
private val npmPackages = mutableListOf<NpmPackage>()
private val requiredByTasks = mutableMapOf<RequiresNpmDependencies, Collection<RequiredKotlinJsDependency>>()
private val gson = GsonBuilder()
.setPrettyPrinting()
.create()
val gradleNodeModules = GradleNodeModulesCache(rootProject)
val nodeJs = NodeJsPlugin.apply(rootProject).root
val packageManager by lazy {
val packageManager = nodeJs.packageManager
packageManager.setup(rootProject)
packageManager
}
class ProjectData(var resolved: ResolvedProject? = null) {
private val allNpmPackages = mutableListOf<NpmProjectPackage>()
class ProjectData(var visit: NpmProjects? = null) {
companion object {
private const val KEY = "npmResolverData"
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(
val npmPackage: NpmPackage?,
val taskDependencies: Map<RequiresNpmDependencies, Collection<RequiredKotlinJsDependency>>
)
fun resolve() {
resolve(rootProject)
if (allNpmPackages.isNotEmpty()) {
removeOutdatedPackages()
packageManager.resolveRootProject(rootProject, allNpmPackages)
}
class NpmPackage(
val project: Project,
val packageJson: PackageJson,
val npmDependencies: Set<NpmDependency>
) {
fun savePackageJson(gson: Gson) {
val packageJsonFile = NpmProject[project].packageJsonFile
packageJsonFile.ensureParentDirsCreated()
packageJsonFile.writer().use {
gson.toJson(packageJson, it)
close()
}
private fun removeOutdatedPackages() {
val packages = allNpmPackages.mapTo(mutableSetOf()) { it.npmProject.name }
nodeJs.projectPackagesDir.listFiles()?.forEach {
if (it.name !in packages) {
it.deleteRecursively()
}
}
}
private var packageManagerInstalled = false
private fun requirePackageManagerSetup() {
if (packageManagerInstalled) return
packageManager.setup(rootProject)
packageManagerInstalled = true
private fun getOrResolve(project: Project): NpmProjects {
return ProjectData[project]?.visit ?: resolve(project)
}
private fun getOrResolve(project: Project): ResolvedProject {
return (ProjectData[project] ?: resolve(project)!!).resolved!!
}
private fun resolve(project: Project): ProjectData? {
val result = ProjectData().also {
private fun resolve(project: Project): NpmProjects {
val data = ProjectData().also {
ProjectData[project] = it
}
@@ -144,195 +122,33 @@ internal class NpmResolver private constructor(val rootProject: Project) : AutoC
getOrResolve(it)
}
val npmPackage = extractNpmPackage(project)
if (npmPackage != null) {
npmPackage.savePackageJson(gson)
val visited = NpmProjectVisitor(this, project).visitProject()
data.visit = visited
allNpmPackages.addAll(visited.npmProjects)
requirePackageManagerSetup()
packageManager.resolveProject(npmPackage)
npmPackages.add(npmPackage)
}
result.resolved = ResolvedProject(npmPackage, requiredByTasks)
if (project == rootProject) {
if (npmPackages.isNotEmpty()) {
requirePackageManagerSetup()
packageManager.resolveRootProject(rootProject, npmPackages)
}
}
return result
return visited
}
private fun extractNpmPackage(project: Project): NpmPackage? {
val packageJson = PackageJson(project.name, project.version.toString())
val npmDependencies = mutableSetOf<NpmDependency>()
val gradleDeps = NpmProjectGradleDeps()
fun findDependentResolvedNpmProject(src: Project, target: Project): NpmProjectPackage? {
// todo: proper finding using KotlinTargetComponent.findUsageContext
val resolvedTarget = getOrResolve(target)
val mainCompilations = resolvedTarget.npmProjectsByCompilation.entries.filter { it.key.name == KotlinCompilation.MAIN_COMPILATION_NAME }
visitTasksRequiredDependencies(project, npmDependencies, gradleDeps)
collectDependencies(project, npmDependencies, gradleDeps, packageJson)
gradleDeps.externalModules.forEach {
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
return if (mainCompilations.isNotEmpty()) {
if (mainCompilations.size > 1) {
error(
"Cannot resolve project dependency $src -> $target." +
"Dependency to project with multiple js compilation not supported yet."
)
}
}
mainCompilations.single().value
} else null
}
fun chooseVersion(oldVersion: String?, newVersion: String): String =
oldVersion ?: newVersion // todo: real versions conflict resolution
override fun close() {
gradleNodeModules.close()
}
@@ -5,6 +5,10 @@
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(
var name: String,
var version: String
@@ -16,10 +20,40 @@ class PackageJson(
dependencies.isEmpty() &&
devDependencies.isEmpty()
val scopedName: ScopedName
get() = scopedName(name)
var private: Boolean? = null
var main: String? = null
var workspaces: Collection<String>? = null
val devDependencies = mutableMapOf<String, String>()
val dependencies = mutableMapOf<String, String>()
companion object {
fun scopedName(name: String): ScopedName = if (name.contains("/")) ScopedName(
scope = name.substringBeforeLast("/").removePrefix("@"),
name = name.substringAfterLast("/")
) else ScopedName(scope = null, name = name)
operator fun invoke(scope: String, name: String, version: String) =
PackageJson(ScopedName(scope, name).toString(), version)
}
data class ScopedName(val scope: String?, val name: String) {
override fun toString() = if (scope == null) name else "@$scope/$name"
}
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
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency
interface RequiresNpmDependencies {
val compilation: KotlinJsCompilation
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.dsl.KotlinJsBrowserDsl
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.webpack.KotlinWebpack
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfigWriter
@@ -20,7 +19,6 @@ class KotlinBrowserJs(target: KotlinJsTarget) :
KotlinJsSubTarget(target, "browser"),
KotlinJsBrowserDsl {
private val versions = project.nodeJs.versions
private val webpackTaskName = disambiguateCamelCased("webpack")
override fun configureDefaultTestFramework(it: KotlinJsTest) {
@@ -39,13 +37,12 @@ class KotlinBrowserJs(target: KotlinJsTarget) :
override fun configureRun(compilation: KotlinJsCompilation) {
val project = compilation.target.project
val npmProject = project.npmProject
project.createOrRegisterTask<KotlinWebpack>(disambiguateCamelCased("webpack")) {
val compileKotlinTask = compilation.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)
}
@@ -59,7 +56,7 @@ class KotlinBrowserJs(target: KotlinJsTarget) :
)
it.bin = "webpack-dev-server"
it.entry = npmProject.compileOutput(compileKotlinTask)
it.compilation = compilation
it.devServer = KotlinWebpackConfigWriter.DevServer(
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.nodejs.NodeJsPlugin
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.tasks.createOrRegisterTask
import org.jetbrains.kotlin.gradle.testing.internal.configureConventions
@@ -32,6 +33,11 @@ abstract class KotlinJsSubTarget(
fun configure() {
configureTests()
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> {
@@ -72,8 +78,8 @@ abstract class KotlinJsSubTarget(
compileTask.outputFile.exists()
}
testJs.compilation = compilation
testJs.targetName = target.disambiguationClassifier + "," + disambiguationClassifier
testJs.nodeModulesToLoad.add(compileTask.outputFile.nameWithoutExtension)
testJs.configureConventions()
}
@@ -5,7 +5,6 @@
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.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget
@@ -35,19 +34,18 @@ class KotlinNodeJs(target: KotlinJsTarget) :
val compileKotlinTask = compilation.compileKotlinTask
runTask.dependsOn(target.project.nodeJs.root.npmResolveTask, compileKotlinTask)
val npmProject = project.npmProject
runTask.args(npmProject.compileOutput(compileKotlinTask))
val npmProject = compilation.npmProject
runTask.args(compileKotlinTask.outputFile)
}
addSourceMapSupport(compilation, runTaskHolder, project)
addSourceMapSupport(compilation, runTaskHolder)
target.runTask.dependsOn(runTaskHolder.getTaskOrProvider())
}
private fun addSourceMapSupport(
compilation: KotlinJsCompilation,
runTaskHolder: TaskHolder<NodeJsExec>,
project: Project
runTaskHolder: TaskHolder<NodeJsExec>
) {
compilation.dependencies {
runtimeOnly(kotlin("test-nodejs-runner"))
@@ -57,7 +55,7 @@ class KotlinNodeJs(target: KotlinJsTarget) :
runTaskHolder.configure { runTask ->
runTask.args(
"--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.util.ConfigureUtil
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.npm.*
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.mocha.KotlinMocha
import org.jetbrains.kotlin.gradle.targets.js.testing.nodejs.KotlinNodeJsTestRunner
@@ -31,27 +34,36 @@ open class KotlinJsTest : KotlinTest(), RequiresNpmDependencies {
@Input
var debug: Boolean = false
@Input
var nodeModulesToLoad: MutableList<String> = mutableListOf()
@Internal
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>
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<*>) {
useNodeJs {
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<*>) {
useMocha {
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<*>) {
useKarma {
ConfigureUtil.configure(fn, this)
@@ -70,17 +82,13 @@ open class KotlinJsTest : KotlinTest(), RequiresNpmDependencies {
}
override fun executeTests() {
NpmResolver.resolve(project)
NpmResolver.checkRequiredDependencies(project, this)
super.executeTests()
}
override fun createTestExecutionSpec(): TCServiceMessagesTestExecutionSpec {
val forkOptions = DefaultProcessForkOptions(fileResolver)
NpmResolver.resolve(project)
forkOptions.workingDir = NpmProject[project].nodeWorkDir
forkOptions.workingDir = compilation.npmProject.dir
forkOptions.executable = NodeJsPlugin.apply(project).root.environment.nodeExecutable
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.TCServiceMessagesClientSettings
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.internal.parseNodeJsStackTraceAsJvm
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.npm.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.NpmPackageVersion
import org.jetbrains.kotlin.gradle.targets.js.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.KotlinJsTestFramework
@@ -29,7 +30,10 @@ import org.jetbrains.kotlin.gradle.testing.internal.reportsDir
import org.slf4j.Logger
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 requiredDependencies = mutableSetOf<NpmPackageVersion>()
@@ -183,7 +187,7 @@ class KotlinKarma(val project: Project) : KotlinJsTestFramework {
ignoreOutOfRootNodes = true
)
val npmProject = task.project.npmProject
val npmProject = compilation.npmProject
val files = task.nodeModulesToLoad.map { npmProject.require(it) }
config.files.addAll(files)
@@ -194,7 +198,7 @@ class KotlinKarma(val project: Project) : KotlinJsTestFramework {
it(task)
}
val karmaConfJs = npmProject.nodeWorkDir.resolve("karma.conf.js")
val karmaConfJs = npmProject.dir.resolve("karma.conf.js")
karmaConfJs.printWriter().use { confWriter ->
confWriter.println("module.exports = function(config) {")
confWriter.println()
@@ -9,15 +9,19 @@ import org.gradle.api.Project
import org.gradle.process.ProcessForkOptions
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings
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.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.targets.js.npm.KotlinGradleNpmPackage
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject
import org.jetbrains.kotlin.gradle.targets.js.npm.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.KotlinGradleNpmPackage
import org.jetbrains.kotlin.gradle.targets.js.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.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
override val settingsState: String
@@ -48,11 +52,11 @@ class KotlinMocha(val project: Project) : KotlinJsTestFramework {
task.nodeModulesToLoad.single()
)
val npmProjectLayout = NpmProject[task.project]
val npmProject = compilation.npmProject
val args = nodeJsArgs +
nodeModules.map {
npmProjectLayout.nodeModulesDir.resolve(it).also { file ->
npmProject.nodeModulesDir.resolve(it).also { file ->
check(file.isFile) { "Cannot find ${file.canonicalPath}" }
}.canonicalPath
} +
@@ -6,21 +6,22 @@
package org.jetbrains.kotlin.gradle.targets.js.testing.nodejs
import org.gradle.api.Project
import org.gradle.api.tasks.Input
import org.gradle.process.ProcessForkOptions
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings
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.npm.KotlinGradleNpmPackage
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject
import org.jetbrains.kotlin.gradle.targets.js.npm.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.KotlinGradleNpmPackage
import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency
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.KotlinJsTest
import org.jetbrains.kotlin.gradle.testing.IgnoredTestSuites
class KotlinNodeJsTestRunner(val project: Project) : KotlinJsTestFramework {
@Input
class KotlinNodeJsTestRunner(override val compilation: KotlinJsCompilation) : KotlinJsTestFramework {
val project: Project
get() = compilation.target.project
var ignoredTestSuites: IgnoredTestSuites = IgnoredTestSuites.showWithContents
override val settingsState: String
@@ -34,7 +35,7 @@ class KotlinNodeJsTestRunner(val project: Project) : KotlinJsTestFramework {
forkOptions: ProcessForkOptions,
nodeJsArgs: MutableList<String>
): TCServiceMessagesTestExecutionSpec {
val npmProject = project.npmProject
val npmProject = compilation.npmProject
val cliArgs = KotlinNodeJsTestRunnerCliArgs(
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"
)
val npmProjectLayout = NpmProject[task.project]
val args = nodeJsArgs +
testRuntimeNodeModules.map {
npmProjectLayout.require(it)
npmProject.require(it)
} +
cliArgs.toList()
@@ -13,8 +13,9 @@ import org.gradle.deployment.internal.DeploymentHandle
import org.gradle.deployment.internal.DeploymentRegistry
import org.gradle.process.internal.ExecHandle
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.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.RequiresNpmDependencies
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
@@ -34,12 +35,20 @@ open class KotlinWebpack : DefaultTask(), RequiresNpmDependencies {
open val execHandleFactory: ExecHandleFactory
get() = injected
@Input
@SkipWhenEmpty
open lateinit var entry: File
@Internal
override lateinit var compilation: KotlinJsCompilation
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
@OutputFile get() = project.npmProject.nodeWorkDir.resolve("webpack.config.js")
@OutputFile get() = compilation.npmProject.dir.resolve("webpack.config.js")
@Input
var saveEvaluatedConfigFile: Boolean = true
@@ -89,7 +98,7 @@ open class KotlinWebpack : DefaultTask(), RequiresNpmDependencies {
}
private fun createRunner() = KotlinWebpackRunner(
project,
compilation!!.npmProject,
configFile,
execHandleFactory,
bin,
@@ -107,7 +116,6 @@ open class KotlinWebpack : DefaultTask(), RequiresNpmDependencies {
@TaskAction
fun execute() {
NpmResolver.resolve(project)
NpmResolver.checkRequiredDependencies(project, this)
val runner = createRunner()
@@ -12,7 +12,6 @@
package org.jetbrains.kotlin.gradle.targets.js.webpack
import org.gradle.api.Project
import org.gradle.process.ExecResult
import org.gradle.process.internal.ExecHandle
import org.gradle.process.internal.ExecHandleFactory
@@ -21,7 +20,7 @@ import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver
import java.io.File
internal class KotlinWebpackRunner(
val project: Project,
val npmProject: NpmProject,
val configFile: File,
val execHandleFactory: ExecHandleFactory,
val bin: String,
@@ -41,10 +40,6 @@ internal class KotlinWebpackRunner(
"${this}: Entry file not existed \"${configWriter.entry}\""
}
NpmResolver.resolve(project)
val npmProjectLayout = NpmProject[project]
configWriter.save(configFile)
val execFactory = execHandleFactory.newExec()
@@ -54,7 +49,7 @@ internal class KotlinWebpackRunner(
args.add("--progress")
}
npmProjectLayout.useTool(execFactory, ".bin/$bin", *args.toTypedArray())
npmProject.useTool(execFactory, ".bin/$bin", *args.toTypedArray())
val exec = execFactory.build()
exec.start()
return exec
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.gradle.targets.js.yarn
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.internal.execWithProgress
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 java.io.File
@@ -16,24 +17,22 @@ object Yarn : NpmApi {
YarnPlugin.apply(project).executeSetup()
}
val Project.packageJsonHashFile: File
get() = buildDir.resolve("package.json.hash")
val NpmProject.packageJsonHashFile: File
get() = dir.resolve("package.json.hash")
fun yarnExec(
project: Project,
dir: File,
description: String,
vararg args: String,
npmProject: NpmProject = NpmProject[project]
vararg args: String
) {
val nodeJsEnv = NodeJsPlugin.apply(project).root.environment
val yarnEnv = YarnPlugin.apply(project).environment
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
exec.workingDir = dir
}
}
@@ -79,70 +78,62 @@ object Yarn : NpmApi {
}
}
@Suppress("EXPOSED_PARAMETER_TYPE")
override fun resolveProject(npmPackage: NpmResolver.NpmPackage) {
val project = npmPackage.project
override fun resolveProject(resolvedNpmProject: NpmProjectPackage) {
val project = resolvedNpmProject.project
if (!project.yarn.useWorkspaces) {
YarnAvoidance(project).updateIfNeeded {
yarnExec(project, NpmApi.resolveOperationDescription("yarn for ${project.path}"))
yarnLockReadTransitiveDependencies(NpmProject[project].nodeWorkDir, npmPackage.npmDependencies)
YarnAvoidance(resolvedNpmProject.npmProject).updateIfNeeded {
yarnExec(project, resolvedNpmProject.npmProject.dir, NpmApi.resolveOperationDescription("yarn for ${project.path}"))
yarnLockReadTransitiveDependencies(resolvedNpmProject.npmProject.dir, resolvedNpmProject.npmDependencies)
}
}
}
@Suppress("EXPOSED_PARAMETER_TYPE")
override fun hookRootPackage(
rootProject: Project,
rootPackageJson: PackageJson,
allWorkspaces: Collection<NpmResolver.NpmPackage>
): Boolean {
if (rootProject.yarn.useWorkspaces) {
rootPackageJson.private = true
rootPackageJson.workspaces = allWorkspaces
.filter { it.project != rootProject }
.map { it.project.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>
) {
override fun resolveRootProject(rootProject: Project, subProjects: MutableList<NpmProjectPackage>) {
check(rootProject == rootProject.rootProject)
if (rootProject.yarn.useWorkspaces) {
resolveWorkspaces(rootProject, subprojects)
resolveWorkspaces(rootProject, subProjects)
}
}
private fun resolveWorkspaces(
rootProject: Project,
subprojects: MutableList<NpmResolver.NpmPackage>
npmProjects: MutableList<NpmProjectPackage>
) {
val upToDateChecks = subprojects.map {
YarnAvoidance(it.project)
val upToDateChecks = npmProjects.map {
YarnAvoidance(it.npmProject)
}
if (upToDateChecks.all { it.upToDate }) return
yarnExec(rootProject, NpmApi.resolveOperationDescription("yarn"))
yarnLockReadTransitiveDependencies(NpmProject[rootProject].nodeWorkDir, subprojects.flatMap { it.npmDependencies })
val nodeJsWorldDir = rootProject.nodeJs.root.rootPackageDir
saveRootProjectWorkspacesPackageJson(rootProject, npmProjects, nodeJsWorldDir)
yarnExec(rootProject, nodeJsWorldDir, NpmApi.resolveOperationDescription("yarn"))
yarnLockReadTransitiveDependencies(nodeJsWorldDir, npmProjects.flatMap { it.npmDependencies })
upToDateChecks.forEach {
it.commit()
}
}
override fun cleanProject(project: Project) {
super.cleanProject(project)
project.packageJsonHashFile.delete()
private fun saveRootProjectWorkspacesPackageJson(
rootProject: Project,
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.jetbrains.kotlin.daemon.common.toHexString
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.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 {
val packageJsonHashFile = project.packageJsonHashFile
val packageJsonHashFile = npmProject.packageJsonHashFile
if (packageJsonHashFile.exists()) packageJsonHashFile.readText() else null
}
private val npmProject = project.npmProject
private val packageJsonFile = npmProject.packageJsonFile
private val hash by lazy {
@@ -34,7 +39,7 @@ class YarnAvoidance(val project: Project) {
fun commit() {
if (!upToDate) {
val packageJsonHashFile = project.packageJsonHashFile
val packageJsonHashFile = npmProject.packageJsonHashFile
packageJsonHashFile.ensureParentDirsCreated()
packageJsonHashFile.writeText(hash)
}
@@ -26,7 +26,7 @@ open class YarnRootExtension(val project: Project) {
var disableWorkspaces: Boolean = false
val useWorkspaces: Boolean
get() = !disableWorkspaces && project.nodeJs.root.layout.allowYarnWorkspaces
get() = !disableWorkspaces
internal fun executeSetup() {
NodeJsPlugin.apply(project).root.executeSetup()