fix(dependency-resolver): patch react-env's tsconfig extends for the global virtual store - #10572
Draft
zkochan wants to merge 9 commits into
Draft
fix(dependency-resolver): patch react-env's tsconfig extends for the global virtual store#10572zkochan wants to merge 9 commits into
zkochan wants to merge 9 commits into
Conversation
`_calculateFinalFileName` stripped the extension from the whole path, so any dot in a parent directory was treated as the extension: `bit doctor --archive /tmp/my.dir/report` wrote `/tmp/my.tar` instead of `/tmp/my.dir/report.tar`. Strip the extension from the last path segment only. Unrelated to the rest of this branch - it surfaced because the e2e temp dir contains a dot, which is why `doctor-infra.e2e.ts` fails locally but passes on CI.
…tore With `enableGlobalVirtualStore` on, dependency directories are created once in a store shared by every workspace and capsule on the machine instead of being re-created inside each `node_modules/.pnpm`. On real workspaces that collapses the project-local virtual store from 20832 to 529 entries (and 4017 to 258), leaving only the injected components. Off by default. Enable per workspace under `teambit.dependencies/dependency-resolver`, machine-wide with `bit config set enable_global_virtual_store true`, or per run with `BIT_ENABLE_GLOBAL_VIRTUAL_STORE` (which is how the e2e suite runs the whole matrix under it). Making it behave identically to the project-local layout needed four fixes: - The core aspects are phantom dependencies of every published env and aspect. Project-locally a package in `node_modules/.pnpm` resolves them by walking up into the workspace's root `node_modules`; nothing in a shared store can walk up into a workspace, so every env failed to load. Mirror the installation's core-aspect links at the root of the store, which every slot in it can reach, and key that store per bit installation so two of them cannot shadow each other's aspects. - A workspace that authors the core aspects (the bit repo) bridges them into the hoisted store only until they compile, then removes the bridge. That is right project-locally and fatal from a shared store, so under it the bridge stays, repointed at the compiled workspace copy. - `getInjectedDirs` reports an injected copy absolute when it cannot be made relative to the lockfile dir - which under a shared store is always. `compiler.task.ts` joined it onto the capsule root regardless, hard-linking build artifacts into a path that does not exist. - Capsules stay on the project-local store. A capsule is a self-contained build sandbox and TypeScript's declaration emit depends on that: it names an inferred type through the package that declares it, which it can only do while that package is inside the capsule. Moving env and aspect packages out made `bit build` of any custom env fail with TS2742. Also adds `patchedDependencies` and `packageExtensions` passthrough. `packageExtensions` is not optional under a shared store: it is the only way to satisfy a phantom dependency of a published package, since the package now lives outside the project and can no longer reach whatever the project hoisted. `bit deps diagnose` and the prune step read the current lockfile instead of assuming `node_modules/.pnpm` holds the packages, and the global store is skipped for `nodeLinker: hoisted`, which uses no virtual store.
Three suites hand-wrote paths that only exist in the project-local virtual store, so they broke under the global one even though the behaviour they check was fine: - `install.e2e` and `node-linker.e2e` asserted on `node_modules/.pnpm` directly. New `helper.fs.getVirtualStoreDirNames()` reads the current lockfile when the directory holds no package dirs, which yields the same names either way. - `dependency-resolver.e2e` verified dependency overrides by reading `node_modules/.pnpm/<depPath>/node_modules/<dep>/package.json`. New `helper.fs.readPackageJsonOfChain(['rimraf','glob','inflight','once'])` walks the chain the way Node resolves it, so it is correct under the project-local, global and hoisted layouts alike. Adds `global-virtual-store.e2e.ts` covering the store itself: deps are not created in the workspace, they resolve out of the shared store, the current lockfile still records them, the component loads and compiles, and a patched dependency is applied to the shared copy.
Documents `enableGlobalVirtualStore`, `globalVirtualStoreDir` and `patchedDependencies`, and adds the script that installs a locally built pnpm engine over the published `@pnpm/napi` - needed until a release carries the options this branch passes (pnpm/pnpm#13648).
`my-react-env.bit-env.ts` opened with `// @bit-no-check`, which turns off Bit's dependency detection for the file. Its imports - notably `@teambit/react.react-env` - therefore never reached the component's generated manifest, so the env only resolved them by reaching packages the workspace had hoisted. That is a phantom dependency, and it stops working the moment the package is not inside the project any more: under the global virtual store the env fails to load with `Cannot find module '@teambit/react.react-env'`. Nothing in this fixture needs dependency detection off - `@ts-nocheck`, which is what the tsconfig scenario actually relies on, stays. None of the real env components in the workspaces this was checked against carry `@bit-no-check`, and they resolve correctly either way.
… global virtual store The core aspects are phantom dependencies of every published env: required without being declared, provided by the running bit installation. Under the project-local virtual store an env resolves them by walking up out of node_modules/.pnpm into the workspace root. Under the global virtual store the env lives in the shared store with no workspace above it, so that fails. Bit worked around this by mirroring the core aspects at the root of the virtual store, which forced a <storeDir>/bit-links/<installationId> root private to one bit installation: no slot reuse across bit versions or with other pnpm projects, a full re-materialization of ~275k directories on every bit upgrade, and nothing to prune stale roots. Use pnpm's documented mechanism for hoisted dependencies in this layout instead. The privately hoisted directory stays project-local at node_modules/.pnpm/node_modules even under the global virtual store, and is reached from a store slot via NODE_PATH. Link the core aspects there and put it on NODE_PATH during bootstrap - pnpm does that for the shims it writes, but bit runs from bvm and loads aspects in its own process. Nothing is written inside the store, so pnpm's shared <storeDir>/links can be used: slots are reused across bit versions and with every other pnpm project, upgrades stay incremental, and pnpm store prune accounts for them. Node ignores NODE_PATH for ESM. Core aspects and published env dists are CommonJS today; @pnpm/plugin-esm-node-path is the escape hatch if that changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… in ESM too NODE_PATH covers only CommonJS - Node ignores it for ESM entirely. Bit supports ESM components, so an ESM package in the shared store could not reach the hoisted directory at all, leaving the workaround covering only half the problem. Register an ESM loader whose resolve hook retries a failed bare specifier against the NODE_PATH entries. Registered in bit's own process via module.register, and exported through NODE_OPTIONS so the processes bit spawns - env build steps, app servers, test workers - inherit it, since they resolve their own dependencies and hit the same wall. The loader and the --import registration are adapted from pnpm's @pnpm/plugin-esm-node-path (MIT), inlined as a data URL so there is no file to ship and no install step to depend on. Skipped when module.register is missing (Node < 20.6); the CommonJS half still applies there. Also generalize the surrounding comments: this is about phantom dependencies as a class, not core aspects specifically. The core aspects are the case that motivated it, but any under-declared package resolves the same way, which is why the whole hoisted directory goes on the path rather than a hand-picked list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The global virtual store work needs two engine changes that are now in a released build: the napi surface for `enableGlobalVirtualStore`, `globalVirtualStoreDir`, `packageExtensions` and `patchedDependencies` (pnpm/pnpm#13648), and the fix for `link:` dependencies vanishing from virtual-store slots (pnpm/pnpm#13658). Verified on the released engine: a global-virtual-store install of the opus-5 workspace reuses all 30,766 existing slots (creates none), leaves the project-local `node_modules/.pnpm` empty, and still applies the react-env tsconfig patch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…global virtual store @teambit/react.react-env 1.x ships a tsconfig whose `extends` names @teambit/react/typescript/tsconfig.json without declaring @teambit/react - a phantom dependency in a published package. It resolved only by TypeScript walking up node_modules into the workspace, which a package in the shared store cannot do, so `bit status` reported "File '@teambit/react/typescript/ tsconfig.json' not found." under the global virtual store and was clean without it. NODE_PATH and the ESM loader cannot cover this: TypeScript resolves `extends` with its own resolver and never consults either. Ship a patch that inlines the extended config so no resolution is needed - the only form that works from any location. Declaring the dependency instead would install a registry copy of a core aspect, duplicating what has to stay a single host-provided instance. BLOCKED, do not merge yet. react-env 2.x already inlines the config, so the phantom dependency only exists on the 1.x line. The key therefore has to be scoped (`@teambit/react.react-env@1`), but pnpm fails an install with ERR_PNPM_UNUSED_PATCH when a configured patch matches no installed package (crates/patching/src/verify.rs, `allow_unused_patches` defaults to false), and that setting is not exposed through @pnpm/napi - 12.0.0-rc.0 accepts `patchedDependencies` only. Keying it bare, as this commit does, makes it fail to apply on 2.x instead. Waiting on a napi release exposing allowUnusedPatches. Then: change the key to `@teambit/react.react-env@1` and pass allowUnusedPatches so workspaces on 2.x, including this repo, do not fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 5, 2026
zkochan
added a commit
to pnpm/pnpm
that referenced
this pull request
Aug 6, 2026
pnpm fails an install when a configured patch matches no installed package (ERR_PNPM_UNUSED_PATCH). The CLI lets a workspace opt out via allowUnusedPatches in pnpm-workspace.yaml, but the binding accepted patchedDependencies only, so an embedder that ships its own patches had no way to reach the setting. Threads the flag through InstallOptions -> ConfigOverlay -> Config::allow_unused_patches, which the fresh-lockfile path already reads when it calls verify_patches. No engine-side change is needed; this is purely the missing binding surface. Bit needs it to ship a patch keyed to a version range (`@teambit/react.react-env@1`) from its own config: workspaces that resolve 2.x match no key and would otherwise fail the install. See teambit/bit#10572.
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.
Ships a patch for
@teambit/react.react-env's phantomtsconfigdependency under the global virtual store.Blocked — do not merge. Split out of #10567 so that PR is not held up by this.
The problem
@teambit/react.react-env1.x ships a tsconfig whoseextendsnames@teambit/react/typescript/tsconfig.jsonwithout declaring@teambit/react. That resolved only by TypeScript walking upnode_modulesinto the workspace, which a package in the shared store cannot do — so under the global virtual storebit statusreports:and is clean without it.
NODE_PATHand the ESM loader cannot cover it: TypeScript resolvesextendswith its own resolver and consults neither. The patch inlines the extended config so no resolution is needed — the only form that works from an arbitrary location in the shared store.Why it is blocked
react-env 2.x already inlines the config, so the phantom dependency only exists on the 1.x line. That makes the patch key version-sensitive:
@teambit/react.react-env(this PR)@teambit/react.react-env@1ERR_PNPM_UNUSED_PATCHpnpm fails an install when a configured patch matches no installed package (
crates/patching/src/verify.rs,verify_patches).allow_unused_patchesdefaults tofalseand is not exposed through@pnpm/napi—12.0.0-rc.0acceptspatchedDependenciesonly.So neither key is safe today. This repo itself resolves only react-env
2.0.3, so merging as-is would break its own global-virtual-store installs.Unblocking
Once a napi release exposes
allowUnusedPatches:@teambit/react.react-env@1allowUnusedPatchesso workspaces on 2.x do not failThen this is mergeable.
Notes
gvs) — it depends onenableGlobalVirtualStore(). Until that lands, the diff here shows its commits too; only the last commit belongs to this PR.shouldCopyNonSupportedFilescopiespatches/*.patchintodist, same as the existingfixtures/cafile.txt. No build changes needed.