# Migrating from npm (/docs/guides/migrating-from-npm)



`lpm migrate` converts an npm project to LPM CLI in one command. It detects the source package manager, parses `package-lock.json`, writes `lpm.lock` plus `lpm.lockb` when the graph fits the binary format, optionally configures `.npmrc`, runs `lpm install` against the new lockfile, and (optionally) verifies that `build` + `test` still pass. Every file it touches is backed up first — `--rollback` restores the original state.

This guide is the migration walkthrough. For the full flag reference, see [`lpm migrate`](/docs/packages/migrate).

## Prerequisites [#prerequisites]

* LPM CLI installed — see [Installation](/docs/installation).
* A working `package-lock.json` in the project root.
* A reasonably-clean working tree (commit before migrating, just to be safe).

## 1. Preview [#1-preview]

```bash
lpm migrate --dry-run
```

Detects npm, parses `package-lock.json`, converts to the LPM CLI format, but **writes nothing**. Reports the package count, any packages that couldn't be converted (with reasons), and the workspace member count.

If anything looks wrong, fix it before doing the real run.

## 2. Run the migration [#2-run-the-migration]

```bash
lpm migrate
```

What runs, in order:

1. **Pre-flight** — confirms `package.json` exists, refuses to clobber an existing `lpm.lock`.
2. **Detect, parse, convert** — reads `package-lock.json`, converts entries to LPM CLI's lockfile shape.
3. **Write** — emits `lpm.lock` and, when supported by the binary format, `lpm.lockb`. Backs up `package-lock.json` to `package-lock.json.backup`.
4. **`.npmrc`** — adds LPM CLI-aware config to `.npmrc` (or creates one). Backs up the original to `.npmrc.backup`.
5. **Install** — runs `lpm install` against the new lockfile (lockfile fast-path, no re-resolution).
6. **Verify** — runs `build` + `test` if those scripts exist, to confirm nothing broke.
7. **CI hint** — when a CI platform is detected (`.github/workflows`, `.gitlab-ci.yml`, etc.), prints a hint suggesting you re-run with `lpm migrate --ci` to generate a workflow template.
8. **Summary** — what was done, where backups live, and how to roll back.

The migrate flow is non-interactive — every step runs straight through. If `lpm.lock` already exists, pass `--force` to overwrite it.

## 3. Verify [#3-verify]

After migration, run a fresh install + your usual checks:

```bash
lpm install --offline       # confirm reproducibility from the new lockfile
lpm test
lpm lint
lpm fmt --check
```

If anything's broken, see [Rollback](#rollback) below.

## 4. Commit [#4-commit]

```bash
git add lpm.lock .npmrc package.json
test ! -f lpm.lockb || git add lpm.lockb
git rm package-lock.json package-lock.json.backup .npmrc.backup     # if you don't want them committed
git commit -m "Migrate to LPM CLI"
```

You probably don't want the `.backup` files in the repo. Add them to `.gitignore`:

```text
*.backup
```

## Rollback [#rollback]

If anything goes wrong (or the migration was a mistake):

```bash
lpm migrate --rollback
```

Walks the backups created by the previous run and restores `package-lock.json`, `.npmrc` (if it was touched), and `.gitattributes` to their pre-migration state. Files the migration newly created — `lpm.lock`, `lpm.lockb`, any new `.gitattributes`, and any patch files copied to `patches/` — are removed. `package.json` is only rolled back if the migration mutated it (i.e., when `pnpm.*` blocks were translated; usually not on the npm path). Safe to run repeatedly.

## Optional flags [#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 the install step                                                       |
| `--skip-verify` | Skip the `build` + `test` verification                                                             |
| `--no-npmrc`    | Don't touch `.npmrc`                                                                               |
| `--ci`          | Also generate a CI workflow template for the detected platform                                     |
| `--no-ci`       | Suppress the CI template hint                                                                      |
| `--rollback`    | Restore from `.backup` files                                                                       |

## What stays the same [#what-stays-the-same]

* `package.json` — `dependencies`, `devDependencies`, `scripts`, `workspaces` are read as-is. No renames, no deletions.
* `node_modules/` — gets rebuilt by the install step. The on-disk shape may change from npm's flat tree to LPM CLI's v2 hoisted layout, or to LPM CLI's isolated layout for workspaces and peer-conflict installs; declared imports continue to work because LPM CLI's resolver respects the same semver semantics npm does.
* `.npmrc` — any existing `@scope:registry=` lines for private registries survive the migration. The migration additively appends `@lpm.dev:registry=https://lpm.dev/api/registry/` (unless you pass `--no-npmrc`); if your `.npmrc` already declared that scope, the step is a no-op. LPM CLI honors `.npmrc` for routing — see [Registries](/docs/registries).

## What changes [#what-changes]

* `package-lock.json` → `lpm.lock` plus `lpm.lockb` when the graph fits the binary format. Commit `lpm.lock` always and `lpm.lockb` when present.
* `node_modules/` layout starts **hoisted** for single packages in LPM CLI's v2 virtual-store layout. **Workspaces auto-flip to isolated** (pnpm-style symlinks) for phantom-dep catching; force hoisted with `lpm install --linker=hoisted` or `package.json > lpm > linker = "hoisted"`.
* New `.gitattributes` line: `lpm.lockb binary` (so git doesn't try to text-merge the binary lockfile).

## Common pitfalls [#common-pitfalls]

* **The lockfile is now strict.** `lpm install --offline` will fail on any drift between `package.json` and `lpm.lock`. Whichever wins, run `lpm install` (without `--offline`) once after migration to settle them.
* **Phantom-dep code breaks under isolated layout.** If you imported a package you didn't declare in `dependencies`, npm hoisted it into `node_modules/` and your code worked anyway. LPM CLI's isolated layout doesn't hoist — undeclared imports become hard errors. Add the missing entries to `dependencies`.
* **Dependency lifecycle scripts don't run on `lpm install` by default.** Bare installs run the root project's lifecycle, including `prepare`, but dependency `postinstall` scripts (esbuild, sharp, etc.) stay blocked until you run `lpm rebuild` and approve the script-running packages with [`lpm approve-scripts`](/docs/packages/approve-scripts).

## See also [#see-also]

* [Migrating from pnpm](/docs/guides/migrating-from-pnpm) — same shape, different source lockfile
* [`lpm migrate`](/docs/packages/migrate) — full flag reference
* [Registries](/docs/registries) — how `.npmrc` routing works after migration
* [Lockfile](/docs/packages/lockfile) — what gets committed and why
* [`lpm install --linker`](/docs/packages/install) — keeping the flat layout if you need to
