Skip to content

chore: stop installing with --legacy-peer-deps - #215

Draft
adamjohnwright wants to merge 10 commits into
mainfrom
chore/drop-legacy-peer-deps
Draft

adamjohnwright wants to merge 10 commits into
mainfrom
chore/drop-legacy-peer-deps

Conversation

@adamjohnwright

Copy link
Copy Markdown
Contributor

You were right that it reads like a workaround. It is one, and the tree stopped
needing it a long time ago.

It is not needed

Measured today on main, in a clean directory:

npm ci --no-audit --no-fund      → added 2109 packages in 41s
npm install --no-audit --no-fund → added 2109 packages in 42s, lockfile unchanged

No flag, no failure. It appears to have come in with the first CI pipeline and was
never revisited.

It was not free

The flag accepts an inconsistent dependency tree, so a bump that cannot resolve
at all passed every check
. Angular's packages peer-depend on one another at
exact versions; dependabot's group (#161) moved eleven of them and left
platform-browser-dynamic and compiler-cli behind:

Conflicting peer dependency: @angular/core@21.2.23
  peer @angular/core@"21.2.23" from @angular/platform-browser-dynamic@21.2.23

Six green checks. The only thing that objected was this repo's pre-push hook,
which runs npm ci without the flag — so the gate existed on a developer's
machine and not in CI, which is exactly backwards.

What changes

--legacy-peer-deps is removed from CI, deploy, release verification, the
Dockerfile and the local setup guide. CONTRIBUTING says why, so it is not added
back the next time an install fails on peers:

If an install fails on peer dependencies, the tree is wrong. Fix the versions
rather than telling npm to stop checking.

What this will do

It will make some future dependabot PRs fail, including #161 as it stands. That
is the point — they fail now at the PR that introduces them, rather than passing
and leaving a tree that only installs when you ask npm not to look.

Deliberately not in this PR

The Dockerfile still uses npm install rather than npm ci. Reproducible
container builds are worth having and are a separate change from this one — I
started making it, noticed it was scope creep, and put it back.

🤖 Generated with Claude Code

It was on every install here -- CI, deploy, release verification, the
Dockerfile, the local setup guide -- and the tree stopped needing it long
ago. Measured 2026-09-16: `npm ci` on main resolves and installs
strictly, 2109 packages, and a strict `npm install` leaves the lockfile
untouched.

It was not free while it stayed. It accepts an inconsistent tree, so a
dependency bump that cannot resolve at all passed every check: Angular's
packages peer-depend on one another at exact versions, dependabot's group
moved eleven of them and left `platform-browser-dynamic` and
`compiler-cli` behind, and CI was green. The only thing that objected was
this repo's pre-push hook, which runs `npm ci` without the flag -- so the
gate existed locally and not where it mattered.

Removing it makes CI the check it was meant to be. A bump that cannot
resolve now fails where it is raised rather than where someone happens to
push from.

CONTRIBUTING says why, so it is not added back the next time an install
fails on peers. If an install fails on peer dependencies the tree is
wrong; the fix is the versions, not telling npm to stop looking.

Left alone deliberately: the Dockerfile still uses `npm install` rather
than `npm ci`. Making container builds reproducible is worth doing and is
a different change from this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adamjohnwright

Copy link
Copy Markdown
Contributor Author

Do not merge this as it stands — the premise is wrong.

I claimed --legacy-peer-deps was vestigial because npm ci resolved strictly
here. It does, on npm 11.12.1 (Node 24), which is what this machine runs. CI
runs Node 22 / npm 10.9.8, and there it is load-bearing:

npm 10, npm ci --ignore-scripts                    → Cannot read properties of null (reading 'edgesOut')
npm 10, npm ci --ignore-scripts --legacy-peer-deps → added 2336 packages
npm 11, npm ci --ignore-scripts                    → works

So removing the flag breaks every CI run, which is exactly what this branch has
been doing all day.

What the finding is actually worth

The underlying point still holds — the flag masks broken dependency trees, and it
hid #161's inconsistent Angular set. But the fix is not to remove the flag under
an npm that cannot cope without it.

