A Gradle plugin that provisions pnpm and integrates a pnpm workspace into a Gradle build.
The pnpm version is pinned once, in the devEngines field of package.json, and pnpm itself
enforces that pin. The plugin's job is just to make sure some pnpm is available to run in the
first place — on a fresh checkout or CI runner, nothing is installed yet. It bootstraps pnpm from a
pnpm distribution it resolves like any other dependency, then lets pnpm's own version resolution,
lockfile checks and checksum verification take it from there. It also installs the workspace
dependencies and exposes pre-defined tasks for common tools like TypeScript, Prettier and ESLint.
Requirements: Gradle 9.0+, Java 21+ and pnpm 11+. Linux, macOS and Windows on x64 and arm64.
Earlier pnpm versions are not supported and can lead to errors, e.g. when one is configured as
executable.
Apply de.cronn.gradle-pnpm-plugin to every project that takes part in the pnpm build:
// settings.gradle.kts
pluginManagement {
repositories {
gradlePluginPortal()
}
}// build.gradle.kts
plugins {
id("de.cronn.gradle-pnpm-plugin") version "<version>"
}Pin the pnpm (and, optionally, Node.js) version in the package.json of your workspace root with
downloads enabled:
// package.json
{
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "11.28.5",
"onFail": "download"
},
"runtime": {
"name": "node",
"version": "24.20.0",
"onFail": "download"
}
}
}The plugin resolves the pnpm distribution as an ordinary dependency, pnpm:pnpm:<version>, so it
goes through the dependency cache, dependency verification, dependency locking and the proxy
settings of your build. It registers the Ivy repository serving it in the workspace root.
repositoryUrl points that repository at an internal mirror of the pnpm releases, or at whatever a
proxy serves them under:
pnpm {
repositoryUrl = "https://artifacts.example.com/github/pnpm/pnpm/releases/download/"
}The plugin registers no repository when the build sets RepositoriesMode.PREFER_SETTINGS or
FAIL_ON_PROJECT_REPOS. In this case, you need to define the repository yourself:
// settings.gradle.kts
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
exclusiveContent {
forRepository {
ivy {
name = "pnpm"
setUrl("https://github.com/pnpm/pnpm/releases/download/")
patternLayout { artifact("v[revision]/[artifact]-[classifier].[ext]") }
metadataSources { artifact() }
}
}
filter { includeModule("pnpm", "pnpm") }
}
}
}To pin the archive by checksum, run ./gradlew pnpmSetup --write-verification-metadata sha256 and
commit gradle/verification-metadata.xml. Dependency locking applies to the
pnpmDistributionArchive configuration.
pnpm {
// The version downloaded unless an executable is set
version = "11.28.5"
// Defaults to <workspaceRootDir>/.gradle/pnpm/<version>
installDirectory = layout.projectDirectory.dir(".gradle/pnpm/11.28.5")
// Skips provisioning entirely
executable = "/usr/local/bin/pnpm"
// Where the pnpm distribution is downloaded from
repositoryUrl = "https://github.com/pnpm/pnpm/releases/download/"
// Gradle project path to the workspace root
workspaceRootPath = ":frontend"
}The settings describe the one pnpm installation the whole workspace shares, so configure them once, in the build script of the workspace root. Every package inherits its values from there. Setting one of them on a package overrides it for that project's own pnpm invocations only — pnpm is still provisioned by the workspace root.
In CI, the pnpm distribution comes out of the Gradle dependency cache, so caching
~/.gradle/caches/modules-2 is enough to avoid downloading it on every run. Caching the workspace
root's .gradle/pnpm (or an installDirectory you already cache) additionally skips the
extraction, and caching pnpm's own download cache skips the package downloads.
| Task | Description |
|---|---|
pnpmSetup |
Resolves and extracts the pinned pnpm, if needed. |
pnpmInstall |
Runs pnpm install. |
pnpmDedupe |
Runs pnpm dedupe. |
pnpmClean |
Runs pnpm clean. |
A workspace root also is a workspace package.
Workspace packages use the tasks provided by Gradle's Base Plugin. The tasks of a supported tool are registered exactly when the project contains a configuration file for it. Tools contribute to the aggregate tasks and provide custom tasks with sensible defaults which should require little to no configuration for most projects.
Every project the plugin is applied to provides the following tasks:
| Task | Description |
|---|---|
check |
Runs the verification tasks of the configured tools. |
fix |
Applies the automatic source fixes of the configured tools. |
test |
Runs the test suites of the configured tools. |
build |
Runs assemble and check. |
assemble |
Assembles the outputs. |
clean |
Deletes the build outputs. |
Prefer these tasks over the tool-specific ones: adding the configuration file of a tool to a project makes it part of aggregate and lifecycle tasks without any further change to the build script.
check always runs test. Whether a test suite takes part in test, and therefore in check and
build, is the tool's own decision: a unit suite does, an end-to-end suite does not, since it is
usually slow and needs a server the build does not start. Run playwrightTest explicitly to run the
Playwright suite.
Each tool has its own extension for configuration, which is also applied to custom tasks using the task classes provided for each tool.
| Task type | Description |
|---|---|
PnpmExecTask |
Runs a binary provided by a workspace dependency. |
PnpmRunTask |
Runs a script declared in a package.json. |
NodeTask |
Runs a Node program. |
import de.cronn.pnpm.task.NodeTask
import de.cronn.pnpm.task.PnpmExecTask
import de.cronn.pnpm.task.PnpmRunTask
tasks.register<PnpmExecTask>("angularBuild") {
group = "build"
command = "ng"
arguments = listOf("build")
inputs.dir(layout.projectDirectory.dir("src"))
outputs.dir(layout.buildDirectory.dir("dist"))
}
tasks.register<PnpmRunTask>("buildFrontend") {
script = "build:frontend"
}
tasks.register<NodeTask>("generateApiClient") {
entryPoint = layout.projectDirectory.file("scripts/generate.mjs")
// Options for Node itself, which go before the program
nodeOptions = listOf("--enable-source-maps")
// Arguments for the program
arguments = listOf("--out", "build/generated")
outputs.dir(layout.buildDirectory.dir("generated"))
}A NodeTask runs its program on the Node version pinned in the devEngines.runtime field
of the package.json (see Setup), which pnpm downloads into its own store — Node is
never installed globally.
./gradlew build # spotless, tests, validation
./gradlew spotlessApply # apply the formatting
./gradlew publishToMavenLocal # publish to the local Maven repositoryTo try out uncommitted changes in a real build, run ./gradlew publishToMavenLocal and add the
following configuration to the target project's settings.gradle.kts:
// settings.gradle.kts
pluginManagement {
repositories {
mavenLocal()
}
}Then set the version to 0.0.0-SNAPSHOT:
// settings.gradle.kts
plugins {
id("de.cronn.gradle-pnpm-plugin") version "0.0.0-SNAPSHOT"
}./gradlew build runs the TestKit suite against the Gradle version of the wrapper only. To
additionally run it against other versions, pass them as a comma-separated list:
./gradlew functionalTest -PpnpmTestGradleVersions=9.0.0,9.7.1Each version is downloaded on demand. Without the property the version-specific tests are skipped. CI runs this tier in a separate job.
Every configuration is locked, so a dependency change fails the build until the lockfiles are regenerated:
./gradlew dependencies --write-locks
./gradlew buildEnvironment --write-locksDependabot does not update gradle.lockfile for dependencies declared in the version catalog, so
its Gradle update pull requests need the lockfiles refreshed by hand.
The changelog is assembled by changesets from the files in .changeset.
Run pnpm changeset add to create a new changeset.
Run ./scripts/prepare-release.sh. It consumes the pending changesets, bumps the plugin version,
and commits the result as chore: Version plugin together with a v<version> tag.
Review the commit, then push it:
git push origin main v<version>The tag starts the release workflow, which publishes to the
Gradle Plugin Portal and creates the GitHub release from the changelog
entry of that version.