@effected/pnpm-plugin-effected
A pnpm config dependency that centralizes versioning for the Effect ecosystem and the @effected/* kit through one pnpm catalog, effected, and pins the kit's build chain through a second, effected:build. It covers effect and its @effect/* satellites on one Effect v4 minor, @effect/tsgo at its own independent pin, and every published @effected/* package except this one. Install it once and catalog:effected is available to every package in your workspace, in dependencies and peer ranges alike.
Pre-
1.0.0. This package is part of the@effected/*kit, built on stable Effect v4 (effect~4.1.0) and still in0.xdevelopment. Stable Effect makes a kit1.0.0possible, not automatic. To keep youreffectand@effect/*versions on the line the kit is built and tested against, install this package as a config dependency (below).Stability: unstable. This package's API surface is not yet considered complete and may change across
0.xreleases. Pin an exact version — even a package marked stable before1.0.0can introduce a breaking change by accident, and an exact pin turns that into a type-check error rather than a runtime surprise. Full policy: release strategy.
Why @effected/pnpm-plugin-effected
Effect ships as a couple of dozen packages that have to move together. Pin them by hand and the pins drift: one @effect/* package advances, its effect peer no longer matches the core you installed, and the failure surfaces as a type error in a file nobody touched, or as a crash at import. Keeping the pins in one place is the whole idea, and pnpm catalogs are the mechanism — catalog:effected in a manifest instead of a version string, and one place that moves when the pin advances.
The @effected/* kit has the same problem with a sharper edge. Every kit package is on 0.x, where a caret does not cross a minor, so a hand-written ^0.14.0 stops resolving anything current the moment that package cuts 0.15.0. Spread across a dozen manifests, those ranges rot quietly and independently. catalog:effected replaces the ranges with a name, and upgrading the config dependency moves the whole kit surface at once.
A catalog only reaches the manifests you write. Your dependencies name effect and its satellites too, and a caret there can pair a satellite with a core it was not built for. The plugin's overrides close that gap across the whole graph. This is a convenience, not a requirement — it packages the way effected pins its own dependencies, so a project that wants the same discipline can adopt it instead of rebuilding it. It ships a catalog and a pnpmfile, not a code API.
Install
Add it as a config dependency — not a regular dependency. Config dependencies are installed ahead of the rest of the tree, which is what lets them contribute catalogs and hooks to the install that follows:
pnpm add --config @effected/pnpm-plugin-effected
Requires pnpm 11 or newer, and Node.js >=24.11.0. There is no npm or bun equivalent: config dependencies and catalogs are pnpm features.
The command writes the package into your pnpm-workspace.yaml at a plain version:
configDependencies:
"@effected/pnpm-plugin-effected": <version>
A plain version is the canonical form. pnpm still honours a +sha512-… integrity suffix on the entry if one is there, but you do not need to add one.
If your workspace sets minimumReleaseAge, add the plugin to minimumReleaseAgeExclude so a release that is only hours old still installs:
minimumReleaseAgeExclude:
- "@effected/pnpm-plugin-effected"
Usage
Once installed, catalog:effected is available to every package in the workspace. Reference it from package.json in place of a version range — in every dependency field.
An application:
{
"dependencies": {
"effect": "catalog:effected",
"@effect/ai-openai": "catalog:effected",
"@effected/workspaces": "catalog:effected"
}
}
A library takes its dev copy from effected and its peer from effected:peers:
{
"devDependencies": {
"effect": "catalog:effected",
"@effected/workspaces": "catalog:effected"
},
"peerDependencies": {
"effect": "catalog:effected:peers",
"@effected/workspaces": "catalog:effected:peers"
}
}
pnpm rewrites catalog: specifiers to concrete ranges when it packs, so what lands on the registry is an ordinary manifest. Nothing downstream needs this plugin, or pnpm. A packed peer advertises the served catalog's value, so spell peers through the :peers catalogs: catalog:effected:peers packs effect: ~4.<minor>.0 and each @effected/* package at ^0.<minor>.0. A peer spelled catalog:effected would pack the exact Effect version the plugin pins and hold every consumer to that patch. If a library needs a wider peer, write that range by hand in its own manifest.
What it ships
| Entries | Range | Notes |
|---|---|---|
effect and every @effect/* package | the exact version the plugin pins graph-wide (4.<minor>.<patch>); ~4.<minor>.0 in effected:peers | a new Effect release arrives with a new plugin release, never through a range |
@effect/tsgo | its exact pin on its own 0.x line | versions independently of effect |
every published @effected/* package | its current release, with a caret; floored to ^0.<minor>.0 in effected:peers | on 0.x a caret pins the minor |
Each catalog has a :peers twin, effected:peers and effected:build:peers, with every range floored to its minor; use it in peerDependencies. catalog:effect and catalog:effect:peers do not exist, and a manifest that still names one fails the install. An entry you declare yourself in a catalog the plugin serves wins over the plugin's for the same package, so a catalogs.effected block an earlier export left in your pnpm-workspace.yaml keeps its old ranges until you re-export or delete it. Your other catalogs are kept.
The build catalog
A second catalog, effected:build, holds the build chain effected package build runs and the toolchain a package builds against:
| Entries | Role |
|---|---|
tsdown, @tsdown/exe, rolldown, rolldown-plugin-dts | bundling and the declaration pass |
@microsoft/api-extractor, @microsoft/api-extractor-model, @microsoft/tsdoc, @microsoft/tsdoc-config | the API report and TSDoc |
typescript, @types/node, tsx | the compiler, Node types and the TypeScript runner |
react, react-dom, @types/react, @types/react-dom, @rsbuild/core | React and its types, and Rsbuild |
A repository that builds with the kit writes catalog:effected:build for these, so its build chain is the one the kit's bundler was built and tested with:
{
"devDependencies": {
"@effected/plugin": "catalog:effected",
"typescript": "catalog:effected:build",
"@types/node": "catalog:effected:build"
}
}
No override is derived from this catalog: it sets the ranges your manifests name and nothing else. The pnpmfile also allows TypeScript 7.1 prereleases under tsdown, whose own typescript peer does not admit them.
@effected/pnpm-plugin-effected itself is deliberately absent from the catalog — it is the package the catalog ships inside, and listing it would make every catalog rewrite bump the package that carries the catalog.
It also ships a pnpmfile, which pnpm loads from the config dependency automatically. There is nothing to import and nothing to call. Besides the catalog, the pnpmfile applies these settings to every consumer:
- Effect family overrides: one override per
effectand@effect/*package,"<name>@^4.0.0": "<catalog version>", derived from the catalog so it moves with it, and one for@effect/tsgoon its own line. They hold every copy in your graph, including the ones your dependencies name, to the catalog version, so a satellite can never pair with a core it was not built for. Never hand-write these pins. - Release-age exclusion:
effect,@effect/*and@effect/tsgo-*are exempt fromminimumReleaseAge, so a pinned Effect release that is only hours old still installs. - Scoped overrides:
@effect/platform-node-sharedis pinned to its parent's version under@effect/[email protected]and@effect/[email protected]. Tools still built on those release candidates take the shared package with a caret, and a fresh resolve would pair it with a newer one built for a differenteffect, which crashes the tool at startup. Each override applies only under its own parent, so a stable install is never touched.
Your own overrides are kept alongside the plugin's. If you set the same selector yourself, your value wins.
The Effect entries move when the kit advances its Effect pin. The kit entries are rebuilt automatically as kit packages release, so each new version of this package carries the kit's current versions; upgrading the config dependency is how a consumer picks them up:
pnpm update --config @effected/pnpm-plugin-effected
pnpm install
# every catalog:effected specifier in the workspace re-resolves to the new pins