Skip to main content

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: {
node: {
packageName: "@mermaid-js/mermaid-cli",
binPath: "node_modules/.bin/mmdc",
version: "11.12.0",
lockFile: "br:...",
},
},
};

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: {
node: {
packageName: "eslint",
binPath: "node_modules/.bin/eslint",
version: "9.17.0",
lockFile: "br:...",
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: {
node: {
packageName: "@stoplight/spectral-cli",
binPath: "node_modules/.bin/spectral",
version: "6.14.2",
lockFile: "br:...",
dependencies: {
"@stoplight/spectral-owasp-ruleset": "2.0.1",
},
},
},
};

Mandatory Lock Files​

Every node app must pin its exact dependency tree with a lockFile. A normal config load fails before any download if the field is absent:

const mapOfApps: BinManager.MapOfApps = {
eslint: {
node: {
packageName: "eslint",
binPath: "node_modules/.bin/eslint",
version: "10.0.0",
lockFile: "br:...", // brotli-compressed lock file content
},
},
};

For every Bun or Node app:

  • PNPM runs with --frozen-lockfile, refusing to modify pnpm-lock.yaml
  • The lock file content is written to the app directory before installation
  • Content prefixed with br: is brotli-compressed and base64-encoded

To bootstrap lock file content:

  1. Declare the app's package, version, dependencies, and binary path.
  2. Run datamitsu config lockfile <appName>.
  3. Add the output to the app's lockFile field.

The generation command deliberately permits a missing lockFile; every other config load requires it.

Workspace Overrides for Packages with Build Scripts​

datamitsu writes a secure pnpm-workspace.yaml automatically before every Bun- or Node-app install. When a package legitimately needs to run a build script — common offenders include puppeteer, sharp, esbuild, and native modules — installs fail with ERR_PNPM_IGNORED_BUILDS. Allowlist the package via App.files["pnpm-workspace.yaml"]; your overrides are shallow-merged on top of the secure baseline, so unspecified keys keep their hardened values.

const mapOfApps: BinManager.MapOfApps = {
mmdc: {
files: {
"pnpm-workspace.yaml": YAML.stringify({
allowBuilds: { puppeteer: true },
}),
},
node: {
packageName: "@mermaid-js/mermaid-cli",
binPath: "node_modules/.bin/mmdc",
version: "11.15.0",
lockFile: "br:...",
},
},
};

After adding allowBuilds, regenerate the lock file so pnpm records the approval:

pnpm exec datamitsu config lockfile mmdc

User-supplied keys always win on conflicts. Setting strictDepBuilds: false would override the default — only do this if you fully trust every transitive dependency.

For the full list of baseline settings, the rationale behind each, and the merged YAML format, see the Supply Chain Security guide.

PNPM Store Isolation​

Each app has an isolated workspace and virtual store, while package content is deduplicated in one shared content-addressable store:

~/.cache/datamitsu/store/
.apps/node/eslint/{hash}/
package.json
pnpm-lock.yaml
node_modules/
.bin/eslint # Executable symlink
.pnpm/ # App-local virtual store
eslint/
eslint-plugin-*/
.pnpm-store/ # Shared content-addressable package store

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 location
  • npm_config_virtual_store_dir - Virtual store for the project
  • npm_config_global_dir - Global packages directory

These prevent any interference with system-level PNPM configurations.