The real fix is to align CI's Node with what developers run. CI is on 22
while local is on 24; the lockfile in main was last written by npm 11 (#214),
which is plausibly why npm 10 chokes on it. Matching them would make strictness
achievable and stop lockfiles being written by one npm and consumed by another.

That deserves its own PR with its own testing, so this one should wait for it
rather than land and be reverted.

Correction

This is the second claim I have made from the wrong instrument today. Testing an
npm-version-sensitive behaviour on a different npm version than CI uses is not a
test of CI.

@adamjohnwright
adamjohnwright marked this pull request as draft September 17, 2026 01:26
adamjohnwright added a commit that referenced this pull request Sep 17, 2026
`engines` pinned `node: 22.x` and `npm: >=10 <11`, which is why the lockfile
and the local machine could not agree: npm 11 records optional platform
packages that npm 10 leaves out, so a lockfile generated by either is a desync
to the other. The pin was the cause, not the cure.

Angular 21 declares `^20.19.0 || ^22.12.0 || >=24.0.0` and 24 is Latest LTS, so
this moves engines, `.nvmrc`, all four workflows, both Dockerfiles and the local
setup document to 24 together.

**No dependency changes versions.** Compared across every path in the tree:
91 packages added, all of them optional platform binaries (@napi-rs/nice,
@oxc-parser bindings, @emnapi); 0 removed; 0 that lost a version. Angular stays
on 21.2.x throughout.

`--legacy-peer-deps` stays, and now with a measured reason. Dropping it needs
@analogjs/vitest-angular to move off 2.3.1, and letting npm re-resolve to get
there dragged in 207 top-level version changes including React 18 -> 19 and
@Angular-devkit 21 -> 22. That is a separate decision, not a side effect of
changing Node. Recorded on #215.

`scripts/preflight.sh` was running a bare `npm ci --dry-run` while every
workflow installs with `--legacy-peer-deps`, so the gate whose whole job is to
reproduce CI was running a different command and failing on a peer conflict the
real build never sees. It now runs CI's command, and says which Node to run on.

Verified: preflight green end to end on Node 24 / npm 11 — lockfile in sync,
format, types, lint, dead code, unit tests, and the download smoke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adamjohnwright

Copy link
Copy Markdown
Contributor Author

Measured while moving the project to Node 24 (#218), so the cost of dropping
this flag is now known rather than guessed.

The blocker is one package. @analogjs/vitest-angular@2.3.1 pins vitest 3,
while @angular/build wants vitest 4:

While resolving: @angular/build@21.2.24
Found: vitest@3.2.7
  peer vitest@"^1.3.1 || ^2.0.0 || ^3.0.0 || ^4.0.0" from @analogjs/vitest-angular@2.3.1
Conflicting peer dependency: vitest@4.1.11

Letting npm re-resolve to clear it does work — a real npm ci with no flag
succeeds on Node 24 / npm 11 — but the resolution that makes it succeed moves
@analogjs/vitest-angular 2.3.1 → 2.7.2 and drags 207 top-level version
changes
with it, including:

  • react 18.3.1 → 19.3.0 and react-dom 18.3.1 → 19.3.0
  • @angular-devkit/core and @angular-devkit/schematics 21.2.24 → 22.1.8
  • vite 6.4.3 → 7.3.6, zod 3.25.76 → 4.3.6, uuid 8.3.2 → 14.0.2
  • and a batch of downgrades: clsx 2.1.1 → 1.2.1, @types/node 25.2.0 →
    22.20.3, cosmiconfig 9.0.2 → 8.3.6

By contrast, keeping the flag and regenerating under npm 11 changes no
package's version at all
— 91 optional platform entries added, 0 removed,
0 changed.

So this is not a cleanup that can ride along with a Node bump. The honest
version of this issue is "move @analogjs/vitest-angular forward deliberately,
then drop the flag", and that wants its own PR with the unit suite watched,
because it is the test runner integration that moves.

#218 therefore keeps --legacy-peer-deps and records why in
scripts/preflight.sh, which was previously running without the flag and so
failing on this exact conflict locally while CI never saw it.

adamjohnwright added a commit that referenced this pull request Sep 17, 2026
* chore(deps): move the whole Angular train to 21.2.23

Replaces dependabot's #161, which cannot be merged as it stands.

That PR moves eleven Angular packages from 21.2.20 to 21.2.22 and leaves
`@angular/platform-browser-dynamic` and `@angular/compiler-cli` behind.
Angular's packages peer-depend on one another at **exact** versions, so
the result does not resolve:

    Conflicting peer dependency: @angular/core@21.2.23
      peer @angular/core@"21.2.23" from @angular/platform-browser-dynamic@21.2.23

Its checks pass only because CI installs with `--legacy-peer-deps`, which
accepts an inconsistent tree. #215 removes that flag; after it, #161 fails
honestly.

So the train moves together: core, common, compiler, compiler-cli, forms,
router, elements, localize, platform-browser and platform-browser-dynamic
to 21.2.23, and the CLI train -- cli, ssr, build-angular -- to its own
21.2.23.

The lockfile is regenerated from scratch rather than hand-resolved,
because a partially-merged lock is what produced the conflict in the first
place. It resolves **strictly**: `npm ci --dry-run` with no
`--legacy-peer-deps`.

Verified: 242 unit tests, types, lint 652, dead code 145, format, a full
build, and 49 e2e cases against the resulting build -- interactors, the
threshold, the custom-resource dialog, the homepage, content pages and the
back button. The diagram is what an Angular bump would break, so it is
what was exercised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(deps): move the whole Angular train to 21.2.23, on the right npm

Two fixes, and the second explains why this branch failed CI all day.

**The Angular train moves together.** Dependabot's #161 bumps eleven
packages and leaves `platform-browser-dynamic` and `compiler-cli` behind;
Angular's packages peer-depend at exact versions, so that set cannot
resolve. Core, common, compiler, compiler-cli, forms, router, elements,
localize, platform-browser and platform-browser-dynamic all go to
21.2.23, and the CLI train with them.

**The lockfile is regenerated under Node 22, which is what CI runs.** I
generated the first one on Node 24 / npm 11, and npm 10 rejected it:

    EBADPLATFORM  lightningcss-android-arm64 wanted os "android", got linux

npm 11 records optional platform packages in a shape npm 10 tries to
install. Regenerated with `node:22` in docker, the android entries are
`optional: true, os: ["android"]` and `npm ci` adds 2212 packages cleanly.

`engines` is tightened from `>=22` to `22.x` so this cannot recur
silently. `.nvmrc` has said 22 all along and CI has always used it; only
`engines` was permissive enough to let a lockfile be written by the wrong
npm. CONTRIBUTING records both failure signatures, because each looks like
a dependency problem rather than a version problem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: run on Node 24, the LTS the toolchain already supports

`engines` pinned `node: 22.x` and `npm: >=10 <11`, which is why the lockfile
and the local machine could not agree: npm 11 records optional platform
packages that npm 10 leaves out, so a lockfile generated by either is a desync
to the other. The pin was the cause, not the cure.

Angular 21 declares `^20.19.0 || ^22.12.0 || >=24.0.0` and 24 is Latest LTS, so
this moves engines, `.nvmrc`, all four workflows, both Dockerfiles and the local
setup document to 24 together.

**No dependency changes versions.** Compared across every path in the tree:
91 packages added, all of them optional platform binaries (@napi-rs/nice,
@oxc-parser bindings, @emnapi); 0 removed; 0 that lost a version. Angular stays
on 21.2.x throughout.

`--legacy-peer-deps` stays, and now with a measured reason. Dropping it needs
@analogjs/vitest-angular to move off 2.3.1, and letting npm re-resolve to get
there dragged in 207 top-level version changes including React 18 -> 19 and
@Angular-devkit 21 -> 22. That is a separate decision, not a side effect of
changing Node. Recorded on #215.

`scripts/preflight.sh` was running a bare `npm ci --dry-run` while every
workflow installs with `--legacy-peer-deps`, so the gate whose whole job is to
reproduce CI was running a different command and failing on a peer conflict the
real build never sees. It now runs CI's command, and says which Node to run on.

Verified: preflight green end to end on Node 24 / npm 11 — lockfile in sync,
format, types, lint, dead code, unit tests, and the download smoke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant