Skip to content

Migrating from pnpm to vlt

Quick Start

Terminal
# Install vlt globally
$ npm install -g vlt
# In your existing project, run:
$ vlt install $ vlt build

vlt reads your existing package.json files and resolves dependencies. The pnpm-lock.yaml file is not migrated — vlt performs a fresh resolution and creates vlt-lock.json.


Command Mapping

pnpmvltNotes
pnpm installvlt installDoes not run lifecycle scripts
pnpm add <pkg>vlt install <pkg>
pnpm add -D <pkg>vlt install -D <pkg>
pnpm remove <pkg>vlt uninstall <pkg>
pnpm run <script>vlt run <script>
pnpm <script>vlt run <script>See fallback-command
pnpm dlx <pkg>vlx <pkg>Run remote packages
pnpm exec <cmd>vlt exec <cmd>
pnpm initvlt init
pnpm packvlt pack
pnpm publishvlt publish
pnpm loginvlt login
pnpm whoamivlt whoami
pnpm listvlt list
pnpm why <pkg>vlt query '#<pkg>'DSS query; see Selectors
pnpm install --frozen-lockfilevlt install --frozen-lockfile
pnpm auditvlt query ':malware'More powerful; see Security
pnpm config set <key> <val>vlt config set <key>=<val>

Shorthand Script Execution

pnpm lets you run scripts without run (e.g., pnpm build). vlt supports this via the fallback-command config:

Terminal
$ vlt config set fallback-command=run-exec

After setting this, vlt build will first check for a vlt command named build, and if none matches, look for a package.json script.


Configuration

.npmrc + pnpm-workspace.yaml → vlt.json

pnpm reads registry config from .npmrc and workspace config from pnpm-workspace.yaml. vlt consolidates everything into vlt.json.

pnpm configuration:

.npmrc
# .npmrc (pnpm)
registry=https://registry.internal.company.com/
@mycompany:registry=https://npm.mycompany.com/
auto-install-peers=true
pnpm-workspace.yaml
# pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"

vlt equivalent (single file):

vlt.json
{
"registry": "https://registry.internal.company.com/",
"scope-registries": {
"@mycompany": "https://npm.mycompany.com/"
},
"workspaces": [
"packages/*",
"apps/*"
]
}

Registry Configuration

Scoped Registries

pnpm’s scoped registries (from .npmrc) map directly to vlt’s scope-registries:

vlt.json
{
"scope-registries": {
"@mycompany": "https://npm.mycompany.com/"
}
}

Named Registry Aliases

vlt also supports named registry aliases which remove the ambiguity of scope-based mapping:

vlt.json
{
"registries": {
"internal": "https://npm.mycompany.com/"
}
}

Then reference packages explicitly in package.json:

package.json
{
"dependencies": {
"@mycompany/utils": "internal:@mycompany/utils@^2.0"
}
}

Authentication

pnpm stores auth tokens in .npmrc, same as npm. vlt stores them in an XDG-compliant keychain file, keeping secrets out of project config files.

Terminal
# Log in to default registry
$ vlt login
# Log in to custom registry
$ vlt login --registry=https://npm.mycompany.com/

CI Environments

Terminal
# pnpm
NPM_TOKEN=abc123 pnpm install
# vlt
VLT_TOKEN=abc123 vlt install

See Authentication for full details.


Lockfile

pnpm uses pnpm-lock.yaml. vlt uses vlt-lock.json.

When you first run vlt install, vlt creates vlt-lock.json from a fresh resolution of your package.json files. The pnpm-lock.yaml is not read.

What to do:

  1. Run vlt install to generate vlt-lock.json
  2. Commit vlt-lock.json
  3. Optionally remove pnpm-lock.yaml once you’ve fully switched

Install Script Protection

pnpm runs lifecycle scripts by default during install (same as npm). pnpm v9+ added onlyBuiltDependencies in package.json as an allowlist, but scripts still run by default for listed packages.

vlt takes a different approach: vlt install runs no scripts at all by default. Building is a separate, explicit step:

Terminal
# Phase 1: Install (no code executes)
$ vlt install
# Phase 2: Build (runs scripts, skipping known malware)
$ vlt build

By default, vlt build uses the target :scripts:not(:built):not(:malware) — it automatically skips packages flagged as malware by Socket.

