diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/build.gradle.kts index 52883ad9c8b..e5a316574b6 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/build.gradle.kts @@ -68,7 +68,8 @@ projectTest { ":examples:annotation-processor-example:install", ":kotlin-scripting-common:install", ":kotlin-scripting-jvm:install", - ":kotlin-scripting-compiler-embeddable:install" + ":kotlin-scripting-compiler-embeddable:install", + ":kotlin-test-nodejs-runner:install" ) exclude(jpsIncrementalTestsClass) } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt index 222374fe705..87bcadbd482 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt @@ -380,6 +380,24 @@ class Kotlin2JsGradlePluginIT : BaseGradleIT() { assertFileExists(moduleDir + "kotlin-js-plugin-1.0-sources.jar") + assertFileExists("build/test_node_modules/kotlin.js") + assertFileExists("build/test_node_modules/kotlin.js.map") + assertFileExists("build/test_node_modules/kotlin-test.js") + assertFileExists("build/test_node_modules/kotlin-test.js.map") + assertFileExists("build/test_node_modules/kotlin-test-nodejs-runner.js") + assertFileExists("build/test_node_modules/kotlin-test-nodejs-runner.js.map") + assertFileExists("build/test_node_modules/kotlin-nodejs-source-map-support.js") + assertFileExists("build/test_node_modules/kotlin-nodejs-source-map-support.js.map") + assertFileExists("build/test_node_modules/kotlin-js-plugin.js") + assertFileExists("build/test_node_modules/kotlin-js-plugin.js.map") + assertFileExists("build/test_node_modules/kotlin-js-plugin_test.js") + assertFileExists("build/test_node_modules/kotlin-js-plugin_test.js.map") + + assertFileContains( + "build/test_node_modules/kotlin-js-plugin_test.js.map", + "\"sources\":[\"../../src/test/kotlin/MainTest.kt\"]" + ) + assertTestResults("testProject/kotlin-js-plugin-project/tests.xml", "test") } } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/MultiplatformGradleIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/MultiplatformGradleIT.kt index 037088caa3c..7409d26e995 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/MultiplatformGradleIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/MultiplatformGradleIT.kt @@ -311,8 +311,6 @@ class MultiplatformGradleIT : BaseGradleIT() { assertSuccessful() assertTasksRegisteredAndNotRealized( - ":kotlinNodeJsTestRuntimeExtract", - ":clientTestNodeModules", ":clientTest", @@ -325,8 +323,6 @@ class MultiplatformGradleIT : BaseGradleIT() { assertSuccessful() assertTasksExecuted( - ":kotlinNodeJsTestRuntimeExtract", - ":clientTestNodeModules", ":clientTest", @@ -334,6 +330,24 @@ class MultiplatformGradleIT : BaseGradleIT() { ":serverTest" ) + assertFileExists("build/client_test_node_modules/kotlin.js") + assertFileExists("build/client_test_node_modules/kotlin.js.map") + assertFileExists("build/client_test_node_modules/kotlin-test.js") + assertFileExists("build/client_test_node_modules/kotlin-test.js.map") + assertFileExists("build/client_test_node_modules/kotlin-test-nodejs-runner.js") + assertFileExists("build/client_test_node_modules/kotlin-test-nodejs-runner.js.map") + assertFileExists("build/client_test_node_modules/kotlin-nodejs-source-map-support.js") + assertFileExists("build/client_test_node_modules/kotlin-nodejs-source-map-support.js.map") + assertFileExists("build/client_test_node_modules/new-mpp-js-tests.js") + assertFileExists("build/client_test_node_modules/new-mpp-js-tests.js.map") + assertFileExists("build/client_test_node_modules/new-mpp-js-tests_test.js") + assertFileExists("build/client_test_node_modules/new-mpp-js-tests_test.js.map") + + assertFileContains( + "build/client_test_node_modules/new-mpp-js-tests_test.js.map", + "\"sources\":[\"../../src/commonTest/kotlin/CommonTest.kt\"]" + ) + assertTestResults("testProject/new-mpp-js-tests/TEST-CommonTest.xml", "clientTest") assertTestResults("testProject/new-mpp-js-tests/TEST-CommonTest.xml", "serverTest") } @@ -343,8 +357,6 @@ class MultiplatformGradleIT : BaseGradleIT() { assertSuccessful() assertTasksUpToDate( - ":kotlinNodeJsTestRuntimeExtract", - ":clientTestNodeModules", ":clientTest", @@ -359,10 +371,6 @@ class MultiplatformGradleIT : BaseGradleIT() { build("check") { assertSuccessful() - assertTasksUpToDate( - ":kotlinNodeJsTestRuntimeExtract" - ) - assertTasksExecuted( ":clientTestNodeModules", ":clientTest", diff --git a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts index 541fad39efd..c0a9d3d1e40 100644 --- a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts @@ -46,6 +46,8 @@ dependencies { compileOnly(project(":kotlin-annotation-processing-gradle")) compileOnly(project(":kotlin-scripting-compiler")) + compile("com.google.code.gson:gson:2.8.5") + compileOnly("com.android.tools.build:gradle:2.0.0") compileOnly("com.android.tools.build:gradle-core:2.0.0") compileOnly("com.android.tools.build:builder:2.0.0") @@ -62,8 +64,7 @@ dependencies { runtime(projectRuntimeJar(":kotlin-scripting-compiler-embeddable")) runtime(project(":kotlin-reflect")) - jarContents(compileOnly(intellijDep()) { includeJars("serviceMessages") }) - jarContents(projectArchives(":kotlin-test-nodejs-runner")) + jarContents(compileOnly(intellijDep()) { includeJars("serviceMessages", "gson-2.8.5") }) // com.android.tools.build:gradle has ~50 unneeded transitive dependencies compileOnly("com.android.tools.build:gradle:3.0.0") { isTransitive = false } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsCompilationTestsConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsCompilationTestsConfigurator.kt index 74cdf6b667e..c026ca32899 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsCompilationTestsConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsCompilationTestsConfigurator.kt @@ -63,6 +63,10 @@ internal class KotlinJsCompilationTestsConfigurator( get() = classpath.plus(project.files(destinationDir)) fun configure() { + compilation.dependencies { + implementation(kotlin("test-nodejs-runner")) + } + val nodeModulesTask = registerTask( project, disambiguateCamelCased("nodeModules", true), @@ -79,20 +83,13 @@ internal class KotlinJsCompilationTestsConfigurator( } val projectWithNodeJsPlugin = NodeJsPlugin.ensureAppliedInHierarchy(target.project) - val kotlinJsExtractTestRunnerTask = project.kotlinNodeJsTestRuntimeExtractTask val testTask = registerTask(project, testTaskName, KotlinNodeJsTestTask::class.java) { testJs -> testJs.group = "verification" - testJs.dependsOn( - nodeModulesTask.getTaskOrProvider(), - kotlinJsExtractTestRunnerTask.getTaskOrProvider() - ) + testJs.dependsOn(nodeModulesTask.getTaskOrProvider()) testJs.onlyIf { - // run only if there is org.jetbrains.kotlin:kotlin-test-js in classpath - // (kotlin-test-nodejs-runner.js requires kotlin-test.js) - nodeModulesDir.resolve("kotlin-test.js").exists() compileTestKotlin2Js.outputFile.exists() } @@ -103,7 +100,10 @@ internal class KotlinJsCompilationTestsConfigurator( testJs.nodeJsProcessOptions.workingDir = project.projectDir testJs.nodeModulesDir = nodeModulesDir - testJs.testRuntimeNodeModule = project.extractedKotlinTestNodeJsRunner + testJs.testRuntimeNodeModules = listOf( + "kotlin-test-nodejs-runner.js", + "kotlin-nodejs-source-map-support.js" + ) testJs.nodeModulesToLoad = setOf(compileTestKotlin2Js.outputFile.name) val htmlReport = DslObject(testJs.reports.html) 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 c7e5bd579e1..89dc98a03fb 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 @@ -22,7 +22,11 @@ class KotlinJsTargetConfigurator(kotlinPluginVersion: String) : super.configureCompilations(platformTarget) platformTarget.compilations.all { - it.compileKotlinTask.kotlinOptions.moduleKind = "umd" + it.compileKotlinTask.kotlinOptions { + moduleKind = "umd" + sourceMap = true + sourceMapEmbedSources = null + } } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/internal/RewriteSourceMapFilterReader.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/internal/RewriteSourceMapFilterReader.kt new file mode 100644 index 00000000000..8eeae52cd6b --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/internal/RewriteSourceMapFilterReader.kt @@ -0,0 +1,210 @@ +package org.jetbrains.kotlin.gradle.targets.js.internal + +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import com.google.gson.stream.MalformedJsonException +import org.slf4j.LoggerFactory +import java.io.* + +open class RewriteSourceMapFilterReader( + val input: Reader +) : FilterReader(input) { + // This implementation works only when source map contents starts + // with prolog `{"version":3,"file":"...","sources":[...],"sourcesContent":...` + // This is always true for Kotlin/JS and many other tools that produces JS source maps. + // + // Full implementation (without loading file contents entirely into memory) requires + // streaming read of string literals inside "mappings", which is not supported by any + // known JSON parsers. We cannot store "mappings" into memory since it can reach tens + // of megabytes for large projects. + // + // Implementation: read contents before [PROLOG_END], parse it as JSON and transform source paths. + // All rest contents will be passed directly from [input]. + + companion object { + internal const val PROLOG_END = "],\"sourcesContent\":" + const val UNSUPPORTED_FORMAT_MESSAGE = + "Unsupported format. Contents should starts with `{\"version\":3,\"file\":\"...\",\"sources\":[...],\"sourcesContent\":...`" + + private val log = LoggerFactory.getLogger("kotlin") + } + + private var wasReadFirst = false + private val prologLimit = 0xfffff + + // buffer with transformed prolog, that wil be emitted first + private lateinit var bufferWriter: StringWriter + private lateinit var bufferJsonWriter: JsonWriter + private val buffer: StringBuffer get() = bufferWriter.buffer + private var bufferReadPos = 0 + private val bufferAvailable get() = buffer.length - bufferReadPos + + private var inputEof = false + + lateinit var srcSourceRoot: String + lateinit var targetSourceRoot: String + + private fun maybeReadFirst() { + if (!wasReadFirst) { + wasReadFirst = true + readFirst() + } + } + + private fun readFirst() { + // read 1Kb chunks from [input] until [PROLOG_END] will be found + val jsonString = StringBuilder() + val readBuffer = CharArray(1024) + var lastRead: Int + var jsonPrologPos: Int + while (true) { + lastRead = input.read(readBuffer) + if (lastRead == -1) { + inputEof = true + writeBackUnsupported(jsonString, "$UNSUPPORTED_FORMAT_MESSAGE. \"sourcesContent\" or \"sources\" not found") + return + } + + // Try find PROLOG_END. Note the case when prolog contents is splitted between reads. + // Like, one buffer ends with `],"sourc`, and the other starts with `esContent"` + val prevEnd = jsonString.length + jsonString.append(readBuffer, 0, lastRead) + jsonPrologPos = jsonString.indexOf(PROLOG_END, prevEnd - PROLOG_END.length) + if (jsonPrologPos == -1) { + if (jsonString.length + lastRead > prologLimit) { + writeBackUnsupported(jsonString, "Too many sources or format is not supported") + return + } + } else break + } + + // create StringWriter to write transformed prolog and contents that was read after PROLOG_END + bufferWriter = StringWriter(jsonString.length) + bufferJsonWriter = JsonWriter(bufferWriter) + + // parse json in prolog and write it back to bufferJsonWriter with transformed source paths + val json = JsonReader(StringReader(jsonString.toString())) + try { + json.beginObject() + bufferJsonWriter.beginObject() + + reading@ while (true) { + val token = json.peek() + check(token == JsonToken.NAME) { "JSON key expected, but $token found" } + val key = json.nextName() + when (key) { + "sources" -> { + json.beginArray() + bufferJsonWriter.name("sources").beginArray() + while (json.peek() != JsonToken.END_ARRAY) { + val path = json.nextString() + bufferJsonWriter.value(transformString(path)) + } + json.endArray() + } + "version" -> bufferJsonWriter.name(key).value(json.nextInt()) + "file" -> bufferJsonWriter.name(key).value(json.nextString()) + "sourcesContent" -> break@reading + else -> throw IllegalStateException("Unknown key \"$key\"") + } + } + + // leave bufferJsonWriter unclosed + + // push back contents that was read after PROLOG_END + bufferWriter.append(jsonString.substring(jsonPrologPos)) + } catch (e: IllegalStateException) { + writeBackUnsupported(jsonString, json, e.message!!) + } catch (e: MalformedJsonException) { + writeBackUnsupported(jsonString, json, "Malformed JSON") + } + } + + private fun writeBackUnsupported(jsonString: StringBuilder, reader: JsonReader, message: String) = + writeBackUnsupported( + jsonString, + "$UNSUPPORTED_FORMAT_MESSAGE. $message at ${reader.toString().replace("JsonReader at ", "")} in `$jsonString" + ) + + private fun writeBackUnsupported(jsonString: StringBuilder, reason: String) = + writeBackUnsupported(jsonString.toString(), reason) + + private fun writeBackUnsupported(jsonString: String, reason: String) { + warnUnsupported(reason) + bufferWriter = StringWriter(jsonString.length) + bufferWriter.append(jsonString) + } + + protected open fun warnUnsupported(reason: String) { + log.warn("Cannot rewrite paths in JavaScript source maps: $reason") + } + + private fun transformString(value: String): String = + File(srcSourceRoot).resolve(value).canonicalFile.relativeToOrSelf(File(targetSourceRoot)).path + + override fun read(): Int { + maybeReadFirst() + if (bufferAvailable == 0) { + if (inputEof) return -1 + val read = input.read() + if (read == -1) { + inputEof = true + return -1 + } else { + return read + } + } + return buffer[bufferReadPos++].toInt() + } + + override fun read(dest: CharArray, initialDestOffset: Int, n: Int): Int { + maybeReadFirst() + + var todo = n + var destOffset = initialDestOffset + while (todo > 0) { + if (bufferAvailable == 0) { + if (inputEof) return if (n == todo) -1 else n - todo + val read = input.read(dest, destOffset, todo) + if (read == -1) { + inputEof = true + return n - todo + } else { + return read + (n - todo) + } + } + + val toRead = Math.min(todo, bufferAvailable) + buffer.getChars(bufferReadPos, bufferReadPos + toRead, dest, destOffset) + bufferReadPos += toRead + destOffset += toRead + todo -= toRead + } + + return n - todo + } + + override fun skip(n: Long): Long { + maybeReadFirst() + + var todo = n.toInt() + while (todo > 0) { + if (bufferAvailable == 0) { + if (inputEof) return n - todo + val read = input.skip(todo.toLong()) + if (read == 0L && todo != 0) { + inputEof = true + return n - todo + } else { + return read + (n - todo) + } + } + + val toRead = Math.min(todo, bufferAvailable) + bufferReadPos += toRead + todo -= toRead + } + return n - todo + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinJsNodeModulesTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinJsNodeModulesTask.kt index e425dc262d4..3feede7b932 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinJsNodeModulesTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinJsNodeModulesTask.kt @@ -8,6 +8,7 @@ package org.jetbrains.kotlin.gradle.targets.js.tasks import org.gradle.api.DefaultTask import org.gradle.api.file.FileCollection import org.gradle.api.tasks.* +import org.jetbrains.kotlin.gradle.targets.js.internal.RewriteSourceMapFilterReader import java.io.File open class KotlinJsNodeModulesTask : DefaultTask() { @@ -34,17 +35,29 @@ open class KotlinJsNodeModulesTask : DefaultTask() { isKotlinJsRuntimeFile(fileTreeElement.file) } + sync.eachFile { + if (it.name.endsWith(".js.map")) { + it.filter( + mapOf( + "srcSourceRoot" to it.file.parentFile, + "targetSourceRoot" to nodeModulesDir + ), + RewriteSourceMapFilterReader::class.java + ) + } + } + sync.into(nodeModulesDir) } } } private val File.isZip - get() = isFile && name.endsWith(".jar") + get() = isFile && (name.endsWith(".jar") || name.endsWith(".zip")) internal fun isKotlinJsRuntimeFile(file: File): Boolean { if (!file.isFile) return false val name = file.name return (name.endsWith(".js") && !name.endsWith(".meta.js")) - || name.endsWith(".js.map") + || name.endsWith(".js.map") } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinNodeJsTestRuntimeExtractTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinNodeJsTestRuntimeExtractTask.kt deleted file mode 100644 index f3d0f80dc85..00000000000 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinNodeJsTestRuntimeExtractTask.kt +++ /dev/null @@ -1,55 +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.tasks - -import org.gradle.api.DefaultTask -import org.gradle.api.Project -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.SkipWhenEmpty -import org.gradle.api.tasks.TaskAction -import org.jetbrains.kotlin.gradle.tasks.locateOrRegisterTask -import java.io.File -import java.nio.file.Files -import java.nio.file.StandardCopyOption - -const val kotlinTestNodeJsRunnerJs = "kotlin-test-nodejs-runner.js" - -internal val Project.extractedKotlinTestNodeJsRunner: File - get() = project.buildDir.resolve("kotlin/$kotlinTestNodeJsRunnerJs") - -internal val Project.kotlinNodeJsTestRuntimeExtractTask - get() = project.locateOrRegisterTask( - "kotlinNodeJsTestRuntimeExtract" - ) { task -> - task.destination = extractedKotlinTestNodeJsRunner - } - -open class KotlinNodeJsTestRuntimeExtractTask : DefaultTask() { - @Input - public var resourceName: String = "/$kotlinTestNodeJsRunnerJs" - - @OutputFile - @SkipWhenEmpty - public lateinit var destination: File - - @TaskAction - fun copyRuntime() { - val testsRuntime = javaClass.getResourceAsStream(resourceName) - ?: error("Cannot find `$resourceName` in resources") - - check(destination.parentFile.exists() || destination.parentFile.mkdirs()) { - "Cannot create directory ${destination.parentFile}" - } - - Files.copy( - testsRuntime, - destination.toPath(), - StandardCopyOption.REPLACE_EXISTING - ) - } -} - diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinNodeJsTestTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinNodeJsTestTask.kt index cfb9c5740d7..694ca57c05a 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinNodeJsTestTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/tasks/KotlinNodeJsTestTask.kt @@ -38,14 +38,14 @@ open class KotlinNodeJsTestTask : AbstractTestTask() { var excludes = mutableSetOf() @InputDirectory - var nodeModulesDir: File? = null + lateinit var nodeModulesDir: File @Input @SkipWhenEmpty var nodeModulesToLoad: Set = setOf() - @InputFile - lateinit var testRuntimeNodeModule: File + @Input + lateinit var testRuntimeNodeModules: Collection @Suppress("UnstableApiUsage") private val filterExt: DefaultTestFilter @@ -69,6 +69,9 @@ open class KotlinNodeJsTestTask : AbstractTestTask() { val nodeJsWorkingDirCanonicalPath: String @Input get() = nodeJsProcessOptions.workingDir.canonicalPath + @Input + var debug: Boolean = false + fun nodeJs(options: ProcessForkOptions.() -> Unit) { options(nodeJsProcessOptions) } @@ -77,7 +80,13 @@ open class KotlinNodeJsTestTask : AbstractTestTask() { val extendedForkOptions = DefaultProcessForkOptions(fileResolver) nodeJsProcessOptions.copyTo(extendedForkOptions) - extendedForkOptions.environment.addPath("NODE_PATH", nodeModulesDir!!.canonicalPath) + extendedForkOptions.environment.addPath("NODE_PATH", nodeModulesDir.canonicalPath) + + val nodeJsArgs = mutableListOf() + + if (debug) { + nodeJsArgs.add("--inspect-brk") + } val cliArgs = KotlinNodeJsTestRunnerCliArgs( nodeModulesToLoad.toList(), @@ -98,7 +107,12 @@ open class KotlinNodeJsTestTask : AbstractTestTask() { return TCServiceMessagesTestExecutionSpec( extendedForkOptions, - listOf(testRuntimeNodeModule.absolutePath) + cliArgs.toList(), + nodeJsArgs + + testRuntimeNodeModules + .map { nodeModulesDir.resolve(it) } + .filter { it.exists() } + .map { it.absolutePath } + + cliArgs.toList(), clientSettings ) } diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/org/jetbrains/kotlin/gradle/targets/js/internal/RewriteSourceMapFilterReaderTest.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/org/jetbrains/kotlin/gradle/targets/js/internal/RewriteSourceMapFilterReaderTest.kt new file mode 100644 index 00000000000..efdc31cc18d --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/org/jetbrains/kotlin/gradle/targets/js/internal/RewriteSourceMapFilterReaderTest.kt @@ -0,0 +1,172 @@ +package org.jetbrains.kotlin.gradle.org.jetbrains.kotlin.gradle.targets.js.internal + +import org.jetbrains.kotlin.gradle.targets.js.internal.RewriteSourceMapFilterReader +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.Reader +import java.io.StringReader + +class RewriteSourceMapFilterReaderTest { + @Test + fun testShort() { + // input json should fit in buffer (1024) + doTest(1) + } + + @Test + fun testLong() { + doTest(100) + } + + @Test + fun testVeryLong() { + doTest(1000) + } + + @Test + fun testPrologSplitByReads() { + // Test the case when prolog contents is splitted between reads. + // Like, one buffer ends with `],"sourc`, and the other starts with `esContent"` + val prolog = sample("", "").substringBefore(RewriteSourceMapFilterReader.PROLOG_END) + doTest(1, addToProlog = "-".repeat(1024 - prolog.length - "],\"sourc".length)) + } + + private fun sample(mappings: String, addToProlog: String): String { + //language=JSON + return """{"version":3,"file":"single-platform${addToProlog}.js","sources":["../../../../src/main/kotlin/main.kt"],"sourcesContent":[null],"names":[],"mappings":"$mappings"}""" + } + + private fun doTest(repeatMappings: Int, addToProlog: String = "") { + val mappings0 = + ";;;;;;;;;;;;;;;;;;;;;;;;;IAQc,c;EAAA,C;;IAQc,O;IACJ,W;EAAA,C;;IAFA,aAAE,wBAAF,kBAA4B,gCAA5B," + + "C;IAGJ,W;EAAA,C;;IAJA,uBAAI,yBAAJ,C;IAKJ,W;EAAA,C;;IANA,wBAAK,kBAAL,C;IAOJ,W;EAAA,C;;IATR," + + "QACqC,KAAb,WAAhB,oBAAgB,CAAa,UAAK,WAAL,CADrC,C;EAWJ,C;;;;;;;;;" + val mappings = mappings0.repeat(repeatMappings) + + val filter = RewriteSourceMapFilterReader( + StringReader(sample(mappings, addToProlog)), + "/root/build/classes/kotlin/test/", + "/root/build/test_node_modules/" + ) + + assertEquals( + //language=JSON + """{"version":3,"file":"single-platform${addToProlog}.js","sources":["../../src/main/kotlin/main.kt"],"sourcesContent":[null],"names":[],"mappings":"$mappings"}""", + filter.readText() + ) + } + + @Test + fun testUnsupportedUnderfindProlog() { + val filter = + RewriteSourceMapFilterReaderMock( + StringReader( + //language=JSON + """{"version":3,"file":"single-platform.js","sources":["../../../../src/main/kotlin/main.kt"],"names":[],"mappings":""}""" + ), + "/root/build/classes/kotlin/test/", + "/root/build/test_node_modules/" + ) + + assertEquals( + //language=JSON + """{"version":3,"file":"single-platform.js","sources":["../../../../src/main/kotlin/main.kt"],"names":[],"mappings":""}""", + filter.readText() + ) + assertEquals( + "Unsupported format. Contents should starts with `{\"version\":3,\"file\":\"...\",\"sources\":[...],\"sourcesContent\":...`. \"sourcesContent\" or \"sources\" not found", + filter.warning + ) + } + + @Test + fun testUnsupportedPrologSize() { + val repeat = "\"../../../../src/main/kotlin/main.kt\",".repeat(0xfffff / 35) + val filter = + RewriteSourceMapFilterReaderMock( + StringReader( + //language=JSON + """{"version":3,"file":"single-platform.js","sources":[$repeat],"sourcesContent":[null],"names":[],"mappings":""}""" + ), + "/root/build/classes/kotlin/test/", + "/root/build/test_node_modules/" + ) + + assertEquals( + //language=JSON + """{"version":3,"file":"single-platform.js","sources":[$repeat],"sourcesContent":[null],"names":[],"mappings":""}""", + filter.readText() + ) + assertEquals("Too many sources or format is not supported", filter.warning) + } + + + @Test + fun testUnsupportedFormat() { + val filter = + RewriteSourceMapFilterReaderMock( + StringReader( + //language=JSON + """{"file":"single-platform.js","version1":3,"sources":["../../../../src/main/kotlin/main.kt"],"sourcesContent":[null],"names":[],"mappings":""}""" + ), + "/root/build/classes/kotlin/test/", + "/root/build/test_node_modules/" + ) + + assertEquals( + //language=JSON + """{"file":"single-platform.js","version1":3,"sources":["../../../../src/main/kotlin/main.kt"],"sourcesContent":[null],"names":[],"mappings":""}""", + filter.readText() + ) + assertEquals( + "Unsupported format. Contents should starts with `{\"version\":3,\"file\":\"...\",\"sources\":[...],\"sourcesContent\":...`. Unknown key \"version1\" at line 1 column 40 path \$.version1 in `{\"file\":\"single-platform.js\",\"version1\":3,\"sources\":[\"../../../../src/main/kotlin/main.kt\"],\"sourcesContent\":[null],\"names\":[],\"mappings\":\"\"}", + filter.warning + ) + } + + @Test + fun testInvalidJson() { + val filter = + RewriteSourceMapFilterReaderMock( + StringReader("{:)],\"sourcesContent\":[null]}"), + "/root/build/classes/kotlin/test/", + "/root/build/test_node_modules/" + ) + + assertEquals("{:)],\"sourcesContent\":[null]}", filter.readText()) + assertEquals( + "Unsupported format. Contents should starts with `{\"version\":3,\"file\":\"...\",\"sources\":[...],\"sourcesContent\":...`. Malformed JSON at line 1 column 3 path \$. in `{:)],\"sourcesContent\":[null]}", + filter.warning + ) + } + + class RewriteSourceMapFilterReaderMock(input: Reader, srcSourceRoot: String, targetSourceRoot: String) : + RewriteSourceMapFilterReader(input) { + init { + setProperties(this, srcSourceRoot, targetSourceRoot) + } + + var warning: String? = null + + override fun warnUnsupported(reason: String) { + warning = reason + } + } +} + +private fun RewriteSourceMapFilterReader( + input: Reader, + srcSourceRoot: String, + targetSourceRoot: String +) = RewriteSourceMapFilterReader(input).also { + setProperties(it, srcSourceRoot, targetSourceRoot) +} + +private fun setProperties( + it: RewriteSourceMapFilterReader, + srcSourceRoot: String, + targetSourceRoot: String +) { + it.srcSourceRoot = srcSourceRoot + it.targetSourceRoot = targetSourceRoot +} \ No newline at end of file diff --git a/libraries/tools/kotlin-test-nodejs-runner/build.gradle.kts b/libraries/tools/kotlin-test-nodejs-runner/build.gradle.kts index c716c3ec0a7..f457a6922fa 100644 --- a/libraries/tools/kotlin-test-nodejs-runner/build.gradle.kts +++ b/libraries/tools/kotlin-test-nodejs-runner/build.gradle.kts @@ -1,10 +1,27 @@ import com.moowork.gradle.node.yarn.YarnTask +description = "Simple Kotlin/JS tests runner with TeamCity reporter" + plugins { id("base") id("com.moowork.node") version "1.2.0" } +val default = configurations.getByName(Dependency.DEFAULT_CONFIGURATION) +val archives = configurations.getByName(Dependency.ARCHIVES_CONFIGURATION) + +default.extendsFrom(archives) + +plugins.apply("maven") + +convention.getPlugin(MavenPluginConvention::class.java).also { + it.conf2ScopeMappings.addMapping(MavenPlugin.RUNTIME_PRIORITY, archives, Conf2ScopeMappingContainer.RUNTIME) +} + +dependencies { + archives(project(":kotlin-test:kotlin-test-js")) +} + node { version = "11.9.0" download = true @@ -25,25 +42,43 @@ tasks { setWorkingDir(projectDir) args = listOf("build") - inputs.dir(projectDir.resolve("src")) - outputs.file(projectDir.resolve("lib/kotlin-test-nodejs-runner.js")) + inputs.dir("src") + inputs.files( + "cli.ts", + "nodejs-source-map-support.js", + "package.json", + "rollup.config.js", + "tsconfig.json", + "yarn.lock" + ) + outputs.dir("lib") } create("cleanYarn") { group = "build" delete = setOf( - projectDir.resolve("node_modules"), - projectDir.resolve("lib"), - projectDir.resolve(".rpt2_cache") + "node_modules", + "lib", + ".rpt2_cache" ) } getByName("clean").dependsOn("cleanYarn") } +val jar = tasks.create("jar") { + dependsOn("yarnBuild") + from(projectDir.resolve("lib")) +} + artifacts { - add("archives", projectDir.resolve("lib/kotlin-test-nodejs-runner.js")) { - builtBy("yarnBuild") + add( + "archives", + jar.archiveFile.get().asFile + ) { + builtBy(jar) } -} \ No newline at end of file +} + +publish() \ No newline at end of file diff --git a/libraries/tools/kotlin-test-nodejs-runner/nodejs-source-map-support.js b/libraries/tools/kotlin-test-nodejs-runner/nodejs-source-map-support.js new file mode 100644 index 00000000000..1398de5fcca --- /dev/null +++ b/libraries/tools/kotlin-test-nodejs-runner/nodejs-source-map-support.js @@ -0,0 +1 @@ +require('source-map-support').install(); \ No newline at end of file diff --git a/libraries/tools/kotlin-test-nodejs-runner/package.json b/libraries/tools/kotlin-test-nodejs-runner/package.json index 0cb73042e2c..36f0ebfbdb8 100644 --- a/libraries/tools/kotlin-test-nodejs-runner/package.json +++ b/libraries/tools/kotlin-test-nodejs-runner/package.json @@ -14,7 +14,8 @@ "build": "rimraf lib/* && rollup -c rollup.config.js" }, "dependencies": { - "@types/node": "^10.12.21" + "@types/node": "^10.12.21", + "source-map-support": "^0.5.11" }, "devDependencies": { "copyfiles": "^2.1.0", diff --git a/libraries/tools/kotlin-test-nodejs-runner/rollup.config.js b/libraries/tools/kotlin-test-nodejs-runner/rollup.config.js index ddd95b6df22..8809a7b256c 100644 --- a/libraries/tools/kotlin-test-nodejs-runner/rollup.config.js +++ b/libraries/tools/kotlin-test-nodejs-runner/rollup.config.js @@ -11,49 +11,66 @@ export default [ output: { file: 'lib/kotlin-test-nodejs-runner.js', format: 'cjs', - banner: '#!/usr/bin/env node' + banner: '#!/usr/bin/env node', + sourcemap: true }, plugins: [ nodeResolve({ - jsnext: true, - main: true - }), + jsnext: true, + main: true + }), commonjs(), typescript({ - tsconfig: "tsconfig.json" - }), + tsconfig: "tsconfig.json" + }), uglify({ - sourcemap: true, - compress: { - // hoist_funs: true, - // hoist_vars: true, - toplevel: true, - unsafe: true, - dead_code: true, - global_defs: { - DEBUG: false, - VERSION: pckg.version, - BIN: Object.keys(pckg.bin)[0], - DESCRIPTION: pckg.description - } - }, - mangle: { - properties: { - keep_quoted: true, - reserved: [ - "argv", "hrtime", - "kotlin_test", "kotlin", "setAdapter", "setAssertHook_4duqou$", - "suite", "test", - "stack" - ] - }, - toplevel: true, - }, - // output: { - // beautify: true - // } - }), - // sourceMaps() + sourcemap: true, + compress: { + // hoist_funs: true, + // hoist_vars: true, + toplevel: true, + unsafe: true, + dead_code: true, + global_defs: { + DEBUG: false, + VERSION: pckg.version, + BIN: Object.keys(pckg.bin)[0], + DESCRIPTION: pckg.description + } + }, + mangle: { + properties: { + keep_quoted: true, + reserved: [ + "argv", "hrtime", + "kotlin_test", "kotlin", "setAdapter", "setAssertHook_4duqou$", + "suite", "test", + "stack" + ] + }, + toplevel: true, + } + }) + ] + }, + { + input: './nodejs-source-map-support.js', + external: ['path', 'fs', 'module'], + output: { + file: 'lib/kotlin-nodejs-source-map-support.js', + format: 'cjs', + sourcemap: true + }, + plugins: [ + nodeResolve({ + jsnext: true, + main: true + }), + commonjs(), + uglify({ + compress: true, + sourcemap: true + }) ] } ] \ No newline at end of file diff --git a/libraries/tools/kotlin-test-nodejs-runner/tsconfig.json b/libraries/tools/kotlin-test-nodejs-runner/tsconfig.json index 19433724709..bb7ef503871 100644 --- a/libraries/tools/kotlin-test-nodejs-runner/tsconfig.json +++ b/libraries/tools/kotlin-test-nodejs-runner/tsconfig.json @@ -2,10 +2,8 @@ "compilerOptions": { "target": "es5", "module": "ESNext", -// "declaration": true, -// "outDir": "./lib", "strict": true, -// "sourceMap": true, + "sourceMap": true, "allowSyntheticDefaultImports": true, "inlineSourceMap": true }, diff --git a/libraries/tools/kotlin-test-nodejs-runner/yarn.lock b/libraries/tools/kotlin-test-nodejs-runner/yarn.lock index e0db42584cc..b4968c02c9b 100644 --- a/libraries/tools/kotlin-test-nodejs-runner/yarn.lock +++ b/libraries/tools/kotlin-test-nodejs-runner/yarn.lock @@ -94,6 +94,11 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + builtin-modules@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.0.0.tgz#1e587d44b006620d90286cc7a9238bbc6129cab1" @@ -889,12 +894,20 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" +source-map-support@^0.5.11: + version "0.5.11" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.11.tgz#efac2ce0800355d026326a0ca23e162aeac9a4e2" + integrity sha512-//sajEx/fGL3iw6fltKMdPvy8kL3kJ2O3iuYlRoT3k9Kb4BjOoZ+BZzaNHeuaruSt+Kf3Zk9tnfAQg9/AJqUVQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= -source-map@~0.6.1: +source-map@^0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==