npm compatibility
What works the same as npm, what's different, and what LPM adds on top.
LPM is drop-in compatible with the npm ecosystem. Reads package.json. Resolves npm-flavored semver. Materializes node_modules/. Installs from registry.npmjs.org by default. Honors .npmrc for routing and auth. Forwards subprocess exit codes. Almost any npm-shaped project works without conversion — lpm install produces a runnable tree.
This page is the explicit scope: what carries over verbatim, what's different, and what LPM adds.
Carries over verbatim
| Surface | Status |
|---|---|
package.json > dependencies / devDependencies / peerDependencies / optionalDependencies | Read identically |
package.json > scripts | Run via lpm run <name> (or bare lpm <name> as shorthand) |
package.json > bin (string or map form) | Both shapes accepted |
package.json > files | Honored at publish time |
package.json > engines.node | Honored as a pin source AND enforced — install aborts if the effective Node version doesn't satisfy the constraint |
package.json > engines.lpm | Enforced against the running CLI version (npm only enforces with engine-strict=true; LPM enforces by default) |
package.json > workspaces (array or object form) | Both shapes accepted |
package.json > overrides | npm-style overrides honored (lower precedence than lpm.overrides) |
package.json > resolutions | yarn-style resolutions honored |
package.json > catalogs and pnpm-workspace.yaml > catalog / catalogs | Centralized version catalogs |
.npmrc | Read for registry=, @scope:registry=, and //host/:_authToken= lines |
| Semver dialect | ^, ~, ` |
| Lifecycle script names | preinstall, install, postinstall execute (see Differences). prepare, prepublishOnly, preuninstall, uninstall, postuninstall are recognized for detection — declaring them puts the package in lpm audit and the lpm approve-scripts review queue, but the install pipeline never runs them. |
If a project's only contract with npm is "I have a package.json with deps and scripts," that project works unchanged.
Differences
The differences are deliberate — LPM defaults differ where npm's defaults aren't safe, and a handful of legacy npm behaviors are simplified.
Lifecycle scripts: deny by default
npm runs preinstall / install / postinstall for every package by default. LPM doesn't.
lpm install # downloads + links, no script execution
lpm rebuild # runs scripts for trusted packages (second step)To match npm's behavior:
lpm install --policy=allow # or --yoloTo approve packages for the default deny path: lpm approve-scripts walks the blocked set interactively.
This is the single biggest behavioral difference. See Security & audit for why.
node_modules layout: hoisted by default, isolated for workspaces and peer conflicts
LPM starts with npm's flat node_modules/ layout for single-package projects. Workspaces auto-flip to isolated (pnpm-style symlinks into the global virtual store) — phantom-dep bugs are most expensive in monorepos, so the safer layout becomes the default there. If a default-hoisted resolve finds incompatible peer requirements, LPM also auto-switches that project to isolated and records the decision in lpm.lock. Force a specific mode:
lpm install --linker=isolated # opt into pnpm-style for non-workspaces
lpm install --linker=hoisted # keep npm-style flat for workspacesOr persist it in package.json > lpm > linker / ~/.lpm/config.toml > linker. Explicit linker settings opt out of peer-conflict auto-switching.
Under isolated layout, code that imports a dep it didn't declare in dependencies (phantom-dep access) breaks. npm's hoisting silently allowed it; isolated doesn't. Add the missing entries to dependencies — the fix is one-line per package.
Lockfile: lpm.lock + lpm.lockb
npm produces package-lock.json. LPM produces lpm.lock (TOML, git-diffable) + lpm.lockb (binary, mmap). Both are committed.
Convert with:
lpm migrateSee Migrating from npm.
Save policy: never *
npm's npm install <pkg> saves ^resolvedVersion. LPM does the same — but LPM also enforces that * can never become a default. Wildcards must be requested per-package via pkg@*. See Save policy.
Cooldown on recent versions
LPM blocks installs of versions published less than 24h ago by default. npm doesn't. Override with lpm install --allow-new. See Recently published packages.
engines enforced by default
npm reads engines.node / engines.npm and only enforces them when engine-strict=true is set in .npmrc. LPM enforces by default — install / rebuild / add abort when the workspace root's engines.lpm constraint doesn't match the running CLI version, or engines.node doesn't match the effective Node runtime.
lpm install --no-engine-strict # opt out for this invocationOr persistent in package.json > lpm > engineStrict = false (per-project) or ~/.lpm/config.toml > engine-strict = false (per-user). See lpm install § engines enforcement.
npm publish --otp → lpm publish with otpRequired
npm prompts for OTP at publish time when 2FA is enabled. LPM mirrors this; configure via lpm.json > publish.npm.otpRequired = true to prompt up-front (saves a round-trip). See lpm.json publish.npm.
What LPM adds
Things LPM offers on top of the npm-compatible baseline:
| Feature | Where |
|---|---|
Strict trust binding for lifecycle scripts ({name, version, integrity, scriptHash}) | lpm approve-scripts |
| Behavioral analysis tags | lpm audit, lpm query |
| Provenance drift detection | Security & audit § Layer 4 |
| Triage gate | Security & audit § Layer 5 |
| Local patches with integrity binding | lpm patch |
| Dual-format lockfile | Lockfile |
Workspace --affected + topology-aware caching | Workspaces, Task runner |
workspace:* protocol | Workspaces |
| Catalog protocol | Workspaces |
Path-selector overrides (baz>foo) | package.json lpm.overrides |
Multi-registry routing via .npmrc (lpm.dev + npm + private in one project) | Registries |
Source delivery (lpm add) | lpm add |
| Built-in lazy-downloaded tools (oxlint, biome) | lpm lint, lpm fmt |
| Zero-config dev server with HTTPS, tunnel, services | lpm dev |
| SE-0292 Swift Package Registry support | Swift Package Registry |
What LPM does not do
Honest scope:
- Lockfile-only PR review — no first-class diff tool yet beyond
git diff lpm.lock. Use the deterministic format and your PR review tool of choice. - Reading the
pnpm.*namespace at install time — plainlpm installdoes not consumepnpm.overrides,pnpm.patchedDependencies, orpnpm.peerDependencyRules.lpm migratetranslates all three into theirlpm.*equivalents in one pass; install-time warnings (and stablelpm doctor --jsoncodespnpm_overrides_drift,pnpm_patches_drift,pnpm_peer_rules_drift) flag any post-migration drift. See Migrating from pnpm. - Yarn berry's
.pnp.cjs— no Plug'n'Play support. LPM materializesnode_modules/(isolated or hoisted). - GUI — there's no
lpm-vault-style desktop app shipped today. The CLI is the surface.
Compatibility with the broader ecosystem
- Tools that read
package.json(TypeScript, ESLint, build tools): work unchanged. - Tools that walk
node_modules/(most bundlers, test runners): work under both isolated and hoisted layouts. - Tools that hard-code
package-lock.json: don't see LPM's lockfile. Either runlpm migrateto addlpm.lockalongside, or uselpm install --linker=hoistedfor a closer-to-npm shape.
See also
lpm migrate— convert an npm/pnpm/yarn/bun project- Migrating from npm — walkthrough
- Registries —
.npmrcrouting - Save policy — npm-compatible defaults with stricter rails
- Security & audit — what's different about LPM's trust model