Migrating from pnpm
Convert a pnpm-lock.yaml project to LPM, isolated layout preserved.
lpm migrate converts a pnpm project to LPM in one command. It detects pnpm, parses pnpm-lock.yaml, writes lpm.lock + lpm.lockb, optionally configures .npmrc, runs lpm install, and (optionally) verifies that build + test still pass. Backups are taken first, so --rollback restores the original state.
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'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 doesn't use it, but doesn't delete it either. If you have one, decide whether you still need it.- Workspaces and catalogs. LPM 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 prunes unused catalog entries after successful installs. .npmrc-declared private registries carry over verbatim. LPM 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
lpm.lock+lpm.lockb. Back uppnpm-lock.yamltopnpm-lock.yaml.backup. - Touch
.npmrc(with--no-npmrcto skip), backing up the original. - Run
lpm installagainst the new lockfile. - 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
lpm install --offline # confirm reproducibility
lpm test
lpm lint
lpm fmt --checkgit add lpm.lock lpm.lockb .npmrc package.json
git rm pnpm-lock.yaml pnpm-lock.yaml.backup .npmrc.backup
git commit -m "Migrate to LPM"What lpm migrate translates from package.json
-
pnpm.overridesis auto-translated topackage.json > lpm.overrides(same selector grammar). The translation runs before any disk mutation: parse errors, unsupported value shapes (object / array / null), and merge conflicts with an existinglpm.overridesare surfaced up-front and abort the migration before any file is touched. Successful entries land inlpm.overrides; the originalpnpm.overridesblock stays in place so a parallelpnpm installkeeps working during the transition. -
pnpm.patchedDependenciesis auto-translated topackage.json > lpm.patchedDependencieswithoriginalIntegrityresolved from the migrated lockfile. Each patch file is copied to LPM's canonicalpatches/<safe_key>.patchlocation (or treated as a validated no-op if the source is already at that path). 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-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'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 for projects whose lpm.overrides, lpm.patchedDependencies, and lpm.peerDependencyRules cover every pnpm-side entry. If they drift apart later (you add a new pnpm.* entry without re-running migrate, or change a target / range), lpm install emits a diff-aware stderr warning suggesting lpm migrate. The same drift signals are exposed as stable codes (pnpm_overrides_drift, pnpm_patches_drift, pnpm_peer_rules_drift) on lpm doctor --json for automation.
pnpm-specific gotchas
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 uses thepackage.jsonvalue.cleanupUnusedCatalogs: trueis honored unlesspackage.json > lpm.cleanupUnusedCatalogsexplicitly says otherwise.shamefully-hoistin.npmrcis pnpm-specific. LPM's equivalent for the npm-flat layout islpm install --linker=hoistedorpackage.json > lpm > linker = "hoisted".
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
LPM defaults to script-policy: deny — lifecycle scripts (postinstall, etc.) don't run during install. After the migration's lpm install step, run:
lpm rebuildFor packages that pnpm allowed to run scripts but LPM 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