PNPM Multi-Package Patterns
Examples of managing Node.js tools that require plugins or peer dependencies.
Basic PNPM App
A simple Node.js tool with no extra dependencies:
const mapOfApps: BinManager.MapOfApps = {
mmdc: {
fnm: {
packageName: "@mermaid-js/mermaid-cli",
binPath: "node_modules/.bin/mmdc",
version: "11.12.0",
},
},
};
This generates a package.json in the isolated environment:
{
"dependencies": {
"@mermaid-js/mermaid-cli": "11.12.0"
}
}
Apps with Plugin Dependencies
Many Node.js tools rely on plugins installed as siblings in node_modules.
Use the dependencies field to install them together:
const mapOfApps: BinManager.MapOfApps = {
eslint: {
fnm: {
packageName: "eslint",
binPath: "node_modules/.bin/eslint",
version: "9.17.0",
dependencies: {
"eslint-plugin-import": "2.31.0",
"eslint-plugin-react": "7.37.3",
"@typescript-eslint/eslint-plugin": "8.18.2",
"@typescript-eslint/parser": "8.18.2",
},
},
},
};
Generated package.json:
{
"dependencies": {
"eslint": "9.17.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-react": "7.37.3",
"@typescript-eslint/eslint-plugin": "8.18.2",
"@typescript-eslint/parser": "8.18.2"
}
}
All packages are installed in the same node_modules, so ESLint can discover
its plugins through normal Node.js resolution.
Spectral with Custom Rulesets
Spectral (OpenAPI linter) often needs custom ruleset packages:
const mapOfApps: BinManager.MapOfApps = {
spectral: {
fnm: {
packageName: "@stoplight/spectral-cli",
binPath: "node_modules/.bin/spectral",
version: "6.14.2",
dependencies: {
"@stoplight/spectral-owasp-ruleset": "2.0.1",
},
},
},
};
Lock File for Reproducibility
Pin the exact dependency tree with a lock file:
const mapOfApps: BinManager.MapOfApps = {
eslint: {
fnm: {
packageName: "eslint",
binPath: "node_modules/.bin/eslint",
version: "10.0.0",
lockFile: "br:...", // brotli-compressed lock file content
},
},
};
When lockFile is present:
- PNPM runs with
--frozen-lockfile, refusing to modifypnpm-lock.yaml - The lock file content is written to the app directory before installation
- Content prefixed with
br:is brotli-compressed and base64-encoded
Without lockFile, PNPM resolves dependencies fresh and generates a new lockfile.
To generate lock file content:
- Run
datamitsu config lockfile <appName>to generate compressed lock file content - Add the output to your config's
lockFilefield
PNPM Store Isolation
Each app environment has isolated PNPM store paths:
~/.cache/datamitsu/.apps/fnm/eslint/{hash}/
package.json
pnpm-lock.yaml
node_modules/
.bin/eslint # Executable symlink
eslint/
eslint-plugin-*/
.pnpm-store/ # Content-addressable store (shared dedup)
PNPM's content-addressable store means identical packages across apps are hard-linked rather than duplicated, saving disk space while maintaining isolation.
Environment Variables
datamitsu sets these PNPM environment variables for isolation:
npm_config_store_dir- PNPM content-addressable store locationnpm_config_virtual_store_dir- Virtual store for the projectnpm_config_global_dir- Global packages directory
These prevent any interference with system-level PNPM configurations.