From f28ff64e9f985418ca877ee44453e33facbbda4f Mon Sep 17 00:00:00 2001 From: Sergey Rostov Date: Tue, 23 Apr 2019 10:13:45 +0300 Subject: [PATCH] Gradle, JS: new nodejs project layout #KT-30530 --- .../kotlin/gradle/internal/fileUtils.kt | 17 +++ .../targets/js/KotlinJsTargetConfigurator.kt | 4 +- .../gradle/targets/js/nodejs/NodeJsPlugin.kt | 12 ++ .../targets/js/nodejs/NodeJsRootExtension.kt | 7 +- .../targets/js/npm/GradleNodeModuleBuilder.kt | 2 +- ...Modules.kt => GradleNodeModulesBuilder.kt} | 57 ++------ .../kotlin/gradle/targets/js/npm/NpmApi.kt | 2 +- .../gradle/targets/js/npm/NpmDependency.kt | 13 +- .../gradle/targets/js/npm/NpmProject.kt | 48 +++--- .../gradle/targets/js/npm/NpmProjectLayout.kt | 105 ++++++------- .../gradle/targets/js/npm/NpmResolver.kt | 138 ++++++++++++------ .../gradle/targets/js/npm/PackageJson.kt | 3 +- .../testing/nodejs/KotlinNodeJsTestRunner.kt | 2 +- .../targets/js/webpack/KotlinWebpack.kt | 11 +- .../targets/js/webpack/KotlinWebpackRunner.kt | 12 +- .../kotlin/gradle/targets/js/yarn/Yarn.kt | 4 +- .../gradle/targets/js/yarn/YarnAvoidance.kt | 14 +- .../targets/js/yarn/YarnRootExtension.kt | 2 +- 18 files changed, 241 insertions(+), 212 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/fileUtils.kt rename libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/{GradleNodeModules.kt => GradleNodeModulesBuilder.kt} (56%) diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/fileUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/fileUtils.kt new file mode 100644 index 00000000000..24e668096a8 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/fileUtils.kt @@ -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" + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetConfigurator.kt index 5e7ba11fe1c..4c8b24b89f6 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetConfigurator.kt @@ -11,10 +11,10 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetProcessor import org.jetbrains.kotlin.gradle.plugin.KotlinTargetConfigurator import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTarget +import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack import org.jetbrains.kotlin.gradle.tasks.KotlinTasksProvider -import org.jetbrains.kotlin.gradle.tasks.createOrRegisterTask open class KotlinJsTargetConfigurator(kotlinPluginVersion: String) : KotlinTargetConfigurator(true, true, kotlinPluginVersion) { @@ -28,6 +28,8 @@ open class KotlinJsTargetConfigurator(kotlinPluginVersion: String) : super.configureCompilations(platformTarget) platformTarget.compilations.all { + platformTarget.project.npmProject.configureCompilation(it) + it.compileKotlinTask.kotlinOptions { moduleKind = "commonjs" sourceMap = true diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsPlugin.kt index 47804396cbe..bcfa125461a 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsPlugin.kt @@ -5,6 +5,7 @@ import org.gradle.api.Project 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.npmProject open class NodeJsPlugin : Plugin { override fun apply(project: Project): Unit = project.run { @@ -35,6 +36,17 @@ open class NodeJsPlugin : Plugin { 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 { diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt index 618ba3d5498..73b37c98ec0 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt @@ -2,6 +2,7 @@ package org.jetbrains.kotlin.gradle.targets.js.nodejs import org.gradle.api.Project import org.jetbrains.kotlin.gradle.logging.kotlinInfo +import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProjectLayout import org.jetbrains.kotlin.gradle.targets.js.npm.NpmApi import org.jetbrains.kotlin.gradle.targets.js.yarn.Yarn import java.io.File @@ -19,10 +20,7 @@ open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) { var nodeCommand = "node" - var npmCustomVersion: String? = null - var npmCommand = "npm" - - var manageNodeModules: Boolean = false + var layout: NpmProjectLayout = NpmProjectLayout.ROOT_PROJECT_BUILD_DIR var packageManager: NpmApi = Yarn val nodeJsSetupTask: NodeJsSetupTask @@ -51,7 +49,6 @@ open class NodeJsRootExtension(project: Project) : NodeJsExtension(project) { nodeDir = nodeDir, nodeBinDir = nodeBinDir, nodeExecutable = getExecutable("node", nodeCommand, "exe"), - npmExecutable = getExecutable("npm", npmCommand, "cmd"), platformName = platform, architectureName = architecture, ivyDependency = getIvyDependency() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModuleBuilder.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModuleBuilder.kt index a0e1da4c50a..3b08c14ebc4 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModuleBuilder.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModuleBuilder.kt @@ -16,7 +16,7 @@ internal class GradleNodeModuleBuilder( val project: Project, val dependency: ResolvedDependency, val artifacts: Set, - val cache: GradleNodeModules + val cache: GradleNodeModulesBuilder ) { var srcPackageJsonFile: File? = null val files = mutableListOf() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModules.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModulesBuilder.kt similarity index 56% rename from libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModules.kt rename to libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModulesBuilder.kt index 0a5ac2d17ce..e20243d712c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModules.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/GradleNodeModulesBuilder.kt @@ -8,23 +8,19 @@ package org.jetbrains.kotlin.gradle.targets.js.npm import org.gradle.api.Project import org.gradle.api.artifacts.ResolvedArtifact import org.gradle.api.artifacts.ResolvedDependency -import org.gradle.api.file.CopySpec +import org.gradle.api.artifacts.component.ProjectComponentIdentifier import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions import org.jetbrains.kotlin.gradle.internal.ProcessedFilesCache import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation -import org.jetbrains.kotlin.gradle.targets.js.internal.RewriteSourceMapFilterReader -import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile -internal class GradleNodeModules(val project: Project) : AutoCloseable { +internal class GradleNodeModulesBuilder(val project: Project) : AutoCloseable { companion object { const val STATE_FILE_NAME = ".visited" } internal val dir = project.buildDir.resolve("node_modules_gradle") - private val nodeModulesDir = project.npmProject.nodeModulesDir private val cache = ProcessedFilesCache(project, dir, STATE_FILE_NAME, "5") private val visited = mutableSetOf() - private val doLast = mutableListOf<() -> Unit>() val modules get() = cache.targets.map { @@ -33,29 +29,16 @@ internal class GradleNodeModules(val project: Project) : AutoCloseable { fun visitCompilation(compilation: KotlinCompilation) { val project = compilation.target.project - val kotlin2JsCompile = compilation.compileKotlinTask as Kotlin2JsCompile - + // classpath - compilation.relatedConfigurationNames.forEach { - val configuration = project.configurations.getByName(it) + compilation.relatedConfigurationNames.forEach { configurationName -> + val configuration = project.configurations.getByName(configurationName) if (configuration.isCanBeResolved) { configuration.resolvedConfiguration.firstLevelModuleDependencies.forEach { visitDependency(compilation, it) } } } - - // output - doLast.add { - // we should do it only after node package manger work (as it can clean files) - if (kotlin2JsCompile.state.executed) { - visitCompile(project, kotlin2JsCompile) - } else { - kotlin2JsCompile.doLast { - visitCompile(project, kotlin2JsCompile) - } - } - } } private fun visitDependency( @@ -82,37 +65,15 @@ internal class GradleNodeModules(val project: Project) : AutoCloseable { } artifacts.forEach { artifact -> - cache.getOrCompute(artifact.file) { - lazyDirName - } - } - } - - private fun visitCompile(project: Project, kotlin2JsCompile: Kotlin2JsCompile) { - project.copy { copy -> - copy.from(kotlin2JsCompile.outputFile) - copy.from(kotlin2JsCompile.outputFile.path + ".map") - copy.into(nodeModulesDir) - copy.withSourceMapRewriter() - } - } - - private fun CopySpec.withSourceMapRewriter() { - eachFile { - if (it.name.endsWith(".js.map")) { - it.filter( - mapOf( - "srcSourceRoot" to it.file.parentFile, - "targetSourceRoot" to nodeModulesDir - ), - RewriteSourceMapFilterReader::class.java - ) + if (artifact.id.componentIdentifier !is ProjectComponentIdentifier) { + cache.getOrCompute(artifact.file) { + lazyDirName + } } } } override fun close() { - doLast.forEach { it() } cache.close() } } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmApi.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmApi.kt index a4b9bd4d115..9d7b98acfa1 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmApi.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmApi.kt @@ -30,7 +30,7 @@ interface NpmApi { allWorkspaces: Collection ): Boolean = false - open fun getHoistGradleNodeModules(project: Project) = false + open fun shouldHoistGradleNodeModules(project: Project) = false companion object { fun resolveOperationDescription(packageManagerTitle: String): String = diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmDependency.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmDependency.kt index 78c80e4eb83..e5783ecc2ed 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmDependency.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmDependency.kt @@ -40,7 +40,7 @@ data class NpmDependency( fun visit(item: NpmDependency) { if (item in visited) return visited.add(item) - item.tryFindNodeModule()?.let { + item.project.npmProject.findModuleEntry(item.key)?.let { all.add(it) } } @@ -50,7 +50,7 @@ data class NpmDependency( return all } - internal fun resolveProject(): NpmResolver.ResolvedProject? { + private fun resolveProject(): NpmResolver.ResolvedProject? { val result = NpmResolver.resolve(project) 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" override fun toString() = "$key: $version" diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmProject.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmProject.kt index 6d9fec9b433..fad7f4f6a2d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmProject.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmProject.kt @@ -10,48 +10,54 @@ import com.google.gson.JsonObject import groovy.transform.TailRecursive 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.targets.js.nodejs.nodeJs import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile import java.io.File -class NpmProject( +open class NpmProject( val project: Project, val nodeWorkDir: File, - val managed: Boolean + val searchInParents: Boolean ) { val nodeModulesDir get() = nodeWorkDir.resolve(NODE_MODULES) val packageJsonFile: File - get() { - return nodeWorkDir.resolve(PACKAGE_JSON) - } + get() = nodeWorkDir.resolve(PACKAGE_JSON) + + open val compileOutputCopyDest: File? + get() = nodeModulesDir fun useTool(exec: ExecSpec, tool: String, vararg args: String) { NpmResolver.resolve(project) exec.workingDir = nodeWorkDir exec.executable = project.nodeJs.root.environment.nodeExecutable - exec.args = listOf(findModule(tool)) + args + exec.args = listOf(getModuleEntryPath(tool)) + args } val hoistGradleNodeModules: Boolean - get() = project.nodeJs.root.packageManager.getHoistGradleNodeModules(project) + get() = project.nodeJs.root.packageManager.shouldHoistGradleNodeModules(project) val gradleNodeModulesDir: File get() = if (hoistGradleNodeModules) project.rootProject.npmProject.nodeModulesDir else nodeModulesDir - fun moduleOutput(compilationTask: Kotlin2JsCompile): File { - return gradleNodeModulesDir.resolve(compilationTask.outputFile.name) + open fun moduleOutput(compilationTask: Kotlin2JsCompile): File { + 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 - fun findModule(name: String, src: NpmProject = this): String { + fun findModuleEntry(name: String): File? { val file = nodeModulesDir.resolve(name) - if (file.isFile) return file.canonicalPath + if (file.isFile) return file if (file.isDirectory) { val packageJsonFile = file.resolve("package.json") val main: String = (if (packageJsonFile.isFile) { @@ -59,18 +65,22 @@ class NpmProject( 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" val mainFile = file.resolve(main) - if (mainFile.isFile) return mainFile.canonicalPath + if (mainFile.isFile) return mainFile } val parent = project.parent - return if (!managed || parent == null) error("Cannot find node module $name in $src") - else NpmProject[parent].findModule(name, src) + return if (!searchInParents || parent == null) null + else NpmProject[parent].findModuleEntry(name) } + open fun configureCompilation(compilation: KotlinJsCompilation) = Unit + override fun toString() = "NpmProject($nodeWorkDir)" companion object { @@ -80,13 +90,7 @@ class NpmProject( operator fun get(project: Project): NpmProject { val nodeJsRootExtension = NodeJsPlugin.apply(project) - val manageNodeModules = nodeJsRootExtension.root.manageNodeModules - - val nodeWorkDir = - if (manageNodeModules) project.projectDir - else project.buildDir - - return NpmProject(project, nodeWorkDir, manageNodeModules) + return nodeJsRootExtension.root.layout.newNpmProject(project) } } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmProjectLayout.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmProjectLayout.kt index f6704ea0491..58a44fb626d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmProjectLayout.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmProjectLayout.kt @@ -5,76 +5,59 @@ package org.jetbrains.kotlin.gradle.targets.js.npm -import com.google.gson.Gson -import com.google.gson.JsonObject -import groovy.transform.TailRecursive import org.gradle.api.Project -import org.gradle.process.ExecSpec -import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin -import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation +import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile import java.io.File -class NpmProjectLayout( - val project: Project, - val nodeWorkDir: File, - val managed: Boolean -) { - val nodeModulesDir - get() = nodeWorkDir.resolve(NODE_MODULES) +enum class NpmProjectLayout { + PROJECT_DIR { + override val allowYarnWorkspaces: Boolean + get() = true - val packageJsonFile: File - get() { - return nodeWorkDir.resolve(PACKAGE_JSON) - } + override fun newNpmProject(project: Project) = + NpmProject(project, project.projectDir, searchInParents = true) + }, + ROOT_PROJECT_BUILD_DIR { + override val allowYarnWorkspaces: Boolean + get() = true - fun useTool(exec: ExecSpec, tool: String, vararg args: String) { - NpmResolver.resolve(project) + override fun newNpmProject(project: Project): NpmProject { + val nodeJsWorldDir = project.rootProject.buildDir.resolve("nodejs-project") - exec.workingDir = nodeWorkDir - exec.executable = project.nodeJs.root.environment.nodeExecutable - exec.args = listOf(findModule(tool)) + args - } - - @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) + val workDir = + if (project == project.rootProject) nodeJsWorldDir + else { + val projectPackage = project.path.removePrefix(":").replace(":", File.separator) + nodeJsWorldDir.resolve("packages").resolve(projectPackage) } - packageJson["main"] as? String? ?: packageJson["module"] as? String? ?: packageJson["browser"] as? String? - } else null) ?: "index.js" + return object : NpmProject( + project, + workDir, + searchInParents = true + ) { + override val compileOutputCopyDest: File? + get() = nodeWorkDir - val mainFile = file.resolve(main) - if (mainFile.isFile) return mainFile.canonicalPath + override fun configureCompilation(compilation: KotlinJsCompilation) { +// 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 - return if (!managed || parent == null) error("Cannot find node module $name in $src") - else NpmProjectLayout[parent].findModule(name, src) - } + open val allowYarnWorkspaces: Boolean + get() = false - companion object { - 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] \ No newline at end of file + abstract fun newNpmProject(project: Project): NpmProject +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmResolver.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmResolver.kt index 6bd0963921f..0d1cbb7617f 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmResolver.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmResolver.kt @@ -8,25 +8,28 @@ 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.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.targets.js.internal.RewriteSourceMapFilterReader 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 /** - * [NpmResolver] runs selected [NodeJsRootExtension.packageManager] on root project - * with configured `package.json` files in all projects. + * Generates `package.json` file for projects with npm or js dependencies and + * runs selected [NodeJsRootExtension.packageManager] to download and install it. * - * `package.json` - * - * - compile(npm(...)) dependencies - * - compile(npm(...)) dependencies from external modules - * - compile(non npm(...)) dependencies transitively + * All [NpmDependency] for configurations related to kotlin/js will be added to `package.json`. + * For external gradle modules, fake npm packages will be created and added to `package.json` + * as path to directory. */ internal class NpmResolver private constructor(val rootProject: Project) { companion object { @@ -55,8 +58,9 @@ internal class NpmResolver private constructor(val rootProject: Project) { } private val nodeJs = NodeJsPlugin.apply(rootProject).root + private val npmProject = rootProject.npmProject private val packageManager = nodeJs.packageManager - private val hoistGradleNodeModules = packageManager.getHoistGradleNodeModules(rootProject) + private val hoistGradleNodeModules = npmProject.hoistGradleNodeModules private val npmPackages = mutableListOf() private val gson = GsonBuilder() .setPrettyPrinting() @@ -82,7 +86,9 @@ internal class NpmResolver private constructor(val rootProject: Project) { val npmDependencies: Set ) { fun savePackageJson(gson: Gson) { - NpmProjectLayout[project].packageJsonFile.writer().use { + val packageJsonFile = NpmProject[project].packageJsonFile + packageJsonFile.ensureParentDirsCreated() + packageJsonFile.writer().use { gson.toJson(packageJson, it) } } @@ -98,7 +104,7 @@ internal class NpmResolver private constructor(val rootProject: Project) { private fun resolve( project: Project, - parentGradleNodeModules: GradleNodeModulesSync? + parentGradleModules: GradleNodeModulesBuilder? ): Boolean { val existedProcess = ProjectData[project] if (existedProcess != null) { @@ -112,15 +118,15 @@ internal class NpmResolver private constructor(val rootProject: Project) { ProjectData[project] = it } - val gradleNodeModules = - if (hoistGradleNodeModules && parentGradleNodeModules != null) parentGradleNodeModules - else newGradleNodeModulesSync(project) + val gradleModules = + if (hoistGradleNodeModules && parentGradleModules != null) parentGradleModules + else GradleNodeModulesBuilder(project) project.subprojects.forEach { - resolve(it, gradleNodeModules) + resolve(it, gradleModules) } - val npmPackage = extractNpmPackage(project, gradleNodeModules) + val npmPackage = extractNpmPackage(project, gradleModules) if (npmPackage != null) { npmPackage.savePackageJson(gson) @@ -137,70 +143,60 @@ internal class NpmResolver private constructor(val rootProject: Project) { } } - if (gradleNodeModules != parentGradleNodeModules) { - gradleNodeModules.sync() + if (gradleModules != parentGradleModules) { + gradleModules.close() } return true } - private fun newGradleNodeModulesSync(project: Project): GradleNodeModulesSync { - return GradleNodeModulesSync(project).also { - it.loadOldState() - } - } - private fun extractNpmPackage( project: Project, - gradleComponentsSync: GradleNodeModulesSync + gradleComponents: GradleNodeModulesBuilder ): NpmPackage? { val packageJson = PackageJson(project.name, project.version.toString()) val npmDependencies = mutableSetOf() - 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 { it(packageJson) } - val packageJsonRequired = if (project == rootProject) { - packageManager.hookRootPackage(rootProject, packageJson, npmPackages) - } else false + val packageJsonRequired = + if (project == rootProject) packageManager.hookRootPackage(rootProject, packageJson, npmPackages) + 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( project: Project, npmDependencies: MutableSet, - gradleComponentsSync: GradleNodeModulesSync, + gradleComponents: GradleNodeModulesBuilder, packageJson: PackageJson ) { val kotlin = project.kotlinExtensionOrNull if (kotlin != null) { - val transitiveDependencies = mutableListOf() - when (kotlin) { - is KotlinSingleTargetExtension -> visitTarget( - kotlin.target, - project, - npmDependencies, - gradleComponentsSync, - transitiveDependencies - ) + is KotlinSingleTargetExtension -> visitTarget(kotlin.target, project, npmDependencies, gradleComponents, packageJson) is KotlinMultiplatformExtension -> kotlin.targets.forEach { - visitTarget(it, project, npmDependencies, gradleComponentsSync, transitiveDependencies) + visitTarget(it, project, npmDependencies, gradleComponents, packageJson) } } npmDependencies.forEach { 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, project: Project, npmDependencies: MutableSet, - gradleComponentsSync: GradleNodeModulesSync, - transitiveDependencies: MutableList + gradleComponents: GradleNodeModulesBuilder, + packageJson: PackageJson ) { if (target.platformType == KotlinPlatformType.js) { 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 + ) } } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/PackageJson.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/PackageJson.kt index c4fac80b7a6..5aba936ccd6 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/PackageJson.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/PackageJson.kt @@ -10,7 +10,8 @@ class PackageJson( var version: String ) { val empty: Boolean - get() = private == null && + get() = main == null && + private == null && workspaces == null && dependencies.isEmpty() && devDependencies.isEmpty() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/nodejs/KotlinNodeJsTestRunner.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/nodejs/KotlinNodeJsTestRunner.kt index ab54815af43..b7ce1d74fd3 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/nodejs/KotlinNodeJsTestRunner.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/nodejs/KotlinNodeJsTestRunner.kt @@ -54,7 +54,7 @@ class KotlinNodeJsTestRunner : KotlinJsTestFramework { val args = nodeJsArgs + testRuntimeNodeModules.map { - npmProjectLayout.findModule(it) + npmProjectLayout.getModuleEntryPath(it) } + cliArgs.toList() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpack.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpack.kt index de11ce54b57..dbf19d58752 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpack.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpack.kt @@ -37,14 +37,11 @@ open class KotlinWebpack : DefaultTask() { open lateinit var entry: File open val configFile: File - @OutputFile get() = project.buildDir.resolve("webpack.config.js") + @OutputFile get() = project.npmProject.nodeWorkDir.resolve("webpack.config.js") @Input var saveEvaluatedConfigFile: Boolean = true - open val evaluatedConfigFile: File - @OutputFile get() = project.buildDir.resolve("webpack.config.evaluated.js") - open val outputPath: File @OutputDirectory get() = project.buildDir.resolve("lib") @@ -57,10 +54,14 @@ open class KotlinWebpack : DefaultTask() { open val reportDir: File @OutputDirectory get() = project.reportsDir.resolve("webpack").resolve(entry.nameWithoutExtension) + open val evaluatedConfigFile: File + @OutputFile get() = reportDir.resolve("webpack.config.evaluated.js") + @Input var bin: String = "webpack" @Input + @Optional var devServer: KotlinWebpackConfig.DevServer? = null internal fun createRunner() = KotlinWebpackRunner( @@ -125,6 +126,8 @@ open class KotlinWebpack : DefaultTask() { it.dependsOn(compileKotlinTask) it.entry = npmProject.moduleOutput(compileKotlinTask) + + it.outputs.upToDateWhen { false } } compilation.dependencies { diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpackRunner.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpackRunner.kt index 2b3367e2c46..6940f0d58a1 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpackRunner.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpackRunner.kt @@ -13,6 +13,7 @@ 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 import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject @@ -26,9 +27,13 @@ internal class KotlinWebpackRunner( val bin: String, val config: KotlinWebpackConfig ) { - fun execute() { + fun execute(): ExecResult { 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 { @@ -43,12 +48,13 @@ internal class KotlinWebpackRunner( config.save(configFile) val execFactory = execHandleFactory.newExec() + val args = mutableListOf("--config", configFile.absolutePath) if (config.showProgress) { args.add("--progress") } - npmProjectLayout.useTool(execFactory, ".bin/$bin") + npmProjectLayout.useTool(execFactory, ".bin/$bin", *args.toTypedArray()) val exec = execFactory.build() exec.start() return exec diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/Yarn.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/Yarn.kt index 2162b94e20c..cc5f796452f 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/Yarn.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/Yarn.kt @@ -103,7 +103,7 @@ object Yarn : NpmApi { rootPackageJson.private = true rootPackageJson.workspaces = allWorkspaces .filter { it.project != rootProject } - .map { it.project.projectDir.relativeTo(rootProject.rootDir).path } + .map { it.project.npmProject.nodeWorkDir.relativeTo(rootProject.npmProject.nodeWorkDir).path } return true } @@ -111,7 +111,7 @@ object Yarn : NpmApi { return false } - override fun getHoistGradleNodeModules(project: Project): Boolean = + override fun shouldHoistGradleNodeModules(project: Project): Boolean = project.rootProject.yarn.useWorkspaces @Suppress("EXPOSED_PARAMETER_TYPE") diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnAvoidance.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnAvoidance.kt index ddbb41e3d00..4d0ef4819e8 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnAvoidance.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnAvoidance.kt @@ -9,6 +9,7 @@ import org.gradle.api.Project import org.gradle.api.internal.project.ProjectInternal import org.gradle.internal.hash.FileHasher import org.jetbrains.kotlin.daemon.common.toHexString +import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject 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 } + private val npmProject = project.npmProject + private val packageJsonFile = npmProject.packageJsonFile + private val hash by lazy { 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 - get() = prevHash == hash + get() = packageJsonFile.exists() && + npmProject.nodeModulesDir.isDirectory && + prevHash == hash fun commit() { if (!upToDate) { - project.packageJsonHashFile.writeText(hash) + val packageJsonHashFile = project.packageJsonHashFile + packageJsonHashFile.ensureParentDirsCreated() + packageJsonHashFile.writeText(hash) } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnRootExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnRootExtension.kt index d0747884e58..0f082dc5ee8 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnRootExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnRootExtension.kt @@ -26,7 +26,7 @@ open class YarnRootExtension(val project: Project) { var disableWorkspaces: Boolean = false val useWorkspaces: Boolean - get() = project.nodeJs.root.manageNodeModules && !disableWorkspaces + get() = !disableWorkspaces && project.nodeJs.root.layout.allowYarnWorkspaces internal fun executeSetup() { NodeJsPlugin.apply(project).root.executeSetup()