Skip to content

deps: migrate pnpm internals to v12#10293

Open
zkochan wants to merge 12 commits into
teambit:masterfrom
zkochan:pnpm11
Open

deps: migrate pnpm internals to v12#10293
zkochan wants to merge 12 commits into
teambit:masterfrom
zkochan:pnpm11

Conversation

@zkochan

@zkochan zkochan commented Apr 14, 2026

Copy link
Copy Markdown
Member

Summary

  • Rename 17 @pnpm/* deps in workspace.jsonc to v11's @pnpm/<domain>.<leaf> naming (e.g. @pnpm/core@pnpm/installing.deps-installer, @pnpm/config@pnpm/config.reader), bump in-monorepo packages to 1100.0.0.
  • Adapt to v11 breaking API changes: mutateModules now uses allowBuilds: Record<string, boolean | string> (plus dangerouslyAllowAllBuilds) in place of the removed neverBuiltDependencies / onlyBuiltDependencies / ignoredBuiltDependencies trio.
  • Config.rawConfigConfig.authConfig rename; read network settings (maxSockets, fetchRetries, etc.) from typed Config fields instead of rawConfig ini lookups.
  • Convert flat auth rawConfig to the new configByUri: Record<string, RegistryConfig> shape for CreateStoreControllerOptions; switch generateResolverAndFetcher to createResolver since both callers only use .resolve (avoids the now-required storeIndex on createClient).
  • Rename sortPackagessortProjects, createPkgGraphcreateProjectsGraph, createOrConnectStoreControllercreateStoreController; supply now-required globalVirtualStoreDir to getPeerDependencyIssues.

Test plan

  • npm run check-types passes
  • bit compile — 310/310 components compile
  • install new dependencies (using pnpm) e2e — 5/5 pass
  • install missing dependencies (when all packages exist) e2e — 2/2 pass (covers add/tag/export/import/reinstall)
  • skipping compilation on install e2e — 2/2 pass
  • Broader e2e suite
  • CI green

@zkochan zkochan force-pushed the pnpm11 branch 2 times, most recently from 3d42363 to 37f4352 Compare May 5, 2026 13:41
@zkochan zkochan marked this pull request as ready for review May 6, 2026 08:20
Copilot AI review requested due to automatic review settings May 6, 2026 08:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 migrates Bit’s internal pnpm integration to pnpm v11-era packages (1100.x), updating dependency names and adapting Bit’s pnpm/yarn package-manager glue code to pnpm’s breaking API and module-format (ESM) changes.

Changes:

  • Renames and bumps @pnpm/* dependencies in workspace.jsonc to pnpm v11 package naming and versions.
  • Updates pnpm/yarn integration code for pnpm v11 APIs (config/auth shape, resolver creation, store controller creation, peer issues options, project graph/sorting).
  • Adds CJS shims + adjusts Babel config to safely load ESM-only pnpm modules in test/build environments without breaking mocha/Babel require chains.

Reviewed changes

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

Show a summary per file
File Description
workspace.jsonc Renames pnpm internal deps to v11 package names and bumps versions.
scopes/workspace/workspace/workspace-aspects-loader.ts Removes unreachable code in error handling path.
scopes/typescript/typescript/schema-extractor-context.ts Removes unreachable return after throwing.
scopes/scope/objects/models/dependencies-graph.ts Updates pnpm dep-path import to new package name.
scopes/pkg/pkg/packer.ts Switches pnpm pack command loading to ESM-safe dynamic loader shim.
scopes/pkg/pkg/load-pnpm-pack.cjs New CJS shim to import() ESM-only pnpm releasing commands for packing.
scopes/harmony/aspect/babel/babel-config.ts Adjusts preset-env to avoid transforming dynamic import().
scopes/dependencies/yarn/yarn.package-manager.ts Updates pnpm overrides parser import to v11 package.
scopes/dependencies/pnpm/read-config.ts Switches pnpm config reader import to v11 package.
scopes/dependencies/pnpm/pnpm.package-manager.ts Adapts network config reading + lockfile graph converter init + updated pnpm module imports.
scopes/dependencies/pnpm/pnpm-error-to-bit-error.spec.ts Avoids importing ESM-only @pnpm/error by stubbing a PnpmError-like object.
scopes/dependencies/pnpm/lynx.ts Migrates pnpm install/resolver/store controller APIs and script/build policy wiring to v11.
scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts Adds async init + ESM loading shim for ESM-only pnpm modules; adjusts types.
scopes/dependencies/pnpm/lockfile-deps-graph-converter.spec.ts Ensures converter init runs before tests; minor structure tweaks.
scopes/dependencies/pnpm/load-pnpm-esm.cjs New CJS shim to import() ESM-only @pnpm/deps.path and @pnpm/lockfile.fs.
scopes/dependencies/pnpm/get-registries.ts Uses config.authConfig in place of rawConfig for credential extraction.
scopes/dependencies/pnpm/get-proxy-config.ts Updates config type import and no-proxy config source.
scopes/dependencies/dependency-resolver/package-manager.ts Updates pnpm peer issues type import to v11 package.
scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts Updates npm resolver import to v11 package.
scopes/component/modules/merge-helper/merge-files.ts Removes unreachable throw err after throwing a new error.
e2e/harmony/pnpm-default-hoisting.e2e.ts Uses ESM-safe dynamic import for modules manifest reading.
e2e/harmony/pkg-manager-config.e2e.ts Removes pnpm-specific capsule rc test section; keeps Yarn case.
e2e/harmony/dependency-resolver.e2e.ts Uses ESM-safe dynamic import for modules manifest reading.
components/legacy/e2e-helper/npm-ci-registry.ts Switches pnpm fetch import to dynamic ESM import (@pnpm/network.fetch).
babel.config.js Ensures Babel doesn’t transform dynamic import() (needed for ESM-only pnpm deps).

Comment on lines +33 to +41
export async function init(): Promise<void> {
if (loaded) return;
const { loadEsm } = require('./load-pnpm-esm.cjs') as {
loadEsm: () => Promise<{ dp: typeof Dp; getLockfileImporterId: typeof GetLockfileImporterId }>;
};
const m = await loadEsm();
dp = m.dp;
getLockfileImporterId = m.getLockfileImporterId;
loaded = true;
Comment on lines 68 to 70
after(() => {
npmCiRegistry.destroy();
});
Copilot AI review requested due to automatic review settings May 7, 2026 12:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 25 out of 26 changed files in this pull request and generated 3 comments.

Comment on lines 1 to 4
import semver from 'semver';
import type { LockfilePackageInfo } from '@pnpm/lockfile.types';
import * as dp from '@pnpm/dependency-path';
import * as dp from '@pnpm/deps.path';

Comment on lines 10 to 13
(supportNpmCiRegistryTesting ? describe : describe.skip)(
'package manager rc file is read from the workspace directory when installation is in a capsule',
'workspace .yarnrc.yml is read by Yarn when installation is in a capsule',
function () {
this.timeout(0);
import { buildDependentsTree } from '@pnpm/reviewing.dependencies-hierarchy';
import { renderDependentsTree } from '@pnpm/list';
import type { Modules } from '@pnpm/installing.modules-yaml';
import { readModulesManifest } from '@pnpm/installing.modules-yaml';
Copilot AI review requested due to automatic review settings June 1, 2026 12:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 25 out of 26 changed files in this pull request and generated 4 comments.

Comment on lines 1 to 4
import semver from 'semver';
import type { LockfilePackageInfo } from '@pnpm/lockfile.types';
import * as dp from '@pnpm/dependency-path';
import * as dp from '@pnpm/deps.path';

Comment on lines 10 to 12
(supportNpmCiRegistryTesting ? describe : describe.skip)(
'package manager rc file is read from the workspace directory when installation is in a capsule',
'workspace .yarnrc.yml is read by Yarn when installation is in a capsule',
function () {
Comment on lines 1 to 4
import semver from 'semver';
import type { LockfilePackageInfo } from '@pnpm/lockfile.types';
import * as dp from '@pnpm/dependency-path';
import * as dp from '@pnpm/deps.path';

Comment on lines 10 to 12
(supportNpmCiRegistryTesting ? describe : describe.skip)(
'package manager rc file is read from the workspace directory when installation is in a capsule',
'workspace .yarnrc.yml is read by Yarn when installation is in a capsule',
function () {
Copilot AI review requested due to automatic review settings June 1, 2026 15:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 25 out of 26 changed files in this pull request and generated 2 comments.

Comment on lines +25 to +31
// @pnpm/deps.path and @pnpm/lockfile.fs are ESM-only; load them through a .cjs
// shim so the require() chain in the build capsule's mocha runner doesn't trip
// on the transitive ESM import. Call `init()` once before invoking the public
// converters; helpers reach for these module-level slots synchronously.
let dp!: typeof Dp;
let getLockfileImporterId!: typeof GetLockfileImporterId;
let loading: Promise<void> | undefined;
Comment on lines 10 to 12
(supportNpmCiRegistryTesting ? describe : describe.skip)(
'package manager rc file is read from the workspace directory when installation is in a capsule',
'workspace .yarnrc.yml is read by Yarn when installation is in a capsule',
function () {
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 15, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (14) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Empty resolver dir 🐞 Bug ≡ Correctness ⭐ New
Description
generateResolverAndFetcher() forwards `dir: resolveOpts.projectDir ?? resolveOpts.lockfileDir ??
'' into nodeApi.resolveDependency`, so callers that pass empty strings end up invoking the
resolver with an empty directory. convertGraphToLockfile() currently calls resolve() with
lockfileDir: '' and projectDir: '', so lockfile generation can fail or resolve using config from
an unintended working directory.
Code

scopes/dependencies/pnpm/lynx.ts[R162-174]

+  const mergedAuthConfig = Object.assign({}, pnpmConfig.config.authConfig, authConfig) as Record<string, unknown>;
+  const authHeaderByUri = buildAuthHeaderByUri(mergedAuthConfig);
+  const registriesMap = registries.toMap();
+  const resolve: ResolveFunction = (wantedDep, resolveOpts) =>
+    nodeApi.resolveDependency(wantedDep, {
+      dir: resolveOpts.projectDir ?? resolveOpts.lockfileDir ?? '',
+      cacheDir,
+      registries: registriesMap,
+      authHeaderByUri,
+      proxyConfig: proxyConfig ? toNodeApiProxyConfig(proxyConfig) : undefined,
+      networkConfig: networkConfig ? toNodeApiNetworkConfig(networkConfig) : undefined,
+      fullMetadata,
+    });
Evidence
The resolver wrapper explicitly forwards dir computed with ?? '', and the lockfile graph
converter passes empty-string lockfileDir/projectDir, so dir becomes '' (not nullish) and
reaches the native resolver unchanged.

scopes/dependencies/pnpm/lynx.ts[147-175]
scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts[369-386]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`nodeApi.resolveDependency` is invoked with `dir` derived via nullish coalescing (`??`). Empty strings are not nullish, so `dir` can become `''`, which is not a meaningful base directory for resolution/config discovery.

## Issue Context
At least one internal caller (`convertGraphToLockfile`) passes empty strings for `projectDir`/`lockfileDir`, which now propagate into the resolver due to the new implementation.

## Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[147-175]
- scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts[369-386]

## Concrete fix
1) In `generateResolverAndFetcher`, treat empty string as missing by using `||` (or explicitly checking for a non-empty string), and fall back to a safe default:

```ts
const dir = resolveOpts.projectDir || resolveOpts.lockfileDir || process.cwd();
...
nodeApi.resolveDependency(wantedDep, { dir, ... })
```

2) In `convertGraphToLockfile`, stop passing `''` for directory fields; pass the real `rootDir` (or `projectDir`) so the resolver runs with the correct context.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. noProxy from authConfig 🐞 Bug ≡ Correctness
Description
getProxyConfig() sets noProxy from config.authConfig.noproxy, which is not the typed
proxy-bypass setting and may not contain the user’s .npmrc no-proxy value. As a result, dependency
downloads can incorrectly go through the proxy even when the user configured domains to bypass it.
Code

scopes/dependencies/pnpm/get-proxy-config.ts[R4-11]

export function getProxyConfig(config: Config): PackageManagerProxyConfig {
const httpProxy = config.httpProxy;
const httpsProxy = config.httpsProxy || httpProxy;
const proxyConfig: PackageManagerProxyConfig = {
 httpProxy,
 httpsProxy,
-    noProxy: config.rawConfig.noproxy,
+    noProxy: config.authConfig.noproxy,
};
Evidence
The pnpm proxy-config adapter reads noProxy from authConfig, while the dependency-resolver
configuration model and merging logic expect a dedicated noProxy value to be carried through; this
mismatch can drop proxy-bypass settings.

scopes/dependencies/pnpm/get-proxy-config.ts[1-13]
scopes/dependencies/dependency-resolver/dependency-resolver-workspace-config.ts[55-59]
scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[805-809]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getProxyConfig()` sources `noProxy` from `config.authConfig.noproxy`. `authConfig` is used primarily for auth/credential configuration, and using it for proxy-bypass settings risks ignoring `.npmrc` `no-proxy`/`noproxy` configuration.
### Issue Context
Bit’s dependency-resolver supports `noProxy` explicitly and threads it through proxy configuration; if `getProxyConfig()` fails to populate it, proxy bypass won’t work.
### Fix Focus Areas
- Prefer the typed config field for proxy bypass if available (e.g. `config.noProxy`).
- If pnpm’s config reader still exposes raw keys, add a safe fallback chain (e.g. `config.noProxy ?? config.authConfig['no-proxy'] ?? config.authConfig.noproxy`).
- scopes/dependencies/pnpm/get-proxy-config.ts[1-13]
- scopes/dependencies/dependency-resolver/dependency-resolver-workspace-config.ts[55-59]
- scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[805-809]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. CJS requires ESM module 🐞 Bug ☼ Reliability
Description
TS files are transpiled to CommonJS (require()), but pnpm integration code still statically imports
@pnpm/lockfile.fs even though this PR documents it as ESM-only. This can crash at module-load time
with ERR_REQUIRE_ESM during installs/prune/lockfile operations in CJS contexts (e.g.
mocha/capsules).
Code

scopes/dependencies/pnpm/lynx.ts[R25-28]

import { BitError } from '@teambit/bit-error';
import { BIT_ROOTS_DIR } from '@teambit/legacy.constants';
-import type { MutatedProject, InstallOptions, PeerDependencyIssuesByProjects, ProjectOptions } from '@pnpm/core';
-import { mutateModules } from '@pnpm/core';
-import * as pnpm from '@pnpm/core';
-import type { ClientOptions } from '@pnpm/client';
-import { createClient } from '@pnpm/client';
-import { restartWorkerPool, finishWorkers } from '@pnpm/worker';
-import { createPkgGraph } from '@pnpm/workspace.pkgs-graph';
import { readWantedLockfile, writeWantedLockfile } from '@pnpm/lockfile.fs';
import { type LockfileFile, type LockfileObject } from '@pnpm/lockfile.types';
Evidence
Babel is configured to transform modules to CommonJS, so static TS imports become require(). The PR
itself states @pnpm/lockfile.fs is ESM-only via the new shim, yet multiple runtime modules still
import it at top-level, which is incompatible with require().

babel.config.js[19-26]
scopes/dependencies/pnpm/load-pnpm-esm.cjs[3-10]
scopes/dependencies/pnpm/lynx.ts[20-33]
scopes/dependencies/pnpm/pnpm.package-manager.ts[24-34]
scopes/dependencies/pnpm/pnpm-prune-modules.ts[1-17]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`@pnpm/lockfile.fs` is treated as ESM-only in this PR, but several TypeScript modules still import it statically. Since the repo transpiles TS to CommonJS (`@babel/plugin-transform-modules-commonjs`), these imports become `require('@pnpm/lockfile.fs')` and can throw `ERR_REQUIRE_ESM` at runtime.
## Issue Context
The PR already introduces a `.cjs` shim (`load-pnpm-esm.cjs`) specifically because `@pnpm/lockfile.fs` is ESM-only. That mitigation is currently incomplete because other runtime code paths still use static imports.
## Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[24-33]
- scopes/dependencies/pnpm/pnpm.package-manager.ts[24-35]
- scopes/dependencies/pnpm/pnpm-prune-modules.ts[1-17]
- scopes/dependencies/pnpm/load-pnpm-esm.cjs[3-12]
- babel.config.js[19-26]
### Suggested implementation direction
- Remove top-level runtime imports from `@pnpm/lockfile.fs`.
- Replace them with a memoized dynamic loader (e.g. `let lockfileFsPromise; async function loadLockfileFs(){...}`) that uses `import('@pnpm/lockfile.fs')`, or extend the existing `load-pnpm-esm.cjs` shim to export the additional functions you need (`readWantedLockfile`, `writeWantedLockfile`, `readCurrentLockfile`, etc.).
- Update call sites to `const { readWantedLockfile } = await loadLockfileFs()` (or equivalent) before use.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (5)
4. Auth registry key missing 🐞 Bug ≡ Correctness
Description
getRegistries() now passes config.authConfig into getOriginalAuthConfigByUri(), but that
helper reads config.registry to compute the default registry nerf-dart; if authConfig doesn’t
carry a registry string, the default-auth fallback logic can crash or behave incorrectly.
Code

scopes/dependencies/pnpm/get-registries.ts[R18-22]

+    let credentials = getCredentialsByURI(config.authConfig, uri);
+    let originalAuthConfig = getOriginalAuthConfigByUri(config.authConfig, uri);
if (isEmpty(credentials)) {
-      credentials = getCredentialsByURI(config.rawConfig, switchTrailingSlash(uri));
-      originalAuthConfig = getOriginalAuthConfigByUri(config.rawConfig, switchTrailingSlash(uri));
+      credentials = getCredentialsByURI(config.authConfig, switchTrailingSlash(uri));
+      originalAuthConfig = getOriginalAuthConfigByUri(config.authConfig, switchTrailingSlash(uri));
Evidence
The new call site passes config.authConfig into getOriginalAuthConfigByUri, while the helper
still dereferences config.registry. This mismatch is introduced by the PR’s switch from
rawConfig to authConfig.

scopes/dependencies/pnpm/get-registries.ts[13-23]
scopes/dependencies/pnpm/get-registries.ts[34-38]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getRegistries()` calls `getOriginalAuthConfigByUri(config.authConfig, uri)`, but `getOriginalAuthConfigByUri()` expects the passed object to contain `registry` (used as the default registry URI). With `@pnpm/config.reader`, `Config.registries` is already the authoritative source of registry URLs, so relying on `authConfig.registry` is brittle and may be `undefined`.
### Issue Context
This impacts building `Registry` objects (default and scoped) used across installs/resolution.
### Fix Focus Areas
- scopes/dependencies/pnpm/get-registries.ts[13-46]
### Suggested fix
Option A (preferred, explicit):
1) Change `getOriginalAuthConfigByUri` signature to accept `defaultRegistryUri: string`.
2) Call it as `getOriginalAuthConfigByUri(config.authConfig, uri, config.registries.default)` (or equivalent default-registry entry).
3) Replace `toNerfDart(config.registry)` with `toNerfDart(defaultRegistryUri)`.
Option B (minimal change):
- Call `getOriginalAuthConfigByUri({ ...config.authConfig, registry: config.registries.default }, uri)` to ensure the expected `registry` key exists.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Install lock not cleared 🐞 Bug ☼ Reliability
Description
In lynx.install(), installsRunning[rootDir] is only deleted on the success path; if
nodeApi.install() rejects, the rejected promise remains cached and subsequent installs for the same
rootDir will immediately fail at await installsRunning[rootDir]. This prevents retries in the same
process after a single install failure.
Code

scopes/dependencies/pnpm/lynx.ts[R401-418]

await installsRunning[rootDir];
-      await restartWorkerPool();
-      installsRunning[rootDir] = mutateModules(packagesToBuild, opts);
-      const installResult = await installsRunning[rootDir];
-      depsRequiringBuild = installResult.depsRequiringBuild?.sort();
-      if (depsRequiringBuild != null) {
-        await addDepsRequiringBuildToLockfile(rootDir, depsRequiringBuild);
+      installsRunning[rootDir] = nodeApi.install(installOptions, onLog, readPackageHookForDeps);
+      const installResult: nodeApi.InstallResult = await installsRunning[rootDir];
+      resolvedStoreDir = installResult.storeDir;
+      const sortedDepsRequiringBuild = installResult.depsRequiringBuild?.sort();
+      if (sortedDepsRequiringBuild != null) {
+        await addDepsRequiringBuildToLockfile(rootDir, sortedDepsRequiringBuild);
+        depsRequiringBuild = sortedDepsRequiringBuild as unknown as DepPath[];
}
dependenciesChanged =
 installResult.stats.added + installResult.stats.removed + installResult.stats.linkedToRoot > 0;
delete installsRunning[rootDir];
} catch (err: any) {
if (logger) {
-        logger.warn('got an error from pnpm mutateModules function', err);
+        logger.warn('got an error from the pnpm install function', err);
}
throw pnpmErrorToBitError(err);
} finally {
Evidence
The lock map is global, awaited before starting a new install, and only deleted on the success path;
on error it is left intact, causing future calls to await the same rejected promise.

scopes/dependencies/pnpm/lynx.ts[42-43]
scopes/dependencies/pnpm/lynx.ts[389-420]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`installsRunning[rootDir]` is used to serialize installs, but it is only deleted on the success path. If `nodeApi.install(...)` throws/rejects, the rejected promise remains stored, so the next call will `await installsRunning[rootDir]` and fail immediately without attempting a new install.
### Issue Context
This is a per-process cache/lock. A single transient error (network, registry, etc.) can permanently block subsequent installs for that workspace root until the process restarts.
### Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[389-420]
### Implementation notes
- Capture the current promise in a local variable (`const p = nodeApi.install(...)`), assign it to the map, and in `finally` do `if (installsRunning[rootDir] === p) delete installsRunning[rootDir]`.
- Ensure the entry is cleared on both success and failure, while still preserving serialization semantics.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Rebuild uses stale storeDir 🐞 Bug ≡ Correctness
Description
lynx.install() updates resolvedStoreDir from installResult.storeDir, but rebuild() calls
nodeApi.rebuild() with the original installOptions object whose storeDir field is never updated.
When storeDir input is undefined or the engine resolves/normalizes it, rebuild can run against the
wrong store location.
Code

scopes/dependencies/pnpm/lynx.ts[R403-440]

+      const installResult: nodeApi.InstallResult = await installsRunning[rootDir];
+      resolvedStoreDir = installResult.storeDir;
+      const sortedDepsRequiringBuild = installResult.depsRequiringBuild?.sort();
+      if (sortedDepsRequiringBuild != null) {
+        await addDepsRequiringBuildToLockfile(rootDir, sortedDepsRequiringBuild);
+        depsRequiringBuild = sortedDepsRequiringBuild as unknown as DepPath[];
}
dependenciesChanged =
 installResult.stats.added + installResult.stats.removed + installResult.stats.linkedToRoot > 0;
delete installsRunning[rootDir];
} catch (err: any) {
if (logger) {
-        logger.warn('got an error from pnpm mutateModules function', err);
+        logger.warn('got an error from the pnpm install function', err);
}
throw pnpmErrorToBitError(err);
} finally {
stopReporting?.();
-      await finishWorkers();
}
}
return {
dependenciesChanged,
-    rebuild: async (rebuildOpts) => {
+    // The Rust engine's rebuild rebuilds every build-needing package and does not
+    // support the pending / skipIfHasSideEffectsCache selectors of the old engine.
+    rebuild: async () => {
let stopReporting: Function | undefined;
-      const _opts = {
-        ...opts,
-        ...rebuildOpts,
-        cacheDir,
-      } as any; // eslint-disable-line @typescript-eslint/no-explicit-any
-      if (!_opts.hidePackageManagerOutput) {
+      if (!options.hidePackageManagerOutput) {
 stopReporting = initReporter({
   appendOnly: true,
   hideLifecycleOutput: true,
 });
}
try {
-        await rebuild.handler(_opts, []);
+        await nodeApi.rebuild(installOptions, onLog, undefined);
} finally {
 stopReporting?.();
}
},
-    storeDir: storeController.dir,
+    storeDir: resolvedStoreDir ?? '',
Evidence
installOptions.storeDir is set once from the incoming storeDir argument; later, resolvedStoreDir is
overwritten from installResult.storeDir and returned, but rebuild still uses the original
installOptions without updating storeDir.

scopes/dependencies/pnpm/lynx.ts[334-343]
scopes/dependencies/pnpm/lynx.ts[401-442]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`installResult.storeDir` is captured into `resolvedStoreDir` and returned, but `rebuild()` reuses the precomputed `installOptions` object, whose `storeDir` property remains the original input `storeDir` (which may be `undefined`). This can make rebuild use a different store directory than install used.
### Issue Context
The code already anticipates storeDir potentially being missing/changed (`resolvedStoreDir ?? ''`). Any mismatch can lead to rebuild operating on the wrong store content or failing to locate it.
### Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[334-387]
- scopes/dependencies/pnpm/lynx.ts[401-442]
### Implementation notes
- After a successful install, either:
- mutate `installOptions.storeDir = resolvedStoreDir` (or `installResult.storeDir`) before returning, or
- create a separate options object for rebuild: `{ ...installOptions, storeDir: resolvedStoreDir }`.
- Keep behavior consistent for dryRun (no installResult).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. ESM import in CJS 🐞 Bug ☼ Reliability
Description
DependenciesGraph now statically imports @pnpm/deps.path, which this PR itself documents as
ESM-only; when Babel transpiles this file to CommonJS, it will become a require() and can crash at
runtime with ERR_REQUIRE_ESM. This can break dependency graph operations anywhere
@teambit/objects is loaded in the CJS-transpiled execution path (e.g. mocha via babel-register).
Code

scopes/scope/objects/models/dependencies-graph.ts[3]

+import * as dp from '@pnpm/deps.path';
Evidence
The modified file now imports @pnpm/deps.path statically. The PR-added shim explicitly states
@pnpm/deps.path is ESM-only and must be loaded via Node’s ESM loader (dynamic import()), while
the Babel configuration used by babel-register transforms TS modules to CommonJS, turning the
static import into a require() which is incompatible with ESM-only packages.

scopes/scope/objects/models/dependencies-graph.ts[1-4]
scopes/dependencies/pnpm/load-pnpm-esm.cjs[3-10]
babel.config.js[19-26]
babel-register.js[1-1]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scopes/scope/objects/models/dependencies-graph.ts` now statically imports `@pnpm/deps.path`. The PR also states `@pnpm/deps.path` is ESM-only, while the repo’s Babel setup transpiles TS modules to CommonJS (turning `import` into `require()`), which can throw `ERR_REQUIRE_ESM` at runtime.
### Issue Context
- This repo uses `@babel/plugin-transform-modules-commonjs` (via `babel-register`) in at least some execution paths.
- The PR already introduced a CJS shim + dynamic `import()` loader for ESM-only pnpm modules elsewhere, which suggests the same problem applies here.
### Fix Focus Areas
- scopes/scope/objects/models/dependencies-graph.ts[1-120]
- scopes/dependencies/pnpm/load-pnpm-esm.cjs[1-12]
- babel.config.js[1-31]
- babel-register.js[1-1]
### Suggested fix directions
Pick one:
1) **Remove the dependency on `@pnpm/deps.path` here** by implementing a small local parser for the *specific* need (extracting `version` from a depPath string) so this code stays synchronous and CJS-safe.
2) **Mirror the shim approach** used in `lockfile-deps-graph-converter.ts`: add a local loader wrapper for `@pnpm/deps.path` and ensure it is initialized before any `DependenciesGraph` code path that calls `dp.parse()`. If you choose this route, add a clear runtime guard/error message when not initialized (instead of an opaque TypeError).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Core-js deny ignored 🐞 Bug ⛨ Security
Description
In resolveScriptPolicies(), the branch that sets a default deny for "core-js" also returns
dangerouslyAllowAllBuilds: true even though the code comment states pnpm ignores allowBuilds when
dangerouslyAllowAllBuilds is set. This makes the default deny ineffective and can unintentionally
allow core-js build scripts to run when dangerouslyAllowAllScripts is enabled without an explicit
neverBuiltDependencies list.
Code

scopes/dependencies/pnpm/lynx.ts[R428-434]

if (dangerouslyAllowAllScripts) {
-    if (resolvedNeverBuilt == null) {
-      // If neverBuiltDependencies is not explicitly set, use a default list
-      // we tell pnpm to allow all scripts to be executed, except the packages listed below.
-      resolvedNeverBuilt = ['core-js'];
+    // pnpm v11's createAllowBuildFunction ignores allowBuilds when dangerouslyAllowAllBuilds is
+    // set, so emit allowBuilds with just the deny entries whenever a deny list exists.
+    if (!neverBuiltDependencies?.length) {
+      allowBuilds['core-js'] = false;
+      return { dangerouslyAllowAllBuilds: true, allowBuilds };
}
-  } else {
-    onlyBuiltDependencies = [];
-    ignoredBuiltDependencies = [];
-    for (const [packageDescriptor, allowedScript] of Object.entries(allowScripts ?? {})) {
-      switch (allowedScript) {
-        case true:
-          onlyBuiltDependencies.push(packageDescriptor);
-          break;
-        case false:
-          ignoredBuiltDependencies.push(packageDescriptor);
-          break;
-        default:
-          // Ignore any non-boolean values. String values are placeholders that the user
-          // should replace with booleans.
-          // pnpm will print a warning about these during installation.
-          break;
-      }
Evidence
The function comment explicitly states allowBuilds is ignored when dangerouslyAllowAllBuilds is set,
yet the code sets a deny entry in allowBuilds and returns dangerouslyAllowAllBuilds: true in that
same branch; therefore the deny entry will not take effect under the stated behavior.

scopes/dependencies/pnpm/lynx.ts[422-434]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`resolveScriptPolicies()` returns `{ dangerouslyAllowAllBuilds: true, allowBuilds }` in the same branch where it sets `allowBuilds['core-js'] = false`, but the adjacent comment states that pnpm ignores `allowBuilds` when `dangerouslyAllowAllBuilds` is set. This makes the default deny entry ineffective.
### Issue Context
The intent (based on existing policy and comments) is to keep a safe default deny list (at least `core-js`) even when `dangerouslyAllowAllScripts` is enabled.
### Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[422-439]
### Suggested fix
Adjust the returned options so that the deny entry is actually honored:
- Do **not** set `dangerouslyAllowAllBuilds` in the code path where you need `allowBuilds` to enforce denies (e.g. for `core-js`).
- Alternatively, if pnpm v11 provides a supported way to express “allow all except these denies”, use that API and add/adjust a regression test for the default `core-js` behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

9. Stale build-deps lockfile flag 🐞 Bug ≡ Correctness
Description
lynx.install() re-applies the pre-install bit: lockfile block but only overwrites
depsRequiringBuild when installResult.depsRequiringBuild is non-null; if the engine omits this
field when nothing needs a build, the old list is preserved. This stale list is later consumed by
convertLockfileToGraph() to mark packages as requiresBuild, producing incorrect dependency graph
metadata.
Code

scopes/dependencies/pnpm/lynx.ts[R412-421]

+      const preInstallBitAttrs = await readBitLockfileAttrs(rootDir);
+      installsRunning[rootDir] = nodeApi.install(installOptions, onLog, readPackageHookForDeps);
+      const installResult: nodeApi.InstallResult = await installsRunning[rootDir];
+      resolvedStoreDir = installResult.storeDir;
+      const sortedDepsRequiringBuild = installResult.depsRequiringBuild?.sort();
+      if (sortedDepsRequiringBuild != null || preInstallBitAttrs != null) {
+        await addBitAttributesToLockfile(rootDir, {
+          ...preInstallBitAttrs,
+          ...(sortedDepsRequiringBuild != null ? { depsRequiringBuild: sortedDepsRequiringBuild } : {}),
+        });
Evidence
The install path merges preInstallBitAttrs back into the lockfile but conditionally adds
depsRequiringBuild only when the engine returns a non-null value, so an omitted field results in
keeping the prior lockfile value. The graph converter later uses that lockfile field to set
requiresBuild, so preserving an old list makes the graph incorrect.

scopes/dependencies/pnpm/lynx.ts[405-425]
scopes/dependencies/pnpm/lynx.ts[728-756]
scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts[222-260]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`depsRequiringBuild` can become stale across installs. If the Rust engine returns `depsRequiringBuild: undefined` (as opposed to `[]`) for an install where nothing requires build, the current merge logic writes back the previous lockfile’s `bit.depsRequiringBuild` unchanged.
### Issue Context
- `addBitAttributesToLockfile()` merges `attrs` over the existing `lockfile.bit` object, so missing keys are preserved.
- `lockfile-deps-graph-converter.ts` uses `lockfile.bit?.depsRequiringBuild` to set `graphPkg.requiresBuild = true`.
### Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[412-425]
- scopes/dependencies/pnpm/lynx.ts[749-755]
### Suggested fix
When re-asserting the `bit:` block after install, explicitly set `depsRequiringBuild` to an empty array when the engine returns it as nullish.
For example:
- Compute `const deps = (installResult.depsRequiringBuild ?? []).slice().sort();`
- Call `addBitAttributesToLockfile(rootDir, { ...preInstallBitAttrs, depsRequiringBuild: deps })`
- Keep returning `depsRequiringBuild` to callers based on `deps.length` (if you want to preserve the current `undefined` return behavior) but ensure the lockfile always gets an accurate array.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Node engine mismatch 🐞 Bug ☼ Reliability
Description
The PR introduces new @pnpm/* 11xx/12.x deps that declare Node >=22.13 in pnpm-lock.yaml, while the
repo still advertises "engines.node": ">=12.22.0" in package.json. This creates misleading
compatibility metadata and can cause installs/tooling to fail unexpectedly on Node versions that
appear supported by the repo.
Code

workspace.jsonc[R55-75]

+        "@pnpm/cli.default-reporter": "1100.3.3",
      "@pnpm/colorize-semver-diff": "1.0.1",
-        "@pnpm/config": "1004.11.0",
-        "@pnpm/core": "1016.2.0",
-        "@pnpm/default-reporter": "1002.1.14",
-        "@pnpm/dependency-path": "1001.1.10",
-        "@pnpm/error": "1000.1.0",
-        "@pnpm/fetch": "1001.0.0",
-        "@pnpm/list": "^1000.3.4",
-        "@pnpm/lockfile.filtering": "^1001.0.31",
-        "@pnpm/lockfile.fs": "^1001.1.32",
-        "@pnpm/lockfile.types": "^1002.1.0",
-        "@pnpm/logger": "1001.0.1",
-        "@pnpm/modules-yaml": "1002.0.1",
+        "@pnpm/config.parse-overrides": "1100.0.2",
+        "@pnpm/config.reader": "1101.3.1",
+        "@pnpm/deps.inspection.list": "1100.0.20",
+        "@pnpm/deps.inspection.tree-builder": "1100.0.17",
+        "@pnpm/deps.path": "1100.0.8",
+        "@pnpm/error": "1100.0.1",
+        "@pnpm/installing.modules-yaml": "1100.0.9",
+        "@pnpm/lockfile.filtering": "1100.1.8",
+        "@pnpm/lockfile.fs": "1100.1.7",
+        "@pnpm/lockfile.types": "1100.0.12",
+        "@pnpm/logger": "1100.0.0",
      "@pnpm/network.ca-file": "3.0.3",
+        "@pnpm/network.fetch": "1100.1.4",
+        "@pnpm/napi": "12.0.0-alpha.6",
      "@pnpm/node-fetch": "^1.0.0",
-        "@pnpm/npm-resolver": "1005.2.3",
-        "@pnpm/package-store": "1007.1.6",
-        "@pnpm/parse-overrides": "1001.0.4",
-        "@pnpm/plugin-commands-publishing": "1000.3.16",
-        "@pnpm/plugin-commands-rebuild": "1008.0.13",
      "@pnpm/plugin-trusted-deps": "0.2.2",
      "@pnpm/registry-mock": "3.48.0",
-        "@pnpm/reviewing.dependencies-hierarchy": "^1001.3.4",
      "@pnpm/semver-diff": "1.1.0",
-        "@pnpm/sort-packages": "1000.0.16",
-        "@pnpm/store-connection-manager": "1002.3.19",
-        "@pnpm/types": "1001.3.0",
-        "@pnpm/worker": "1000.6.7",
-        "@pnpm/workspace.pkgs-graph": "1000.0.39",
+        "@pnpm/types": "1101.3.2",
Evidence
The repo declares Node >=12.22.0, but the lockfile shows the newly-added pnpm internals require Node
>=22.13; additionally, workspace config pins Node 22.22.0 with engineStrict enabled, indicating
actual runtime expectations are Node 22+.

package.json[5-7]
workspace.jsonc[55-75]
workspace.jsonc[720-722]
pnpm-lock.yaml[32843-32858]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The repository's declared Node engine requirement is out of sync with the actual Node version required by the upgraded pnpm internal dependencies.
- `package.json` still declares `engines.node: >=12.22.0`.
- The PR adds pnpm v11/v12-era packages (11xx/12.x) which, per `pnpm-lock.yaml`, require Node `>=22.13`.
- `workspace.jsonc` already pins `nodeVersion: 22.22.0` with `engineStrict: true`, reinforcing that the real minimum Node version is much higher than `package.json` claims.
This mismatch misleads consumers and CI/tooling that relies on `package.json#engines`.
### Issue Context
The pnpm internal dependency set introduced by this PR appears to be Node-22+ only (lockfile `engines` fields), but the repo-level engine metadata wasn’t updated accordingly.
### Fix Focus Areas
- package.json[5-7]
- workspace.jsonc[720-722]
- pnpm-lock.yaml[32843-32858]
### Suggested fix
Update `package.json#engines.node` to reflect the real minimum Node version required by the new pnpm internals (at least `>=22.13`, or align exactly with the workspace’s pinned `22.22.0` policy), and ensure any documentation/CI configuration that depends on `engines` remains consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Eager NAPI import 🐞 Bug ☼ Reliability
Description
dependency-resolver.main.runtime.ts now imports parseBareSpecifier from @pnpm/napi at module scope,
forcing the native binding to load whenever the dependency-resolver aspect is loaded. This can
increase startup cost and can hard-fail process initialization in environments where the native
binding cannot be loaded, even if pnpm isn’t the active package manager.
Code

scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[R1476-1482]

isValidVersionSpecifier(spec: string): boolean {
+    // parseBareSpecifier returns null for empty/unparsable input.
 return (
   parseBareSpecifier(
     spec,
-        'pkgname', // This argument is the package but we don't need it
-        'latest',
-        'https://registry.npmjs.org/'
+        'pkgname' // This argument is the package name but we don't need it
   ) != null
Evidence
The dependency-resolver runtime now imports from @pnpm/napi at file load time and calls it during
aspect-data validation. Elsewhere, pnpm code is intentionally lazy-loaded for performance,
indicating eager pnpm/native loads are undesirable in this codebase.

scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[36-46]
scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[1469-1484]
scopes/dependencies/pnpm/pnpm.package-manager.ts[127-134]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`dependency-resolver.main.runtime.ts` performs a module-scope import from `@pnpm/napi` and uses it in `isValidVersionSpecifier()`. This eagerly loads the native binding whenever the dependency-resolver aspect is loaded, which can slow startup and can crash early if the native module can’t be loaded.
### Issue Context
Other pnpm integration points in this repo explicitly lazy-load pnpm internals for performance, so this eager import is inconsistent and increases blast radius.
### Fix Focus Areas
- Replace the module-scope `@pnpm/napi` import with a non-native, lightweight validator (e.g. `semver.valid/validRange` + allow-list for tags/aliases/URLs), OR
- If `parseBareSpecifier` is required for parity, load it lazily inside `isValidVersionSpecifier()` with a guarded `require()`/cached getter and provide a safe fallback when the binding fails to load.
- scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[36-46]
- scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[1469-1484]
- scopes/dependencies/pnpm/pnpm.package-manager.ts[127-134]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
12. Peer issues suppressed silently 🐞 Bug ◔ Observability
Description
lynx.getPeerDependencyIssues() returns {} for ERR_PNPM_NAPI_UNIMPLEMENTED, which makes
dependency-resolver.getMissingPeerDependencies() return undefined and causes the install flow to
report “No missing peer dependencies found” even when peer detection is actually unavailable.
Code

scopes/dependencies/pnpm/lynx.ts[R206-212]

+  } catch (err: any) {
+    if (err?.code === 'ERR_PNPM_NAPI_UNIMPLEMENTED') {
+      // TODO: getPeerDependencyIssues not yet implemented in the Rust engine.
+      // Returning no issues keeps installs working until the binding lands it.
+      return {};
+    }
+    throw pnpmErrorToBitError(err);
Evidence
The pnpm layer explicitly returns {} on the unimplemented error; the dependency-resolver then
accesses peerDependencyIssues['.']?.intersections, and the install flow treats an undefined/empty
result as “no missing peers,” masking the fact that the computation was skipped.

scopes/dependencies/pnpm/lynx.ts[190-213]
scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[896-922]
scopes/workspace/install/install.main.runtime.ts[200-225]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `@pnpm/napi` throws `ERR_PNPM_NAPI_UNIMPLEMENTED`, `getPeerDependencyIssues()` currently returns `{}`. Downstream, Bit interprets this as “no peer issues,” so features like `--add-missing-peers` can silently become a no-op without any indication that peer-issue calculation is unsupported.
### Issue Context
- `dependency-resolver.getMissingPeerDependencies()` reads `peerDependencyIssues['.']?.intersections`.
- The install command uses falsy result to print “No missing peer dependencies found.”
### Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[177-213]
- scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[896-922]
### Suggested fix
- In the `ERR_PNPM_NAPI_UNIMPLEMENTED` branch, emit a warning (once per process) indicating peer-dependency detection is currently unavailable with the pnpm napi engine.
- Return a safe, expected shape for the caller to avoid `undefined`, e.g. `{ '.': { intersections: {} } }` (or whatever minimal `PeerDependencyIssuesByProjects` shape the rest of the code expects).
- Alternatively, plumb a flag to allow throwing a clear BitError when `addMissingPeers` is requested, instead of silently returning empty results.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Pack tarballPath mishandled 🐞 Bug ≡ Correctness
Description
pnpmPack() reconstructs the origin tarball path as cwd + basename(tarballPath) when tarballPath is
relative, dropping any relative directory segments. If nodeApi.pack() returns a relative tarball
path that includes subdirectories, fs.move() will look for the wrong source path and fail.
Code

scopes/pkg/pkg/packer.ts[R136-140]

+      const packResult = await nodeApi.pack({ dir: cwd });
+      const tgzName = path.basename(packResult.tarballPath);
this.logger.debug(`successfully packed tarball at ${cwd}`);
-      const tgzOriginPath = path.join(cwd, tgzName);
+      const tgzOriginPath = path.isAbsolute(packResult.tarballPath) ? packResult.tarballPath : path.join(cwd, tgzName);
let tarPath = path.join(outputPath, tgzName);
Evidence
The code explicitly replaces a relative tarballPath with cwd + basename(tarballPath), which is only
correct if the tarball is emitted directly under cwd.

scopes/pkg/pkg/packer.ts[136-160]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `packResult.tarballPath` is relative, the code computes:
- `tgzName = basename(tarballPath)`
- `tgzOriginPath = join(cwd, tgzName)`
This discards any relative folder components included in `tarballPath`.
### Issue Context
The subsequent `fs.move(tgzOriginPath, tarPath)` relies on `tgzOriginPath` accurately pointing to the tarball created by pnpm.
### Fix Focus Areas
- scopes/pkg/pkg/packer.ts[136-160]
### Implementation notes
- Compute origin path as:
- `const origin = path.isAbsolute(tarballPath) ? tarballPath : path.join(cwd, tarballPath)`
- Keep `tgzName` for naming, but don’t rebuild the origin path from basename.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Init precondition unguarded 🐞 Bug ☼ Reliability
Description
lockfile-deps-graph-converter.ts now relies on module-level dp/getLockfileImporterId variables that
are only populated via an async init(), but converter functions use dp synchronously without a
runtime guard. If any caller misses/forgets the init() precondition, this will crash at runtime with
an opaque TypeError rather than a clear error.
Code

scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts[R25-43]

+// @pnpm/deps.path and @pnpm/lockfile.fs are ESM-only; load them through a .cjs
+// shim so the require() chain in the build capsule's mocha runner doesn't trip
+// on the transitive ESM import. Call `init()` once before invoking the public
+// converters; helpers reach for these module-level slots synchronously.
+let dp!: typeof Dp;
+let getLockfileImporterId!: typeof GetLockfileImporterId;
+let loading: Promise<void> | undefined;
+
+export function init(): Promise<void> {
+  loading ??= (async () => {
+    const { loadEsm } = require('./load-pnpm-esm.cjs') as {
+      loadEsm: () => Promise<{ dp: typeof Dp; getLockfileImporterId: typeof GetLockfileImporterId }>;
+    };
+    const m = await loadEsm();
+    dp = m.dp;
+    getLockfileImporterId = m.getLockfileImporterId;
+  })();
+  return loading;
+}
Evidence
The module-level slots are declared uninitialized and assigned only inside init(); later code uses
dp.refToRelative synchronously, so calling converters before init() will dereference an unset dp
variable.

scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts[25-43]
scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts[64-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The module introduces an `init()` that asynchronously sets module-level `dp` and `getLockfileImporterId`, but exported converter functions access `dp.*` synchronously and assume `init()` has been awaited.
### Issue Context
Current call sites appear to call `await initLockfileDepsGraphConverter()`, but this is a fragile contract for exported utilities and will be easy to violate in future usages.
### Fix Focus Areas
- scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts[25-43]
- scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts[64-75]
- scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts[77-110]
### Suggested fix
- Add a small runtime guard (e.g. `assertInitialized()`) that checks `dp`/`getLockfileImporterId` are set and throws a clear, actionable error message ("call and await init() before convert*").
- Optionally, in async entry points (e.g. `convertGraphToLockfile`) call `await init()` internally as a safety net.
- Keep `convertLockfileToGraph` sync, but guard it with the explicit error to avoid a cryptic crash.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines 428 to 434
if (dangerouslyAllowAllScripts) {
if (resolvedNeverBuilt == null) {
// If neverBuiltDependencies is not explicitly set, use a default list
// we tell pnpm to allow all scripts to be executed, except the packages listed below.
resolvedNeverBuilt = ['core-js'];
// pnpm v11's createAllowBuildFunction ignores allowBuilds when dangerouslyAllowAllBuilds is
// set, so emit allowBuilds with just the deny entries whenever a deny list exists.
if (!neverBuiltDependencies?.length) {
allowBuilds['core-js'] = false;
return { dangerouslyAllowAllBuilds: true, allowBuilds };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Core-js deny ignored 🐞 Bug ⛨ Security

In resolveScriptPolicies(), the branch that sets a default deny for "core-js" also returns
dangerouslyAllowAllBuilds: true even though the code comment states pnpm ignores allowBuilds when
dangerouslyAllowAllBuilds is set. This makes the default deny ineffective and can unintentionally
allow core-js build scripts to run when dangerouslyAllowAllScripts is enabled without an explicit
neverBuiltDependencies list.
Agent Prompt
### Issue description
`resolveScriptPolicies()` returns `{ dangerouslyAllowAllBuilds: true, allowBuilds }` in the same branch where it sets `allowBuilds['core-js'] = false`, but the adjacent comment states that pnpm ignores `allowBuilds` when `dangerouslyAllowAllBuilds` is set. This makes the default deny entry ineffective.

### Issue Context
The intent (based on existing policy and comments) is to keep a safe default deny list (at least `core-js`) even when `dangerouslyAllowAllScripts` is enabled.

### Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[422-439]

### Suggested fix
Adjust the returned options so that the deny entry is actually honored:
- Do **not** set `dangerouslyAllowAllBuilds` in the code path where you need `allowBuilds` to enforce denies (e.g. for `core-js`).
- Alternatively, if pnpm v11 provides a supported way to express “allow all except these denies”, use that API and add/adjust a regression test for the default `core-js` behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 03d4bc6

import semver from 'semver';
import type { LockfilePackageInfo } from '@pnpm/lockfile.types';
import * as dp from '@pnpm/dependency-path';
import * as dp from '@pnpm/deps.path';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Esm import in cjs 🐞 Bug ☼ Reliability

DependenciesGraph now statically imports @pnpm/deps.path, which this PR itself documents as
ESM-only; when Babel transpiles this file to CommonJS, it will become a require() and can crash at
runtime with ERR_REQUIRE_ESM. This can break dependency graph operations anywhere
@teambit/objects is loaded in the CJS-transpiled execution path (e.g. mocha via babel-register).
Agent Prompt
### Issue description
`scopes/scope/objects/models/dependencies-graph.ts` now statically imports `@pnpm/deps.path`. The PR also states `@pnpm/deps.path` is ESM-only, while the repo’s Babel setup transpiles TS modules to CommonJS (turning `import` into `require()`), which can throw `ERR_REQUIRE_ESM` at runtime.

### Issue Context
- This repo uses `@babel/plugin-transform-modules-commonjs` (via `babel-register`) in at least some execution paths.
- The PR already introduced a CJS shim + dynamic `import()` loader for ESM-only pnpm modules elsewhere, which suggests the same problem applies here.

### Fix Focus Areas
- scopes/scope/objects/models/dependencies-graph.ts[1-120]
- scopes/dependencies/pnpm/load-pnpm-esm.cjs[1-12]
- babel.config.js[1-31]
- babel-register.js[1-1]

### Suggested fix directions
Pick one:
1) **Remove the dependency on `@pnpm/deps.path` here** by implementing a small local parser for the *specific* need (extracting `version` from a depPath string) so this code stays synchronous and CJS-safe.
2) **Mirror the shim approach** used in `lockfile-deps-graph-converter.ts`: add a local loader wrapper for `@pnpm/deps.path` and ensure it is initialized before any `DependenciesGraph` code path that calls `dp.parse()`. If you choose this route, add a clear runtime guard/error message when not initialized (instead of an opaque TypeError).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 93dcf91

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

Sorry, something went wrong

We weren't able to complete the code review on our side. Please try again

Grey Divider

Qodo Logo

Migrate Bit's pnpm integration to the latest v11 (11xx) line of @pnpm/*
packages. Adapts the dependency-resolver and pnpm aspects to upstream
API changes, loads ESM-only @pnpm modules via .cjs shims, and refreshes
the lockfile and workspace.jsonc pins accordingly.

All @pnpm/* deps are bumped to their newest 11xx release, including the
1102.x store/resolver/installer line and worker 1100.2.2 (the new
content-addressable store generation). lynx.ts therefore keeps the
frozenStore flag and the scope-keyed auth translation that the new
generation's store.controller / network.auth-header expect.

Exception: @pnpm/config.reader is held at 1101.3.1, the last release
that depends on @pnpm/config.env-replace 3.x. Newer config.reader needs
env-replace ^4.1.0, which bit.cloud's registry mirror does not carry.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 637e002

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 767e5ae

zkochan added 2 commits July 7, 2026 14:02
Route Bit's programmatic pnpm usage through `@pnpm/napi` (NAPI bindings
over pnpm v12's Rust engine, pacquet) instead of the pnpm v11 TypeScript
`@pnpm/*` engine packages.

- `lynx.ts`: `install` now calls `nodeApi.install` (in-memory importers;
  importer manifests pre-transformed by Bit's readPackage hooks with their
  workspaceDir, dependency manifests transformed via the synchronous
  readPackageHook); `resolveRemoteVersion` uses `nodeApi.resolveDependency`;
  the rebuild closure uses `nodeApi.rebuild`. Auth is resolved from the
  npmrc-style authConfig into Authorization headers (via the kept
  `@pnpm/config.reader`) and passed as `authHeaderByUri`. Engine log events
  bridge to `@pnpm/cli.default-reporter` through the kept `streamParser`.
- `packer.ts`: `bit pack` uses `nodeApi.pack`; `load-pnpm-pack.cjs` removed.
- `parseBareSpecifier` and the `PeerDependencyIssuesByProjects` type now come
  from `@pnpm/napi`; the pnpm-error converter duck-types on `.code`/`.hint`.
- `workspace.jsonc`: drop the ten engine packages now provided by the Rust
  binding and add `@pnpm/napi`; keep the data/format packages (lockfile,
  deps.path, config.reader, logger, cli.default-reporter, types, ...).

`getPeerDependencyIssues` degrades to no issues while that engine op is
unimplemented in the binding.
feat(dependency-resolver): use the Rust pnpm engine via @pnpm/napi
@zkochan zkochan changed the title deps: migrate pnpm internals to v11 (1100.x) deps: migrate pnpm internals to v12 Jul 7, 2026
Comment on lines 401 to 418
await installsRunning[rootDir];
await restartWorkerPool();
installsRunning[rootDir] = mutateModules(packagesToBuild, opts);
const installResult = await installsRunning[rootDir];
depsRequiringBuild = installResult.depsRequiringBuild?.sort();
if (depsRequiringBuild != null) {
await addDepsRequiringBuildToLockfile(rootDir, depsRequiringBuild);
installsRunning[rootDir] = nodeApi.install(installOptions, onLog, readPackageHookForDeps);
const installResult: nodeApi.InstallResult = await installsRunning[rootDir];
resolvedStoreDir = installResult.storeDir;
const sortedDepsRequiringBuild = installResult.depsRequiringBuild?.sort();
if (sortedDepsRequiringBuild != null) {
await addDepsRequiringBuildToLockfile(rootDir, sortedDepsRequiringBuild);
depsRequiringBuild = sortedDepsRequiringBuild as unknown as DepPath[];
}
dependenciesChanged =
installResult.stats.added + installResult.stats.removed + installResult.stats.linkedToRoot > 0;
delete installsRunning[rootDir];
} catch (err: any) {
if (logger) {
logger.warn('got an error from pnpm mutateModules function', err);
logger.warn('got an error from the pnpm install function', err);
}
throw pnpmErrorToBitError(err);
} finally {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Install lock not cleared 🐞 Bug ☼ Reliability

In lynx.install(), installsRunning[rootDir] is only deleted on the success path; if
nodeApi.install() rejects, the rejected promise remains cached and subsequent installs for the same
rootDir will immediately fail at await installsRunning[rootDir]. This prevents retries in the same
process after a single install failure.
Agent Prompt
### Issue description
`installsRunning[rootDir]` is used to serialize installs, but it is only deleted on the success path. If `nodeApi.install(...)` throws/rejects, the rejected promise remains stored, so the next call will `await installsRunning[rootDir]` and fail immediately without attempting a new install.

### Issue Context
This is a per-process cache/lock. A single transient error (network, registry, etc.) can permanently block subsequent installs for that workspace root until the process restarts.

### Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[389-420]

### Implementation notes
- Capture the current promise in a local variable (`const p = nodeApi.install(...)`), assign it to the map, and in `finally` do `if (installsRunning[rootDir] === p) delete installsRunning[rootDir]`.
- Ensure the entry is cleared on both success and failure, while still preserving serialization semantics.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +403 to +440
const installResult: nodeApi.InstallResult = await installsRunning[rootDir];
resolvedStoreDir = installResult.storeDir;
const sortedDepsRequiringBuild = installResult.depsRequiringBuild?.sort();
if (sortedDepsRequiringBuild != null) {
await addDepsRequiringBuildToLockfile(rootDir, sortedDepsRequiringBuild);
depsRequiringBuild = sortedDepsRequiringBuild as unknown as DepPath[];
}
dependenciesChanged =
installResult.stats.added + installResult.stats.removed + installResult.stats.linkedToRoot > 0;
delete installsRunning[rootDir];
} catch (err: any) {
if (logger) {
logger.warn('got an error from pnpm mutateModules function', err);
logger.warn('got an error from the pnpm install function', err);
}
throw pnpmErrorToBitError(err);
} finally {
stopReporting?.();
await finishWorkers();
}
}
return {
dependenciesChanged,
rebuild: async (rebuildOpts) => {
// The Rust engine's rebuild rebuilds every build-needing package and does not
// support the pending / skipIfHasSideEffectsCache selectors of the old engine.
rebuild: async () => {
let stopReporting: Function | undefined;
const _opts = {
...opts,
...rebuildOpts,
cacheDir,
} as any; // eslint-disable-line @typescript-eslint/no-explicit-any
if (!_opts.hidePackageManagerOutput) {
if (!options.hidePackageManagerOutput) {
stopReporting = initReporter({
appendOnly: true,
hideLifecycleOutput: true,
});
}
try {
await rebuild.handler(_opts, []);
await nodeApi.rebuild(installOptions, onLog, undefined);
} finally {
stopReporting?.();
}
},
storeDir: storeController.dir,
storeDir: resolvedStoreDir ?? '',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Rebuild uses stale storedir 🐞 Bug ≡ Correctness

lynx.install() updates resolvedStoreDir from installResult.storeDir, but rebuild() calls
nodeApi.rebuild() with the original installOptions object whose storeDir field is never updated.
When storeDir input is undefined or the engine resolves/normalizes it, rebuild can run against the
wrong store location.
Agent Prompt
### Issue description
`installResult.storeDir` is captured into `resolvedStoreDir` and returned, but `rebuild()` reuses the precomputed `installOptions` object, whose `storeDir` property remains the original input `storeDir` (which may be `undefined`). This can make rebuild use a different store directory than install used.

### Issue Context
The code already anticipates storeDir potentially being missing/changed (`resolvedStoreDir ?? ''`). Any mismatch can lead to rebuild operating on the wrong store content or failing to locate it.

### Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[334-387]
- scopes/dependencies/pnpm/lynx.ts[401-442]

### Implementation notes
- After a successful install, either:
  - mutate `installOptions.storeDir = resolvedStoreDir` (or `installResult.storeDir`) before returning, or
  - create a separate options object for rebuild: `{ ...installOptions, storeDir: resolvedStoreDir }`.
- Keep behavior consistent for dryRun (no installResult).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread scopes/pkg/pkg/packer.ts
Comment on lines +136 to 140
const packResult = await nodeApi.pack({ dir: cwd });
const tgzName = path.basename(packResult.tarballPath);
this.logger.debug(`successfully packed tarball at ${cwd}`);
const tgzOriginPath = path.join(cwd, tgzName);
const tgzOriginPath = path.isAbsolute(packResult.tarballPath) ? packResult.tarballPath : path.join(cwd, tgzName);
let tarPath = path.join(outputPath, tgzName);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Pack tarballpath mishandled 🐞 Bug ≡ Correctness

pnpmPack() reconstructs the origin tarball path as cwd + basename(tarballPath) when tarballPath is
relative, dropping any relative directory segments. If nodeApi.pack() returns a relative tarball
path that includes subdirectories, fs.move() will look for the wrong source path and fail.
Agent Prompt
### Issue description
When `packResult.tarballPath` is relative, the code computes:
- `tgzName = basename(tarballPath)`
- `tgzOriginPath = join(cwd, tgzName)`
This discards any relative folder components included in `tarballPath`.

### Issue Context
The subsequent `fs.move(tgzOriginPath, tarPath)` relies on `tgzOriginPath` accurately pointing to the tarball created by pnpm.

### Fix Focus Areas
- scopes/pkg/pkg/packer.ts[136-160]

### Implementation notes
- Compute origin path as:
  - `const origin = path.isAbsolute(tarballPath) ? tarballPath : path.join(cwd, tarballPath)`
- Keep `tgzName` for naming, but don’t rebuild the origin path from basename.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit cd274da

…deep'

- @pnpm/napi 12.0.0-alpha.3 -> 12.0.0-alpha.4, which adds the
  linkWorkspacePackages install option (pnpm/pnpm#12844).
- lynx.ts sets linkWorkspacePackages: 'deep' so bare-semver deps on workspace
  components — including auto-installed peers that name a sibling component —
  resolve from the workspace instead of 404ing against the registry. Fixes the
  peer-dependency-component e2e failures.
Comment on lines +18 to +22
let credentials = getCredentialsByURI(config.authConfig, uri);
let originalAuthConfig = getOriginalAuthConfigByUri(config.authConfig, uri);
if (isEmpty(credentials)) {
credentials = getCredentialsByURI(config.rawConfig, switchTrailingSlash(uri));
originalAuthConfig = getOriginalAuthConfigByUri(config.rawConfig, switchTrailingSlash(uri));
credentials = getCredentialsByURI(config.authConfig, switchTrailingSlash(uri));
originalAuthConfig = getOriginalAuthConfigByUri(config.authConfig, switchTrailingSlash(uri));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Auth registry key missing 🐞 Bug ≡ Correctness

getRegistries() now passes config.authConfig into getOriginalAuthConfigByUri(), but that
helper reads config.registry to compute the default registry nerf-dart; if authConfig doesn’t
carry a registry string, the default-auth fallback logic can crash or behave incorrectly.
Agent Prompt
### Issue description
`getRegistries()` calls `getOriginalAuthConfigByUri(config.authConfig, uri)`, but `getOriginalAuthConfigByUri()` expects the passed object to contain `registry` (used as the default registry URI). With `@pnpm/config.reader`, `Config.registries` is already the authoritative source of registry URLs, so relying on `authConfig.registry` is brittle and may be `undefined`.

### Issue Context
This impacts building `Registry` objects (default and scoped) used across installs/resolution.

### Fix Focus Areas
- scopes/dependencies/pnpm/get-registries.ts[13-46]

### Suggested fix
Option A (preferred, explicit):
1) Change `getOriginalAuthConfigByUri` signature to accept `defaultRegistryUri: string`.
2) Call it as `getOriginalAuthConfigByUri(config.authConfig, uri, config.registries.default)` (or equivalent default-registry entry).
3) Replace `toNerfDart(config.registry)` with `toNerfDart(defaultRegistryUri)`.

Option B (minimal change):
- Call `getOriginalAuthConfigByUri({ ...config.authConfig, registry: config.registries.default }, uri)` to ensure the expected `registry` key exists.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +206 to +212
} catch (err: any) {
if (err?.code === 'ERR_PNPM_NAPI_UNIMPLEMENTED') {
// TODO: getPeerDependencyIssues not yet implemented in the Rust engine.
// Returning no issues keeps installs working until the binding lands it.
return {};
}
throw pnpmErrorToBitError(err);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Peer issues suppressed silently 🐞 Bug ◔ Observability

lynx.getPeerDependencyIssues() returns {} for ERR_PNPM_NAPI_UNIMPLEMENTED, which makes
dependency-resolver.getMissingPeerDependencies() return undefined and causes the install flow to
report “No missing peer dependencies found” even when peer detection is actually unavailable.
Agent Prompt
### Issue description
When `@pnpm/napi` throws `ERR_PNPM_NAPI_UNIMPLEMENTED`, `getPeerDependencyIssues()` currently returns `{}`. Downstream, Bit interprets this as “no peer issues,” so features like `--add-missing-peers` can silently become a no-op without any indication that peer-issue calculation is unsupported.

### Issue Context
- `dependency-resolver.getMissingPeerDependencies()` reads `peerDependencyIssues['.']?.intersections`.
- The install command uses falsy result to print “No missing peer dependencies found.”

### Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[177-213]
- scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[896-922]

### Suggested fix
- In the `ERR_PNPM_NAPI_UNIMPLEMENTED` branch, emit a warning (once per process) indicating peer-dependency detection is currently unavailable with the pnpm napi engine.
- Return a safe, expected shape for the caller to avoid `undefined`, e.g. `{ '.': { intersections: {} } }` (or whatever minimal `PeerDependencyIssuesByProjects` shape the rest of the code expects).
- Alternatively, plumb a flag to allow throwing a clear BitError when `addMissingPeers` is requested, instead of silently returning empty results.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit a76f6b7

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines 25 to 28
import { BitError } from '@teambit/bit-error';
import { BIT_ROOTS_DIR } from '@teambit/legacy.constants';
import type { MutatedProject, InstallOptions, PeerDependencyIssuesByProjects, ProjectOptions } from '@pnpm/core';
import { mutateModules } from '@pnpm/core';
import * as pnpm from '@pnpm/core';
import type { ClientOptions } from '@pnpm/client';
import { createClient } from '@pnpm/client';
import { restartWorkerPool, finishWorkers } from '@pnpm/worker';
import { createPkgGraph } from '@pnpm/workspace.pkgs-graph';
import { readWantedLockfile, writeWantedLockfile } from '@pnpm/lockfile.fs';
import { type LockfileFile, type LockfileObject } from '@pnpm/lockfile.types';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Cjs requires esm module 🐞 Bug ☼ Reliability

TS files are transpiled to CommonJS (require()), but pnpm integration code still statically imports
@pnpm/lockfile.fs even though this PR documents it as ESM-only. This can crash at module-load time
with ERR_REQUIRE_ESM during installs/prune/lockfile operations in CJS contexts (e.g.
mocha/capsules).
Agent Prompt
## Issue description
`@pnpm/lockfile.fs` is treated as ESM-only in this PR, but several TypeScript modules still import it statically. Since the repo transpiles TS to CommonJS (`@babel/plugin-transform-modules-commonjs`), these imports become `require('@pnpm/lockfile.fs')` and can throw `ERR_REQUIRE_ESM` at runtime.

## Issue Context
The PR already introduces a `.cjs` shim (`load-pnpm-esm.cjs`) specifically because `@pnpm/lockfile.fs` is ESM-only. That mitigation is currently incomplete because other runtime code paths still use static imports.

## Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[24-33]
- scopes/dependencies/pnpm/pnpm.package-manager.ts[24-35]
- scopes/dependencies/pnpm/pnpm-prune-modules.ts[1-17]
- scopes/dependencies/pnpm/load-pnpm-esm.cjs[3-12]
- babel.config.js[19-26]

### Suggested implementation direction
- Remove top-level runtime imports from `@pnpm/lockfile.fs`.
- Replace them with a memoized dynamic loader (e.g. `let lockfileFsPromise; async function loadLockfileFs(){...}`) that uses `import('@pnpm/lockfile.fs')`, or extend the existing `load-pnpm-esm.cjs` shim to export the additional functions you need (`readWantedLockfile`, `writeWantedLockfile`, `readCurrentLockfile`, etc.).
- Update call sites to `const { readWantedLockfile } = await loadLockfileFs()` (or equivalent) before use.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 35185fd

parse-package-name returns an empty string for a spec with no version
(e.g. 'is-positive', '@teambit/react.react-env'). The napi resolver
rejects an empty bareSpecifier with "isn't supported by any available
resolver" — only undefined (or an explicit range) resolves as latest.
This broke every 'bit install <pkg>' / 'bit create' path that resolves
a package without a version (~60 e2e failures on CI).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ee19e33

Carries the engine fixes for Bit's e2e failures (pnpm/pnpm#12899):
root-component member linking without on-disk manifests, injectedDeps
in .modules.yaml, hoisted-linker peered-snapshot and importer-subtree
materialization, out-of-root declared importers, lockfile-excluded
link: deps, empty-specifier resolution, hoistWorkspacePackages
name-links, and an implemented getPeerDependencyIssues.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines 1476 to 1482
isValidVersionSpecifier(spec: string): boolean {
// parseBareSpecifier returns null for empty/unparsable input.
return (
parseBareSpecifier(
spec,
'pkgname', // This argument is the package but we don't need it
'latest',
'https://registry.npmjs.org/'
'pkgname' // This argument is the package name but we don't need it
) != null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Eager napi import 🐞 Bug ☼ Reliability

dependency-resolver.main.runtime.ts now imports parseBareSpecifier from @pnpm/napi at module scope,
forcing the native binding to load whenever the dependency-resolver aspect is loaded. This can
increase startup cost and can hard-fail process initialization in environments where the native
binding cannot be loaded, even if pnpm isn’t the active package manager.
Agent Prompt
### Issue description
`dependency-resolver.main.runtime.ts` performs a module-scope import from `@pnpm/napi` and uses it in `isValidVersionSpecifier()`. This eagerly loads the native binding whenever the dependency-resolver aspect is loaded, which can slow startup and can crash early if the native module can’t be loaded.

### Issue Context
Other pnpm integration points in this repo explicitly lazy-load pnpm internals for performance, so this eager import is inconsistent and increases blast radius.

### Fix Focus Areas
- Replace the module-scope `@pnpm/napi` import with a non-native, lightweight validator (e.g. `semver.valid/validRange` + allow-list for tags/aliases/URLs), OR
- If `parseBareSpecifier` is required for parity, load it lazily inside `isValidVersionSpecifier()` with a guarded `require()`/cached getter and provide a safe fallback when the binding fails to load.

- scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[36-46]
- scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[1469-1484]
- scopes/dependencies/pnpm/pnpm.package-manager.ts[127-134]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 4 to 11
export function getProxyConfig(config: Config): PackageManagerProxyConfig {
const httpProxy = config.httpProxy;
const httpsProxy = config.httpsProxy || httpProxy;
const proxyConfig: PackageManagerProxyConfig = {
httpProxy,
httpsProxy,
noProxy: config.rawConfig.noproxy,
noProxy: config.authConfig.noproxy,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Noproxy from authconfig 🐞 Bug ≡ Correctness

getProxyConfig() sets noProxy from config.authConfig.noproxy, which is not the typed
proxy-bypass setting and may not contain the user’s .npmrc no-proxy value. As a result, dependency
downloads can incorrectly go through the proxy even when the user configured domains to bypass it.
Agent Prompt
### Issue description
`getProxyConfig()` sources `noProxy` from `config.authConfig.noproxy`. `authConfig` is used primarily for auth/credential configuration, and using it for proxy-bypass settings risks ignoring `.npmrc` `no-proxy`/`noproxy` configuration.

### Issue Context
Bit’s dependency-resolver supports `noProxy` explicitly and threads it through proxy configuration; if `getProxyConfig()` fails to populate it, proxy bypass won’t work.

### Fix Focus Areas
- Prefer the typed config field for proxy bypass if available (e.g. `config.noProxy`).
- If pnpm’s config reader still exposes raw keys, add a safe fallback chain (e.g. `config.noProxy ?? config.authConfig['no-proxy'] ?? config.authConfig.noproxy`).

- scopes/dependencies/pnpm/get-proxy-config.ts[1-13]
- scopes/dependencies/dependency-resolver/dependency-resolver-workspace-config.ts[55-59]
- scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts[805-809]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f5c3302

…installs

The Rust engine rewrites pnpm-lock.yaml from its typed model, which
does not round-trip Bit's bit: block — the v11 TS engine mutated the
loaded object in place, so attributes like restoredFromModel survived
installs. Capture the block before the engine runs and re-assert it
(merged with the fresh depsRequiringBuild) afterwards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread workspace.jsonc
Comment on lines +55 to +75
"@pnpm/cli.default-reporter": "1100.3.3",
"@pnpm/colorize-semver-diff": "1.0.1",
"@pnpm/config": "1004.11.0",
"@pnpm/core": "1016.2.0",
"@pnpm/default-reporter": "1002.1.14",
"@pnpm/dependency-path": "1001.1.10",
"@pnpm/error": "1000.1.0",
"@pnpm/fetch": "1001.0.0",
"@pnpm/list": "^1000.3.4",
"@pnpm/lockfile.filtering": "^1001.0.31",
"@pnpm/lockfile.fs": "^1001.1.32",
"@pnpm/lockfile.types": "^1002.1.0",
"@pnpm/logger": "1001.0.1",
"@pnpm/modules-yaml": "1002.0.1",
"@pnpm/config.parse-overrides": "1100.0.2",
"@pnpm/config.reader": "1101.3.1",
"@pnpm/deps.inspection.list": "1100.0.20",
"@pnpm/deps.inspection.tree-builder": "1100.0.17",
"@pnpm/deps.path": "1100.0.8",
"@pnpm/error": "1100.0.1",
"@pnpm/installing.modules-yaml": "1100.0.9",
"@pnpm/lockfile.filtering": "1100.1.8",
"@pnpm/lockfile.fs": "1100.1.7",
"@pnpm/lockfile.types": "1100.0.12",
"@pnpm/logger": "1100.0.0",
"@pnpm/network.ca-file": "3.0.3",
"@pnpm/network.fetch": "1100.1.4",
"@pnpm/napi": "12.0.0-alpha.6",
"@pnpm/node-fetch": "^1.0.0",
"@pnpm/npm-resolver": "1005.2.3",
"@pnpm/package-store": "1007.1.6",
"@pnpm/parse-overrides": "1001.0.4",
"@pnpm/plugin-commands-publishing": "1000.3.16",
"@pnpm/plugin-commands-rebuild": "1008.0.13",
"@pnpm/plugin-trusted-deps": "0.2.2",
"@pnpm/registry-mock": "3.48.0",
"@pnpm/reviewing.dependencies-hierarchy": "^1001.3.4",
"@pnpm/semver-diff": "1.1.0",
"@pnpm/sort-packages": "1000.0.16",
"@pnpm/store-connection-manager": "1002.3.19",
"@pnpm/types": "1001.3.0",
"@pnpm/worker": "1000.6.7",
"@pnpm/workspace.pkgs-graph": "1000.0.39",
"@pnpm/types": "1101.3.2",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Node engine mismatch 🐞 Bug ☼ Reliability

The PR introduces new @pnpm/* 11xx/12.x deps that declare Node >=22.13 in pnpm-lock.yaml, while the
repo still advertises "engines.node": ">=12.22.0" in package.json. This creates misleading
compatibility metadata and can cause installs/tooling to fail unexpectedly on Node versions that
appear supported by the repo.
Agent Prompt
### Issue description
The repository's declared Node engine requirement is out of sync with the actual Node version required by the upgraded pnpm internal dependencies.

- `package.json` still declares `engines.node: >=12.22.0`.
- The PR adds pnpm v11/v12-era packages (11xx/12.x) which, per `pnpm-lock.yaml`, require Node `>=22.13`.
- `workspace.jsonc` already pins `nodeVersion: 22.22.0` with `engineStrict: true`, reinforcing that the real minimum Node version is much higher than `package.json` claims.

This mismatch misleads consumers and CI/tooling that relies on `package.json#engines`.

### Issue Context
The pnpm internal dependency set introduced by this PR appears to be Node-22+ only (lockfile `engines` fields), but the repo-level engine metadata wasn’t updated accordingly.

### Fix Focus Areas
- package.json[5-7]
- workspace.jsonc[720-722]
- pnpm-lock.yaml[32843-32858]

### Suggested fix
Update `package.json#engines.node` to reflect the real minimum Node version required by the new pnpm internals (at least `>=22.13`, or align exactly with the workspace’s pinned `22.22.0` policy), and ensure any documentation/CI configuration that depends on `engines` remains consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2348960

zkochan and others added 2 commits July 11, 2026 08:48
Carries the plain file: importer-ref serialization fix (pnpm/pnpm#12924),
which resolves the bit tag crash in the lockfile-to-graph converter
(Cannot read properties of undefined) under BIT_FEATURES=deps-graph.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… graph calc

calcDependenciesGraph scrubs other workspace components from the cloned
importers before filtering the lockfile, so a sibling entry never leaks
into the component's graph. In the Rust engine's per-importer capsule
layout, though, an injected sibling ("file:" ref) in the component's own
importer is a real direct dependency — the converter rewrites it to the
component's semver id — so only non-file entries (and entries in other
importers, e.g. the capsule/workspace root) are scrubbed now. The v11
layouts never record file: siblings in a component's own importer, so
this is a no-op for lockfiles written by the TS engine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +412 to +421
const preInstallBitAttrs = await readBitLockfileAttrs(rootDir);
installsRunning[rootDir] = nodeApi.install(installOptions, onLog, readPackageHookForDeps);
const installResult: nodeApi.InstallResult = await installsRunning[rootDir];
resolvedStoreDir = installResult.storeDir;
const sortedDepsRequiringBuild = installResult.depsRequiringBuild?.sort();
if (sortedDepsRequiringBuild != null || preInstallBitAttrs != null) {
await addBitAttributesToLockfile(rootDir, {
...preInstallBitAttrs,
...(sortedDepsRequiringBuild != null ? { depsRequiringBuild: sortedDepsRequiringBuild } : {}),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Stale build-deps lockfile flag 🐞 Bug ≡ Correctness

lynx.install() re-applies the pre-install bit: lockfile block but only overwrites
depsRequiringBuild when installResult.depsRequiringBuild is non-null; if the engine omits this
field when nothing needs a build, the old list is preserved. This stale list is later consumed by
convertLockfileToGraph() to mark packages as requiresBuild, producing incorrect dependency graph
metadata.
Agent Prompt
### Issue description
`depsRequiringBuild` can become stale across installs. If the Rust engine returns `depsRequiringBuild: undefined` (as opposed to `[]`) for an install where nothing requires build, the current merge logic writes back the previous lockfile’s `bit.depsRequiringBuild` unchanged.

### Issue Context
- `addBitAttributesToLockfile()` merges `attrs` over the existing `lockfile.bit` object, so missing keys are preserved.
- `lockfile-deps-graph-converter.ts` uses `lockfile.bit?.depsRequiringBuild` to set `graphPkg.requiresBuild = true`.

### Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[412-425]
- scopes/dependencies/pnpm/lynx.ts[749-755]

### Suggested fix
When re-asserting the `bit:` block after install, explicitly set `depsRequiringBuild` to an empty array when the engine returns it as nullish.

For example:
- Compute `const deps = (installResult.depsRequiringBuild ?? []).slice().sort();`
- Call `addBitAttributesToLockfile(rootDir, { ...preInstallBitAttrs, depsRequiringBuild: deps })`
- Keep returning `depsRequiringBuild` to callers based on `deps.length` (if you want to preserve the current `undefined` return behavior) but ensure the lockfile always gets an accurate array.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 0a5e620

…nces

The engine hands the readPackage hook a workspace project's importer
manifest when it resolves that project as an injected "file:" dependency.
That manifest was already stripped of workspace-sibling deps by the
up-front with-dir transform (Bit wires siblings itself inside importers),
so the injected instance lost its component deps — and with them the
component graph edges recorded at tag time (the deps-graph e2e
"should replace pending version in direct dependency" cluster).

The next @pnpm/napi release passes the lockfile-root-relative directory
of a directory resolution as the hook's second argument; use it to
substitute the project's raw manifest for its dependency instances,
mirroring how pnpm's TS engine applies the hook chain contextually to
raw project manifests. With the current binding the argument is simply
absent and behavior is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +162 to +174
const mergedAuthConfig = Object.assign({}, pnpmConfig.config.authConfig, authConfig) as Record<string, unknown>;
const authHeaderByUri = buildAuthHeaderByUri(mergedAuthConfig);
const registriesMap = registries.toMap();
const resolve: ResolveFunction = (wantedDep, resolveOpts) =>
nodeApi.resolveDependency(wantedDep, {
dir: resolveOpts.projectDir ?? resolveOpts.lockfileDir ?? '',
cacheDir,
registries: registriesMap,
authHeaderByUri,
proxyConfig: proxyConfig ? toNodeApiProxyConfig(proxyConfig) : undefined,
networkConfig: networkConfig ? toNodeApiNetworkConfig(networkConfig) : undefined,
fullMetadata,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Empty resolver dir 🐞 Bug ≡ Correctness

generateResolverAndFetcher() forwards `dir: resolveOpts.projectDir ?? resolveOpts.lockfileDir ??
'' into nodeApi.resolveDependency`, so callers that pass empty strings end up invoking the
resolver with an empty directory. convertGraphToLockfile() currently calls resolve() with
lockfileDir: '' and projectDir: '', so lockfile generation can fail or resolve using config from
an unintended working directory.
Agent Prompt
## Issue description
`nodeApi.resolveDependency` is invoked with `dir` derived via nullish coalescing (`??`). Empty strings are not nullish, so `dir` can become `''`, which is not a meaningful base directory for resolution/config discovery.

## Issue Context
At least one internal caller (`convertGraphToLockfile`) passes empty strings for `projectDir`/`lockfileDir`, which now propagate into the resolver due to the new implementation.

## Fix Focus Areas
- scopes/dependencies/pnpm/lynx.ts[147-175]
- scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts[369-386]

## Concrete fix
1) In `generateResolverAndFetcher`, treat empty string as missing by using `||` (or explicitly checking for a non-empty string), and fall back to a safe default:

```ts
const dir = resolveOpts.projectDir || resolveOpts.lockfileDir || process.cwd();
...
nodeApi.resolveDependency(wantedDep, { dir, ... })
```

2) In `convertGraphToLockfile`, stop passing `''` for directory fields; pass the real `rootDir` (or `projectDir`) so the resolver runs with the correct context.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 9c45e72

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.

2 participants