Skip to content

fix(install): skip linking into .bit_roots when rootComponents is disabled#10356

Merged
zkochan merged 4 commits into
teambit:masterfrom
zkochan:fix/skip-bit-roots-link-when-disabled
May 12, 2026
Merged

fix(install): skip linking into .bit_roots when rootComponents is disabled#10356
zkochan merged 4 commits into
teambit:masterfrom
zkochan:fix/skip-bit-roots-link-when-disabled

Conversation

@zkochan
Copy link
Copy Markdown
Member

@zkochan zkochan commented May 8, 2026

Summary

The post-install path runs through linkCodemodslinkToNodeModulesByIdsNodeModuleLinker.link(), and that ended with an unconditional call to linkPkgsToRootComponents at node-modules-linker.ts:73. The sibling call site in install.main.runtime.ts (_linkAllComponentsToBitRoots, line 1272) is already gated on dependencyResolver.hasRootComponents() — this aligns the linker with that.

When this matters

A user toggles rootComponents from true to false and runs bit install:

  1. Prior install (with rootComponents: true) populated node_modules/.bit_roots/<env>/node_modules/....
  2. New install (with rootComponents: false): pnpm no longer treats .bit_roots/<env> as a workspace project, and _updateRootDirs is skipped, so that subtree is no longer managed by anyone. Its internal layout can drift — e.g. ancestors of <env>/node_modules/<pkg> may have been reshaped, leaving a regular file or a broken link where a directory used to be.
  3. NodeModuleLinker.link() then tries to hard-link the workspace's node_modules/<pkg> into that stale tree, and mkdir(... { recursive: true }) throws ENOTDIR (or ENOENT through a broken symlink). The whole install aborts.

Concrete report from a user:

✔ done running package installation using pnpm (completed in 5s)
✔ running post install subscribers
ENOTDIR: not a directory, mkdir '/home/user/hope-mobile/node_modules/.bit_roots/bitdev.react-native_react-native-env@2.0.0/node_modules/@teambit/hope.hope-mobile'

Changes

  • Workspace.hasRootComponents() — small public method that delegates to the private dependencyResolver.hasRootComponents(). The linker (and any other consumer of Workspace) can now check this without reaching into the private resolver.
  • NodeModuleLinker.link() — gates the linkPkgsToRootComponents call on workspace.hasRootComponents(). When root components are off, the stale .bit_roots tree is left strictly alone.

Notes

  • The defensive recovery in fix(install): recover from non-directory entries blocking .bit_roots links #10355 turns the underlying ENOTDIR into a recoverable warning when it does happen. This PR is the upstream fix that prevents bit from touching .bit_roots in the first place when it shouldn't. The two are complementary — happy to land in either order.
  • This PR does not clean up an existing .bit_roots when the user toggles to false. The stale tree is wasted disk but no longer actively harmful once we stop writing to it. If you want bit to also remove it on toggle, that's a separate change worth thinking through (someone may have tooling that reads from there independently).

Test plan

  • npm run lint — same 38 pre-existing TS errors as master (all in unrelated @pnpm/* imports), no new errors in the changed files
  • e2e test — the existing e2e/harmony/root-components.e2e.ts is the natural home for a "toggle off and reinstall" scenario but I held off on adding one in this draft; let me know if you want it bundled in.

…abled

The post-install path (linkCodemods → linkToNodeModulesByIds → NodeModuleLinker.link) called linkPkgsToRootComponents unconditionally, even when the workspace had `rootComponents` set to `false`. The sibling call site in install.main.runtime.ts (`_linkAllComponentsToBitRoots`) is already gated on `hasRootComponents()`; this aligns the linker with that.

When a user toggles `rootComponents` from `true` to `false` and reinstalls, the previous install's `.bit_roots/<env>` tree is no longer managed by pnpm, so its internal layout can be inconsistent (e.g. ancestors of `<env>/node_modules/<pkg>` may have been reshaped). Hard-linking into that stale tree was failing with `ENOTDIR` / `ENOENT` and aborting the install.

Expose `hasRootComponents()` on `Workspace` (delegates to `dependencyResolver`) so the linker can check it without reaching into the private dep resolver.
@zkochan zkochan marked this pull request as ready for review May 8, 2026 13:43
Copilot AI review requested due to automatic review settings May 8, 2026 13:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR prevents post-install linking from writing into node_modules/.bit_roots when the workspace is configured with rootComponents: false, avoiding install failures caused by stale/unmanaged .bit_roots trees after toggling the setting off.

Changes:

  • Added Workspace.hasRootComponents() as a public wrapper around DependencyResolverMain.hasRootComponents().
  • Updated NodeModuleLinker.link() to call linkPkgsToRootComponents() only when workspace.hasRootComponents() is true.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
scopes/workspace/workspace/workspace.ts Exposes a public hasRootComponents() helper for consumers that shouldn’t reach into the private dependency-resolver field.
scopes/workspace/modules/node-modules-linker/node-modules-linker.ts Skips linking packages into .bit_roots when root components are disabled, preventing errors from stale .bit_roots structures.

Comment thread scopes/workspace/workspace/workspace.ts Outdated

/**
* Whether the workspace is configured to use root components (the `.bit_roots`
* per-env install layout under `node_modules`).
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — updated the JSDoc to mention the configurable location (745f572).

@zkochan zkochan enabled auto-merge (squash) May 11, 2026 19:26
Copilot AI review requested due to automatic review settings May 11, 2026 19:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

/**
* Whether the workspace is configured to use root components — the per-env install
* layout written to `rootComponentsPath` (defaults to `node_modules/.bit_roots`,
* relocatable via `dependencyResolver.rootComponentsDirectory`).
Comment on lines +73 to +86
// Only update `.bit_roots` when root components are actually in use. Otherwise
// we'd be hard-linking into a stale tree left over from a previous install where
// rootComponents was enabled — pnpm no longer manages that subtree, so its layout
// can be inconsistent and `mkdir` may throw ENOTDIR/ENOENT through a broken
// ancestor inside `.bit_roots/<env>/node_modules/...`.
if (this.workspace.hasRootComponents()) {
await linkPkgsToRootComponents(
{
rootComponentsPath: this.workspace.rootComponentsPath,
workspacePath,
},
this.components.map((comp) => componentIdToPackageName(comp.state._consumer))
);
}
@zkochan zkochan merged commit 5c9346b into teambit:master May 12, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants