Problem
pnpm-workspace.yaml carries two hoisting settings inherited verbatim from the pre-pnpm-11 .npmrc (#3700):
nodeLinker: hoisted
shamefullyHoist: true
shamefullyHoist: true is publicHoistPattern: '*' — every transitive package is republished at the root of node_modules. A workspace package can therefore import a module it never declared in its own package.json, and nothing complains: not install, not build, not CI. The declaration is simply missing, and the import rides on someone else's copy.
nodeLinker: hoisted is a separate problem. Its .npmrc annotation read "Use the Node-modules linker (default), not pnpm's PnP", which is wrong on its own terms — pnpm's default is the node-modules linker, but its default mode is isolated, not hoisted. On top of being unnecessary, the pair is a combination pnpm has an open bug on: nodeLinker: hoisted with shamefullyHoist: true is pnpm/pnpm#6620, spurious ModuleNotFoundErrors that only clear after toggling the setting off and back on.
Evidence
Flipped shamefullyHoist to false locally (pnpm 11.10.0), recreated node_modules, and ran build / test / typecheck / Storybook / Next builds. Eight undeclared imports surfaced — each verified by checking the missing module against the importing package's own package.json:
| Package |
Undeclared module |
Symptom |
root scripts/ |
@babel/core, glob, chokidar, @testing-library/jest-dom, @stylexjs/babel-plugin |
pnpm build fails at @astryxdesign/core |
lab, charts, richtext |
@babel/cli, @babel/core, @babel/preset-react, @babel/preset-typescript, @stylexjs/babel-plugin |
build:esm → sh: babel: command not found |
lab, charts, richtext |
@testing-library/react |
27 test files load 0 tests |
core |
@types/babel__core |
TS7016 |
storybook |
@heroicons/react, @stylexjs/stylex, @lexical/react, lexical |
storybook:build fails to resolve |
docsite |
@babel/parser |
next build type error after a clean compile |
cli |
@heroicons/react, @stylexjs/stylex, lucide-react, recharts |
145+ TS errors in assets/templates/**, which tsconfig.strict.json includes and CI runs (ci.yml) |
All eight are devDependencies, so no published package is missing a runtime dependency today — the cost is in-repo hygiene and the absence of a guard against the next one.
Scope Check
linkWorkspacePackages: true was audited and must stay: 64 internal deps are declared as bare "*" (only 4 use workspace:*), and pnpm ≥ 10 defaults this to false, which would resolve them from the registry instead of local source — silently, with no error.
- Three failures found during the sweep are pre-existing and unrelated, each baselined with hoisting restored: 7 test files (
plainDate, Calendar, DateInputTouch, Schedule, TransferListSelector, 2 CLI suites — 69 failing cases on main vs 56 with the change), lab typecheck (relative .doc.mjs imports plus a Drawer onClose prop error; CI runs typecheck:docs, which passes), and the template-viewer build (packages/build aliases the @astryxdesign/core prefix to core/src, so core/locales/*.json rewrites to a nonexistent path; CI does not build this app).
- No symlink-hostile environment exists in the repo: no Dockerfile, no react-native, all CI runners are Ubuntu — so nothing depends on the flat layout for platform reasons.
Proposed Direction
Declare what each package actually imports, then drop both settings in the same change — splitting them would land dependency declarations whose correctness cannot be demonstrated while the flag is still on.
Deliberately left alone: linkWorkspacePackages, and any move back toward nodeLinker: hoisted.
Problem
pnpm-workspace.yamlcarries two hoisting settings inherited verbatim from the pre-pnpm-11.npmrc(#3700):shamefullyHoist: trueispublicHoistPattern: '*'— every transitive package is republished at the root ofnode_modules. A workspace package can therefore import a module it never declared in its ownpackage.json, and nothing complains: not install, not build, not CI. The declaration is simply missing, and the import rides on someone else's copy.nodeLinker: hoistedis a separate problem. Its.npmrcannotation read "Use the Node-modules linker (default), not pnpm's PnP", which is wrong on its own terms — pnpm's default is the node-modules linker, but its default mode isisolated, nothoisted. On top of being unnecessary, the pair is a combination pnpm has an open bug on:nodeLinker: hoistedwithshamefullyHoist: trueis pnpm/pnpm#6620, spuriousModuleNotFoundErrors that only clear after toggling the setting off and back on.Evidence
Flipped
shamefullyHoisttofalselocally (pnpm 11.10.0), recreatednode_modules, and ran build / test / typecheck / Storybook / Next builds. Eight undeclared imports surfaced — each verified by checking the missing module against the importing package's ownpackage.json:scripts/@babel/core,glob,chokidar,@testing-library/jest-dom,@stylexjs/babel-pluginpnpm buildfails at@astryxdesign/corelab,charts,richtext@babel/cli,@babel/core,@babel/preset-react,@babel/preset-typescript,@stylexjs/babel-pluginbuild:esm→sh: babel: command not foundlab,charts,richtext@testing-library/reactcore@types/babel__corestorybook@heroicons/react,@stylexjs/stylex,@lexical/react,lexicalstorybook:buildfails to resolvedocsite@babel/parsernext buildtype error after a clean compilecli@heroicons/react,@stylexjs/stylex,lucide-react,rechartsassets/templates/**, whichtsconfig.strict.jsonincludes and CI runs (ci.yml)All eight are devDependencies, so no published package is missing a runtime dependency today — the cost is in-repo hygiene and the absence of a guard against the next one.
Scope Check
linkWorkspacePackages: truewas audited and must stay: 64 internal deps are declared as bare"*"(only 4 useworkspace:*), and pnpm ≥ 10 defaults this tofalse, which would resolve them from the registry instead of local source — silently, with no error.plainDate,Calendar,DateInputTouch,Schedule,TransferListSelector, 2 CLI suites — 69 failing cases on main vs 56 with the change),lab typecheck(relative.doc.mjsimports plus aDraweronCloseprop error; CI runstypecheck:docs, which passes), and thetemplate-viewerbuild (packages/buildaliases the@astryxdesign/coreprefix tocore/src, socore/locales/*.jsonrewrites to a nonexistent path; CI does not build this app).Proposed Direction
Declare what each package actually imports, then drop both settings in the same change — splitting them would land dependency declarations whose correctness cannot be demonstrated while the flag is still on.
Deliberately left alone:
linkWorkspacePackages, and any move back towardnodeLinker: hoisted.