pnpm onlyBuiltDependencies vs vlt build —target

package.json (pnpm)
{
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"node-gyp"
]
}
}
Terminal (vlt)
$ vlt build --target='#esbuild, #node-gyp'

You can persist the target:

Terminal
$ vlt config set "command.build.target=#esbuild, #node-gyp"

See vlt build for full details.


Workspaces

pnpm

pnpm defines workspaces in pnpm-workspace.yaml:

pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"

vlt

vlt defines workspaces in vlt.json:

vlt.json
{
"workspaces": [
"packages/*",
"apps/*"
]
}

vlt also supports named workspace groups:

vlt.json
{
"workspaces": {
"apps": "apps/*",
"libs": [
"packages/*",
"shared/*"
]
}
}

Workspace Commands

pnpmvlt
pnpm --filter <name> <cmd>vlt <cmd> -w <path> or cd <path> && vlt <cmd>
pnpm --filter ./packages/* <cmd>vlt <cmd> -w packages/*
pnpm -r <cmd>vlt <cmd> --recursive
pnpm -r run testvlt run test --recursive

Note: vlt’s --workspace (-w) flag takes paths or glob patterns, not package names.

Terminal
# Run tests in a specific workspace
$ vlt run test -w packages/core
# Run build across a workspace group
$ vlt run build -g libs

See Workspaces for full details.


Catalogs

If you use pnpm’s catalog feature (pnpm-workspace.yaml), vlt has direct support for catalogs with compatible syntax.

pnpm:

pnpm-workspace.yaml
# pnpm-workspace.yaml
packages:
- "packages/*"
catalog: typescript: "^5.0.0" eslint: "^8.0.0"
catalogs: testing: vitest: "^1.0.0"

vlt:

vlt.json
{
"workspaces": [
"packages/*"
],
"catalog": {
"typescript": "^5.0.0",
"eslint": "^8.0.0"
},
"catalogs": {
"testing": {
"vitest": "^1.0.0"
}
}
}

The catalog: protocol in package.json works the same way:

package.json
{
"devDependencies": {
"typescript": "catalog:",
"vitest": "catalog:testing"
}
}

See Catalogs for full details.


Overrides → Graph Modifiers

pnpm uses pnpm.overrides in package.json. vlt uses Graph Modifiers in vlt.json.

package.json (pnpm)
{
"pnpm": {
"overrides": {
"lodash": "^4.17.21",
"express>qs": "6.10.0"
}
}
}
vlt.json
{
"modifiers": {
"#lodash": "^4.17.21",
":root > #express > #qs": "=6.10.0"
}
}

Graph Modifiers use DSS selectors, giving you more precise control (e.g., target by path, workspace, or semver range).


node_modules Layout

pnpm uses a content-addressable store with symlinks to create a strict node_modules layout where packages can only access their declared dependencies.

vlt also creates a node_modules directory but uses a different internal layout (under node_modules/.vlt/). The result is similar in that packages resolve correctly at runtime — your application code doesn’t need to change.


Features Not in vlt

Some pnpm-specific features don’t have direct equivalents:

  • Content-addressable store — vlt uses its own on-disk cache but doesn’t hard-link from a global store
  • Side-effects cache — Not available in vlt
  • pnpm patch — Not yet available in vlt
  • pnpm deploy — Not available; use standard deployment tooling
  • pnpm.peerDependencyRules — vlt handles peer dependencies automatically with context isolation

Migration Checklist

  1. Install vlt: npm install -g vlt
  2. Create vlt.json combining your .npmrc registry config and pnpm-workspace.yaml workspace definitions
  3. Move scoped registry config from .npmrc to vlt.json
  4. Move catalogs from pnpm-workspace.yaml to vlt.json (catalog and catalogs fields)
  5. Move pnpm.overrides from package.json to modifiers in vlt.json
  6. Run vlt install then vlt build
  7. Commit vlt-lock.json
  8. Update CI scripts: replace pnpm install --frozen-lockfile with vlt install --frozen-lockfile && vlt build
  9. Update CI auth: replace NPM_TOKEN with VLT_TOKEN
  10. Update any pnpm dlx usage to vlx
  11. Remove pnpm-workspace.yaml and pnpm-specific .npmrc settings