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.
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.
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:
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.