fix: resolve @webjsdev versions through Node so a hoisted install passes - #1351
Conversation
webjs doctor's WEBJS_VERSIONS check read <appDir>/node_modules/<dep>/package.json directly. Under npm workspaces the @webjsdev/* deps hoist to the root node_modules, so an app subdirectory has no local copy and every declared dep was reported not installed on a perfectly healthy install. Both in-repo apps warned that way, while FRAMEWORK_RESOLVE in the same output said the framework resolves fine, so the two checks openly contradicted each other. Ask Node's resolver instead, anchored at the app dir. That is the question the check actually asks, so it needs no hoist-awareness of its own and picks up symlinked workspace links and nested trees for free. Both halves of the resolve are load-bearing: @webjsdev/cli is bin-only so it has no main entry, and @webjsdev/server locks ./package.json out of its exports map. The check now becomes gatable, which it could not be while it warned on a healthy install.
The workspace fixture builder wrote an index.js for every package, so the
bin-only case had no main and no exports but did have an entry file to fall
back to. require.resolve('<dep>') therefore succeeded and the case stopped
pinning the resolve ORDER it exists to pin: swapping the two attempts left it
green. Write an entry file only for a manifest that declares main or exports.
Verified by the swap: entry-first ordering now reds the bin-only case.
vivek7405
left a comment
There was a problem hiding this comment.
Read the whole diff. The resolution change itself is right: the check was asking a resolution question with a directory read, and asking Node instead is the only version of this that stays correct as install layouts change. I checked the two-half resolve is genuinely load-bearing rather than defensive padding, and it is, on both real packages.
One real problem, in the tests rather than the source. The bin-only fixture was not bin-only, so the case that claims to pin the resolve ORDER did not pin it, and the docblock in doctor.js says it does. That is the kind of test that reads as coverage and is not. Fixed by writing an entry file only where the manifest declares one, and I proved the fixed fixture discriminates by swapping the two attempts and watching it red.
The other note is a miscount in the counterfactual comment. Both are fixed on the branch.
Part 3 of #1300, the priority slice. This is PR 1 of three sequenced against that issue, so it deliberately carries no
Closesline and #1300 stays open for parts 1 and 2.webjs doctor'sWEBJS_VERSIONScheck read<appDir>/node_modules/<dep>/package.jsonto find a declared dependency's installed version. Under npm workspaces the@webjsdev/*deps hoist to the ROOTnode_modules, so an app subdirectory has no local copy and every declared dep came back reported as not installed on a perfectly healthy install. Both in-repo apps warned that way (examples/blognamed all four deps,websiteall three), whileFRAMEWORK_RESOLVEin the same output said@webjsdev/core resolves from the app directory, so the two checks openly contradicted each other. It also made the check ungatable: no app could set"WEBJS_VERSIONS": "error"without redding CI on a healthy tree.It now asks Node's resolver, anchored at the app dir. That is the question the check actually asks ("would this app resolve this dependency at runtime, and at what version"), so it needs no hoist-awareness of its own and picks up symlinked workspace links, nested non-hoisted trees, and
importsfor free.What changed
readInstalledVersion(dep, appDir), a module-private helper inpackages/cli/lib/doctor.js, resolving throughcreateRequireanchored at the app dir. Both halves of the resolve are load-bearing and neither works alone:@webjsdev/cliis bin-only, sorequire.resolve('@webjsdev/cli')throwsMODULE_NOT_FOUNDand only the direct<dep>/package.jsonattempt finds it, while@webjsdev/serverlocks./package.jsonout of itsexportsmap, so the direct attempt is refused withERR_PACKAGE_PATH_NOT_EXPORTEDand only the main-entry plus bounded-walk fallback finds it.checkWebjsVersionscalls it. Every message string, thepass/warnstatuses, thebestEffortposture,DOCTOR_CODES, and thesatisfiesRangenull (unverifiable range shape) treatment are untouched. The check's contract does not change, only its resolution.test/cli/doctor.test.mjs, plus a strengthened assertion on the existing missing-dep case so it names the dep.packages/cli/AGENTS.md, thewebjs doctorrow, recording how the version is resolved and why each half of the fallback exists.The helper is local rather than
getPackageVersionfrom@webjsdev/serverfor two reasons: doctor must stay usable when the framework does not resolve from the app dir at all, which is the #954 fresh-worktree case doctor exists to diagnose, and that helper resolves the main entry only, so it returnsnullfor a bin-only package and would leave@webjsdev/clireported missing anyway.No
webjs.doctor.gateblock and no default severity moved. The deliverable is that the check becomes gatable; whether to gate it is the app's call, and gating it now would be a no-op today and an untested trap later.Test plan
test/cli/doctor.test.mjs: 106 pass. New fixtures cover the workspace-hoisted app (pass, counting every dep), a bin-only package, an exports-locked package, drift still reported through a hoisted resolve (anundefinedversion could not produce a drift message, so that case doubles as the non-undefinedproof), a workspace app declaring a dep nothing installed (stillwarn, naming it), and an unverifiable range shape (still no warn).329572c1and re-proven at21812bd8: restore thejoin(appDir, 'node_modules', dep, 'package.json')read and all six new fixtures red with "N @webjsdev/* dependency not installed", which is the measured before-state on both apps.webjs doctor --jsonfromexamples/blogreportsWEBJS_VERSIONS pass, All 4 @webjsdev/* dependency satisfy their declared ranges, and fromwebsitethe same with 3. Before the fix, the same command in the primary checkout reportswarn, 4 @webjsdev/* dependency not installed: @webjsdev/cli, @webjsdev/core, @webjsdev/server, @webjsdev/intellisense.webjs checkpasses andwebjs doctorexits 0 in both apps.npm test: 4119 of 4125 pass. The five failures are environmental to a linked worktree and unrelated to this diff.packages/server/test/elision/differential-elision.test.jsfails the same way in an unrelated worktree at a different base commit and passes in the primary checkout (the fixture app renders 500 there), and the twotest/bun/*wrappers fail in the primary checkout onmaintoo. CI builds from the branch with a real install, so it is the authority on those.mainorexports, so a bin-only manifest has nothing to fall back to, which is what a real bin-only package looks like.websiteboots in prod mode throughcreateRequestHandlerand serves 200 on/,/docs/configuration,/ui, and/ui/button, with 7, 12, 12, and 50 modulepreload hints and none broken.examples/blogis covered by the e2e suite, which this diff does not touch.node:crypto, the TS stripper, and auth / session / cors. AcreateRequireprobe in a CLI check is on none of them, and the doc states the check tooling stays on Node.packages/cli/AGENTS.mdupdated. RootAGENTS.md:525does not enumerateWEBJS_VERSIONS, andwebsite/app/docs/configuration/page.ts:62describes it only as "@webjsdev/*version coherence", both of which stay accurate, so neither changed. No scaffold, MCP, editor-plugin, or README surface touches this check.