Kotlin
A concise multiplatform language developed by JetBrains
Ensuring control over npm dependencies in Kotlin/JS projects
Recently we published a blog post about a potential security issue caused by ua-parser-js, a dependent package that is used in the popular testing framework Karma, which in turn is the default choice for Kotlin/JS and Kotlin Multiplatform applications targeting JS.
In the post, we recommended that you may want to consider locking packages you depend on to a certain version. Here, we want to share with you how you can accomplish that with Kotlin/JS. We recommend everyone to add the snippets below to their Kotlin projects targeting JavaScript.
How npm dependencies are managed
Dependencies from npm are specified in terms of their package name and version (or range). When you execute a Gradle task in a Kotlin/JS project, the plugin downloads and installs dependencies that are required for this particular task (following Gradle’s principle of Task Configuration Avoidance). However, when a dependency version is specified as a range instead of a specific version, this can cause these (possibly transitive) dependencies to be updated automatically.
The package manager used by Kotlin/JS, Yarn, provides a mechanism to lock versions and consequently ensure consistency across multiple installations. It does this via an auto-generated file named yarn.lock, which stores the exact version of all installed dependencies.
For a normal JavaScript project built with Yarn, you use the Yarn CLI to add, upgrade, and remove dependencies. The package manager then takes care of updating the yarn.lock
file accordingly, and uses its content during subsequent installations.
However, Kotlin/JS applications specify their dependencies in the build.gradle(.kts)
file, instead of using a CLI tool. To still make use of version locking, you need to add some additional Gradle configuration for your project. Let’s see how that is done.
Persisting yarn.lock in your Kotlin/JS projects
To persist the yarn.lock
file of your project, add the following Gradle configuration to make sure that all Gradle tasks use the same set of dependencies:
Add the following snippet to ensure that after running the kotlinNpmInstall
Gradle task, a copy of the auto-generated yarn.lock
file is stored in the root of the project (named yarn.lock.bak).
Make sure to add the yarn.lock.bak
file to your version control system.
Every time your build process calls kotlinNpmInstall
, your build then uses the stored yarn.lock.bak
in place of an auto-generated yarn.lock, locking the used dependencies in place:
To validate that your yarn.lock.bak
is equal to the yarn.lock
file generated from a fresh install, add the following validateYarnLock
task to your Gradle build file:
The snippets above ensure that your dependencies are version-locked. In addition to this, you can disable the execution of Yarn’s lifecycle scripts. This prevents your dependencies from executing code during their installation:
Moving forward
We recommend everyone to add the snippets above to your Kotlin/JS projects to make sure that any dependencies you have – either directly or transitively – are version-locked.
We are also working on providing direct support for persistent yarn.lock
files and fine-grained control over Yarn’s lifecycle scripts in Kotlin/JS and Kotlin Multiplatform projects – we suggest following KT-34014 for updates. Our aim is also to make sure that projects maintain compatibility with Gradle’s Task Configuration Avoidance, when the set of dependencies may differ based on the individual Gradle task.