Skip to content

Switch the frontend toolchain from npm to pnpm - #19542

Open
brianjhanson wants to merge 10 commits into
6.xfrom
chore/pnpm
Open

Switch the frontend toolchain from npm to pnpm#19542
brianjhanson wants to merge 10 commits into
6.xfrom
chore/pnpm

Conversation

@brianjhanson

@brianjhanson brianjhanson commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Important

You'll need to install pnpm once this is merged, and you'll need to replace npm install with pnpm install. npm install doesn't get along with pnpm

One-time setup

  1. Install pnpm (Node ≥ 22 is still required). Any recent pnpm works, because it switches itself to the pinned version:
npm install -g pnpm

corepack enable pnpm also works on Node 22/24. You can skip installing pnpm entirely and use vp install, which resolves the pinned pnpm for you.

  1. Move your Font Awesome token. The committed .npmrc no longer reads ${CRAFT_FONTAWESOME_TOKEN}, because pnpm won't expand environment variables in credentials from a project-level .npmrc. Store the token once in pnpm's user-level auth store:
pnpm config set "//npm.fontawesome.com/:_authToken" "$CRAFT_FONTAWESOME_TOKEN"

Without the token, the optional @awesome.me/* kit is skipped and the install carries on without it.

  1. Remove npm's node_modules. npm's flat layout and pnpm's isolated layout don't mix:
rm -rf node_modules packages/*/node_modules
  1. Install:
pnpm install
  1. Carry on as usual. pnpm run dev still builds any missing dist/ artifacts on first run through predev. If something looks stale, run pnpm run build:all once.

Adds pnpm-workspace.yaml and pnpm-lock.yaml, drops package-lock.json, and converts the local packages to the workspace: protocol.

Config that doesn't map 1:1:

  • overrides and allowScripts move to pnpm-workspace.yaml, where the latter is spelled allowBuilds and keys on the bare package name rather than name@version.
  • enablePrePostScripts: true, since pnpm doesn't run pre*/post* hooks by default and prebuild/predev/prestorybook are load-bearing here.
  • The Font Awesome token leaves the committed .npmrc — pnpm won't expand environment variables in registry credentials read from a project-level file. CI writes it to ~/.npmrc before installing; locally it goes in pnpm's own auth store via pnpm config set.

pnpm's isolated node_modules exposed 15 dependencies that only resolved through npm's hoisting. Two of them affect packages we publish: @craftcms/ui's dist imports autosize without declaring it, and @craftcms/webpack owns the webpack loaders even though webpack resolves them from the consuming config's context (fixed with resolveLoader.modules). The rest are direct imports in resources/js that were never declared.

The lockfile was derived with pnpm import so installed versions match what package-lock.json resolved — this changes the package manager only, not the dependency tree.

brianjhanson and others added 6 commits September 2, 2026 11:07
Adds pnpm-workspace.yaml and pnpm-lock.yaml, drops package-lock.json, and
converts the local packages to the workspace: protocol.

Config that doesn't map 1:1:

- `overrides` and `allowScripts` move to pnpm-workspace.yaml, where the
  latter is spelled `allowBuilds` and keys on the bare package name rather
  than name@version.
- `enablePrePostScripts: true`, since pnpm doesn't run pre*/post* hooks by
  default and prebuild/predev/prestorybook are load-bearing here.
- The Font Awesome token leaves the committed .npmrc — pnpm won't expand
  environment variables in registry credentials read from a project-level
  file. CI writes it to ~/.npmrc before installing; locally it goes in
  pnpm's own auth store via `pnpm config set`.

pnpm's isolated node_modules exposed 15 dependencies that only resolved
through npm's hoisting. Two of them affect packages we publish:
@craftcms/ui's dist imports autosize without declaring it, and
@craftcms/webpack owns the webpack loaders even though webpack resolves
them from the consuming config's context (fixed with resolveLoader.modules).
The rest are direct imports in resources/js that were never declared.

The lockfile was derived with `pnpm import` so installed versions match
what package-lock.json resolved — this changes the package manager only,
not the dependency tree.
Both packages invoke `webpack` and `webpack-dev-server` from their own
scripts, and craftcms-legacy also runs `vp`, but only @craftcms/webpack
declared any of them. Under npm those binaries were hoisted into the root
node_modules/.bin; pnpm only links the bins of a package's direct
dependencies, so `pnpm run build:bundles` fails from a clean install with
"webpack: command not found".

