Gradle, JS: new nodejs project layout

#KT-30530
This commit is contained in:
Sergey Rostov
2019-04-23 10:13:45 +03:00
parent eb7d09a562
commit f28ff64e9f
18 changed files with 241 additions and 212 deletions
@@ -0,0 +1,17 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.internal
import java.io.File
fun File.ensureParentDirsCreated() {
val parentFile = parentFile
if (!parentFile.exists()) {
check(parentFile.mkdirs()) {
"Cannot create parent directories for $this"
}
}
}
@@ -11,10 +11,10 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetProcessor
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetConfigurator import org.jetbrains.kotlin.gradle.plugin.KotlinTargetConfigurator
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTarget
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import org.jetbrains.kotlin.gradle.targets.js.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.tasks.KotlinTasksProvider import org.jetbrains.kotlin.gradle.tasks.KotlinTasksProvider
import org.jetbrains.kotlin.gradle.tasks.createOrRegisterTask
open class KotlinJsTargetConfigurator(kotlinPluginVersion: String) : open class KotlinJsTargetConfigurator(kotlinPluginVersion: String) :
KotlinTargetConfigurator<KotlinJsCompilation>(true, true, kotlinPluginVersion) { KotlinTargetConfigurator<KotlinJsCompilation>(true, true, kotlinPluginVersion) {
@@ -28,6 +28,8 @@ open class KotlinJsTargetConfigurator(kotlinPluginVersion: String) :
super.configureCompilations(platformTarget) super.configureCompilations(platformTarget)
platformTarget.compilations.all { platformTarget.compilations.all {
platformTarget.project.npmProject.configureCompilation(it)
it.compileKotlinTask.kotlinOptions { it.compileKotlinTask.kotlinOptions {
moduleKind = "commonjs" moduleKind = "commonjs"
sourceMap = true sourceMap = true
@@ -5,6 +5,7 @@ import org.gradle.api.Project
import org.gradle.api.plugins.BasePlugin 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.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 {
@@ -35,6 +36,17 @@ open class NodeJsPlugin : Plugin<Project> {
project.tasks.maybeCreate(BasePlugin.CLEAN_TASK_NAME).dependsOn(it) project.tasks.maybeCreate(BasePlugin.CLEAN_TASK_NAME).dependsOn(it)
} }
project.tasks.create("cleanGradleNodeModules", Delete::class.java) {
it.description = "Deletes node_modules_gradle"
it.group = BasePlugin.BUILD_GROUP
it.doLast {
project.npmProject.gradleNodeModulesDir.deleteRecursively()
}
project.tasks.maybeCreate(BasePlugin.CLEAN_TASK_NAME).dependsOn(it)
}
} }
companion object { companion object {
@@ -2,6 +2,7 @@ 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.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.yarn.Yarn import org.jetbrains.kotlin.gradle.targets.js.yarn.Yarn
import java.io.File import java.io.File
@@ -19,10 +20,7 @@ open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) {
var nodeCommand = "node" var nodeCommand = "node"
var npmCustomVersion: String? = null var layout: NpmProjectLayout = NpmProjectLayout.ROOT_PROJECT_BUILD_DIR
var npmCommand = "npm"
var manageNodeModules: Boolean = false
var packageManager: NpmApi = Yarn var packageManager: NpmApi = Yarn
val nodeJsSetupTask: NodeJsSetupTask val nodeJsSetupTask: NodeJsSetupTask
@@ -51,7 +49,6 @@ open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) {
nodeDir = nodeDir, nodeDir = nodeDir,
nodeBinDir = nodeBinDir, nodeBinDir = nodeBinDir,
nodeExecutable = getExecutable("node", nodeCommand, "exe"), nodeExecutable = getExecutable("node", nodeCommand, "exe"),
npmExecutable = getExecutable("npm", npmCommand, "cmd"),
platformName = platform, platformName = platform,
architectureName = architecture, architectureName = architecture,
ivyDependency = getIvyDependency() ivyDependency = getIvyDependency()
@@ -16,7 +16,7 @@ internal class GradleNodeModuleBuilder(
val project: Project, val project: Project,
val dependency: ResolvedDependency, val dependency: ResolvedDependency,
val artifacts: Set<ResolvedArtifact>, val artifacts: Set<ResolvedArtifact>,
val cache: GradleNodeModules val cache: GradleNodeModulesBuilder
) { ) {
var srcPackageJsonFile: File? = null var srcPackageJsonFile: File? = null
val files = mutableListOf<File>() val files = mutableListOf<File>()
@@ -8,23 +8,19 @@ package org.jetbrains.kotlin.gradle.targets.js.npm
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.artifacts.ResolvedArtifact import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.artifacts.ResolvedDependency import org.gradle.api.artifacts.ResolvedDependency
import org.gradle.api.file.CopySpec import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions
import org.jetbrains.kotlin.gradle.internal.ProcessedFilesCache import org.jetbrains.kotlin.gradle.internal.ProcessedFilesCache
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.targets.js.internal.RewriteSourceMapFilterReader
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
internal class GradleNodeModules(val project: Project) : AutoCloseable { internal class GradleNodeModulesBuilder(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.buildDir.resolve("node_modules_gradle")
private val nodeModulesDir = project.npmProject.nodeModulesDir
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>()
private val doLast = mutableListOf<() -> Unit>()
val modules val modules
get() = cache.targets.map { get() = cache.targets.map {
@@ -33,29 +29,16 @@ internal class GradleNodeModules(val project: Project) : AutoCloseable {
fun visitCompilation(compilation: KotlinCompilation<KotlinCommonOptions>) { fun visitCompilation(compilation: KotlinCompilation<KotlinCommonOptions>) {
val project = compilation.target.project val project = compilation.target.project
val kotlin2JsCompile = compilation.compileKotlinTask as Kotlin2JsCompile
// classpath // classpath
compilation.relatedConfigurationNames.forEach { compilation.relatedConfigurationNames.forEach { configurationName ->
val configuration = project.configurations.getByName(it) val configuration = project.configurations.getByName(configurationName)
if (configuration.isCanBeResolved) { if (configuration.isCanBeResolved) {
configuration.resolvedConfiguration.firstLevelModuleDependencies.forEach { configuration.resolvedConfiguration.firstLevelModuleDependencies.forEach {
visitDependency(compilation, it) visitDependency(compilation, it)
} }
} }
} }
// output
doLast.add {
// we should do it only after node package manger work (as it can clean files)
if (kotlin2JsCompile.state.executed) {
visitCompile(project, kotlin2JsCompile)
} else {
kotlin2JsCompile.doLast {
visitCompile(project, kotlin2JsCompile)
}
}
}
} }
private fun visitDependency( private fun visitDependency(
@@ -82,37 +65,15 @@ internal class GradleNodeModules(val project: Project) : AutoCloseable {
} }
artifacts.forEach { artifact -> artifacts.forEach { artifact ->
cache.getOrCompute(artifact.file) { if (artifact.id.componentIdentifier !is ProjectComponentIdentifier) {
lazyDirName cache.getOrCompute(artifact.file) {
} lazyDirName
} }
}
private fun visitCompile(project: Project, kotlin2JsCompile: Kotlin2JsCompile) {
project.copy { copy ->
copy.from(kotlin2JsCompile.outputFile)
copy.from(kotlin2JsCompile.outputFile.path + ".map")
copy.into(nodeModulesDir)
copy.withSourceMapRewriter()
}
}
private fun CopySpec.withSourceMapRewriter() {
eachFile {
if (it.name.endsWith(".js.map")) {
it.filter(
mapOf(
"srcSourceRoot" to it.file.parentFile,
"targetSourceRoot" to nodeModulesDir
),
RewriteSourceMapFilterReader::class.java
)
} }
} }
} }
override fun close() { override fun close() {
doLast.forEach { it() }
cache.close() cache.close()
} }
} }
@@ -30,7 +30,7 @@ interface NpmApi {
allWorkspaces: Collection<NpmResolver.NpmPackage> allWorkspaces: Collection<NpmResolver.NpmPackage>
): Boolean = false ): Boolean = false
open fun getHoistGradleNodeModules(project: Project) = false open fun shouldHoistGradleNodeModules(project: Project) = false
companion object { companion object {
fun resolveOperationDescription(packageManagerTitle: String): String = fun resolveOperationDescription(packageManagerTitle: String): String =
@@ -40,7 +40,7 @@ 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.tryFindNodeModule()?.let { item.project.npmProject.findModuleEntry(item.key)?.let {
all.add(it) all.add(it)
} }
} }
@@ -50,7 +50,7 @@ data class NpmDependency(
return all return all
} }
internal fun resolveProject(): NpmResolver.ResolvedProject? { private fun resolveProject(): NpmResolver.ResolvedProject? {
val result = NpmResolver.resolve(project) val result = NpmResolver.resolve(project)
return when (result) { return when (result) {
@@ -72,15 +72,6 @@ data class NpmDependency(
} }
} }
private fun tryFindNodeModule(): File? {
var p = project
do {
val result = NpmProjectLayout[p].nodeModulesDir.resolve(key)
if (result.exists()) return result
p = project.parent ?: return null
} while (true)
}
val key: String = if (org == null) name else "@$org/$name" val key: String = if (org == null) name else "@$org/$name"
override fun toString() = "$key: $version" override fun toString() = "$key: $version"
@@ -10,48 +10,54 @@ import com.google.gson.JsonObject
import groovy.transform.TailRecursive import groovy.transform.TailRecursive
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.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.nodejs.nodeJs import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
import java.io.File import java.io.File
class NpmProject( open class NpmProject(
val project: Project, val project: Project,
val nodeWorkDir: File, val nodeWorkDir: File,
val managed: Boolean val searchInParents: Boolean
) { ) {
val nodeModulesDir val nodeModulesDir
get() = nodeWorkDir.resolve(NODE_MODULES) get() = nodeWorkDir.resolve(NODE_MODULES)
val packageJsonFile: File val packageJsonFile: File
get() { get() = nodeWorkDir.resolve(PACKAGE_JSON)
return nodeWorkDir.resolve(PACKAGE_JSON)
} open val compileOutputCopyDest: File?
get() = nodeModulesDir
fun useTool(exec: ExecSpec, tool: String, vararg args: String) { fun useTool(exec: ExecSpec, tool: String, vararg args: String) {
NpmResolver.resolve(project) NpmResolver.resolve(project)
exec.workingDir = nodeWorkDir exec.workingDir = nodeWorkDir
exec.executable = project.nodeJs.root.environment.nodeExecutable exec.executable = project.nodeJs.root.environment.nodeExecutable
exec.args = listOf(findModule(tool)) + args exec.args = listOf(getModuleEntryPath(tool)) + args
} }
val hoistGradleNodeModules: Boolean val hoistGradleNodeModules: Boolean
get() = project.nodeJs.root.packageManager.getHoistGradleNodeModules(project) get() = project.nodeJs.root.packageManager.shouldHoistGradleNodeModules(project)
val gradleNodeModulesDir: File val gradleNodeModulesDir: File
get() = get() =
if (hoistGradleNodeModules) project.rootProject.npmProject.nodeModulesDir if (hoistGradleNodeModules) project.rootProject.npmProject.nodeModulesDir
else nodeModulesDir else nodeModulesDir
fun moduleOutput(compilationTask: Kotlin2JsCompile): File { open fun moduleOutput(compilationTask: Kotlin2JsCompile): File {
return gradleNodeModulesDir.resolve(compilationTask.outputFile.name) return compileOutputCopyDest?.resolve(compilationTask.outputFile.name) ?: compilationTask.outputFile
}
fun getModuleEntryPath(name: String): String {
return findModuleEntry(name)?.canonicalPath ?: error("Cannot find node module $name in $this")
} }
@TailRecursive @TailRecursive
fun findModule(name: String, src: NpmProject = this): String { fun findModuleEntry(name: String): File? {
val file = nodeModulesDir.resolve(name) val file = nodeModulesDir.resolve(name)
if (file.isFile) return file.canonicalPath if (file.isFile) return file
if (file.isDirectory) { if (file.isDirectory) {
val packageJsonFile = file.resolve("package.json") val packageJsonFile = file.resolve("package.json")
val main: String = (if (packageJsonFile.isFile) { val main: String = (if (packageJsonFile.isFile) {
@@ -59,18 +65,22 @@ class NpmProject(
Gson().fromJson(it, JsonObject::class.java) Gson().fromJson(it, JsonObject::class.java)
} }
packageJson["main"] as? String? ?: packageJson["module"] as? String? ?: packageJson["browser"] as? String? packageJson["main"] as? String?
?: packageJson["module"] as? String?
?: packageJson["browser"] as? String?
} else null) ?: "index.js" } else null) ?: "index.js"
val mainFile = file.resolve(main) val mainFile = file.resolve(main)
if (mainFile.isFile) return mainFile.canonicalPath if (mainFile.isFile) return mainFile
} }
val parent = project.parent val parent = project.parent
return if (!managed || parent == null) error("Cannot find node module $name in $src") return if (!searchInParents || parent == null) null
else NpmProject[parent].findModule(name, src) else NpmProject[parent].findModuleEntry(name)
} }
open fun configureCompilation(compilation: KotlinJsCompilation) = Unit
override fun toString() = "NpmProject($nodeWorkDir)" override fun toString() = "NpmProject($nodeWorkDir)"
companion object { companion object {
@@ -80,13 +90,7 @@ class NpmProject(
operator fun get(project: Project): NpmProject { operator fun get(project: Project): NpmProject {
val nodeJsRootExtension = NodeJsPlugin.apply(project) val nodeJsRootExtension = NodeJsPlugin.apply(project)
val manageNodeModules = nodeJsRootExtension.root.manageNodeModules return nodeJsRootExtension.root.layout.newNpmProject(project)
val nodeWorkDir =
if (manageNodeModules) project.projectDir
else project.buildDir
return NpmProject(project, nodeWorkDir, manageNodeModules)
} }
} }
} }
@@ -5,76 +5,59 @@
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.JsonObject
import groovy.transform.TailRecursive
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.process.ExecSpec import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import java.io.File import java.io.File
class NpmProjectLayout( enum class NpmProjectLayout {
val project: Project, PROJECT_DIR {
val nodeWorkDir: File, override val allowYarnWorkspaces: Boolean
val managed: Boolean get() = true
) {
val nodeModulesDir
get() = nodeWorkDir.resolve(NODE_MODULES)
val packageJsonFile: File override fun newNpmProject(project: Project) =
get() { NpmProject(project, project.projectDir, searchInParents = true)
return nodeWorkDir.resolve(PACKAGE_JSON) },
} ROOT_PROJECT_BUILD_DIR {
override val allowYarnWorkspaces: Boolean
get() = true
fun useTool(exec: ExecSpec, tool: String, vararg args: String) { override fun newNpmProject(project: Project): NpmProject {
NpmResolver.resolve(project) val nodeJsWorldDir = project.rootProject.buildDir.resolve("nodejs-project")
exec.workingDir = nodeWorkDir val workDir =
exec.executable = project.nodeJs.root.environment.nodeExecutable if (project == project.rootProject) nodeJsWorldDir
exec.args = listOf(findModule(tool)) + args else {
} val projectPackage = project.path.removePrefix(":").replace(":", File.separator)
nodeJsWorldDir.resolve("packages").resolve(projectPackage)
@TailRecursive
fun findModule(name: String, src: NpmProjectLayout = this): String {
val file = nodeModulesDir.resolve(name)
if (file.isFile) return file.canonicalPath
if (file.isDirectory) {
val packageJsonFile = file.resolve("package.json")
val main: String = (if (packageJsonFile.isFile) {
val packageJson = packageJsonFile.reader().use {
Gson().fromJson(it, JsonObject::class.java)
} }
packageJson["main"] as? String? ?: packageJson["module"] as? String? ?: packageJson["browser"] as? String? return object : NpmProject(
} else null) ?: "index.js" project,
workDir,
searchInParents = true
) {
override val compileOutputCopyDest: File?
get() = nodeWorkDir
val mainFile = file.resolve(main) override fun configureCompilation(compilation: KotlinJsCompilation) {
if (mainFile.isFile) return mainFile.canonicalPath // val moduleName = compilation.compileKotlinTask.moduleName
// workDir.mkdirs()
// compilation.kotlinOptions.outputFile = workDir.resolve("$moduleName.js").absolutePath
}
// override fun moduleOutput(compilationTask: Kotlin2JsCompile) =
// compilationTask.outputFile
}
} }
},
BUILD_DIR {
override fun newNpmProject(project: Project) =
NpmProject(project, project.buildDir, searchInParents = false)
};
val parent = project.parent open val allowYarnWorkspaces: Boolean
return if (!managed || parent == null) error("Cannot find node module $name in $src") get() = false
else NpmProjectLayout[parent].findModule(name, src)
}
companion object { abstract fun newNpmProject(project: Project): NpmProject
const val PACKAGE_JSON = "package.json"
const val NODE_MODULES = "node_modules"
operator fun get(project: Project): NpmProjectLayout {
val nodeJsRootExtension = NodeJsPlugin.apply(project)
val manageNodeModules = nodeJsRootExtension.root.manageNodeModules
val nodeWorkDir =
if (manageNodeModules) project.projectDir
else project.buildDir
return NpmProjectLayout(project, nodeWorkDir, manageNodeModules)
}
}
} }
val Project.npmProject
get() = NpmProjectLayout[this]
@@ -8,25 +8,28 @@ package org.jetbrains.kotlin.gradle.targets.js.npm
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.file.CopySpec
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension
import org.jetbrains.kotlin.gradle.dsl.kotlinExtensionOrNull 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.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
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.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
/** /**
* [NpmResolver] runs selected [NodeJsRootExtension.packageManager] on root project * Generates `package.json` file for projects with npm or js dependencies and
* with configured `package.json` files in all projects. * runs selected [NodeJsRootExtension.packageManager] to download and install it.
* *
* `package.json` * All [NpmDependency] for configurations related to kotlin/js will be added to `package.json`.
* * For external gradle modules, fake npm packages will be created and added to `package.json`
* - compile(npm(...)) dependencies * as path to directory.
* - compile(npm(...)) dependencies from external modules
* - compile(non npm(...)) dependencies transitively
*/ */
internal class NpmResolver private constructor(val rootProject: Project) { internal class NpmResolver private constructor(val rootProject: Project) {
companion object { companion object {
@@ -55,8 +58,9 @@ internal class NpmResolver private constructor(val rootProject: Project) {
} }
private val nodeJs = NodeJsPlugin.apply(rootProject).root private val nodeJs = NodeJsPlugin.apply(rootProject).root
private val npmProject = rootProject.npmProject
private val packageManager = nodeJs.packageManager private val packageManager = nodeJs.packageManager
private val hoistGradleNodeModules = packageManager.getHoistGradleNodeModules(rootProject) private val hoistGradleNodeModules = npmProject.hoistGradleNodeModules
private val npmPackages = mutableListOf<NpmPackage>() private val npmPackages = mutableListOf<NpmPackage>()
private val gson = GsonBuilder() private val gson = GsonBuilder()
.setPrettyPrinting() .setPrettyPrinting()
@@ -82,7 +86,9 @@ internal class NpmResolver private constructor(val rootProject: Project) {
val npmDependencies: Set<NpmDependency> val npmDependencies: Set<NpmDependency>
) { ) {
fun savePackageJson(gson: Gson) { fun savePackageJson(gson: Gson) {
NpmProjectLayout[project].packageJsonFile.writer().use { val packageJsonFile = NpmProject[project].packageJsonFile
packageJsonFile.ensureParentDirsCreated()
packageJsonFile.writer().use {
gson.toJson(packageJson, it) gson.toJson(packageJson, it)
} }
} }
@@ -98,7 +104,7 @@ internal class NpmResolver private constructor(val rootProject: Project) {
private fun resolve( private fun resolve(
project: Project, project: Project,
parentGradleNodeModules: GradleNodeModulesSync? parentGradleModules: GradleNodeModulesBuilder?
): Boolean { ): Boolean {
val existedProcess = ProjectData[project] val existedProcess = ProjectData[project]
if (existedProcess != null) { if (existedProcess != null) {
@@ -112,15 +118,15 @@ internal class NpmResolver private constructor(val rootProject: Project) {
ProjectData[project] = it ProjectData[project] = it
} }
val gradleNodeModules = val gradleModules =
if (hoistGradleNodeModules && parentGradleNodeModules != null) parentGradleNodeModules if (hoistGradleNodeModules && parentGradleModules != null) parentGradleModules
else newGradleNodeModulesSync(project) else GradleNodeModulesBuilder(project)
project.subprojects.forEach { project.subprojects.forEach {
resolve(it, gradleNodeModules) resolve(it, gradleModules)
} }
val npmPackage = extractNpmPackage(project, gradleNodeModules) val npmPackage = extractNpmPackage(project, gradleModules)
if (npmPackage != null) { if (npmPackage != null) {
npmPackage.savePackageJson(gson) npmPackage.savePackageJson(gson)
@@ -137,70 +143,60 @@ internal class NpmResolver private constructor(val rootProject: Project) {
} }
} }
if (gradleNodeModules != parentGradleNodeModules) { if (gradleModules != parentGradleModules) {
gradleNodeModules.sync() gradleModules.close()
} }
return true return true
} }
private fun newGradleNodeModulesSync(project: Project): GradleNodeModulesSync {
return GradleNodeModulesSync(project).also {
it.loadOldState()
}
}
private fun extractNpmPackage( private fun extractNpmPackage(
project: Project, project: Project,
gradleComponentsSync: GradleNodeModulesSync gradleComponents: GradleNodeModulesBuilder
): NpmPackage? { ): NpmPackage? {
val packageJson = PackageJson(project.name, project.version.toString()) val packageJson = PackageJson(project.name, project.version.toString())
val npmDependencies = mutableSetOf<NpmDependency>() val npmDependencies = mutableSetOf<NpmDependency>()
visitNpmDependencies(project, npmDependencies, gradleComponentsSync, packageJson) visitNpmDependencies(project, npmDependencies, gradleComponents, packageJson)
if (!hoistGradleNodeModules || project == project.rootProject) {
gradleComponents.modules.forEach {
val relativePath = it.path.relativeTo(NpmProject[project].nodeWorkDir)
packageJson.dependencies[it.name] = "file:$relativePath"
}
}
project.nodeJs.packageJsonHandlers.forEach { project.nodeJs.packageJsonHandlers.forEach {
it(packageJson) it(packageJson)
} }
val packageJsonRequired = if (project == rootProject) { val packageJsonRequired =
packageManager.hookRootPackage(rootProject, packageJson, npmPackages) if (project == rootProject) packageManager.hookRootPackage(rootProject, packageJson, npmPackages)
} else false else false
return if (packageJsonRequired || !packageJson.empty) NpmPackage(project, packageJson, npmDependencies) else null return if (!packageJsonRequired && packageJson.empty) null
else NpmPackage(project, packageJson, npmDependencies)
} }
private fun visitNpmDependencies( private fun visitNpmDependencies(
project: Project, project: Project,
npmDependencies: MutableSet<NpmDependency>, npmDependencies: MutableSet<NpmDependency>,
gradleComponentsSync: GradleNodeModulesSync, gradleComponents: GradleNodeModulesBuilder,
packageJson: PackageJson packageJson: PackageJson
) { ) {
val kotlin = project.kotlinExtensionOrNull val kotlin = project.kotlinExtensionOrNull
if (kotlin != null) { if (kotlin != null) {
val transitiveDependencies = mutableListOf<GradleNodeModulesSync.TransitiveNpmDependency>()
when (kotlin) { when (kotlin) {
is KotlinSingleTargetExtension -> visitTarget( is KotlinSingleTargetExtension -> visitTarget(kotlin.target, project, npmDependencies, gradleComponents, packageJson)
kotlin.target,
project,
npmDependencies,
gradleComponentsSync,
transitiveDependencies
)
is KotlinMultiplatformExtension -> kotlin.targets.forEach { is KotlinMultiplatformExtension -> kotlin.targets.forEach {
visitTarget(it, project, npmDependencies, gradleComponentsSync, transitiveDependencies) visitTarget(it, project, npmDependencies, gradleComponents, packageJson)
} }
} }
npmDependencies.forEach { npmDependencies.forEach {
packageJson.dependencies[it.key] = chooseVersion(packageJson.dependencies[it.key], it.version) packageJson.dependencies[it.key] = chooseVersion(packageJson.dependencies[it.key], it.version)
} }
transitiveDependencies.forEach {
packageJson.dependencies[it.key] = chooseVersion(packageJson.dependencies[it.key], it.version)
}
} }
} }
@@ -211,8 +207,8 @@ internal class NpmResolver private constructor(val rootProject: Project) {
target: KotlinTarget, target: KotlinTarget,
project: Project, project: Project,
npmDependencies: MutableSet<NpmDependency>, npmDependencies: MutableSet<NpmDependency>,
gradleComponentsSync: GradleNodeModulesSync, gradleComponents: GradleNodeModulesBuilder,
transitiveDependencies: MutableList<GradleNodeModulesSync.TransitiveNpmDependency> packageJson: PackageJson
) { ) {
if (target.platformType == KotlinPlatformType.js) { if (target.platformType == KotlinPlatformType.js) {
target.compilations.toList().forEach { compilation -> target.compilations.toList().forEach { compilation ->
@@ -224,7 +220,55 @@ internal class NpmResolver private constructor(val rootProject: Project) {
} }
} }
gradleComponentsSync.visitCompilation(compilation, project, transitiveDependencies) gradleComponents.visitCompilation(compilation)
if (compilation is KotlinJsCompilation) {
visitKotlinJsCompilation(project, compilation)
if (compilation.name == "main") {
packageJson.main = compilation.compileKotlinTask.outputFile.name
}
}
}
}
}
private fun visitKotlinJsCompilation(
project: Project,
compilation: KotlinJsCompilation
) {
val npmProject = project.npmProject
val kotlin2JsCompile = compilation.compileKotlinTask
if (npmProject.compileOutputCopyDest != null) {
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
)
} }
} }
} }
@@ -10,7 +10,8 @@ class PackageJson(
var version: String var version: String
) { ) {
val empty: Boolean val empty: Boolean
get() = private == null && get() = main == null &&
private == null &&
workspaces == null && workspaces == null &&
dependencies.isEmpty() && dependencies.isEmpty() &&
devDependencies.isEmpty() devDependencies.isEmpty()
@@ -54,7 +54,7 @@ class KotlinNodeJsTestRunner : KotlinJsTestFramework {
val args = nodeJsArgs + val args = nodeJsArgs +
testRuntimeNodeModules.map { testRuntimeNodeModules.map {
npmProjectLayout.findModule(it) npmProjectLayout.getModuleEntryPath(it)
} + } +
cliArgs.toList() cliArgs.toList()
@@ -37,14 +37,11 @@ open class KotlinWebpack : DefaultTask() {
open lateinit var entry: File open lateinit var entry: File
open val configFile: File open val configFile: File
@OutputFile get() = project.buildDir.resolve("webpack.config.js") @OutputFile get() = project.npmProject.nodeWorkDir.resolve("webpack.config.js")
@Input @Input
var saveEvaluatedConfigFile: Boolean = true var saveEvaluatedConfigFile: Boolean = true
open val evaluatedConfigFile: File
@OutputFile get() = project.buildDir.resolve("webpack.config.evaluated.js")
open val outputPath: File open val outputPath: File
@OutputDirectory get() = project.buildDir.resolve("lib") @OutputDirectory get() = project.buildDir.resolve("lib")
@@ -57,10 +54,14 @@ open class KotlinWebpack : DefaultTask() {
open val reportDir: File open val reportDir: File
@OutputDirectory get() = project.reportsDir.resolve("webpack").resolve(entry.nameWithoutExtension) @OutputDirectory get() = project.reportsDir.resolve("webpack").resolve(entry.nameWithoutExtension)
open val evaluatedConfigFile: File
@OutputFile get() = reportDir.resolve("webpack.config.evaluated.js")
@Input @Input
var bin: String = "webpack" var bin: String = "webpack"
@Input @Input
@Optional
var devServer: KotlinWebpackConfig.DevServer? = null var devServer: KotlinWebpackConfig.DevServer? = null
internal fun createRunner() = KotlinWebpackRunner( internal fun createRunner() = KotlinWebpackRunner(
@@ -125,6 +126,8 @@ open class KotlinWebpack : DefaultTask() {
it.dependsOn(compileKotlinTask) it.dependsOn(compileKotlinTask)
it.entry = npmProject.moduleOutput(compileKotlinTask) it.entry = npmProject.moduleOutput(compileKotlinTask)
it.outputs.upToDateWhen { false }
} }
compilation.dependencies { compilation.dependencies {
@@ -13,6 +13,7 @@
package org.jetbrains.kotlin.gradle.targets.js.webpack package org.jetbrains.kotlin.gradle.targets.js.webpack
import org.gradle.api.Project import org.gradle.api.Project
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
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject
@@ -26,9 +27,13 @@ internal class KotlinWebpackRunner(
val bin: String, val bin: String,
val config: KotlinWebpackConfig val config: KotlinWebpackConfig
) { ) {
fun execute() { fun execute(): ExecResult {
val exec = start() val exec = start()
exec.waitForFinish() val result = exec.waitForFinish()
check(result.exitValue == 0) {
"Webpack exited with non zero exit code (${result.exitValue})"
}
return result
} }
fun start(): ExecHandle { fun start(): ExecHandle {
@@ -43,12 +48,13 @@ internal class KotlinWebpackRunner(
config.save(configFile) config.save(configFile)
val execFactory = execHandleFactory.newExec() val execFactory = execHandleFactory.newExec()
val args = mutableListOf<String>("--config", configFile.absolutePath) val args = mutableListOf<String>("--config", configFile.absolutePath)
if (config.showProgress) { if (config.showProgress) {
args.add("--progress") args.add("--progress")
} }
npmProjectLayout.useTool(execFactory, ".bin/$bin") npmProjectLayout.useTool(execFactory, ".bin/$bin", *args.toTypedArray())
val exec = execFactory.build() val exec = execFactory.build()
exec.start() exec.start()
return exec return exec
@@ -103,7 +103,7 @@ object Yarn : NpmApi {
rootPackageJson.private = true rootPackageJson.private = true
rootPackageJson.workspaces = allWorkspaces rootPackageJson.workspaces = allWorkspaces
.filter { it.project != rootProject } .filter { it.project != rootProject }
.map { it.project.projectDir.relativeTo(rootProject.rootDir).path } .map { it.project.npmProject.nodeWorkDir.relativeTo(rootProject.npmProject.nodeWorkDir).path }
return true return true
} }
@@ -111,7 +111,7 @@ object Yarn : NpmApi {
return false return false
} }
override fun getHoistGradleNodeModules(project: Project): Boolean = override fun shouldHoistGradleNodeModules(project: Project): Boolean =
project.rootProject.yarn.useWorkspaces project.rootProject.yarn.useWorkspaces
@Suppress("EXPOSED_PARAMETER_TYPE") @Suppress("EXPOSED_PARAMETER_TYPE")
@@ -9,6 +9,7 @@ import org.gradle.api.Project
import org.gradle.api.internal.project.ProjectInternal 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.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
@@ -18,17 +19,24 @@ class YarnAvoidance(val project: Project) {
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 hash by lazy { private val hash by lazy {
val hasher = (project as ProjectInternal).services.get(FileHasher::class.java) val hasher = (project as ProjectInternal).services.get(FileHasher::class.java)
hasher.hash(project.npmProject.packageJsonFile).toByteArray().toHexString() hasher.hash(packageJsonFile).toByteArray().toHexString()
} }
val upToDate: Boolean val upToDate: Boolean
get() = prevHash == hash get() = packageJsonFile.exists() &&
npmProject.nodeModulesDir.isDirectory &&
prevHash == hash
fun commit() { fun commit() {
if (!upToDate) { if (!upToDate) {
project.packageJsonHashFile.writeText(hash) val packageJsonHashFile = project.packageJsonHashFile
packageJsonHashFile.ensureParentDirsCreated()
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() = project.nodeJs.root.manageNodeModules && !disableWorkspaces get() = !disableWorkspaces && project.nodeJs.root.layout.allowYarnWorkspaces
internal fun executeSetup() { internal fun executeSetup() {
NodeJsPlugin.apply(project).root.executeSetup() NodeJsPlugin.apply(project).root.executeSetup()