fix(global virtual store): bridge the resolution paths a store slot lost - #10589
fix(global virtual store): bridge the resolution paths a store slot lost#10589zkochan wants to merge 9 commits into
Conversation
Under the global virtual store a package's real directory sits outside the project, so the ancestor walk that used to satisfy its undeclared requires reaches nothing. The bridge restored one of the two directories that walk passed through, and only for require - not for tsc. Both directories, not just the hoisted one. pnpm hoists non-direct dependencies into node_modules/.pnpm/node_modules; a direct dependency of the root is reachable through the root's own node_modules and nowhere else, which is exactly the half that was missing. TypeScript reads neither NODE_PATH nor the ESM loader, so a .d.ts in a store slot resolves `react` to react/index.js, gets no typings, and every type derived from it degrades into errors that name a prop rather than the cause. Its only lever is `paths`, a redirect where NODE_PATH is a fallback, so the mapping is kept to what the walk actually used to find: a @types/x mapping only when x itself ships no typings - otherwise packages carrying their own modern types get dragged back to stale ones - and @teambit from the root alone, since core aspects have to be the single copy from the running installation and the hoisted directory holds older transitive ones. Both roots are gated on the layout their own last install recorded, so a project-local installation is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR Summary by QodoFix global virtual store resolution bridge for runtime and TypeScript
AI Description
Diagram
High-Level Assessment
Files changed (7)
|
Code Review by Qodo
1.
|
A false negative in shipsOwnTypes is not a missed optimization: the specifier gets redirected to @types and TypeScript stops seeing the package's own, usually newer, declarations. The check read the types/typings fields and a root index.d.ts, which misses a package that says nothing and lets the resolver infer dist/index.d.ts from its entry point, one that declares a types condition inside exports, and one that maps declarations through typesVersions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit 2f83431 |
…at is missing Order in NODE_PATH is resolution order, and an entry already present is not necessarily in front of the one it has to beat. A bit whose bridge added the hoisted directory alone leaves it in NODE_PATH for its children, where adding the root's node_modules in front of it inverts the walk the bridge exists to reproduce. Rebuild the two entries at the front in walk order and keep everything else behind them, unchanged in relative order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit d539648 |
…inside the test ensureHoistedDependencyResolution mutates state no afterEach can reach by restoring an environment variable: _initPaths rederives Module.globalPaths, and module.register installs an ESM loader for the life of the process. The cases restored NODE_PATH and left the resolver pointing into directories they then deleted, and each of them chained another loader. Rederive the paths from the restored variable, and take `register` away for the duration so the irreversible half never runs - through the same guard that carries runtimes without it. These cases are about NODE_PATH order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit fa00cbd |
…oint shape TypeScript pairs .mjs with .d.mts and .cjs with .d.cts, and infers a package's declarations from whichever entry point the resolver picked - main for the classic algorithm, an exports target for the modern one. The check looked only for .d.ts beside main, so a package shipping types either of those ways read as untyped and had its specifier redirected to @types, burying the declarations it actually ships. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit a9abe6a |
…spelling An entry naming a directory the bridge owns, written in another spelling - a trailing separator, a redundant segment, a relative path, or on Windows a difference in case - was treated as foreign and kept alongside the canonical entry the bridge then added. path.relative normalizes both sides, and the module already reasons about platform path comparison that way for containment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit f2450a0 |
|
Code review by qodo was updated up to the latest commit 261fb25 |
globalVirtualStoreTypePaths read as though it returned nothing for a project-local root, which it has no way of knowing - it derives the mapping from the directories alone. Say what it does, and why the check sits at the call site: a project-local root would get a mapping pointing at the same @types its own walk already reaches, and the caller is the one holding several roots and deciding which of them participate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit f5d7435 |
…changes The loader inlines its entry list and searches it in order, so a reordered NODE_PATH needs a new registration - but the skip test asked whether the entry *set* had grown, which a reorder leaves untouched. Children then inherit an --import flag whose precedence contradicts the NODE_PATH beside it. Track the ordered list instead of the set. In-process the correction is partial by construction, and the comments now say so: node runs the last registered loader first and it delegates before its own fallback, so an earlier registration keeps precedence for entries it already had. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit 82540b4 |
Closes #10588.
Under pnpm's global virtual store a package's real directory sits outside the project, so the ancestor walk that used to satisfy its undeclared requires reaches nothing. The bridge in
hoisted-resolution-bridge.tsrestored one of the two directories that walk passed through, and only forrequire— not fortsc. This restores both, on both resolvers.Both directories, not just the hoisted one
pnpm hoists only non-direct dependencies into
node_modules/.pnpm/node_modules. A direct dependency of the root lives in the root's ownnode_modulesand nowhere else, so the walk out of a store slot reached two directories with disjoint contents and the bridge covered one. Measured in this repo:hoistedResolutionDirs(root)now returns both, in walk order, andensureHoistedDependencyResolutionputs both onNODE_PATH— the hoisted one still first, so it keeps winning exactly as the walk had it. The ESM loader mirrors the entry list, so it follows automatically.The type-resolution half
TypeScript reads neither
NODE_PATHnor the ESM loader, so under this layout a.d.tsin a store slot resolvesreacttoreact/index.js, gets no typings, and every type derived from it degrades quietly.--traceResolutionon this repo:The two copies never unify, and it surfaces as
Property 'children' does not exist on type 'IntrinsicAttributes & CheckboxLabelProps'— a message that names a prop rather than the cause. This repo already patches it by hand intsconfig.jsonfor its owntsc, for two packages, in one file.TypeScript's only lever is
paths, which is a redirect whereNODE_PATHis a fallback, so a blanket mapping does real damage. The mapping is therefore kept to what the walk actually used to find. Full-repotsc --noEmit, one rule at a time:@typespackage from both directoriesglob,react-router-domand everything else that ships its own modern types@types/xonly wherexitself ships no typings@teambit/*from the hoisted directory and the root@teambit/*from the root onlySo: a
@types/xmapping only whenxships no typings of its own — that is the case the walk-up existed for, since a self-typedxstopped the walk before it ever reached@types/x— and@teambit/*from the root alone, because core aspects have to be the single copy from the running installation (the invariantDependencyLinkeralready maintains).Applied in
TypescriptMain.createCompilerafter every transformer, so anything configured deliberately keeps its mapping and the bridge only fills in what would otherwise resolve to nothing. Two roots, each gated on the layout its own last install recorded: the workspace, whose components are what gets compiled, and the running installation, whose core aspects the compiled program reaches through the linksDependencyLinkerwrites. A bvm installation is project-local and stays out entirely.Verification
bit teston both components: 119 passing, 16 of them new.global-virtual-store.e2e.ts→ "building an aspect": builds an aspect in a workspace withenableGlobalVirtualStore: true. Passes on this branch; with the TypeScript half reverted and the aspect recompiled it fails with 47error TS, so it fails for the reason it exists.e2e/harmony/extensions-config-diff.e2e.ts, which is what surfaced this: 7 passing, 0 failing, 0 TypeScript errors. On a global-virtual-store workspace before this change it failed in itsbeforehook withFailed task 1: "teambit.compilation/compiler:TSCompiler"and 188 errors attributed to one component.npm run lintclean.Not included
pathsblock in this repo'stsconfig.jsonstays.npm run lintrunstscdirectly against that file and never goes through bit's compiler, so those pins are still load-bearing; retiring them is a separate change with its own verification.hoistedResolutionDirsis unit-tested, but exercising a phantom require of a root-direct dependency from a store slot needs a published fixture package that under-declares one, and a test that would pass with or without the change is worse than none. The type-side e2e covers the shared directory logic end to end.🤖 Generated with Claude Code