Skip to content

Instantly share code, notes, and snippets.

@karlrwjohnson
Created September 1, 2019 18:51
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save karlrwjohnson/a631cbda6b2fb316bc0a9f37156eae5d to your computer and use it in GitHub Desktop.
Save karlrwjohnson/a631cbda6b2fb316bc0a9f37156eae5d to your computer and use it in GitHub Desktop.
Customizing Kotlin/JS's Webpack configuration

This is mostly notes to myself in case I ever do this again.

So, I've been doing a lot of frontend development in Typescript/React, but I'm also interested in Kotlin. I'm a full-stack engineer, so the idea of making a whole application in the same language appeals to me. (I'm aware that backend development in NodeJS exists, but I'm a fan of strict typing.)

So I've been exploring Kotlin's JS backend support.

So anyways, here's how my project is set up right now:

From IntelliJ Ultimate, I created a Gradle Kotlin Multiplatform project, and I immediately commented-out the JVM and "common" bits of the Gradle file. I figured if I end up wanting to re-add backend stuff later, it's easier to start with Multiplatform rather than retrofit it back in.

I also changed the names of all the base project folders so they wouldn't accidentally be picked up. I didn't need the Ktor sample code because I'm not using a framework for this particular thing, but I wanted to keep it around for a little while in case I decide to pick it up later.

Since I didn't need to run the full stack, I created an index.html file inside of jsMain/resources/ and use this command to just run the dev server:

./gradlew jsBrowserRun --stacktrace --info --continuous

Soon I realized that my development flow doens't quite fit the norm, and I found myself wanting to change the default dev server behavior. It was hard.

(Specifically, I wanted to disable the devServer.open flag so it would stop opening a new browser tab every time I ran a build. But if you want to, say, change the dev server port from the default value of 8080, you'd have to follow similar steps.)

So you see, for better and for worse the default Kotlin/JS project uses Webpack to bundle your compiled Javascript code with the Kotlin/JS runtime into the final bundle you can load into a browser. The Gradle build literally creates a NodeJS project inside the build/ directory and runs npm install to download the 340 dependencies required by Webpack (on top of the stuff Gradle pulls in in order to build Kotlin). So even though you're avoiding NPM bloat in your runtime code, you can't escape it in your build.

Since this Webpack config is generated by Gradle on every build, you can't just go in and change it (it lives inside /build/js/packages/${project name}/webpack.config.js) because it'll just be overwritten. Also, you should never hand-modify a build asset.

Instead, you should modify the Gradle task. This is the magic code to do it. Add this to your build.gradle if you chose to use the Groovy DSL like I did. (Kotlin would have it SO much easier.)

// The Kotlin Gradle Plugin generates a KotlinWebpack task (suffixed "*Run") for every KotlinBrowserJs DSL
// block (kotlin > js > browser) that exists (hence jsBrowserRun).
jsBrowserRun {
    // Disable Webpack's DevServer.open property, which drives me insane.
    boolean open = false;

    // This is ridiculous, but it's the only way I've figured out how to do it.
    //
    // A KotlinWebpack task builds writes-out a webpack configuration file when it runs.
    // The "devServer" block is represented by a data class (KotlinWebpackConfig.DevServer)
    // with immutable properties. The only way to change it is to instantiate a new one or
    // to copy() it.
    //
    // Since we're running this Groovy and not Kotlin though, we don't get Kotlin's default
    // variables (i.e. .copy(open = false)). We have to pass *every value* of the devServer
    // block back into it.
    //
    // The definition of the DevServer object is here:
    // https://github.com/JetBrains/kotlin/blob/3e251668324f3ac2f0819ca6bef0ce97ca26f39b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/webpack/KotlinWebpackConfig.kt
    devServer = devServer.copy(
        devServer.inline,
        devServer.lazy,
        devServer.noInfo,
        open,
        devServer.overlay,
        devServer.port,
        devServer.proxy,
        devServer.contentBase,
    )
}
@alexdzeshko
Copy link

alexdzeshko commented Dec 19, 2019

Have you been able to run React app with Kotlin Multiplatform project? I'm trying for a few days without success

@karlrwjohnson
Copy link
Author

I'm not sure whether you're referring to just using a React the library or Create-React-App, but the answer is no to both (not that I've really tried.)

@fpiechowski
Copy link

fpiechowski commented Feb 6, 2020

After hours spent on researching this topic I ended up examining kotlin multiplatform gradle plugin sources and found out that the KotlinWebpack creates a runner passing KotlinWebpackConfig to it. KotlinWebpackConfig basically creates the webpack.config.js file. The interesting part is that this class appends the contents of every file in configDirectory, which is defined as

open val configDirectory: File?
        @Optional @InputDirectory get() = project.projectDir.resolve("webpack.config.d").takeIf { it.isDirectory }

to the webpack.config.js file.

So I guess all you need to do to provide you custom config is to create a directory called webpack.config.d in your project directory (I believe it's the directory where your build.gradle is placed) and put all your custom configs there.

For more info look at (kotlin-gradle-plugin:1.3.61):
org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack.kt:85
org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack.kt:107
org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig:145
org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig:145
org.jetbrains.kotlin.gradle.targets.js.utils.kt:10

Edit: just tested it and it works

@BoD
Copy link

BoD commented Mar 29, 2020

@AKJAW
Copy link

AKJAW commented Jun 2, 2020

Here's the config file for webpack.config.d that worked for me

config.devServer = Object.assign(
    {},
    config.devServer || {},
    { open: false }
)

I've used Object.assign to prevent overriding the default devServer config.

@mice777
Copy link

mice777 commented Jan 19, 2021

Here is what I do in build.gradle.kts to achieve this:

kotlin {
    js(LEGACY) {
        browser {
           runTask {
               devServer = org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig.DevServer(`open` = false)
           }
        }
    }
}

@apatrida
Copy link

apatrida commented Jul 5, 2021

Now you can just set the open property directly in the configuration.

js(IR) {
        binaries.executable()
        browser {
            commonWebpackConfig {
                cssSupport.enabled = true
                devServer?.`open` = false. // <------ HERE
                devServer?.port = 3000
                outputFileName = "app.js"
            }
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment