Switch the frontend toolchain from npm to pnpm - #19542
Draft
brianjhanson wants to merge 4 commits into
Draft
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
overridesandallowScriptsmove to pnpm-workspace.yaml, where the latter is spelledallowBuildsand 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.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 importso installed versions match what package-lock.json resolved — this changes the package manager only, not the dependency tree.