From 8e2075ebccf356a66b97b7151882096574f566c1 Mon Sep 17 00:00:00 2001 From: Sergey Rostov Date: Tue, 14 May 2019 11:15:48 +0300 Subject: [PATCH] Gradle, js: implement resolving node modules according to https://nodejs.org/api/modules.html #KT-30530 --- .../gradle/targets/js/npm/NpmDependency.kt | 2 +- .../gradle/targets/js/npm/NpmProject.kt | 82 +++++++++++++------ .../gradle/targets/js/npm/NpmResolver.kt | 12 +-- 3 files changed, 64 insertions(+), 32 deletions(-) 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 e5783ecc2ed..822c91c1328 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.project.npmProject.findModuleEntry(item.key)?.let { + item.project.npmProject.resolve(item.key)?.let { all.add(it) } } 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 86dbca1ae29..3ce5d0bb755 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 @@ -7,10 +7,8 @@ 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.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 @@ -35,7 +33,7 @@ open class NpmProject( exec.workingDir = nodeWorkDir exec.executable = project.nodeJs.root.environment.nodeExecutable - exec.args = listOf(getModuleEntryPath(tool)) + args + exec.args = listOf(require(tool)) + args } val hoistGradleNodeModules: Boolean @@ -50,33 +48,67 @@ open class NpmProject( 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") + /** + * Require [request] nodejs module and return canonical path to it's main js file. + */ + fun require(request: String): String { + NpmResolver.requireResolved(project) + return resolve(request)?.canonicalPath ?: error("Cannot find node module \"$request\" in \"$this\"") } - @TailRecursive - fun findModuleEntry(name: String): File? { - val file = nodeModulesDir.resolve(name) + /** + * Find node module according to https://nodejs.org/api/modules.html#modules_all_together, + * with exception that instead of traversing parent folders, we are traversing parent projects + */ + internal fun resolve(name: String, context: File = nodeWorkDir): File? = + if (name.startsWith("/")) resolve(name.removePrefix("/"), File("/")) + else resolveAsRelative("./", name, context) + ?: resolveAsRelative("/", name, context) + ?: resolveAsRelative("../", name, context) + ?: resolveInNodeModulesDir(name, nodeModulesDir) + ?: if (searchInParents) project.parent?.let { NpmProject[it].resolve(name) } else null + + private fun resolveAsRelative(prefix: String, name: String, context: File): File? { + if (!name.startsWith(prefix)) return null + + val relative = context.resolve(name.removePrefix(prefix)) + return resolveAsFile(relative) + ?: resolveAsDirectory(relative) + } + + private fun resolveInNodeModulesDir(name: String, nodeModulesDir: File): File? { + return resolveAsFile(nodeModulesDir.resolve(name)) + ?: resolveAsDirectory(nodeModulesDir.resolve(name)) + } + + private fun resolveAsDirectory(dir: File): File? { + val packageJsonFile = dir.resolve("package.json") + val main: String? = if (packageJsonFile.isFile) { + val packageJson = packageJsonFile.reader().use { + Gson().fromJson(it, JsonObject::class.java) + } + + packageJson["main"] as? String? + ?: packageJson["module"] as? String? + ?: packageJson["browser"] as? String? + } else null + + return if (main != null) { + val mainFile = dir.resolve(main) + resolveAsFile(mainFile) + ?: resolveIndex(mainFile) + } else resolveIndex(dir) + } + + private fun resolveIndex(dir: File): File? = resolveAsFile(dir.resolve("index")) + + private fun resolveAsFile(file: File): File? { if (file.isFile) return file - if (file.isDirectory) { - val packageJsonFile = file.resolve("package.json") - val main: String = (if (packageJsonFile.isFile) { - val packageJson = packageJsonFile.reader().use { - Gson().fromJson(it, JsonObject::class.java) - } - packageJson["main"] as? String? - ?: packageJson["module"] as? String? - ?: packageJson["browser"] as? String? - } else null) ?: "index.js" + val js = File(file.path + ".js") + if (js.isFile) return js - val mainFile = file.resolve(main) - if (mainFile.isFile) return mainFile - } - - val parent = project.parent - return if (!searchInParents || parent == null) null - else NpmProject[parent].findModuleEntry(name) + return null } override fun toString() = "NpmProject($nodeWorkDir)" 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 6f9e1bcc3fe..7f06bcef5c7 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 @@ -55,13 +55,13 @@ internal class NpmResolver private constructor(val rootProject: Project) { } } - fun checkRequiredDependencies(project: Project, target: RequiresNpmDependencies) { - val required = ProjectData[project.rootProject]?.resolved?.requiredByTasks - check(required != null) { - "NPM dependencies should be resolved before $target execution" - } + fun requireResolved(project: Project, reason: String = ""): ResolvedProject = + ProjectData[project.rootProject]?.resolved + ?: error("NPM dependencies should be resolved$reason") - val targetRequired = required!![target]?.toSet() ?: setOf() + fun checkRequiredDependencies(project: Project, target: RequiresNpmDependencies) { + val required = requireResolved(project, "before $target execution").requiredByTasks + val targetRequired = required[target]?.toSet() ?: setOf() target.requiredNpmDependencies.forEach { check(it in targetRequired) {