Gradle, js, browser: fix karma.conf.js writer

#KT-31011
This commit is contained in:
Sergey Rostov
2019-05-15 09:59:13 +03:00
parent 14d399e9ab
commit 236dfa98b6
9 changed files with 394 additions and 193 deletions
@@ -33,10 +33,13 @@ class NpmPackages {
val karmaMocha = NpmPackageVersion("karma-mocha", "1.3.0")
val karmaWebpack = NpmPackageVersion("karma-webpack", "*")
val karmaCoverage = NpmPackageVersion("karma-coverage", "*")
val karmaSourceMapSupport = NpmPackageVersion("karma-source-map-support", "1.4.0")
val kotlinNodeJsTestRunner = KotlinGradleNpmPackage("test-nodejs-runner")
val istanbulInstrumenterLoader = NpmPackageVersion("istanbul-instrumenter-loader", "*")
}
interface RequiredKotlinJsDependency {
@@ -13,7 +13,7 @@ import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfigWriter
import org.jetbrains.kotlin.gradle.tasks.createOrRegisterTask
class KotlinBrowserJs(target: KotlinJsTarget) :
@@ -25,6 +25,7 @@ class KotlinBrowserJs(target: KotlinJsTarget) :
override fun configureDefaultTestFramework(it: KotlinJsTest) {
it.useKarma {
useMocha()
useWebpack()
useChromeHeadless()
}
@@ -62,7 +63,7 @@ class KotlinBrowserJs(target: KotlinJsTarget) :
it.bin = "webpack-dev-server"
it.entry = npmProject.compileOutput(compileKotlinTask)
it.devServer = KotlinWebpackConfig.DevServer(
it.devServer = KotlinWebpackConfigWriter.DevServer(
open = true,
contentBase = listOf(compilation.output.resourcesDir.canonicalPath)
)
@@ -10,6 +10,7 @@ import org.gradle.api.Project
import org.gradle.process.ProcessForkOptions
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec
import org.jetbrains.kotlin.gradle.targets.js.appendConfigsFromDir
import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTraceAsJvm
import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmPackageVersion
@@ -17,12 +18,20 @@ import org.jetbrains.kotlin.gradle.targets.js.npm.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTestFramework
import org.jetbrains.kotlin.gradle.targets.js.testing.karma.KarmaConfig.CoverageReporter.Reporter
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfigWriter
import org.jetbrains.kotlin.gradle.testing.internal.reportsDir
import java.io.File
class KotlinKarma(val project: Project) : KotlinJsTestFramework {
private val config: KarmaConfig = KarmaConfig()
private val requiredDependencies = mutableSetOf<NpmPackageVersion>()
private val versions = project.nodeJs.versions
private val configurators = mutableListOf<(KotlinJsTest) -> Unit>()
private val confJsWriters = mutableListOf<(Appendable) -> Unit>()
private var sourceMaps = false
private var configDirectory: File? = project.projectDir.resolve("karma.config.d").takeIf { it.isDirectory }
override val requiredNpmDependencies: Collection<RequiredKotlinJsDependency>
get() = requiredDependencies.toList()
@@ -30,12 +39,20 @@ class KotlinKarma(val project: Project) : KotlinJsTestFramework {
init {
requiredDependencies.add(versions.karma)
requiredDependencies.add(versions.karmaTeamcityReporter)
config.reporters.add("teamcity")
useTeamcityReporter()
config.singleRun = true
}
private fun useTeamcityReporter() {
requiredDependencies.add(versions.karmaTeamcityReporter)
config.reporters.add("teamcity")
}
fun useConfigDirectory(dir: File) {
configDirectory = dir
}
fun useChrome() = useBrowser("Chrome", versions.karmaChromeLauncher)
fun useChromeCanary() = useBrowser("ChromeCanary", versions.karmaChromeLauncher)
@@ -60,15 +77,85 @@ class KotlinKarma(val project: Project) : KotlinJsTestFramework {
fun useMocha() {
requiredDependencies.add(versions.karmaMocha)
requiredDependencies.add(versions.mocha)
config.frameworks.add("mocha")
}
fun useWebpack() {
requiredDependencies.add(versions.karmaWebpack)
addPreprocessor("webpack")
confJsWriters.add {
it.appendln()
it.appendln("// webpack config")
it.appendln("function createWebpackConfig() {")
KotlinWebpackConfigWriter(
sourceMaps = true,
export = false
).appendTo(it)
it.appendln(" return config;")
it.appendln("}")
it.appendln()
it.appendln("config.set({webpack: createWebpackConfig()});")
it.appendln()
}
}
fun useCoverage(
html: Boolean = true,
lcov: Boolean = true,
cobertura: Boolean = false,
teamcity: Boolean = true,
text: Boolean = false,
textSummary: Boolean = false,
json: Boolean = false,
jsonSummary: Boolean = false
) {
if (listOf(
html, lcov, cobertura,
teamcity, text, textSummary,
json, jsonSummary
).all { !it }
) return
requiredDependencies.add(versions.karmaCoverage)
config.reporters.add("coverage")
addPreprocessor("coverage") { !it.endsWith("_test.js") }
configurators.add {
val reportDir = project.reportsDir.resolve("coverage/${it.name}")
reportDir.mkdirs()
config.coverageReporter = KarmaConfig.CoverageReporter(reportDir.canonicalPath).also { coverage ->
if (html) coverage.reporters.add(Reporter("html"))
if (lcov) coverage.reporters.add(Reporter("lcovonly"))
if (cobertura) coverage.reporters.add(Reporter("cobertura"))
if (teamcity) coverage.reporters.add(Reporter("teamcity"))
if (text) coverage.reporters.add(Reporter("text"))
if (textSummary) coverage.reporters.add(Reporter("text-summary"))
if (json) coverage.reporters.add(Reporter("json"))
if (jsonSummary) coverage.reporters.add(Reporter("json-summary"))
}
}
}
fun useSourceMapSupport() {
config.frameworks.add("source-map-support")
requiredDependencies.add(versions.karmaSourceMapSupport)
sourceMaps = true
addPreprocessor("sourcemap")
}
private fun addPreprocessor(name: String, predicate: (String) -> Boolean = { true }) {
configurators.add {
config.files.forEach {
if (predicate(it)) {
config.preprocessors.getOrPut(it) { mutableListOf() }.add(name)
}
}
}
}
override fun createTestExecutionSpec(
@@ -86,19 +173,30 @@ class KotlinKarma(val project: Project) : KotlinJsTestFramework {
val npmProject = task.project.npmProject
config.files.addAll(task.nodeModulesToLoad.map {
npmProject.require(it)
})
val files = task.nodeModulesToLoad.map { npmProject.require(it) }
config.files.addAll(files)
config.basePath = npmProject.nodeModulesDir.absolutePath
configurators.forEach {
it(task)
}
val karmaConfJs = npmProject.nodeWorkDir.resolve("karma.conf.js")
karmaConfJs.printWriter().use {
it.println("module.exports = function(config) {")
it.print(" config.set(")
GsonBuilder().setPrettyPrinting().create().toJson(config, it)
it.println(");")
it.println("}")
karmaConfJs.printWriter().use { confWriter ->
confWriter.println("module.exports = function(config) {")
confWriter.println()
confWriter.print("config.set(")
GsonBuilder().setPrettyPrinting().create().toJson(config, confWriter)
confWriter.println(");")
confJsWriters.forEach { it(confWriter) }
confWriter.appendFromConfigDir()
confWriter.println()
confWriter.println("}")
}
val nodeModules = listOf("karma/bin/karma")
@@ -118,4 +216,16 @@ class KotlinKarma(val project: Project) : KotlinJsTestFramework {
clientSettings
)
}
private fun Appendable.appendFromConfigDir() {
val configDirectory = configDirectory ?: return
check(configDirectory.isDirectory) {
"\"$configDirectory\" is not a directory"
}
appendln()
appendConfigsFromDir(configDirectory)
appendln()
}
}
@@ -14,5 +14,16 @@ class KarmaConfig {
val frameworks: MutableList<String> = mutableListOf()
val browsers: MutableList<String> = mutableListOf()
val reporters: MutableList<String> = mutableListOf()
val preprocessors: MutableMap<String, List<String>> = mutableMapOf()
val preprocessors: MutableMap<String, MutableList<String>> = mutableMapOf()
var coverageReporter: CoverageReporter? = null
class CoverageReporter(var dir: String) {
val reporters = mutableListOf<Reporter>()
class Reporter(
val type: String,
val subDir: String? = null,
val file: String? = null
)
}
}
@@ -0,0 +1,22 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.targets.js
import java.io.File
fun Appendable.appendConfigsFromDir(confDir: File) {
if (!confDir.isDirectory) return
confDir.listFiles()
?.toList()
?.filter { it.name.endsWith(".js") }
?.forEach {
appendln("// ${it.name}")
append(it.readText())
appendln()
appendln()
}
}
@@ -67,7 +67,7 @@ open class KotlinWebpack : DefaultTask(), RequiresNpmDependencies {
@Input
@Optional
var devServer: KotlinWebpackConfig.DevServer? = null
var devServer: KotlinWebpackConfigWriter.DevServer? = null
override val requiredNpmDependencies: Collection<NpmPackageVersion>
get() = mutableListOf<NpmPackageVersion>().also {
@@ -93,14 +93,15 @@ open class KotlinWebpack : DefaultTask(), RequiresNpmDependencies {
configFile,
execHandleFactory,
bin,
KotlinWebpackConfig(
entry,
if (saveEvaluatedConfigFile) evaluatedConfigFile else null,
outputPath,
configDirectory,
if (report) reportDir else null,
devServer,
sourceMaps = sourceMaps
KotlinWebpackConfigWriter(
entry = entry,
reportEvaluatedConfigFile = if (saveEvaluatedConfigFile) evaluatedConfigFile else null,
outputPath = outputPath,
configDirectory = configDirectory,
bundleAnalyzerReportDir = if (report) reportDir else null,
devServer = devServer,
sourceMaps = sourceMaps,
sourceMapsRuntime = sourceMaps
)
)
@@ -1,165 +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.
*/
@file:Suppress("NodeJsCodingAssistanceForCoreModules", "JSUnresolvedFunction")
package org.jetbrains.kotlin.gradle.targets.js.webpack
import com.google.gson.GsonBuilder
import com.google.gson.stream.JsonWriter
import java.io.File
import java.io.Serializable
import java.io.StringWriter
@Suppress("MemberVisibilityCanBePrivate")
class KotlinWebpackConfig(
val entry: File,
val reportEvaluatedConfigFile: File?,
val outputPath: File,
val configDirectory: File?,
val reportDir: File?,
var devServer: DevServer? = null,
val showProgress: Boolean = false,
val sourceMaps: Boolean = false
) {
@Suppress("unused")
data class BundleAnalyzerPlugin(
val analyzerMode: String,
val reportFilename: String,
val openAnalyzer: Boolean,
val generateStatsFile: Boolean,
val statsFilename: String
) : Serializable
@Suppress("unused")
data class DevServer(
val inline: Boolean = true,
val lazy: Boolean = false,
val noInfo: Boolean = true,
val open: Any = true,
val overlay: Any = false,
val port: Int = 8080,
val proxy: Map<String, Any>? = null,
val contentBase: List<String>
) : Serializable
fun save(configFile: File) {
configFile.writeText(build())
}
fun build() = buildString {
//language=JavaScript 1.8
appendln(
"""
var config = {
mode: 'development',
entry: ['${entry.canonicalPath}'],
output: {
path: '${outputPath.canonicalPath}',
filename: '${entry.name}'
},
resolve: {
modules: [
"node_modules"
]
},
plugins: [],
module: {
rules: [
{
test: /\.js${'$'}/,
use: ["source-map-loader"],
enforce: "pre"
}
]
},
devtool: 'eval-source-map'
};""".trim()
)
appendln()
if (sourceMaps) {
//language=JavaScript 1.8
appendln("""
// source maps
config.entry.push('source-map-support/browser-source-map-support.js');
config.module.rules.push({test: /\.js${'$'}/, use: ['source-map-loader'], enforce: 'pre'});
""".trim())
appendln()
}
if (devServer != null) {
appendln("// dev server")
appendln("config.devServer = ${json(devServer!!)};")
appendln()
}
if (reportDir != null) {
val reportBasePath = "${reportDir.canonicalPath}/${entry.name}"
val config = BundleAnalyzerPlugin(
"static",
"$reportBasePath.report.html",
false,
true,
"$reportBasePath.stats.json"
)
//language=JavaScript 1.8
appendln(
"""
// save webpack-bundle-analyzer report
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
config.plugins.push(new BundleAnalyzerPlugin(${json(config)}));
"""
)
}
if (configDirectory != null) {
appendln()
loadConfigs(configDirectory)
}
appendln()
if (reportEvaluatedConfigFile != null) {
val filePath = jsQuotedString(reportEvaluatedConfigFile.canonicalPath)
//language=JavaScript 1.8
appendln(
"""
// save evaluated config file
var util = require('util');
var fs = require("fs");
var evaluatedConfig = util.inspect(config, {showHidden: false, depth: null, compact: false});
fs.writeFile($filePath, evaluatedConfig, function (err) {});
"""
)
appendln()
}
appendln("module.exports = config")
}
private fun json(obj: Any) = StringWriter().also {
GsonBuilder().setPrettyPrinting().create().toJson(obj, it)
}.toString()
private fun jsQuotedString(str: String) = StringWriter().also {
JsonWriter(it).value(str)
}.toString()
private fun StringBuilder.loadConfigs(confDir: File) {
if (confDir.isDirectory) confDir
.listFiles()
?.toList()
?.filter { it.name.endsWith(".js") }
?.forEach {
appendln("// ${it.name}")
append(it.readText())
appendln()
appendln()
}
}
}
@@ -0,0 +1,218 @@
/*
* 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.
*/
@file:Suppress("NodeJsCodingAssistanceForCoreModules", "JSUnresolvedFunction")
package org.jetbrains.kotlin.gradle.targets.js.webpack
import com.google.gson.GsonBuilder
import com.google.gson.stream.JsonWriter
import org.jetbrains.kotlin.gradle.targets.js.appendConfigsFromDir
import java.io.File
import java.io.Serializable
import java.io.StringWriter
@Suppress("MemberVisibilityCanBePrivate")
class KotlinWebpackConfigWriter(
val mode: Mode = Mode.DEVELOPMENT,
val entry: File? = null,
val outputPath: File? = null,
val configDirectory: File? = null,
val bundleAnalyzerReportDir: File? = null,
val reportEvaluatedConfigFile: File? = null,
var devServer: DevServer? = null,
val showProgress: Boolean = false,
val sourceMaps: Boolean = false,
val sourceMapsRuntime: Boolean = false,
val export: Boolean = true
) {
enum class Mode(val code: String) {
DEVELOPMENT("development"),
PRODUCTION("production")
}
@Suppress("unused")
data class BundleAnalyzerPlugin(
val analyzerMode: String,
val reportFilename: String,
val openAnalyzer: Boolean,
val generateStatsFile: Boolean,
val statsFilename: String
) : Serializable
@Suppress("unused")
data class DevServer(
val inline: Boolean = true,
val lazy: Boolean = false,
val noInfo: Boolean = true,
val open: Any = true,
val overlay: Any = false,
val port: Int = 8080,
val proxy: Map<String, Any>? = null,
val contentBase: List<String>
) : Serializable
fun save(configFile: File) {
configFile.writer().use {
appendTo(it)
}
}
fun appendTo(target: Appendable) {
with(target) {
//language=JavaScript 1.8
appendln(
"""
var config = {
mode: '${mode.code}',
resolve: {
modules: [
"node_modules"
]
},
plugins: [],
module: {
rules: []
}
};
""".trimIndent()
)
appendEntry()
appendSourceMaps()
appendSourceMapsRuntime()
appendDevServer()
appendReport()
appendFromConfigDir()
appendEvaluatedFileReport()
if (export) {
//language=JavaScript 1.8
appendln("module.exports = config")
}
}
}
private fun Appendable.appendEvaluatedFileReport() {
if (reportEvaluatedConfigFile == null) return
val filePath = jsQuotedString(reportEvaluatedConfigFile.canonicalPath)
//language=JavaScript 1.8
appendln(
"""
// save evaluated config file
var util = require('util');
var fs = require("fs");
var evaluatedConfig = util.inspect(config, {showHidden: false, depth: null, compact: false});
fs.writeFile($filePath, evaluatedConfig, function (err) {});
""".trimIndent()
)
}
private fun Appendable.appendFromConfigDir() {
if (configDirectory == null) return
appendln()
appendConfigsFromDir(configDirectory)
appendln()
}
private fun Appendable.appendReport() {
if (bundleAnalyzerReportDir == null) return
entry ?: error("Entry should be defined for report")
val reportBasePath = "${bundleAnalyzerReportDir.canonicalPath}/${entry.name}"
val config = BundleAnalyzerPlugin(
"static",
"$reportBasePath.report.html",
false,
true,
"$reportBasePath.stats.json"
)
//language=JavaScript 1.8
appendln(
"""
// save webpack-bundle-analyzer report
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
config.plugins.push(new BundleAnalyzerPlugin(${json(config)}));
""".trimIndent()
)
appendln()
}
private fun Appendable.appendDevServer() {
if (devServer == null) return
appendln("// dev server")
appendln("config.devServer = ${json(devServer!!)};")
appendln()
}
private fun Appendable.appendSourceMapsRuntime() {
if (!sourceMapsRuntime) return
//language=JavaScript 1.8
appendln(
"""
// source maps runtime
if (!config.entry) config.entry = [];
config.entry.push('source-map-support/browser-source-map-support.js');
""".trimIndent()
)
}
private fun Appendable.appendSourceMaps() {
if (!sourceMaps) return
//language=JavaScript 1.8
appendln(
"""
// source maps
config.module.rules.push({
test: /\.js${'$'}/,
use: ["source-map-loader"],
enforce: "pre"
});
config.module.rules.push({test: /\.js${'$'}/, use: ['source-map-loader'], enforce: 'pre'});
config.devtool = 'eval-source-map';
""".trimIndent()
)
}
private fun Appendable.appendEntry() {
if (entry == null || outputPath == null) return
//language=JavaScript 1.8
appendln(
"""
// entry
if (!config.entry) config.entry = [];
config.entry.push('${entry.canonicalPath}');
config.output = {
path: '${outputPath.canonicalPath}',
filename: '${entry.name}'
};
""".trimIndent()
)
}
private fun json(obj: Any) = StringWriter().also {
GsonBuilder().setPrettyPrinting().create().toJson(obj, it)
}.toString()
private fun jsQuotedString(str: String) = StringWriter().also {
JsonWriter(it).value(str)
}.toString()
}
@@ -25,7 +25,7 @@ internal class KotlinWebpackRunner(
val configFile: File,
val execHandleFactory: ExecHandleFactory,
val bin: String,
val config: KotlinWebpackConfig
val configWriter: KotlinWebpackConfigWriter
) {
fun execute(): ExecResult {
val exec = start()
@@ -37,20 +37,20 @@ internal class KotlinWebpackRunner(
}
fun start(): ExecHandle {
check(config.entry.isFile) {
"${this}: Entry file not existed \"${config.entry}\""
check(configWriter.entry?.isFile == true) {
"${this}: Entry file not existed \"${configWriter.entry}\""
}
NpmResolver.resolve(project)
val npmProjectLayout = NpmProject[project]
config.save(configFile)
configWriter.save(configFile)
val execFactory = execHandleFactory.newExec()
val args = mutableListOf<String>("--config", configFile.absolutePath)
if (config.showProgress) {
if (configWriter.showProgress) {
args.add("--progress")
}