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",
},
},
};

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",
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",
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: {
node: {
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 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

Without lockFile, PNPM resolves dependencies fresh and generates a new lockfile.

To generate lock file content:

  1. Run datamitsu config lockfile <appName> to generate compressed lock file content
  2. Add the output to your config's lockFile field

Workspace Overrides for Packages with Build Scripts

datamitsu writes a secure pnpm-workspace.yaml automatically before every 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 environment has isolated PNPM store paths:

~/.cache/datamitsu/.apps/node/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 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.