From eb7d09a562b5f8351f3107d1d0c00de0fe57beff Mon Sep 17 00:00:00 2001 From: Sergey Rostov Date: Mon, 22 Apr 2019 15:35:16 +0300 Subject: [PATCH] Gradle, JS: webpack support #KT-31013 --- .../gradle/plugin/KotlinTargetConfigurator.kt | 4 +- .../gradle/plugin/mpp/kotlinCompilations.kt | 4 + .../KotlinJsCompilationTestsConfigurator.kt | 37 ++-- .../targets/js/KotlinJsTargetConfigurator.kt | 46 +++-- .../gradle/targets/js/KotlinJsTargetPreset.kt | 3 + .../gradle/targets/js/npm/NpmProject.kt | 2 + .../targets/js/webpack/KotlinWebpack.kt | 163 ++++++++---------- .../targets/js/webpack/KotlinWebpackConfig.kt | 144 ++++++++++++++++ .../targets/js/webpack/KotlinWebpackRunner.kt | 56 ++++++ .../kotlin/gradle/targets/js/yarn/Yarn.kt | 48 ++++-- .../gradle/targets/js/yarn/YarnAvoidance.kt | 41 +++++ .../gradle/targets/js/yarn/YarnSetupTask.kt | 3 +- .../kotlin/gradle/tasks/TasksProvider.kt | 10 +- 13 files changed, 410 insertions(+), 151 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpackConfig.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpackRunner.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnAvoidance.kt diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt index 8d40c68ed4e..c21789dddda 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt @@ -46,8 +46,8 @@ abstract class AbstractKotlinTargetConfigurator } - abstract protected fun configureArchivesAndComponent(target: KotlinTargetType) - abstract protected fun configureTest(target: KotlinTargetType) + protected abstract fun configureArchivesAndComponent(target: KotlinTargetType) + protected abstract fun configureTest(target: KotlinTargetType) private fun Project.registerOutputsForStaleOutputCleanup(kotlinCompilation: KotlinCompilation<*>) { val cleanTask = tasks.getByName(LifecycleBasePlugin.CLEAN_TASK_NAME) as Delete diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinCompilations.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinCompilations.kt index 43c7af4777e..1fcb6569729 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinCompilations.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinCompilations.kt @@ -18,6 +18,7 @@ import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.sources.defaultSourceSetLanguageSettingsChecker import org.jetbrains.kotlin.gradle.plugin.sources.getSourceSetHierarchy import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile +import org.jetbrains.kotlin.gradle.tasks.locateTask import org.jetbrains.kotlin.gradle.utils.addExtendsFromRelation import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName import java.util.* @@ -48,6 +49,9 @@ abstract class AbstractKotlinCompilation( override val compileKotlinTask: KotlinCompile get() = (target.project.tasks.getByName(compileKotlinTaskName) as KotlinCompile) + val compileKotlinTaskHolder: TaskHolder> + get() = target.project.locateTask(compileKotlinTaskName)!! + // Don't declare this property in the constructor to avoid NPE // when an overriding property of a subclass is accessed instead. override val target: KotlinTarget = target 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 ecb3011096e..e8343dc578d 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 @@ -1,22 +1,19 @@ package org.jetbrains.kotlin.gradle.targets.js import org.gradle.language.base.plugins.LifecycleBasePlugin -import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationToRunnableFiles +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest -import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile import org.jetbrains.kotlin.gradle.tasks.createOrRegisterTask import org.jetbrains.kotlin.gradle.testing.internal.configureConventions import org.jetbrains.kotlin.gradle.testing.internal.registerTestTask import org.jetbrains.kotlin.utils.addIfNotNull -internal class KotlinJsCompilationTestsConfigurator( - val compilation: KotlinCompilationToRunnableFiles<*> -) { +internal open class KotlinJsCompilationTestsConfigurator(val compilation: KotlinJsCompilation) { private val target get() = compilation.target private val disambiguationClassifier get() = target.disambiguationClassifier private val project get() = target.project - private val compileTestKotlin2Js get() = compilation.compileKotlinTask as Kotlin2JsCompile + private val compileTask get() = compilation.compileKotlinTask private fun disambiguate(name: String, includeCompilation: Boolean = false): MutableList { val components = mutableListOf() @@ -37,35 +34,37 @@ internal class KotlinJsCompilationTestsConfigurator( get() = disambiguateCamelCased("test", false) fun configure() { - // apply plugin (cannot do it lazy) + // apply plugin (cannot be done at task instantiation time) val nodeJs = NodeJsPlugin.apply(target.project).root val testJs = project.createOrRegisterTask(testTaskName) { testJs -> testJs.group = LifecycleBasePlugin.VERIFICATION_GROUP - testJs.dependsOn(compileTestKotlin2Js, nodeJs.nodeJsSetupTask) + testJs.dependsOn(compileTask, nodeJs.nodeJsSetupTask) testJs.onlyIf { - compileTestKotlin2Js.outputFile.exists() + compileTask.outputFile.exists() } testJs.runtimeDependencyHandler = compilation testJs.targetName = disambiguationClassifier - testJs.nodeModulesToLoad.add(compileTestKotlin2Js.outputFile.name) + testJs.nodeModulesToLoad.add(compileTask.outputFile.name) testJs.configureConventions() } - testJs.doGetTask().useNodeJs { } - registerTestTask(testJs) -// project.afterEvaluate { -// testJs.configure { -// if (it.testFramework == null) { -// it.useNodeJs { } -// } -// } -// } + project.afterEvaluate { + testJs.configure { + if (it.testFramework == null) { + configureDefaultTestFramework(it) + } + } + } + } + + protected open fun configureDefaultTestFramework(it: KotlinJsTest) { + it.useNodeJs { } } } \ 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 8a914efe97b..5e7ba11fe1c 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 @@ -5,17 +5,19 @@ package org.jetbrains.kotlin.gradle.targets.js -import org.jetbrains.kotlin.gradle.plugin.* +import org.jetbrains.kotlin.gradle.plugin.Kotlin2JsSourceSetProcessor +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +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.NpmProjectLayout +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 -import org.jetbrains.kotlin.gradle.tasks.registerTask -class KotlinJsTargetConfigurator(kotlinPluginVersion: String) : - KotlinTargetConfigurator(true, true, kotlinPluginVersion) { +open class KotlinJsTargetConfigurator(kotlinPluginVersion: String) : + KotlinTargetConfigurator(true, true, kotlinPluginVersion) { override fun buildCompilationProcessor(compilation: KotlinJsCompilation): KotlinSourceSetProcessor<*> { val tasksProvider = KotlinTasksProvider(compilation.target.targetName) @@ -35,20 +37,42 @@ class KotlinJsTargetConfigurator(kotlinPluginVersion: String) : } override fun configureTest(target: KotlinOnlyTarget) { - val project = target.project - val npmProject = NpmProjectLayout[project] - target.compilations.all { compilation -> if (isTestCompilation(compilation)) { - KotlinJsCompilationTestsConfigurator(compilation).configure() - } else { - KotlinWebpack.configure(compilation) + newTestsConfigurator(compilation).configure() } } } + internal open fun newTestsConfigurator(compilation: KotlinJsCompilation) = + KotlinJsCompilationTestsConfigurator(compilation) + companion object { internal fun isTestCompilation(it: KotlinJsCompilation) = it.name == KotlinCompilation.TEST_COMPILATION_NAME } +} + +class KotlinJsSingleTargetConfigurator(kotlinPluginVersion: String) : + KotlinJsTargetConfigurator(kotlinPluginVersion) { + + override fun configureTarget(target: KotlinOnlyTarget) { + super.configureTarget(target) + configureApplication(target) + } + + private fun configureApplication(target: KotlinOnlyTarget) { + target.compilations.all { + if (it.name == KotlinCompilation.MAIN_COMPILATION_NAME) { + KotlinWebpack.configure(it) + } + } + } + + override fun newTestsConfigurator(compilation: KotlinJsCompilation) = + object : KotlinJsCompilationTestsConfigurator(compilation) { + override fun configureDefaultTestFramework(it: KotlinJsTest) { + it.useMocha { } + } + } } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetPreset.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetPreset.kt index bf130a23766..b605042cf63 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetPreset.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTargetPreset.kt @@ -8,6 +8,7 @@ package org.jetbrains.kotlin.gradle.plugin.mpp import org.gradle.api.Project import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.targets.js.KotlinJsSingleTargetConfigurator import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTargetConfigurator open class KotlinJsTargetPreset( @@ -42,4 +43,6 @@ class KotlinJsSingleTargetPreset( // In a Kotlin/JS single-platform project, we don't need any disambiguation suffixes or prefixes in the names: override fun provideTargetDisambiguationClassifier(target: KotlinOnlyTarget): String? = null + + override fun createKotlinTargetConfigurator() = KotlinJsSingleTargetConfigurator(kotlinPluginVersion) } \ No newline at end of file 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 b899d196000..6d9fec9b433 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 @@ -71,6 +71,8 @@ class NpmProject( else NpmProject[parent].findModule(name, src) } + override fun toString() = "NpmProject($nodeWorkDir)" + companion object { const val PACKAGE_JSON = "package.json" const val NODE_MODULES = "node_modules" 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 159753327ee..de11ce54b57 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 @@ -5,21 +5,22 @@ package org.jetbrains.kotlin.gradle.targets.js.webpack -import com.google.gson.stream.JsonWriter import org.gradle.api.DefaultTask import org.gradle.api.internal.file.FileResolver import org.gradle.api.tasks.* +import org.gradle.deployment.internal.Deployment +import org.gradle.deployment.internal.DeploymentHandle +import org.gradle.deployment.internal.DeploymentRegistry +import org.gradle.process.internal.ExecHandle import org.gradle.process.internal.ExecHandleFactory +import org.gradle.process.internal.ExecHandleState import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationToRunnableFiles -import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProjectLayout -import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile import org.jetbrains.kotlin.gradle.tasks.createOrRegisterTask import org.jetbrains.kotlin.gradle.testing.internal.reportsDir import org.jetbrains.kotlin.gradle.utils.injected import java.io.File -import java.io.StringWriter import javax.inject.Inject open class KotlinWebpack : DefaultTask() { @@ -39,9 +40,9 @@ open class KotlinWebpack : DefaultTask() { @OutputFile get() = project.buildDir.resolve("webpack.config.js") @Input - var saveConfigFileEffective: Boolean = true + var saveEvaluatedConfigFile: Boolean = true - open val configFileEffective: File + open val evaluatedConfigFile: File @OutputFile get() = project.buildDir.resolve("webpack.config.evaluated.js") open val outputPath: File @@ -56,103 +57,54 @@ open class KotlinWebpack : DefaultTask() { open val reportDir: File @OutputDirectory get() = project.reportsDir.resolve("webpack").resolve(entry.nameWithoutExtension) + @Input + var bin: String = "webpack" + + @Input + var devServer: KotlinWebpackConfig.DevServer? = null + + internal fun createRunner() = KotlinWebpackRunner( + project, + configFile, + execHandleFactory, + bin, + KotlinWebpackConfig( + entry, + if (saveEvaluatedConfigFile) evaluatedConfigFile else null, + outputPath, + configDirectory, + if (report) reportDir else null, + devServer + ) + ) + @TaskAction fun execute() { - check(entry.isFile) { - "${this}: Entry file not existed \"$entry\"" - } + val runner = createRunner() - NpmResolver.resolve(project) - - val npmProjectLayout = NpmProjectLayout[project] - - configFile.writeText(buildConfig()) - - val execFactory = execHandleFactory.newExec() - npmProjectLayout.useTool( - execFactory, - ".bin/webpack", - "--config", configFile.absolutePath - ) - val exec = execFactory.build() - exec.start() - exec.waitForFinish() - } - - private fun buildConfig(): String { - return buildString { - //language=JavaScript 1.8 - append( - """ - const config = { - mode: 'development', - entry: '${entry.canonicalPath}', - output: { - path: '${outputPath.canonicalPath}', - filename: '${entry.name}' - }, - resolve: { - modules: [ - "node_modules" - ] - }, - plugins: [], - module: { - rules: [] - } - }; - """ - ) - - if (saveConfigFileEffective) { - //language=JavaScript 1.8 - append( - """ - const util = require('util'); - const fs = require("fs"); - const evaluatedConfig = util.inspect(config, {showHidden: false, depth: null, compact: false}); - fs.writeFile(${jsQuotedString(configFileEffective.canonicalPath)}, evaluatedConfig, (err) => {}); - """ - ) + if (project.gradle.startParameter.isContinuous) { + val deploymentRegistry = services.get(DeploymentRegistry::class.java) + val deploymentHandle = deploymentRegistry.get("webpack", Handle::class.java) + if (deploymentHandle == null) { + deploymentRegistry.start("webpack", DeploymentRegistry.ChangeBehavior.BLOCK, Handle::class.java, runner) } - - if (report) { - //language=JavaScript 1.8 - append( - """ - const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; - config.plugins.push(new BundleAnalyzerPlugin({ - analyzerMode: "static", - reportFilename: "${entry.name}.report.html", - openAnalyzer: false, - generateStatsFile: true, - statsFilename: "${entry.name}.stats.json" - }) - ) - """ - ) - } - - loadConfigs(configDirectory) - - append("module.exports = config") + } else { + runner.execute() } } - private fun jsQuotedString(str: String) = StringWriter().also { - JsonWriter(it).value(str) - }.toString() + internal open class Handle @Inject constructor(val runner: KotlinWebpackRunner) : DeploymentHandle { + var process: ExecHandle? = null - private fun StringBuilder.loadConfigs(confDir: File) { - if (confDir.isDirectory) confDir - .listFiles() - ?.toList() - ?.filter { it.name.endsWith(".js") } - ?.forEach { - append(it.readText()) - appendln() - appendln() - } + override fun isRunning() = process != null + + override fun start(deployment: Deployment) { + process = runner.start() + } + + override fun stop() { + process?.abort() + } } companion object { @@ -174,6 +126,27 @@ open class KotlinWebpack : DefaultTask() { it.entry = npmProject.moduleOutput(compileKotlinTask) } + + compilation.dependencies { + runtimeOnly(npm("webpack-dev-server", "3.3.1")) + } + + project.createOrRegisterTask("run") { + val compileKotlinTask = compilation.compileKotlinTask + compileKotlinTask as Kotlin2JsCompile + + it.dependsOn(compileKotlinTask) + + it.bin = "webpack-dev-server" + it.entry = npmProject.moduleOutput(compileKotlinTask) + + val projectDir = compilation.target.project.projectDir.canonicalPath + it.devServer = KotlinWebpackConfig.DevServer( + contentBase = listOf("$projectDir/src/main/resources") + ) + + it.outputs.upToDateWhen { false } + } } } } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpackConfig.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpackConfig.kt new file mode 100644 index 00000000000..cc54ceff197 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpackConfig.kt @@ -0,0 +1,144 @@ +/* + * 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 +) { + @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? = null, + val contentBase: List + ) : 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: [] + } +};""" + ) + appendln() + + if (devServer != null) { + appendln("// dev server") + appendln("config.devServer = ${json(devServer!!)};") + appendln() + } + + if (reportDir != null) { + val reportBasePath = "${reportDir.canonicalPath}/${entry.name}" + val config = KotlinWebpackConfig.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)})); +""" + ) + } + + 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() + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..2b3367e2c46 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpackRunner.kt @@ -0,0 +1,56 @@ +/* + * 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( + "JSAnnotator", + "NodeJsCodingAssistanceForCoreModules", + "BadExpressionStatementJS", + "JSUnresolvedFunction" +) + +package org.jetbrains.kotlin.gradle.targets.js.webpack + +import org.gradle.api.Project +import org.gradle.process.internal.ExecHandle +import org.gradle.process.internal.ExecHandleFactory +import org.jetbrains.kotlin.gradle.targets.js.npm.NpmProject +import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolver +import java.io.File + +internal class KotlinWebpackRunner( + val project: Project, + val configFile: File, + val execHandleFactory: ExecHandleFactory, + val bin: String, + val config: KotlinWebpackConfig +) { + fun execute() { + val exec = start() + exec.waitForFinish() + } + + fun start(): ExecHandle { + check(config.entry.isFile) { + "${this}: Entry file not existed \"${config.entry}\"" + } + + NpmResolver.resolve(project) + + val npmProjectLayout = NpmProject[project] + + config.save(configFile) + + val execFactory = execHandleFactory.newExec() + val args = mutableListOf("--config", configFile.absolutePath) + if (config.showProgress) { + args.add("--progress") + } + + npmProjectLayout.useTool(execFactory, ".bin/$bin") + val exec = execFactory.build() + exec.start() + return exec + } +} \ No newline at end of file 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 d97c5842bac..2162b94e20c 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 @@ -26,28 +26,19 @@ object Yarn : NpmApi { project: Project, description: String, vararg args: String, - npmProjectLayout: NpmProjectLayout = NpmProjectLayout[project] + npmProject: NpmProject = NpmProject[project] ) { val nodeJsEnv = NodeJsPlugin.apply(project).root.environment val yarnEnv = YarnPlugin.apply(project).environment - val packageJsonHashFile = project.packageJsonHashFile - val packageJsonHash = if (packageJsonHashFile.exists()) packageJsonHashFile.readText() else null - - val hasher = (project as ProjectInternal).services.get(FileHasher::class.java) - val hash = hasher.hash(npmProjectLayout.packageJsonFile).toByteArray().toHexString() - - if (packageJsonHash == hash) return - - packageJsonHashFile.writeText(hash) - - val nodeWorkDir = npmProjectLayout.nodeWorkDir + val nodeWorkDir = npmProject.nodeWorkDir project.execWithProgress(description) { exec -> exec.executable = nodeJsEnv.nodeExecutable exec.args = listOf(yarnEnv.home.resolve("bin/yarn.js").absolutePath) + args exec.workingDir = nodeWorkDir } + } private fun yarnLockReadTransitiveDependencies( @@ -67,11 +58,13 @@ object Yarn : NpmApi { val deps = byKey[key] if (deps != null) { src.dependencies.addAll(deps.dependencies.map { dep -> + val scopedName = dep.scopedName + resolveRecursively( NpmDependency( src.project, - dep.group, - dep.packageName, + scopedName.scope, + scopedName.name, dep.version ?: "*" ) ) @@ -93,8 +86,10 @@ object Yarn : NpmApi { override fun resolveProject(npmPackage: NpmResolver.NpmPackage) { val project = npmPackage.project if (!project.yarn.useWorkspaces) { - yarnExec(project, NpmApi.resolveOperationDescription("yarn for ${project.path}")) - yarnLockReadTransitiveDependencies(NpmProjectLayout[project].nodeWorkDir, npmPackage.npmDependencies) + YarnAvoidance(project).updateIfNeeded { + yarnExec(project, NpmApi.resolveOperationDescription("yarn for ${project.path}")) + yarnLockReadTransitiveDependencies(NpmProject[project].nodeWorkDir, npmPackage.npmDependencies) + } } } @@ -127,8 +122,7 @@ object Yarn : NpmApi { check(rootProject == rootProject.rootProject) if (rootProject.yarn.useWorkspaces) { - yarnExec(rootProject, NpmApi.resolveOperationDescription("yarn")) - yarnLockReadTransitiveDependencies(NpmProjectLayout[rootProject].nodeWorkDir, subprojects.flatMap { it.npmDependencies }) + resolveWorkspaces(rootProject, subprojects) } else { if (subprojects.any { it.project != rootProject }) { // todo: proofread message @@ -144,6 +138,24 @@ object Yarn : NpmApi { } } + private fun resolveWorkspaces( + rootProject: Project, + subprojects: MutableList + ) { + val upToDateChecks = subprojects.map { + YarnAvoidance(it.project) + } + + if (upToDateChecks.all { it.upToDate }) return + + yarnExec(rootProject, NpmApi.resolveOperationDescription("yarn")) + yarnLockReadTransitiveDependencies(NpmProject[rootProject].nodeWorkDir, subprojects.flatMap { it.npmDependencies }) + + upToDateChecks.forEach { + it.commit() + } + } + override fun cleanProject(project: Project) { super.cleanProject(project) project.packageJsonHashFile.delete() 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 new file mode 100644 index 00000000000..ddbb41e3d00 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnAvoidance.kt @@ -0,0 +1,41 @@ +/* + * 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.yarn + +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.targets.js.npm.npmProject +import org.jetbrains.kotlin.gradle.targets.js.yarn.Yarn.packageJsonHashFile + +class YarnAvoidance(val project: Project) { + private val prevHash by lazy { + val packageJsonHashFile = project.packageJsonHashFile + if (packageJsonHashFile.exists()) packageJsonHashFile.readText() else null + } + + private val hash by lazy { + val hasher = (project as ProjectInternal).services.get(FileHasher::class.java) + hasher.hash(project.npmProject.packageJsonFile).toByteArray().toHexString() + } + + val upToDate: Boolean + get() = prevHash == hash + + fun commit() { + if (!upToDate) { + project.packageJsonHashFile.writeText(hash) + } + } + + inline fun updateIfNeeded(body: () -> Unit) { + if (!upToDate) { + body() + commit() + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnSetupTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnSetupTask.kt index 8840b53ae05..145f93e4484 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnSetupTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/yarn/YarnSetupTask.kt @@ -50,10 +50,11 @@ open class YarnSetupTask : DefaultTask() { } private fun extract(archive: File, destination: File) { - val dirInTar = archive.name.removeSuffix(".tar.gz") + File.pathSeparator + val dirInTar = archive.name.removeSuffix(".tar.gz") + File.separator project.copy { it.from(project.tarTree(archive)) it.into(destination) + it.includeEmptyDirs = false it.eachFile { fileCopy -> fileCopy.path = fileCopy.path.removePrefix(dirInTar) } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/TasksProvider.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/TasksProvider.kt index d0f00981dc8..998d2c7999c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/TasksProvider.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/TasksProvider.kt @@ -53,16 +53,16 @@ internal fun Project.createOrRegisterTask(name: String, type: Class locateTask(project: Project, name: String, type: Class): TaskHolder? = +internal inline fun Project.locateTask(name: String): TaskHolder? = if (canLocateTask) { try { - TaskProviderHolder(name, project, project.tasks.named(name, type)) + TaskProviderHolder(name, this, tasks.named(name, T::class.java)) } catch (e: UnknownTaskException) { null } } else { - project.tasks.findByName(name)?.let { - check(type.isInstance(it)) + tasks.findByName(name)?.let { + check(T::class.java.isInstance(it)) @Suppress("UNCHECKED_CAST") LegacyTaskHolder(it as T) @@ -74,7 +74,7 @@ internal fun locateTask(project: Project, name: String, type: Class Project.locateOrRegisterTask(name: String, noinline body: (T) -> (Unit)): TaskHolder { - return locateTask(project, name, T::class.java) ?: registerTask(project, name, T::class.java, body) + return project.locateTask(name) ?: registerTask(project, name, T::class.java, body) } internal open class KotlinTasksProvider(val targetName: String) {