Migrating from pnpm
Convert a pnpm-lock.yaml project to LPM CLI, isolated layout preserved.
lpm migrate converts a pnpm project to LPM CLI. It writes a version 12 staging lockfile, then runs an online install by default.
The install writes version 13 with exact package-instance identities. The command creates backups before it changes files.
The migration shape is the same as migrating from npm. This page covers the pnpm-specific differences.
What carries over cleanly
- Isolated
node_moduleslayout for workspaces and peer conflicts. Workspace projects auto-flip to LPM CLI's isolated layout (symlinks into a virtual store) — same shape pnpm uses, so your imports keep working unchanged and you get the same phantom-dep protection. Single-package projects start hoisted, but default installs auto-switch to isolated when resolution detects incompatible peer requirements. Force isolated withlpm install --linker=isolatedorpackage.json > lpm > linker = "isolated"if you want pnpm's shape there for every install. .pnpmfile.cjsis unaffected. LPM CLI doesn't use it, but doesn't delete it either. If you have one, decide whether you still need it.- Workspaces and catalogs. LPM CLI reads
package.json > workspacesfirst; if it's absent, it falls back topnpm-workspace.yamlfor member globs. It also reads pnpm-stylepnpm-workspace.yaml > catalogandcatalogsfor default and named catalog entries, socatalog:dependencies keep resolving without moving catalog data intopackage.json. IfcleanupUnusedCatalogs: trueis set inpnpm-workspace.yaml, LPM CLI prunes unused catalog entries after successful installs. .npmrc-declared private registries carry over verbatim. LPM CLI honors.npmrcfor routing.
Run the migration
lpm migrate --dry-run # preview
lpm migrate # do itSame flow as the npm path:
- Detect pnpm, parse
pnpm-lock.yaml, convert. - Write a version 12 staging
lpm.lock. If the graph fits the binary format, writelpm.lockb. - Touch
.npmrc(with--no-npmrcto skip), backing up the original. - Run an online
lpm install. This step writes version 13 and removes the staginglpm.lockb. - Run
build+testto verify (skip with--skip-verify). - Print summary + CI hint.
Optional flags
| Flag | Effect |
|---|---|
--dry-run | Parse + convert only, write nothing |
--force | Overwrite an existing lpm.lock |
-y, --yes | Reserved. The flow is non-interactive today, so this flag is a no-op. It does NOT imply --force. |
--no-install | Convert lockfile only, skip install |
--skip-verify | Skip build + test verification |
--no-npmrc | Don't touch .npmrc |
--ci | Generate a CI workflow template |
--no-ci | Suppress CI hint |
--rollback | Restore from .backup files |
Verify and commit
If you used --no-install, run lpm install before the offline command.
lpm install --offline # confirm reproducibility
lpm test
lpm lint
lpm fmt --checkgit add lpm.lock .npmrc package.json
test ! -f lpm.lockb || git add lpm.lockb
git rm pnpm-lock.yaml pnpm-lock.yaml.backup .npmrc.backup
git commit -m "Migrate to LPM CLI"What lpm migrate translates from package.json
-
pnpm.overridesis auto-translated topackage.json > lpm.overrideswith the same selector grammar.lpm installdoes not use the originalpnpm.overridesblock as resolution input. Successful entries land inlpm.overrides, and the original block stays in place for parallel pnpm use. The translation validates every entry before it changes a file. Invalid values and merge conflicts stop the migration without a partial update. -
pnpm.patchedDependenciesis auto-translated topackage.json > lpm.patchedDependencieswithoriginalIntegrityresolved from the migrated lockfile. Each patch file is copied to LPM CLI's canonicalpatches/<safe_key>.patchlocation (or treated as a validated no-op if the source is already at that path), andlpm.lock > [patches]records the patch path + SHA-256 so the first LPM CLI install can verify the bytes it replays. The plan validates everything up front: missing source files, paths outside the project root, paths through symlinked ancestors, directory targets, and any package whose lockfile entry is missing or has no integrity hash all abort the migration with a structured error before any disk mutation. Like the overrides block, the originalpnpm.patchedDependenciesis left in place after migration. -
pnpm.peerDependencyRulesis auto-translated topackage.json > lpm.peerDependencyRuleswith the same shape verbatim —ignoreMissing,allowedVersions, andallowAnyall carry over. List entries (ignoreMissing/allowAny) are unioned with any pre-existing LPM CLI-side entries; map entries (allowedVersions) merge selector-by-selector. Same selector with the same range is a no-op idempotent merge; same selector with a different range is a hard conflict that aborts the migration before any disk mutation. Range strings that don't parse as valid semver, and selector keys that don't match LPM CLI's grammar, are surfaced up-front as parse errors with the offending entry named.allowedVersionsselectors mirrorlpm.overrides: bare peer names ("react"), scoped peers ("@scope/foo"), and parent-scoped forms ("foo>react","foo@^2>react","@scope/foo@^2>react") all carry over verbatim; multi-segment paths and bare-name-with-version forms ("foo@2"without>) are rejected on both surfaces. Pattern semantics (@scope/*,*-suffix, etc.) work forignoreMissingandallowAny. The same parser runs at install time, so hand-edits tolpm.peerDependencyRulesafter migration fail loud (LpmError::Script) instead of silently no-op'ing.
After migration, lpm install is silent when the LPM CLI fields cover every pnpm entry. Later drift causes a human-output warning that suggests lpm migrate.
The --json flag suppresses that stderr warning. Use lpm doctor --json to detect drift through pnpm_overrides_drift, pnpm_patches_drift, and pnpm_peer_rules_drift.
pnpm-specific gotchas
pnpm.overridesis migration input, not install input. Runlpm migrateafter each pnpm-side change, or updatelpm.overridesdirectly.pnpm-workspace.yamlis read as a fallback whenpackage.json > workspacesis absent, and itscatalog/catalogsblocks are read as root catalogs. If the same catalog package entry exists in bothpackage.json > catalogsandpnpm-workspace.yaml, LPM CLI uses thepackage.jsonvalue.cleanupUnusedCatalogs: trueis honored unlesspackage.json > lpm.cleanupUnusedCatalogsexplicitly says otherwise.shamefully-hoistin.npmrcis pnpm-specific. LPM CLI's closest equivalent islpm install --linker=hoistedorpackage.json > lpm > linker = "hoisted"; hoisted mode still uses LPM CLI's v2 virtual-store layout instead of flattening every transitive dependency to the project root.
Rollback
lpm migrate --rollbackRestores pnpm-lock.yaml, .npmrc (if touched), .gitattributes, and package.json (rolled back when the pnpm.* translations mutated it) to pre-migration state. Files the migration newly created — lpm.lock, lpm.lockb, any new .gitattributes, and any patch files copied to patches/ — are removed.
Lifecycle scripts
Bare lpm install runs your root project lifecycle, including prepare. Dependency lifecycle scripts (postinstall, etc.) remain under script-policy: deny by default. After the migration's lpm install step, run:
lpm rebuildFor packages that pnpm allowed to run scripts but LPM CLI hasn't trusted yet, lpm approve-scripts walks the blocked set interactively.
If your project depends on postinstall for a tight inner loop and you're confident about the trust set:
lpm install --auto-buildAuto-runs lpm rebuild for trusted packages right after install. If a trusted lifecycle script fails, install exits non-zero. Or set package.json > lpm > scripts.autoBuild: true to make it sticky.
See also
- Migrating from npm — same shape, different source lockfile
lpm migrate— full flag reference- Registries —
.npmrcrouting lpm install --linker— keeping a flat npm-style layout if you need itlpm approve-scripts— review blocked lifecycle scripts after first install