Resolves to the webpack 5.102.1 already in the tree — no new version, no
duplicate.
31 dependencies were declared in more than one workspace manifest, and 14
of those had already drifted onto different ranges. Catalogs make the
version a single fact: manifests say `catalog:` and pnpm substitutes the
real version at publish time, exactly as it does for `workspace:`.

@craftcms/legacy is deliberately held on older majors (vue 2, tailwind 3,
stylelint 16, TypeScript 5.9) because it builds the Craft 5 control panel
bundles, so those get a named `legacy` catalog rather than being forced onto
the modern versions.

Three published ranges had their floor raised to what is actually resolved
and tested against: @craftcms/playwright's @playwright/test (^1.47.0 ->
^1.59.1), @craftcms/webpack's dotenv (^16.3.1 -> ^16.6.1), and
@craftcms/legacy's inputmask (^5.0.9 -> ^5.0.10).

Not catalogued:

- peerDependencies, which are intentionally wide consumer contracts —
  @craftcms/garnish keeps jquery ^3.5.0.
- vite and vitest, which stay in `overrides`; Vite+ has to replace Vite
  inside third-party packages too, which a catalog cannot do.
- @types/jquery and pkg-dir, which disagree across packages in ways that
  aren't a modern/legacy split and need a decision of their own.

Every one of the 217 importer resolutions in the lockfile is unchanged.
Sets pnpm's minimumReleaseAge, which refuses to resolve or install any
version published less than a week ago. Compromised npm releases are
generally caught and unpublished within hours, so this skips the window in
which they're installable at close to no cost — Craft's build output reaches
a lot of sites, and none of our dependencies need same-day upgrades.

pnpm enforces this both when resolving and as a verification pass over an
existing lockfile, so a lockfile someone produced with the policy disabled
fails CI rather than silently installing.

Enabling it flagged two packages already in the tree: @codemirror/state
6.7.2 and @codemirror/view 6.43.10, both published two days ago and pulled
in transitively by @codemirror/language on open ranges. They're pinned via
overrides to 6.7.1 and 6.43.9 — the newest releases that clear the window.
Those pins can be dropped once the newer versions age out.

No direct dependency resolution changed.
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

📚 Storybook previews

@craftcms/uiopen Storybook

Changed components:

resources/jsopen Storybook

No changed components detected in this Storybook.

@brianjhanson
brianjhanson marked this pull request as ready for review September 10, 2026 15:02
@github-actions

Copy link
Copy Markdown

This PR had conflicts with 6.x. Claude attempted an automatic resolution and pushed a merge commit. Please review the merge commit carefully before approving/merging.

Claude's response

Merge completed and pushed as 8b852fe.

Summary

There was a single conflict: package-lock.json, deleted by chore/pnpm and modified by 6.x (DU).

Resolution: kept the deletion and ported the incoming intent forward. chore/pnpm removed package-lock.json wholesale in 887a803 when switching the toolchain to pnpm, while 6.x's only change to that file was dropping htmx.org as part of replacing the condition builder with a Vue editor and removing HTMX. Resurrecting the lockfile would have undone the branch's entire purpose, so instead I applied the HTMX removal to pnpm-lock.yaml, which is the lockfile that now governs installs.

pnpm install --lockfile-only couldn't regenerate the lockfile here because the Font Awesome registry credential isn't available in this environment (401), so I removed the three htmx.org@1.9.12 entries by hand — the importer entry under packages/craftcms-legacy, the packages resolution block, and the empty snapshots entry. htmx.org is a leaf dependency: nothing else in the lockfile depends on it, and no other entries needed adjusting.

Verification: the lockfile still parses, and every importer's dependency set now matches its package.json (the only difference repo-wide is garnish's optional jquery peer dep, which was already resolved that way before the merge). No stale htmx/conditionbuilder references remain in the pnpm workspace config, build scripts, or CI workflows. packages/craftcms-legacy/package.json auto-merged cleanly, taking 6.x's htmx removal while keeping the pnpm workspace scripts.

Nothing here was ambiguous, but one thing is worth a reviewer's eye: pnpm-lock.yaml was edited by hand rather than regenerated, so it would be good to confirm a real pnpm install in CI (with the registry token) produces no further lockfile churn.

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.

2 participants