Skip to content

Missing Packages After bit new with Forked Env #10200

@zkochan

Description

@zkochan

Symptom

After running bit new vue-hello-world <name> --aspect teambit.community/starters/hello-world-vue, bit status shows:

⚠ environment with ID: bitdev.general/envs/bit-env@3.0.7 configured on component org.scope-name/envs/my-vue-env was not loaded (run "bit install")
...
components with issues (1)
     > org.scope-name/envs/my-vue-env ...
       missing packages that were manually set (run "bit install"):
          @vitest/coverage-v8, @mdx-js/mdx

Running bit install again works as a temporary workaround. The issue does not occur on version ≤1.13.42.


Bisection

Version Status
1.13.21 ✅ Works
1.13.35 ✅ Works
1.13.39 ✅ Works
1.13.42 ✅ Works
1.13.44 ❌ Broken
1.13.46 ❌ Broken

The regression was introduced by PR #10150 (0f3672651), which landed between 1.13.42 and 1.13.43.


PR #10150 Change

File: scopes/dependencies/dependency-resolver/manifest/workspace-manifest-factory.ts

-      depManifest.dependencies = {
-        ...defaultPeerDependencies,
+      const usedPeerDependencies = pickBy(defaultPeerDependencies, (_val, pkgName) => {
+        return (
+          depManifestBeforeFiltering.dependencies[pkgName] ||
+          depManifestBeforeFiltering.devDependencies[pkgName] ||
+          depManifestBeforeFiltering.peerDependencies[pkgName]
+        );
+      });
+
+      depManifest.dependencies = {
+        ...usedPeerDependencies,
         ...unresolvedRuntimeMissingRootDeps,

Intent: Stop env peer-dependencies from being written into the dependencies of user components (visible in external-package-manager mode).


Root Cause: Two Interacting Bugs

Bug 1 — Forking copies stale "+" dependency markers

When bit new forks bitdev.vue/examples/my-vue-env@1.1.18, the forking process copies the dep-resolver aspect config verbatim from the forked version. In my-vue-env@1.1.18 the packages @vitest/coverage-v8 and @mdx-js/mdx existed in the dep config with version "+" (the MANUALLY_ADD_DEPENDENCY sentinel), meaning "include this dep but resolve the version from the workspace package.json."

The current (latest) version of my-vue-env removed these entries — they are no longer needed — but the forked component inherits them from @1.1.18.

The source: "env-own" label seen in bit deps debug comes from getPoliciesFromEnvForItself(): because the forked component has an env.jsonc with an extends directive, the extended parent env's peers section is resolved and folded in as the component's own self-policy. Gilad's suspicion about the env.jsonc extends bug is on point here.

Code path:

  • workspace-component-loader.ts:921mergeVariantPolicies(…, envExtendsDeps)
  • dependency-resolver.main.runtime.ts:1291getPoliciesFromEnvForItself(id, legacyFiles, envExtendsDeps)
  • dependency-resolver.main.runtime.ts:1308 — reads envPolicy.selfPolicy from the resolved env.jsonc extends chain
  • Result: packages from the parent env's peers section end up in the forked component's effective policy with source: "env-own"

Bug 2 — PR #10150 created a chicken-and-egg installation problem

usedPeerDependencies is a subset of defaultPeerDependencies (env self-policy) filtered to packages already present in depManifestBeforeFiltering. That snapshot is computed from getDependencies(component, { includeHidden: true }), which reads the component's stored serialized dep data.

For packages with version "+" in the dep config:

  1. _manuallyAddPackage() (overrides-dependencies.ts:71) tries to resolve the version by looking in the workspace root package.json.
  2. On a fresh workspace the package is not yet installed → not in package.jsonversionToAdd = null → added to missingPackageDependencies, not added to packageDependencies.
  3. Not in packageDependencies → not returned by fromLegacyComponent() → not in getDependencies() output → not in depManifestBeforeFiltering.
  4. The usedPeerDependencies filter excludes the package.
  5. PNPM does not install it.
  6. On every subsequent bit status / bit install the cycle repeats.

Before PR #10150 the unconditional ...defaultPeerDependencies spread broke this cycle on the first install, so the problem was never visible.

Why both manuallyAddedDependencies AND missingPackageDependencies appear for the same package (apply-overrides.ts:328-338):

const addedPkg = this.overridesDependencies._manuallyAddPackage(
  depField, dependency, dependencyValue, packageJson
);
if (addedPkg) {
  packages[depField] = Object.assign(packages[depField] || {}, addedPkg);
  if (componentData && !componentData.packageName) {   // package NOT in node_modules
    this.overridesDependencies.missingPackageDependencies.push(dependency);
  }
}

If an explicit version IS found (e.g. "^3.1.2" stored from a partial state), addedPkg is set and the entry goes into manuallyAddedDependencies. But because the package is not in node_modules, _resolvePackageData() returns undefined, componentData.packageName is undefined, and the package is also pushed into missingPackageDependencies. This is the state visible in the bit deps debug output from the issue.

Why bit deps unset did not work

bit deps unset removes entries from componentOverridesData (the component's explicit dep-resolver config stored in .bitmap). If the packages entered the policy purely through the env.jsonc extends chain (not written directly into the bitmap entry), there is nothing for unset to remove.


Data from bit deps debug Explained

"manuallyAddedDependencies": {
  "dependencies": ["@vitest/coverage-v8@^3.1.2", "@mdx-js/mdx@1.6.22", ...]
},
"missingPackageDependencies": ["@vitest/coverage-v8", "@mdx-js/mdx"],
"sources": [
  { "id": "@vitest/coverage-v8", "source": "env-own" },
  { "id": "@mdx-js/mdx",         "source": "env-own" }
]
Field Meaning
manuallyAddedDependencies Version was found (from componentOverridesData explicit value or from a partially-updated package.json); package added to the legacy dep list
missingPackageDependencies Package is not in node_modules; _resolvePackageData() returned undefined
source: "env-own" Entry attributed to the env's self-policy (peers section of the resolved env.jsonc extends chain)

Key Files

File Relevance
scopes/dependencies/dependency-resolver/manifest/workspace-manifest-factory.ts:215 usedPeerDependencies filter introduced by PR #10150
scopes/dependencies/dependency-resolver/manifest/workspace-manifest-factory.ts:262 _getDefaultPeerDependencies() — reads env's selfPolicy
scopes/dependencies/dependencies/dependencies-loader/overrides-dependencies.ts:65 _manuallyAddPackage() — version lookup logic for "+" marker
scopes/dependencies/dependencies/dependencies-loader/apply-overrides.ts:328 Sets both manuallyAddedDependencies and missingPackageDependencies
scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts:1308 getPoliciesFromEnvForItself() — includes extended env's peers as env-own
scopes/dependencies/dependency-resolver/policy/env-policy/env-policy.ts:70 selfPeersEntries created with source: 'env-own' from peers key
scopes/envs/envs/environments.main.runtime.ts:335 calculateEnvManifest() — recursive extends resolution
scopes/workspace/workspace/workspace-component/workspace-component-loader.ts:915 envExtendsDeps collection and passing to mergeVariantPolicies
scopes/component/forking/forking.main.runtime.ts Forking copies dep-resolver aspect config verbatim from source version

Proposed Fixes

Three approaches, in order of surgical precision:

Option A — Fix the forking process (addresses Bug 1)

During forking, filter out dep-resolver policy entries whose version is "+" and whose source in the original component was "env-own". These are transient, env-provided values and should not be hardcoded into the new component's config.

Option B — Fix the usedPeerDependencies filter (addresses the PR #10150 regression)

Widen the filter to also include packages that are explicitly listed in the component's dep-resolver config, even if they are not yet reflected in depManifestBeforeFiltering (the chicken-and-egg case):

// Pseudocode
const componentExplicitPkgs = new Set(
  Object.keys(component.config.extensions
    .findExtension(DependencyResolverAspect.id)?.config?.policy?.dependencies ?? {})
);

const usedPeerDependencies = pickBy(defaultPeerDependencies, (_val, pkgName) => {
  return (
    depManifestBeforeFiltering.dependencies[pkgName] ||
    depManifestBeforeFiltering.devDependencies[pkgName] ||
    depManifestBeforeFiltering.peerDependencies[pkgName] ||
    componentExplicitPkgs.has(pkgName)   // <-- new condition
  );
});

Option C — Targeted fix in the "+" resolution path (addresses Bug 1 differently)

In _manuallyAddPackage(), when dependencyValue === "+" and the package cannot be found in the workspace package.json, instead of immediately pushing to missingPackageDependencies, check whether the package exists in the env's self-policy and, if so, defer to installation via that path rather than flagging it as missing.


Workaround (user-facing)

Run bit install a second time after bit new. The first run partially populates the workspace policy; the second run finds the version and completes the installation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions